// // MainFrame.cpp - ノーコード画像検査(ビジュアル・パイプライン + テンプレ + ウィザード)。 // 左: 各ステップをカード化し適用後サムネイルを縦に並べる(画像が変化する様子が見える)。 // 右: 選択カードの設定をスライダーでライブ調整 / ステップ追加パレット。 // 上: [画像を開く][テンプレート][ウィザード][実行] と 大きな合否バナー。 // #include "stdafx.h" #include "MainFrame.h" #include "core/StepCatalog.h" #include "core/Templates.h" #include "core/RecipeStore.h" #include "Wizard.h" #include "SelfTest.h" #include // SHBrowseForFolder using namespace core; BEGIN_MESSAGE_MAP(CMainFrame, CFrameWnd) ON_WM_CREATE() ON_WM_SIZE() ON_WM_PAINT() ON_WM_CTLCOLOR() ON_WM_HSCROLL() ON_WM_LBUTTONDOWN() ON_WM_MOUSEMOVE() ON_WM_LBUTTONUP() ON_WM_RBUTTONDOWN() ON_BN_CLICKED(IDC_BTN_OPEN, OnBtnOpen) ON_BN_CLICKED(IDC_BTN_RUN, OnBtnRun) ON_BN_CLICKED(IDC_BTN_ADD, OnBtnAdd) ON_BN_CLICKED(IDC_BTN_DEL, OnBtnDel) ON_BN_CLICKED(IDC_BTN_UP, OnBtnUp) ON_BN_CLICKED(IDC_BTN_DOWN, OnBtnDown) ON_BN_CLICKED(IDC_BTN_TMPL, OnBtnTemplate) ON_BN_CLICKED(IDC_BTN_WIZ, OnBtnWizard) ON_COMMAND(ID_FILE_OPENIMAGE, OnBtnOpen) ON_COMMAND(ID_RECIPE_OPEN, OnRecipeOpen) ON_COMMAND(ID_RECIPE_SAVE, OnRecipeSave) ON_COMMAND(ID_APP_EXIT, OnAppExit) ON_COMMAND(ID_ROI_CLEAR, OnRoiClear) ON_COMMAND(ID_ROI_UNDO, OnRoiUndo) ON_COMMAND(ID_RESULT_SAVE, OnResultSave) ON_COMMAND(ID_LOG_SETFOLDER, OnLogSetFolder) ON_COMMAND(ID_LOG_RECORD, OnLogRecord) ON_COMMAND(ID_LOG_OPENFOLDER, OnLogOpenFolder) ON_MESSAGE(WM_PV_SELCHANGED, OnPipeSelChanged) ON_MESSAGE(WM_PV_MOVESTEP, OnPipeMoveStep) ON_COMMAND_RANGE(ID_RECENT_BASE, ID_RECENT_BASE + RECENT_MAX - 1, OnRecentFile) END_MESSAGE_MAP() void CMainFrame::BuildMenu() { m_menu.CreateMenu(); CMenu file; file.CreatePopupMenu(); file.AppendMenu(MF_STRING, ID_FILE_OPENIMAGE, L"画像を開く...\tCtrl+O"); file.AppendMenu(MF_SEPARATOR); file.AppendMenu(MF_STRING, ID_RECIPE_OPEN, L"レシピを開く..."); file.AppendMenu(MF_STRING, ID_RECIPE_SAVE, L"レシピを保存...\tCtrl+S"); { CMenu recent; recent.CreatePopupMenu(); m_hRecentMenu = recent.GetSafeHmenu(); file.AppendMenu(MF_POPUP, (UINT_PTR)recent.Detach(), L"最近使ったレシピ"); } file.AppendMenu(MF_SEPARATOR); file.AppendMenu(MF_STRING, ID_RESULT_SAVE, L"判定結果画像を保存..."); file.AppendMenu(MF_SEPARATOR); file.AppendMenu(MF_STRING, ID_APP_EXIT, L"終了"); m_menu.AppendMenu(MF_POPUP, (UINT_PTR)file.Detach(), L"ファイル(&F)"); CMenu roi; roi.CreatePopupMenu(); roi.AppendMenu(MF_STRING, ID_ROI_UNDO, L"最後の検査範囲を取り消す"); roi.AppendMenu(MF_STRING, ID_ROI_CLEAR, L"すべてリセット(画像全体)"); m_menu.AppendMenu(MF_POPUP, (UINT_PTR)roi.Detach(), L"検査範囲(&R)"); CMenu log; log.CreatePopupMenu(); log.AppendMenu(MF_STRING, ID_LOG_SETFOLDER, L"記録フォルダを指定..."); log.AppendMenu(MF_STRING, ID_LOG_RECORD, L"今の結果を記録する"); log.AppendMenu(MF_STRING, ID_LOG_OPENFOLDER, L"記録フォルダを開く"); m_menu.AppendMenu(MF_POPUP, (UINT_PTR)log.Detach(), L"検査ログ(&L)"); SetMenu(&m_menu); } CMainFrame::CMainFrame() {} CString CMainFrame::FmtNum(double v) { CString s; if (v == (double)(long)v) s.Format(L"%ld", (long)v); else s.Format(L"%.2f", v); return s; } int CMainFrame::OnCreate(LPCREATESTRUCT lpcs) { if (CFrameWnd::OnCreate(lpcs) == -1) return -1; m_uiFont.CreatePointFont(100, L"Yu Gothic UI"); m_bigFont.CreatePointFont(200, L"Yu Gothic UI"); m_brPass.CreateSolidBrush(RGB(40, 170, 80)); m_brFail.CreateSolidBrush(RGB(220, 60, 60)); m_brNeutral.CreateSolidBrush(RGB(90, 100, 110)); DWORD bs = WS_CHILD | WS_VISIBLE | WS_TABSTOP | BS_PUSHBUTTON; m_btnOpen.Create(L"画像を開く…", bs, CRect(0, 0, 10, 10), this, IDC_BTN_OPEN); m_btnTemplate.Create(L"テンプレートから作る", bs, CRect(0, 0, 10, 10), this, IDC_BTN_TMPL); m_btnWizard.Create(L"ウィザードで作る", bs, CRect(0, 0, 10, 10), this, IDC_BTN_WIZ); m_btnRun.Create(L"実行(検査する)", bs, CRect(0, 0, 10, 10), this, IDC_BTN_RUN); m_result.Create(L"画像を開いて[実行]してください", WS_CHILD | WS_VISIBLE | SS_CENTER | SS_CENTERIMAGE, CRect(0, 0, 10, 10), this, IDC_RESULT); m_result.SetFont(&m_bigFont); for (CButton* b : { &m_btnOpen, &m_btnTemplate, &m_btnWizard, &m_btnRun }) b->SetFont(&m_uiFont); // 左: パイプライン m_pipe.Create(this, IDC_PIPE); m_pipe.SetData(&m_recipe, &m_engine); // 右: 設定 m_settingsTitle.Create(L"② 選んだステップの設定", WS_CHILD | WS_VISIBLE | SS_LEFT, CRect(0, 0, 10, 10), this, (UINT)-1); m_settingsTitle.SetFont(&m_uiFont); for (int i = 0; i < PARAM_ROWS; i++) { m_slabel[i].Create(L"", WS_CHILD | SS_LEFT | SS_CENTERIMAGE, CRect(0, 0, 10, 10), this, (UINT)-1); m_slabel[i].SetFont(&m_uiFont); m_slider[i].Create(WS_CHILD | WS_TABSTOP | TBS_HORZ | TBS_AUTOTICKS, CRect(0, 0, 10, 10), this, IDC_SLIDER0 + i); m_sval[i].Create(L"", WS_CHILD | SS_LEFT | SS_CENTERIMAGE, CRect(0, 0, 10, 10), this, (UINT)-1); m_sval[i].SetFont(&m_uiFont); } // 右: 追加パレット m_addTitle.Create(L"③ ステップを追加/並べ替え", WS_CHILD | WS_VISIBLE | SS_LEFT, CRect(0, 0, 10, 10), this, (UINT)-1); m_addTitle.SetFont(&m_uiFont); m_combo.Create(WS_CHILD | WS_VISIBLE | WS_TABSTOP | CBS_DROPDOWNLIST | WS_VSCROLL, CRect(0, 0, 10, 240), this, IDC_COMBO); m_combo.SetFont(&m_uiFont); int nk = 0; const StepKind* ks = AllStepKinds(nk); for (int i = 0; i < nk; i++) { int item = m_combo.AddString(StepName(ks[i])); m_combo.SetItemData(item, (DWORD_PTR)ks[i]); } m_combo.SetCurSel(0); m_btnAdd.Create(L"追加", bs, CRect(0, 0, 10, 10), this, IDC_BTN_ADD); m_btnDel.Create(L"削除", bs, CRect(0, 0, 10, 10), this, IDC_BTN_DEL); m_btnUp.Create(L"↑ 上へ", bs, CRect(0, 0, 10, 10), this, IDC_BTN_UP); m_btnDown.Create(L"↓ 下へ", bs, CRect(0, 0, 10, 10), this, IDC_BTN_DOWN); for (CButton* b : { &m_btnAdd, &m_btnDel, &m_btnUp, &m_btnDown }) b->SetFont(&m_uiFont); // 既定レシピ(明るさ確認の例) m_recipe.steps.push_back(MakeStep(StepKind::Grayscale)); { Step s = MakeStep(StepKind::InspectBrightness); s.SetParam(L"min", 80); s.SetParam(L"max", 180); m_recipe.steps.push_back(s); } BuildMenu(); LoadRecent(); UpdateRecentMenu(); m_logFolder = AfxGetApp()->GetProfileString(L"Log", L"Folder", L""); m_pipe.SetSel(0); ConfigureSliders(0); SetWindowText(L"ImgTestSystem - ノーコード画像検査"); SetWindowPos(NULL, 0, 0, 1180, 780, SWP_NOMOVE | SWP_NOZORDER); return 0; } int CMainFrame::Sel() const { return const_cast(m_pipe).GetSel(); } void CMainFrame::OnSize(UINT nType, int cx, int cy) { CFrameWnd::OnSize(nType, cx, cy); LayoutControls(); } void CMainFrame::LayoutControls() { CRect cr; GetClientRect(&cr); int cx = cr.Width(), cy = cr.Height(); if (cx < 100 || cy < 100) return; const int M = 10; // 上部バー(2段: ボタン行 + バナー) int bw = 150, bh = 30, bx = M; m_btnOpen.MoveWindow(bx, M, bw, bh); bx += bw + 8; m_btnTemplate.MoveWindow(bx, M, bw + 20, bh); bx += bw + 28; m_btnWizard.MoveWindow(bx, M, bw, bh); bx += bw + 8; m_btnRun.MoveWindow(bx, M, bw, bh); bx += bw + 8; int bannerX = bx + 6; m_result.MoveWindow(bannerX, M, cx - bannerX - M, bh + 8); int top = M + bh + 12; // 左: パイプライン(右カラム幅を残す) int rightW = 380; if (cx - rightW - M * 3 < 360) rightW = cx - 360 - M * 3; int leftW = cx - rightW - M * 3; m_pipe.MoveWindow(M, top, leftW, cy - top - M); // 右カラム int rx = M + leftW + M; int rw = rightW + M; // 使える幅 if (rx + rw > cx - M) rw = cx - M - rx; // ① 大プレビュー(正方形) int pv = rw < (cy - top - 40) ? rw : 240; if (pv > 300) pv = 300; m_bigPreviewRect = CRect(rx, top + 20, rx + rw, top + 20 + pv); // (プレビュー上のラベルは OnPaint で描く) // ② 設定スライダー int sy = m_bigPreviewRect.bottom + 12; m_settingsTitle.MoveWindow(rx, sy, rw, 20); sy += 24; for (int i = 0; i < PARAM_ROWS; i++) { m_slabel[i].MoveWindow(rx, sy, rw - 70, 20); m_sval[i].MoveWindow(rx + rw - 66, sy, 66, 20); m_slider[i].MoveWindow(rx, sy + 20, rw, 26); sy += 52; } // ③ 追加パレット(下部) int ay = cy - M - 92; if (ay < sy + 6) ay = sy + 6; m_addTitle.MoveWindow(rx, ay, rw, 20); ay += 24; m_combo.MoveWindow(rx, ay, rw - 76, 240); m_btnAdd.MoveWindow(rx + rw - 70, ay, 70, 26); ay += 32; int q = (rw - 16) / 3; m_btnDel.MoveWindow(rx, ay, q, 26); m_btnUp.MoveWindow(rx + q + 8, ay, q, 26); m_btnDown.MoveWindow(rx + 2 * q + 16, ay, rw - 2 * q - 16, 26); Invalidate(); } void CMainFrame::OnPaint() { CPaintDC dc(this); // プレビュー見出し dc.SetBkMode(TRANSPARENT); CFont* of = dc.SelectObject(&m_uiFont); dc.SetTextColor(RGB(60, 70, 80)); CRect lbl(m_bigPreviewRect.left, m_bigPreviewRect.top - 20, m_bigPreviewRect.right, m_bigPreviewRect.top); int sel = Sel(); CString cap = L"① プレビュー(ドラッグで検査範囲を追加/右クリックで削除)"; if (sel >= 0 && sel < (int)m_recipe.steps.size()) cap.Format(L"① ステップ%d: %s(ドラッグで範囲追加/右クリックで削除)", sel + 1, (LPCWSTR)StepName(m_recipe.steps[sel].kind)); dc.DrawText(cap, lbl, DT_LEFT | DT_BOTTOM | DT_SINGLELINE | DT_END_ELLIPSIS); dc.SelectObject(of); // プレビュー本体 dc.FillSolidRect(m_bigPreviewRect, RGB(30, 30, 30)); CRect inner = m_bigPreviewRect; inner.DeflateRect(3, 3); CRect fit(0, 0, 0, 0); if (m_engine.HasImage() && sel >= 0 && m_engine.HasShot(sel)) fit = m_engine.DrawShot(&dc, inner, sel); else if (m_engine.HasImage()) fit = m_engine.DrawSource(&dc, inner); else { dc.SetBkMode(TRANSPARENT); dc.SetTextColor(RGB(200, 200, 200)); dc.DrawText(L"[画像を開く]で画像を読み込みます", inner, DT_CENTER | DT_VCENTER | DT_WORDBREAK); } m_previewFitRect = fit; // 確定済み ROI(複数)を黄枠+番号で表示 if (m_engine.HasImage() && !m_recipe.rois.empty() && !fit.IsRectEmpty()) { int sw = m_engine.SrcWidth(), sh = m_engine.SrcHeight(); if (sw > 0 && sh > 0) { double kx = (double)fit.Width() / sw, ky = (double)fit.Height() / sh; CPen yp(PS_SOLID, 2, RGB(255, 210, 0)); CPen* oyp = dc.SelectObject(&yp); dc.SelectStockObject(NULL_BRUSH); dc.SetBkMode(TRANSPARENT); dc.SetTextColor(RGB(255, 210, 0)); dc.SelectObject(&m_uiFont); for (size_t i = 0; i < m_recipe.rois.size(); i++) { const CRect& roi = m_recipe.rois[i]; CRect r(fit.left + (int)(roi.left * kx), fit.top + (int)(roi.top * ky), fit.left + (int)(roi.right * kx), fit.top + (int)(roi.bottom * ky)); dc.Rectangle(r); CString no; no.Format(L"%d", (int)i + 1); dc.TextOut(r.left + 2, r.top, no); } dc.SelectObject(oyp); } } // ドラッグ中のラバーバンド if (m_roiDragging) { CRect rb(m_roiStart, m_roiCur); rb.NormalizeRect(); CPen dp(PS_DOT, 1, RGB(255, 255, 0)); int obk = dc.SetBkMode(TRANSPARENT); CPen* odp = dc.SelectObject(&dp); dc.SelectStockObject(NULL_BRUSH); dc.Rectangle(rb); dc.SelectObject(odp); dc.SetBkMode(obk); } CPen pen(PS_SOLID, 1, RGB(120, 120, 120)); CPen* op = dc.SelectObject(&pen); dc.SelectStockObject(NULL_BRUSH); dc.Rectangle(m_bigPreviewRect); dc.SelectObject(op); } // --- ROI マウス操作(大プレビュー上) --- void CMainFrame::OnLButtonDown(UINT nFlags, CPoint pt) { if (m_engine.HasImage() && !m_previewFitRect.IsRectEmpty() && m_previewFitRect.PtInRect(pt)) { m_roiDragging = true; m_roiStart = m_roiCur = pt; SetCapture(); return; } CFrameWnd::OnLButtonDown(nFlags, pt); } void CMainFrame::OnMouseMove(UINT nFlags, CPoint pt) { if (m_roiDragging) { // プレビュー内にクランプ CPoint p = pt; if (p.x < m_previewFitRect.left) p.x = m_previewFitRect.left; if (p.x > m_previewFitRect.right) p.x = m_previewFitRect.right; if (p.y < m_previewFitRect.top) p.y = m_previewFitRect.top; if (p.y > m_previewFitRect.bottom) p.y = m_previewFitRect.bottom; m_roiCur = p; InvalidateRect(m_bigPreviewRect, FALSE); return; } CFrameWnd::OnMouseMove(nFlags, pt); } void CMainFrame::OnLButtonUp(UINT nFlags, CPoint pt) { if (m_roiDragging) { m_roiDragging = false; ReleaseCapture(); CRect rb(m_roiStart, m_roiCur); rb.NormalizeRect(); CRect fit = m_previewFitRect; int sw = m_engine.SrcWidth(), sh = m_engine.SrcHeight(); if (rb.Width() >= 6 && rb.Height() >= 6 && sw > 0 && sh > 0 && !fit.IsRectEmpty()) { double kx = (double)sw / fit.Width(), ky = (double)sh / fit.Height(); CRect r( (int)((rb.left - fit.left) * kx), (int)((rb.top - fit.top) * ky), (int)((rb.right - fit.left) * kx), (int)((rb.bottom - fit.top) * ky)); if (r.left < 0) r.left = 0; if (r.top < 0) r.top = 0; if (r.right > sw) r.right = sw; if (r.bottom > sh) r.bottom = sh; m_recipe.rois.push_back(r); // 追加(複数指定) RunAndRefresh(-2); } else { InvalidateRect(m_bigPreviewRect, FALSE); } return; } CFrameWnd::OnLButtonUp(nFlags, pt); } // 右クリック: その位置を含む検査範囲を削除。 void CMainFrame::OnRButtonDown(UINT nFlags, CPoint pt) { if (m_engine.HasImage() && !m_previewFitRect.IsRectEmpty() && m_previewFitRect.PtInRect(pt) && !m_recipe.rois.empty()) { int sw = m_engine.SrcWidth(), sh = m_engine.SrcHeight(); CRect fit = m_previewFitRect; if (sw > 0 && sh > 0) { double kx = (double)sw / fit.Width(), ky = (double)sh / fit.Height(); int ix = (int)((pt.x - fit.left) * kx), iy = (int)((pt.y - fit.top) * ky); // 後に追加したもの(手前)から探す for (int i = (int)m_recipe.rois.size() - 1; i >= 0; i--) { CRect rr = m_recipe.rois[i]; rr.NormalizeRect(); if (rr.PtInRect(CPoint(ix, iy))) { m_recipe.rois.erase(m_recipe.rois.begin() + i); RunAndRefresh(-2); return; } } } } CFrameWnd::OnRButtonDown(nFlags, pt); } void CMainFrame::OnRoiClear() { m_recipe.rois.clear(); RunAndRefresh(-2); } void CMainFrame::OnRoiUndo() { if (m_recipe.rois.empty()) return; m_recipe.rois.pop_back(); RunAndRefresh(-2); } // --- 判定結果画像の保存 / 検査ログ --- void CMainFrame::OnResultSave() { if (!m_engine.HasImage()) { AfxMessageBox(L"先に画像を開いてください"); return; } CFileDialog dlg(FALSE, L"png", L"result.png", OFN_OVERWRITEPROMPT | OFN_HIDEREADONLY, L"PNG (*.png)|*.png|BMP (*.bmp)|*.bmp|JPEG (*.jpg)|*.jpg||", this); if (dlg.DoModal() != IDOK) return; RunResult rr = m_engine.Run(m_recipe); if (!m_engine.SaveResultImage(dlg.GetPathName(), m_recipe, rr)) { AfxMessageBox(L"結果画像の保存に失敗しました"); return; } AfxMessageBox(L"判定結果画像を保存しました:\n" + dlg.GetPathName(), MB_ICONINFORMATION); } static CString BrowseFolder(CWnd* parent, LPCWSTR title) { BROWSEINFO bi = {}; bi.hwndOwner = parent->GetSafeHwnd(); bi.lpszTitle = title; bi.ulFlags = BIF_RETURNONLYFSDIRS | BIF_NEWDIALOGSTYLE; LPITEMIDLIST pidl = SHBrowseForFolder(&bi); if (!pidl) return L""; CString path; SHGetPathFromIDList(pidl, path.GetBuffer(MAX_PATH)); path.ReleaseBuffer(); CoTaskMemFree(pidl); return path; } static CString CsvField(const CString& s) { if (s.Find(L',') < 0 && s.Find(L'"') < 0 && s.Find(L'\n') < 0) return s; CString t = s; t.Replace(L"\"", L"\"\""); return L"\"" + t + L"\""; } void CMainFrame::OnLogSetFolder() { CString f = BrowseFolder(this, L"検査ログの記録フォルダを選んでください"); if (f.IsEmpty()) return; m_logFolder = f; AfxGetApp()->WriteProfileString(L"Log", L"Folder", m_logFolder); AfxMessageBox(L"記録フォルダ:\n" + m_logFolder, MB_ICONINFORMATION); } void CMainFrame::OnLogRecord() { if (!m_engine.HasImage()) { AfxMessageBox(L"先に画像を開いてください"); return; } if (m_logFolder.IsEmpty()) { OnLogSetFolder(); if (m_logFolder.IsEmpty()) return; } RunResult rr = m_engine.Run(m_recipe); CTime now = CTime::GetCurrentTime(); CString ts = now.Format(L"%Y%m%d_%H%M%S"); CString imgName = L"result_" + ts + L".png"; CString imgPath = m_logFolder + L"\\" + imgName; m_engine.SaveResultImage(imgPath, m_recipe, rr); // 検査要約テキスト + 集計 CString detail; int inspectN = 0, okN = 0, ngN = 0; for (const Step& st : m_recipe.steps) { if (!IsInspect(st.kind)) continue; inspectN++; if (st.result.ok) okN++; else ngN++; if (!detail.IsEmpty()) detail += L" | "; detail += StepName(st.kind) + L": " + st.result.text + (st.result.ok ? L"[OK]" : L"[NG]"); } CString overall = rr.error ? L"ERROR" : (rr.hasInspect ? (rr.pass ? L"PASS" : L"FAIL") : L"-"); // パスからファイル名だけ取り出す。 auto fileName = [](const CString& p) -> CString { if (p.IsEmpty()) return L""; int s = p.ReverseFind(L'\\'); return (s >= 0) ? p.Mid(s + 1) : p; }; // CSV 追記(無ければヘッダ付きで新規) CString csvPath = m_logFolder + L"\\inspect_log.csv"; bool exists = (::GetFileAttributes(csvPath) != INVALID_FILE_ATTRIBUTES); CFile f; if (f.Open(csvPath, CFile::modeCreate | CFile::modeNoTruncate | CFile::modeWrite)) { f.SeekToEnd(); CString line; if (!exists) { const unsigned char bom[3] = { 0xEF,0xBB,0xBF }; f.Write(bom, 3); line = L"日時,総合判定,元画像,レシピ,ステップ数,検査数,合格数,不合格数,検査範囲数,結果画像,詳細\r\n"; } CString stamp = now.Format(L"%Y-%m-%d %H:%M:%S"); CString cols; cols.Format(L"%d,%d,%d,%d,%d", (int)m_recipe.steps.size(), inspectN, okN, ngN, (int)m_recipe.rois.size()); line += stamp + L"," + overall + L"," + CsvField(fileName(m_imagePath)) + L"," + CsvField(fileName(m_recipePath)) + L"," + cols + L"," + CsvField(imgName) + L"," + CsvField(detail) + L"\r\n"; int n = WideCharToMultiByte(CP_UTF8, 0, line, line.GetLength(), NULL, 0, NULL, NULL); CStringA a; WideCharToMultiByte(CP_UTF8, 0, line, line.GetLength(), a.GetBuffer(n), n, NULL, NULL); a.ReleaseBuffer(n); f.Write((LPCSTR)a, a.GetLength()); f.Close(); } ShowResult(rr); AfxMessageBox(L"記録しました:\n" + imgName + L"\n" + overall, MB_ICONINFORMATION); } void CMainFrame::OnLogOpenFolder() { if (m_logFolder.IsEmpty()) { AfxMessageBox(L"先に記録フォルダを指定してください"); return; } ShellExecute(GetSafeHwnd(), L"open", m_logFolder, NULL, NULL, SW_SHOWNORMAL); } HBRUSH CMainFrame::OnCtlColor(CDC* pDC, CWnd* pWnd, UINT nCtlColor) { if (pWnd->GetDlgCtrlID() == IDC_RESULT) { pDC->SetTextColor(RGB(255, 255, 255)); pDC->SetBkMode(TRANSPARENT); switch (m_resultState) { case 1: return (HBRUSH)m_brPass; case 2: return (HBRUSH)m_brFail; default: return (HBRUSH)m_brNeutral; } } return CFrameWnd::OnCtlColor(pDC, pWnd, nCtlColor); } void CMainFrame::ShowResult(const RunResult& rr) { m_resultState = rr.error ? 2 : (rr.hasInspect ? (rr.pass ? 1 : 2) : 0); m_result.SetWindowText(rr.message); m_result.Invalidate(); } void CMainFrame::RunAndRefresh(int selectAfter) { RunResult rr = m_engine.Run(m_recipe); if (selectAfter != -2) { int s = selectAfter; if (s < 0) s = 0; if (s >= (int)m_recipe.steps.size()) s = (int)m_recipe.steps.size() - 1; m_pipe.SetSel(s); ConfigureSliders(s); } m_pipe.RefreshView(); ShowResult(rr); InvalidateRect(CRect(m_bigPreviewRect.left, m_bigPreviewRect.top - 22, m_bigPreviewRect.right + 2, m_bigPreviewRect.bottom + 2), TRUE); } // --- スライダー構成 --- void CMainFrame::ConfigureSliders(int idx) { m_pCount = 0; if (idx >= 0 && idx < (int)m_recipe.steps.size()) { const Step& st = m_recipe.steps[idx]; m_pCount = (int)st.params.size(); if (m_pCount > PARAM_ROWS) m_pCount = PARAM_ROWS; CString t; t.Format(L"② 「%s」の設定", (LPCWSTR)StepName(st.kind)); m_settingsTitle.SetWindowText(t); for (int i = 0; i < m_pCount; i++) { const StepParam& p = st.params[i]; m_pLo[i] = p.lo; m_pHi[i] = p.hi; bool integral = (p.lo == (double)(long)p.lo) && (p.hi == (double)(long)p.hi) && (p.hi - p.lo <= 1000); m_pFloat[i] = !integral; m_slabel[i].SetWindowText(p.label); if (integral) { m_slider[i].SetRange((int)p.lo, (int)p.hi, TRUE); m_slider[i].SetPos((int)p.value); } else { m_slider[i].SetRange(0, 1000, TRUE); int pos = (int)((p.value - p.lo) / (p.hi - p.lo) * 1000.0 + 0.5); m_slider[i].SetPos(pos); } m_sval[i].SetWindowText(FmtNum(p.value)); m_slabel[i].ShowWindow(SW_SHOW); m_slider[i].ShowWindow(SW_SHOW); m_sval[i].ShowWindow(SW_SHOW); } } else { m_settingsTitle.SetWindowText(L"② 選んだステップの設定"); } for (int i = m_pCount; i < PARAM_ROWS; i++) { m_slabel[i].ShowWindow(SW_HIDE); m_slider[i].ShowWindow(SW_HIDE); m_sval[i].ShowWindow(SW_HIDE); } } double CMainFrame::ReadSlider(int i) const { int pos = const_cast(m_slider[i]).GetPos(); if (m_pFloat[i]) return m_pLo[i] + (m_pHi[i] - m_pLo[i]) * pos / 1000.0; return (double)pos; } void CMainFrame::OnHScroll(UINT nSBCode, UINT nPos, CScrollBar* pBar) { int sel = Sel(); if (sel < 0 || sel >= (int)m_recipe.steps.size()) { CFrameWnd::OnHScroll(nSBCode, nPos, pBar); return; } for (int i = 0; i < m_pCount; i++) { if (pBar && pBar->GetSafeHwnd() == m_slider[i].GetSafeHwnd()) { double v = ReadSlider(i); m_recipe.steps[sel].params[i].value = v; m_sval[i].SetWindowText(FmtNum(v)); RunAndRefresh(-2); // 選択維持のままライブ再計算 return; } } CFrameWnd::OnHScroll(nSBCode, nPos, pBar); } // --- コマンド --- void CMainFrame::OnBtnOpen() { CFileDialog dlg(TRUE, NULL, NULL, OFN_FILEMUSTEXIST | OFN_HIDEREADONLY, L"画像 (*.bmp;*.jpg;*.jpeg;*.png;*.tif;*.tiff)|*.bmp;*.jpg;*.jpeg;*.png;*.tif;*.tiff|すべて (*.*)|*.*||", this); if (dlg.DoModal() != IDOK) return; if (!m_engine.LoadImage(dlg.GetPathName())) { AfxMessageBox(L"画像の読み込みに失敗しました"); return; } m_imagePath = dlg.GetPathName(); RunAndRefresh(Sel() < 0 ? 0 : Sel()); } void CMainFrame::OnBtnRun() { if (!m_engine.HasImage()) { AfxMessageBox(L"先に[画像を開く]で画像を読み込んでください"); return; } RunAndRefresh(-2); } void CMainFrame::OnBtnAdd() { int sel = m_combo.GetCurSel(); if (sel < 0) return; StepKind k = (StepKind)m_combo.GetItemData(sel); int at = Sel(); int insertAt = (at < 0) ? (int)m_recipe.steps.size() : at + 1; m_recipe.steps.insert(m_recipe.steps.begin() + insertAt, MakeStep(k)); RunAndRefresh(insertAt); } void CMainFrame::OnBtnDel() { int at = Sel(); if (at < 0 || at >= (int)m_recipe.steps.size()) return; m_recipe.steps.erase(m_recipe.steps.begin() + at); RunAndRefresh(at); } void CMainFrame::OnBtnUp() { int at = Sel(); if (at <= 0) return; std::swap(m_recipe.steps[at], m_recipe.steps[at - 1]); RunAndRefresh(at - 1); } void CMainFrame::OnBtnDown() { int at = Sel(); if (at < 0 || at >= (int)m_recipe.steps.size() - 1) return; std::swap(m_recipe.steps[at], m_recipe.steps[at + 1]); RunAndRefresh(at + 1); } void CMainFrame::OnBtnTemplate() { Recipe out; if (!wizard::ChooseTemplate(this, out)) return; m_recipe = out; RunAndRefresh(0); } void CMainFrame::OnBtnWizard() { Recipe out; if (!wizard::BuildWithWizard(this, out)) return; m_recipe = out; RunAndRefresh(0); } void CMainFrame::LoadDemo() { CString path; ::GetTempPath(MAX_PATH, path.GetBuffer(MAX_PATH)); path.ReleaseBuffer(); path += L"imgtest_demo.bmp"; if (!selftest::MakeDemoImage(path)) return; if (!m_engine.LoadImage(path)) return; m_imagePath = path; auto tps = AllTemplates(); if (!tps.empty()) m_recipe = tps[0].recipe; // キズ・汚れ // 例として2つの検査範囲(左上と下中央の欠陥)を設定=複数ROIのデモ(右の欠陥は除外)。 m_recipe.rois.clear(); m_recipe.rois.push_back(CRect(40, 40, 130, 115)); m_recipe.rois.push_back(CRect(105, 140, 200, 225)); RunAndRefresh((int)m_recipe.steps.size() - 1); } void CMainFrame::OpenRecipeFile(const CString& path) { Recipe r; if (!LoadRecipe(path, r)) { AfxMessageBox(L"レシピの読み込みに失敗しました:\n" + path); return; } m_recipe = r; m_recipePath = path; AddRecent(path); RunAndRefresh(0); } void CMainFrame::OnRecipeOpen() { CFileDialog dlg(TRUE, L"itr", NULL, OFN_FILEMUSTEXIST | OFN_HIDEREADONLY, L"検査レシピ (*.itr)|*.itr|すべて (*.*)|*.*||", this); if (dlg.DoModal() != IDOK) return; OpenRecipeFile(dlg.GetPathName()); } void CMainFrame::OnRecipeSave() { CFileDialog dlg(FALSE, L"itr", m_recipePath.IsEmpty() ? L"recipe.itr" : (LPCWSTR)m_recipePath, OFN_OVERWRITEPROMPT | OFN_HIDEREADONLY, L"検査レシピ (*.itr)|*.itr|すべて (*.*)|*.*||", this); if (dlg.DoModal() != IDOK) return; if (!SaveRecipe(dlg.GetPathName(), m_recipe)) { AfxMessageBox(L"レシピの保存に失敗しました"); return; } m_recipePath = dlg.GetPathName(); AddRecent(m_recipePath); AfxMessageBox(L"レシピを保存しました:\n" + m_recipePath, MB_ICONINFORMATION); } // --- 最近使ったレシピ(MRU) --- void CMainFrame::LoadRecent() { m_recent.clear(); CWinApp* app = AfxGetApp(); for (int i = 0; i < RECENT_MAX; i++) { CString key; key.Format(L"File%d", i + 1); CString v = app->GetProfileString(L"Recent", key, L""); if (!v.IsEmpty()) m_recent.push_back(v); } } void CMainFrame::SaveRecent() { CWinApp* app = AfxGetApp(); for (int i = 0; i < RECENT_MAX; i++) { CString key; key.Format(L"File%d", i + 1); app->WriteProfileString(L"Recent", key, i < (int)m_recent.size() ? m_recent[i] : L""); } } void CMainFrame::AddRecent(const CString& path) { // 既存重複を除去して先頭へ、最大 RECENT_MAX 件。 for (int i = (int)m_recent.size() - 1; i >= 0; i--) if (m_recent[i].CompareNoCase(path) == 0) m_recent.erase(m_recent.begin() + i); m_recent.insert(m_recent.begin(), path); if ((int)m_recent.size() > RECENT_MAX) m_recent.resize(RECENT_MAX); SaveRecent(); UpdateRecentMenu(); } void CMainFrame::UpdateRecentMenu() { CMenu* rm = CMenu::FromHandle(m_hRecentMenu); if (!rm) return; while (rm->GetMenuItemCount() > 0) rm->DeleteMenu(0, MF_BYPOSITION); if (m_recent.empty()) rm->AppendMenu(MF_STRING | MF_GRAYED, 0, L"(なし)"); else for (int i = 0; i < (int)m_recent.size(); i++) { CString disp; disp.Format(L"&%d %s", i + 1, (LPCWSTR)m_recent[i]); rm->AppendMenu(MF_STRING, ID_RECENT_BASE + i, disp); } DrawMenuBar(); } void CMainFrame::OnRecentFile(UINT id) { int idx = (int)id - ID_RECENT_BASE; if (idx < 0 || idx >= (int)m_recent.size()) return; CString path = m_recent[idx]; if (::GetFileAttributes(path) == INVALID_FILE_ATTRIBUTES) { if (AfxMessageBox(L"ファイルが見つかりません。一覧から削除しますか?\n" + path, MB_YESNO | MB_ICONWARNING) == IDYES) { m_recent.erase(m_recent.begin() + idx); SaveRecent(); UpdateRecentMenu(); } return; } OpenRecipeFile(path); } void CMainFrame::OnAppExit() { PostMessage(WM_CLOSE); } LRESULT CMainFrame::OnPipeMoveStep(WPARAM w, LPARAM l) { int from = (int)w, slot = (int)l; int n = (int)m_recipe.steps.size(); if (from < 0 || from >= n) return 0; if (slot < 0) slot = 0; if (slot > n) slot = n; Step s = m_recipe.steps[from]; m_recipe.steps.erase(m_recipe.steps.begin() + from); int dst = (slot > from) ? slot - 1 : slot; if (dst < 0) dst = 0; if (dst > (int)m_recipe.steps.size()) dst = (int)m_recipe.steps.size(); m_recipe.steps.insert(m_recipe.steps.begin() + dst, s); RunAndRefresh(dst); return 0; } LRESULT CMainFrame::OnPipeSelChanged(WPARAM w, LPARAM) { int idx = (int)w; ConfigureSliders(idx); InvalidateRect(CRect(m_bigPreviewRect.left, m_bigPreviewRect.top - 22, m_bigPreviewRect.right + 2, m_bigPreviewRect.bottom + 2), TRUE); return 0; }