// // 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; }