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>
This commit is contained in:
+851
@@ -0,0 +1,851 @@
|
||||
//
|
||||
// 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;
|
||||
}
|
||||
Reference in New Issue
Block a user