Files
yoshikazuandClaude Opus 4.8 e4eca6b303 Initial commit: ノーコード画像検査アプリ (MVP + 追加機能一式)
検査レシピ=処理/検査ステップの縦リスト(配線なし)。yfoxlib(yfoximg/yfoxbase)使用。

- ビジュアル・パイプライン(カード+適用後サムネイル+緑/赤 合否)
- テンプレート/対話式ウィザードでレシピ生成(CTaskDialog)
- パラメータのスライダー・ライブ調整
- レシピ保存/読込(*.itr) + 最近使ったファイル(MRU)
- 検査範囲(ROI)を複数マウス指定(ドラッグ追加/右クリック削除)
- カードのドラッグ並べ替え
- 判定結果画像の保存 + 検査ログCSV(項目拡張)
- 処理6種+検査3種(明るさ/白画素率/塊の数)
- /selftest(実行+レシピ往復+結果画像+複数ROI塊カウント検証)、/demo

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 09:26:10 +09:00

852 lines
27 KiB
C++

//
// 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 <shlobj.h> // 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<CPipelineView&>(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<CSliderCtrl&>(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;
}