検査レシピ=処理/検査ステップの縦リスト(配線なし)。yfoxlib(yfoximg/yfoxbase)使用。 - ビジュアル・パイプライン(カード+適用後サムネイル+緑/赤 合否) - テンプレート/対話式ウィザードでレシピ生成(CTaskDialog) - パラメータのスライダー・ライブ調整 - レシピ保存/読込(*.itr) + 最近使ったファイル(MRU) - 検査範囲(ROI)を複数マウス指定(ドラッグ追加/右クリック削除) - カードのドラッグ並べ替え - 判定結果画像の保存 + 検査ログCSV(項目拡張) - 処理6種+検査3種(明るさ/白画素率/塊の数) - /selftest(実行+レシピ往復+結果画像+複数ROI塊カウント検証)、/demo Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
373 lines
10 KiB
C++
373 lines
10 KiB
C++
//
|
|
// PipelineView.cpp - ビジュアル・パイプライン(カード + サムネイル + 合否)。
|
|
//
|
|
#include "stdafx.h"
|
|
#include "PipelineView.h"
|
|
#include "core/StepCatalog.h"
|
|
|
|
using namespace core;
|
|
|
|
BEGIN_MESSAGE_MAP(CPipelineView, CWnd)
|
|
ON_WM_PAINT()
|
|
ON_WM_ERASEBKGND()
|
|
ON_WM_LBUTTONDOWN()
|
|
ON_WM_MOUSEMOVE()
|
|
ON_WM_LBUTTONUP()
|
|
ON_WM_VSCROLL()
|
|
ON_WM_MOUSEWHEEL()
|
|
ON_WM_SIZE()
|
|
END_MESSAGE_MAP()
|
|
|
|
CPipelineView::CPipelineView() {}
|
|
|
|
BOOL CPipelineView::Create(CWnd* parent, UINT id)
|
|
{
|
|
// 独自ウィンドウクラス(背景ブラシ・矢印カーソル)。
|
|
LPCTSTR cls = AfxRegisterWndClass(CS_HREDRAW | CS_VREDRAW,
|
|
::LoadCursor(NULL, IDC_ARROW), (HBRUSH)(COLOR_WINDOW + 1), NULL);
|
|
if (!CWnd::Create(cls, L"", WS_CHILD | WS_VISIBLE | WS_VSCROLL | WS_BORDER,
|
|
CRect(0, 0, 10, 10), parent, id))
|
|
return FALSE;
|
|
|
|
m_fTitle.CreatePointFont(115, L"Yu Gothic UI");
|
|
m_fHint.CreatePointFont(90, L"Yu Gothic UI");
|
|
m_fBadge.CreatePointFont(130, L"Yu Gothic UI");
|
|
m_fNum.CreatePointFont(120, L"Yu Gothic UI");
|
|
return TRUE;
|
|
}
|
|
|
|
void CPipelineView::SetData(Recipe* recipe, InspectEngine* engine)
|
|
{
|
|
m_recipe = recipe;
|
|
m_engine = engine;
|
|
RefreshView();
|
|
}
|
|
|
|
int CPipelineView::ContentHeight() const
|
|
{
|
|
int n = m_recipe ? (int)m_recipe->steps.size() : 0;
|
|
return 8 + n * (CardHeight() + 8);
|
|
}
|
|
|
|
CRect CPipelineView::CardRect(int i) const
|
|
{
|
|
CRect cr; GetClientRect(&cr);
|
|
int x = 8, w = cr.Width() - 16;
|
|
int y = 8 + i * (CardHeight() + 8) - m_scrollY;
|
|
return CRect(x, y, x + w, y + CardHeight());
|
|
}
|
|
|
|
void CPipelineView::UpdateScroll()
|
|
{
|
|
CRect cr; GetClientRect(&cr);
|
|
int content = ContentHeight();
|
|
SCROLLINFO si = { sizeof(si) };
|
|
si.fMask = SIF_RANGE | SIF_PAGE | SIF_POS;
|
|
si.nMin = 0;
|
|
si.nMax = content > 0 ? content - 1 : 0;
|
|
si.nPage = cr.Height();
|
|
int maxPos = content - cr.Height();
|
|
if (maxPos < 0) maxPos = 0;
|
|
if (m_scrollY > maxPos) m_scrollY = maxPos;
|
|
if (m_scrollY < 0) m_scrollY = 0;
|
|
si.nPos = m_scrollY;
|
|
SetScrollInfo(SB_VERT, &si, TRUE);
|
|
}
|
|
|
|
void CPipelineView::RefreshView()
|
|
{
|
|
if (m_sel >= 0 && m_recipe && m_sel >= (int)m_recipe->steps.size())
|
|
m_sel = (int)m_recipe->steps.size() - 1;
|
|
UpdateScroll();
|
|
Invalidate();
|
|
}
|
|
|
|
void CPipelineView::SetSel(int idx)
|
|
{
|
|
m_sel = idx;
|
|
// 選択カードを見えるようにスクロール。
|
|
if (m_recipe && idx >= 0 && idx < (int)m_recipe->steps.size())
|
|
{
|
|
CRect cr; GetClientRect(&cr);
|
|
int top = 8 + idx * (CardHeight() + 8);
|
|
int bot = top + CardHeight();
|
|
if (top - m_scrollY < 0) m_scrollY = top;
|
|
else if (bot - m_scrollY > cr.Height()) m_scrollY = bot - cr.Height();
|
|
UpdateScroll();
|
|
}
|
|
Invalidate();
|
|
}
|
|
|
|
BOOL CPipelineView::OnEraseBkgnd(CDC*) { return TRUE; } // ちらつき防止(OnPaintで塗る)
|
|
|
|
void CPipelineView::OnSize(UINT nType, int cx, int cy)
|
|
{
|
|
CWnd::OnSize(nType, cx, cy);
|
|
UpdateScroll();
|
|
Invalidate();
|
|
}
|
|
|
|
void CPipelineView::OnPaint()
|
|
{
|
|
CPaintDC dc(this);
|
|
CRect cr; GetClientRect(&cr);
|
|
|
|
// ダブルバッファ。
|
|
CDC mem; mem.CreateCompatibleDC(&dc);
|
|
CBitmap bmp; bmp.CreateCompatibleBitmap(&dc, cr.Width(), cr.Height());
|
|
CBitmap* pOld = mem.SelectObject(&bmp);
|
|
|
|
mem.FillSolidRect(cr, RGB(244, 246, 248));
|
|
|
|
if (!m_recipe || m_recipe->steps.empty())
|
|
{
|
|
mem.SetBkMode(TRANSPARENT);
|
|
mem.SetTextColor(RGB(140, 140, 140));
|
|
CFont* of = mem.SelectObject(&m_fHint);
|
|
mem.DrawText(L"ステップがありません。\n上の[テンプレート]や[ウィザード]、右の[追加]で作成できます。",
|
|
cr, DT_CENTER | DT_VCENTER | DT_WORDBREAK);
|
|
mem.SelectObject(of);
|
|
}
|
|
else
|
|
{
|
|
for (int i = 0; i < (int)m_recipe->steps.size(); i++)
|
|
{
|
|
CRect rc = CardRect(i);
|
|
if (rc.bottom < 0 || rc.top > cr.Height()) continue; // 画面外
|
|
DrawCard(&mem, i, rc);
|
|
}
|
|
|
|
// ドラッグ中: 挿入位置インジケータ(青い太線)
|
|
if (m_dragging && m_dragFrom >= 0)
|
|
{
|
|
int slot = InsertSlotAt(m_dragY);
|
|
int unit = CardHeight() + 8;
|
|
int y = 8 + slot * unit - 4 - m_scrollY;
|
|
mem.FillSolidRect(CRect(6, y - 2, cr.Width() - 6, y + 2), RGB(60, 130, 220));
|
|
// つまんでいるカードの番号を近くに表示
|
|
mem.SetBkMode(TRANSPARENT);
|
|
mem.SelectObject(&m_fHint);
|
|
mem.SetTextColor(RGB(60, 130, 220));
|
|
CString tip; tip.Format(L"ステップ%d をここへ移動", m_dragFrom + 1);
|
|
mem.DrawText(tip, CRect(12, y + 4, cr.Width() - 6, y + 24), DT_LEFT | DT_SINGLELINE);
|
|
}
|
|
}
|
|
|
|
dc.BitBlt(0, 0, cr.Width(), cr.Height(), &mem, 0, 0, SRCCOPY);
|
|
mem.SelectObject(pOld);
|
|
}
|
|
|
|
void CPipelineView::DrawCard(CDC* dc, int i, const CRect& rc)
|
|
{
|
|
const Step& st = m_recipe->steps[i];
|
|
bool inspect = IsInspect(st.kind);
|
|
bool sel = (i == m_sel);
|
|
|
|
// 背景 + 枠
|
|
COLORREF bg = sel ? RGB(225, 238, 252) : RGB(255, 255, 255);
|
|
dc->FillSolidRect(rc, bg);
|
|
CPen penBorder(PS_SOLID, sel ? 2 : 1, sel ? RGB(60, 130, 220) : RGB(210, 214, 218));
|
|
CPen* op = dc->SelectObject(&penBorder);
|
|
CBrush* ob = (CBrush*)dc->SelectStockObject(NULL_BRUSH);
|
|
dc->Rectangle(rc);
|
|
dc->SelectObject(ob);
|
|
dc->SelectObject(op);
|
|
|
|
// 左の色ストライプ(処理=青 / 検査=橙)
|
|
CRect stripe(rc.left, rc.top, rc.left + 6, rc.bottom);
|
|
dc->FillSolidRect(stripe, inspect ? RGB(240, 150, 40) : RGB(70, 130, 190));
|
|
|
|
dc->SetBkMode(TRANSPARENT);
|
|
|
|
// 番号バッジ
|
|
int cx = rc.left + 32, cy = rc.top + 26, r = 15;
|
|
CBrush numBr(inspect ? RGB(240, 150, 40) : RGB(70, 130, 190));
|
|
CBrush* obn = dc->SelectObject(&numBr);
|
|
CPen* opn = (CPen*)dc->SelectStockObject(NULL_PEN);
|
|
dc->Ellipse(cx - r, cy - r, cx + r, cy + r);
|
|
dc->SelectObject(opn);
|
|
dc->SelectObject(obn);
|
|
CString no; no.Format(L"%d", i + 1);
|
|
CFont* of = dc->SelectObject(&m_fNum);
|
|
dc->SetTextColor(RGB(255, 255, 255));
|
|
CRect nr(cx - r, cy - r, cx + r, cy + r);
|
|
dc->DrawText(no, nr, DT_CENTER | DT_VCENTER | DT_SINGLELINE);
|
|
|
|
// サムネイル域(右)
|
|
int thumbW = 132, thumbH = CardHeight() - 20;
|
|
CRect thumb(rc.right - thumbW - 10, rc.top + 10, rc.right - 10, rc.top + 10 + thumbH);
|
|
dc->FillSolidRect(thumb, RGB(30, 30, 30));
|
|
if (m_engine && m_engine->HasShot(i))
|
|
{
|
|
CRect inner = thumb; inner.DeflateRect(2, 2);
|
|
CRect fit = m_engine->DrawShot(dc, inner, i);
|
|
// ROI(複数)を黄枠でオーバーレイ
|
|
if (m_recipe && !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, 1, RGB(255, 210, 0));
|
|
CPen* oyp = dc->SelectObject(&yp);
|
|
dc->SelectStockObject(NULL_BRUSH);
|
|
for (const CRect& roi : m_recipe->rois)
|
|
{
|
|
CRect rr(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(rr);
|
|
}
|
|
dc->SelectObject(oyp);
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
dc->SelectObject(&m_fHint);
|
|
dc->SetTextColor(RGB(160, 160, 160));
|
|
dc->DrawText(L"実行前", thumb, DT_CENTER | DT_VCENTER | DT_SINGLELINE);
|
|
}
|
|
CPen tb(PS_SOLID, 1, RGB(120, 120, 120));
|
|
CPen* opt = dc->SelectObject(&tb);
|
|
dc->SelectStockObject(NULL_BRUSH);
|
|
dc->Rectangle(thumb);
|
|
dc->SelectObject(opt);
|
|
|
|
// テキスト域
|
|
// ※ CDiBitmap::Draw(サムネイル)が DC の BkMode を OPAQUE/暗色に戻すため、
|
|
// 文字描画の前に透過へ再設定する(でないと暗地に明字で反転して見える)。
|
|
dc->SetBkMode(TRANSPARENT);
|
|
int tx = rc.left + 56;
|
|
int tw = thumb.left - tx - 10;
|
|
// タイトル
|
|
dc->SelectObject(&m_fTitle);
|
|
dc->SetTextColor(RGB(30, 40, 50));
|
|
CRect tr(tx, rc.top + 10, tx + tw, rc.top + 34);
|
|
dc->DrawText(StepName(st.kind), tr, DT_LEFT | DT_VCENTER | DT_SINGLELINE | DT_END_ELLIPSIS);
|
|
// 説明
|
|
dc->SelectObject(&m_fHint);
|
|
dc->SetTextColor(RGB(110, 120, 130));
|
|
CRect hr(tx, rc.top + 36, tx + tw, rc.top + 74);
|
|
dc->DrawText(StepHint(st.kind), hr, DT_LEFT | DT_WORDBREAK | DT_END_ELLIPSIS);
|
|
|
|
// 検査ステップは合否バッジ + 測定値
|
|
if (inspect)
|
|
{
|
|
bool ran = st.result.ran;
|
|
bool ok = st.result.ok;
|
|
CString badge = !ran ? L"未実行" : (ok ? L"✓ 合格" : L"✕ 不合格");
|
|
COLORREF bc = !ran ? RGB(150, 150, 150) : (ok ? RGB(40, 170, 80) : RGB(220, 60, 60));
|
|
CRect br(tx, rc.bottom - 30, tx + 96, rc.bottom - 6);
|
|
dc->FillSolidRect(br, bc);
|
|
dc->SelectObject(&m_fBadge);
|
|
dc->SetTextColor(RGB(255, 255, 255));
|
|
dc->DrawText(badge, br, DT_CENTER | DT_VCENTER | DT_SINGLELINE);
|
|
// 測定値テキスト
|
|
if (ran && !st.result.text.IsEmpty())
|
|
{
|
|
dc->SelectObject(&m_fHint);
|
|
dc->SetTextColor(RGB(70, 80, 90));
|
|
CRect mr(tx + 104, rc.bottom - 30, thumb.left - 10, rc.bottom - 6);
|
|
dc->DrawText(st.result.text, mr, DT_LEFT | DT_VCENTER | DT_SINGLELINE | DT_END_ELLIPSIS);
|
|
}
|
|
}
|
|
|
|
dc->SelectObject(of);
|
|
}
|
|
|
|
// クライアント Y から挿入スロット(0..n)を求める。
|
|
int CPipelineView::InsertSlotAt(int y) const
|
|
{
|
|
int n = m_recipe ? (int)m_recipe->steps.size() : 0;
|
|
int unit = CardHeight() + 8;
|
|
int slot = (int)((double)(y + m_scrollY - 8) / unit + 0.5);
|
|
if (slot < 0) slot = 0;
|
|
if (slot > n) slot = n;
|
|
return slot;
|
|
}
|
|
|
|
void CPipelineView::OnLButtonDown(UINT, CPoint pt)
|
|
{
|
|
if (!m_recipe) return;
|
|
for (int i = 0; i < (int)m_recipe->steps.size(); i++)
|
|
{
|
|
if (CardRect(i).PtInRect(pt))
|
|
{
|
|
m_sel = i;
|
|
Invalidate();
|
|
GetParent()->SendMessage(WM_PV_SELCHANGED, (WPARAM)i, 0);
|
|
// ドラッグ開始候補
|
|
m_dragFrom = i;
|
|
m_dragging = false;
|
|
m_dragStart = pt;
|
|
SetCapture();
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
void CPipelineView::OnMouseMove(UINT, CPoint pt)
|
|
{
|
|
if (m_dragFrom < 0) return;
|
|
if (!m_dragging)
|
|
{
|
|
if (abs(pt.y - m_dragStart.y) + abs(pt.x - m_dragStart.x) > 5)
|
|
{
|
|
m_dragging = true;
|
|
SetCursor(::LoadCursor(NULL, IDC_SIZENS));
|
|
}
|
|
}
|
|
if (m_dragging)
|
|
{
|
|
m_dragY = pt.y;
|
|
Invalidate();
|
|
}
|
|
}
|
|
|
|
void CPipelineView::OnLButtonUp(UINT, CPoint pt)
|
|
{
|
|
if (m_dragFrom < 0) return;
|
|
int from = m_dragFrom;
|
|
bool wasDragging = m_dragging;
|
|
m_dragFrom = -1;
|
|
m_dragging = false;
|
|
if (GetCapture() == this) ReleaseCapture();
|
|
|
|
if (wasDragging)
|
|
{
|
|
int slot = InsertSlotAt(pt.y);
|
|
// 実質移動が無いケース(同じ位置)は無視
|
|
if (slot != from && slot != from + 1)
|
|
GetParent()->SendMessage(WM_PV_MOVESTEP, (WPARAM)from, (LPARAM)slot);
|
|
else
|
|
Invalidate();
|
|
}
|
|
}
|
|
|
|
void CPipelineView::OnVScroll(UINT nSBCode, UINT nPos, CScrollBar*)
|
|
{
|
|
CRect cr; GetClientRect(&cr);
|
|
int line = 24, page = cr.Height();
|
|
int y = m_scrollY;
|
|
switch (nSBCode)
|
|
{
|
|
case SB_LINEUP: y -= line; break;
|
|
case SB_LINEDOWN: y += line; break;
|
|
case SB_PAGEUP: y -= page; break;
|
|
case SB_PAGEDOWN: y += page; break;
|
|
case SB_THUMBTRACK:
|
|
case SB_THUMBPOSITION: y = nPos; break;
|
|
}
|
|
m_scrollY = y;
|
|
UpdateScroll();
|
|
Invalidate();
|
|
}
|
|
|
|
BOOL CPipelineView::OnMouseWheel(UINT, short zDelta, CPoint)
|
|
{
|
|
m_scrollY -= (zDelta / WHEEL_DELTA) * 48;
|
|
UpdateScroll();
|
|
Invalidate();
|
|
return TRUE;
|
|
}
|