// Canvas.cpp #include "pch.h" #include "Canvas.h" #include "LabelLayout.h" #include "RenderScene.h" #include "TextItem.h" #include "ShapeItem.h" #include "ImageItem.h" #include "TextPropsDlg.h" BEGIN_MESSAGE_MAP(CCanvas, CWnd) ON_WM_PAINT() ON_WM_ERASEBKGND() ON_WM_LBUTTONDOWN() ON_WM_LBUTTONUP() ON_WM_MOUSEMOVE() ON_WM_LBUTTONDBLCLK() ON_WM_SETCURSOR() ON_WM_KEYDOWN() ON_WM_GETDLGCODE() END_MESSAGE_MAP() // コンストラクタ。ズーム3倍・スナップ有効・グリッド1mm を既定値に初期化。 CCanvas::CCanvas() : m_zoom(3.0) , m_pLayout(nullptr) , m_pSelected(nullptr) , m_pClipboard(nullptr) , m_dirty(FALSE) , m_snap(TRUE) , m_gridHu(100) // 1mm , m_drag(Drag::None) , m_dragHandle(-1) , m_dragStartHu(0, 0) , m_origRect(0, 0, 0, 0) { } // デストラクタ。コピー用クリップボードのアイテムを解放する。 CCanvas::~CCanvas() { delete m_pClipboard; } // 値(1/100mm)を最も近いグリッド線へ丸める。スナップ移動・リサイズで使用。 int CCanvas::SnapHu(int hu) const { if (m_gridHu <= 0) return hu; int half = m_gridHu / 2; return ((hu + (hu >= 0 ? half : -half)) / m_gridHu) * m_gridHu; } // 編集キャンバス(子ウィンドウ)を生成する。ダブルクリック受信・背景灰色。 BOOL CCanvas::Create(CWnd* pParent, const CRect& rc, UINT id) { LPCTSTR cls = AfxRegisterWndClass( CS_DBLCLKS | CS_HREDRAW | CS_VREDRAW, ::LoadCursor(nullptr, IDC_ARROW), (HBRUSH)::GetStockObject(LTGRAY_BRUSH)); return CWnd::Create(cls, _T(""), WS_CHILD | WS_VISIBLE | WS_BORDER | WS_TABSTOP, rc, pParent, id); } // 指定 DPI と現在のズーム・余白から座標変換器(CoordMap)を作る。 CoordMap CCanvas::MakeMap(int dpiX, int dpiY) const { return CoordMap(dpiX, dpiY, m_zoom, kMargin, kMargin); } // 画面 DC の実 DPI を使って座標変換器を作る(上のオーバーロードの簡易版)。 CoordMap CCanvas::MakeMap() const { CClientDC dc(const_cast(this)); return MakeMap(dc.GetDeviceCaps(LOGPIXELSX), dc.GetDeviceCaps(LOGPIXELSY)); } // クライアント座標(ピクセル)を内容座標(1/100mm)へ変換する。 CPoint CCanvas::HuFromClient(CPoint pt) { return MakeMap().DevToMm(pt); // スクロール無し: クライアント=内容座標 } // ---- 描画 ---------------------------------------------------------------- BOOL CCanvas::OnEraseBkgnd(CDC* /*pDC*/) { return TRUE; // OnPaint で全面描画(ちらつき防止) } // 再描画。ちらつき防止のためメモリ DC(ダブルバッファ)に一旦描いてから転送する。 // 用紙外を灰色で塗り、Render::DrawLayout で用紙・原紙・グリッド・アイテムを描く。 void CCanvas::OnPaint() { CPaintDC dc(this); CRect client; GetClientRect(client); // ダブルバッファ CDC mem; mem.CreateCompatibleDC(&dc); CBitmap bmp; bmp.CreateCompatibleBitmap(&dc, client.Width(), client.Height()); CBitmap* pOldBmp = mem.SelectObject(&bmp); // 用紙外(灰色) mem.FillSolidRect(client, RGB(200, 200, 200)); if (m_pLayout) { CoordMap map = MakeMap(mem.GetDeviceCaps(LOGPIXELSX), mem.GetDeviceCaps(LOGPIXELSY)); Render::DrawLayout(mem, *m_pLayout, map, /*screen=*/true, m_pSelected, m_snap ? m_gridHu : 0); } dc.BitBlt(0, 0, client.Width(), client.Height(), &mem, 0, 0, SRCCOPY); mem.SelectObject(pOldBmp); } // ---- ズーム -------------------------------------------------------------- // 表示倍率を設定する(0.25〜30倍にクランプ)。再描画も行う。 void CCanvas::SetZoom(double z) { if (z < 0.25) z = 0.25; if (z > 30.0) z = 30.0; m_zoom = z; Invalidate(); } // 用紙全体がキャンバスに収まる倍率へ自動調整する。 void CCanvas::FitZoom() { if (!m_pLayout) return; CRect client; GetClientRect(client); CClientDC dc(this); int dpiX = dc.GetDeviceCaps(LOGPIXELSX); int dpiY = dc.GetDeviceCaps(LOGPIXELSY); // zoom=1 のときの用紙デバイス幅/高さ double pw = (double)m_pLayout->m_paperWidthHu * dpiX / 2540.0; double ph = (double)m_pLayout->m_paperHeightHu * dpiY / 2540.0; double zx = (client.Width() - kMargin * 2) / max(1.0, pw); double zy = (client.Height() - kMargin * 2) / max(1.0, ph); SetZoom(min(zx, zy)); } // ---- マウス -------------------------------------------------------------- // 左ボタン押下。選択アイテムのハンドル上ならリサイズ開始、 // アイテム本体上なら選択+移動開始、何も無ければ選択解除。 void CCanvas::OnLButtonDown(UINT nFlags, CPoint point) { SetFocus(); if (!m_pLayout) return; CPoint huPt = HuFromClient(point); if (m_pSelected) { CoordMap map = MakeMap(); CRect devRect = map.MmToDev(m_pSelected->Rect()); int handle = CPrintItem::HitHandle(devRect, point); if (handle >= 0) { m_drag = Drag::Resize; m_dragHandle = handle; m_dragStartHu = huPt; m_origRect = m_pSelected->Rect(); SetCapture(); return; } } CPrintItem* hit = m_pLayout->HitTest(huPt); if (hit != m_pSelected) { m_pSelected = hit; Invalidate(); } if (hit) { m_drag = Drag::Move; m_dragStartHu = huPt; m_origRect = hit->Rect(); SetCapture(); } else { m_drag = Drag::None; } CWnd::OnLButtonDown(nFlags, point); } // マウス移動。ドラッグ中なら選択アイテムを移動/リサイズする。 // スナップ有効時は結果の座標をグリッドへ吸着。最小サイズも保証。 void CCanvas::OnMouseMove(UINT nFlags, CPoint point) { if (m_drag != Drag::None && m_pSelected) { CPoint huPt = HuFromClient(point); int dx = huPt.x - m_dragStartHu.x; int dy = huPt.y - m_dragStartHu.y; CRect r = m_origRect; if (m_drag == Drag::Move) { r.OffsetRect(dx, dy); if (m_snap) { // 左上をグリッドへスナップ(サイズは保つ) r.OffsetRect(SnapHu(r.left) - r.left, SnapHu(r.top) - r.top); } } else { switch (m_dragHandle) { case 0: r.left += dx; r.top += dy; break; case 1: r.top += dy; break; case 2: r.right += dx; r.top += dy; break; case 3: r.right += dx; break; case 4: r.right += dx; r.bottom += dy; break; case 5: r.bottom += dy; break; case 6: r.left += dx; r.bottom += dy; break; case 7: r.left += dx; break; } if (m_snap) { r.left = SnapHu(r.left); r.top = SnapHu(r.top); r.right = SnapHu(r.right); r.bottom = SnapHu(r.bottom); } if (r.Width() < 50) r.right = r.left + 50; if (r.Height() < 50) r.bottom = r.top + 50; } m_pSelected->SetRect(r); m_dirty = TRUE; Invalidate(); } CWnd::OnMouseMove(nFlags, point); } // 左ボタン離し。ドラッグ(移動/リサイズ)を終了しキャプチャを解放する。 void CCanvas::OnLButtonUp(UINT nFlags, CPoint point) { if (m_drag != Drag::None) { m_drag = Drag::None; m_dragHandle = -1; if (GetCapture() == this) ReleaseCapture(); } CWnd::OnLButtonUp(nFlags, point); } // ダブルクリック。アイテム上ならそれを選択してプロパティ編集を開く。 void CCanvas::OnLButtonDblClk(UINT nFlags, CPoint point) { if (m_pLayout) { CPoint huPt = HuFromClient(point); CPrintItem* hit = m_pLayout->HitTest(huPt); if (hit) { m_pSelected = hit; EditSelectedProperties(); Invalidate(); return; } } CWnd::OnLButtonDblClk(nFlags, point); } // マウスカーソル形状の制御。選択アイテムのハンドル上では方向に応じた // リサイズカーソル(↔ ↕ ⤡ ⤢)に切り替える。 BOOL CCanvas::OnSetCursor(CWnd* pWnd, UINT nHitTest, UINT message) { if (nHitTest == HTCLIENT && m_pSelected) { CPoint pt; GetCursorPos(&pt); ScreenToClient(&pt); CoordMap map = MakeMap(); CRect devRect = map.MmToDev(m_pSelected->Rect()); int h = CPrintItem::HitHandle(devRect, pt); if (h >= 0) { LPCTSTR cur = IDC_SIZEALL; switch (h) { case 0: case 4: cur = IDC_SIZENWSE; break; case 2: case 6: cur = IDC_SIZENESW; break; case 1: case 5: cur = IDC_SIZENS; break; case 3: case 7: cur = IDC_SIZEWE; break; } ::SetCursor(::LoadCursor(nullptr, cur)); return TRUE; } } return CWnd::OnSetCursor(pWnd, nHitTest, message); } // キー操作。Delete=削除、Ctrl+C/V/D=コピー/貼付/複製、Ctrl+]/[ =前面/背面、 // 矢印=微調整(スナップ時はグリッド幅・通常0.1mm、Shiftで10倍)。 void CCanvas::OnKeyDown(UINT nChar, UINT nRepCnt, UINT nFlags) { BOOL ctrl = (::GetKeyState(VK_CONTROL) & 0x8000) != 0; BOOL shift = (::GetKeyState(VK_SHIFT) & 0x8000) != 0; switch (nChar) { case VK_DELETE: DeleteSelected(); return; case 'C': if (ctrl) { CopySelected(); return; } break; case 'V': if (ctrl) { Paste(); return; } break; case 'D': if (ctrl) { DuplicateSelected(); return; } break; case VK_OEM_6: if (ctrl) { BringSelectedForward(); return; } break; // ']' case VK_OEM_4: if (ctrl) { SendSelectedBackward(); return; } break; // '[' case VK_LEFT: case VK_RIGHT: case VK_UP: case VK_DOWN: if (m_pSelected) { int step = m_snap ? m_gridHu : 10; // 10hu = 0.1mm if (shift) step *= 10; int dx = 0, dy = 0; if (nChar == VK_LEFT) dx = -step; else if (nChar == VK_RIGHT) dx = step; else if (nChar == VK_UP) dy = -step; else dy = step; CRect r = m_pSelected->Rect(); r.OffsetRect(dx, dy); m_pSelected->SetRect(r); m_dirty = TRUE; Invalidate(); return; } break; } CWnd::OnKeyDown(nChar, nRepCnt, nFlags); } // このコントロールが欲しいキー種別を返す。矢印キーがダイアログのフォーカス移動に // 奪われないよう DLGC_WANTARROWS を返し、矢印での微調整を確実に受け取る。 UINT CCanvas::OnGetDlgCode() { return DLGC_WANTARROWS | DLGC_WANTCHARS; } // ---- 挿入・編集 ---------------------------------------------------------- // アイテムをレイアウトへ追加し、それを選択状態にして再描画する(挿入系の共通処理)。 void CCanvas::AddAndSelect(CPrintItem* p) { m_pLayout->AddItem(p); m_pSelected = p; m_dirty = TRUE; Invalidate(); } // テキストアイテムを追加。用紙中央に置き、プロパティ入力ダイアログで // 内容を確定したら追加する(キャンセル時は破棄)。 void CCanvas::InsertText() { if (!m_pLayout) return; CTextItem* p = new CTextItem(); int w = 3000, h = 800; int x = (m_pLayout->m_paperWidthHu - w) / 2; int y = (m_pLayout->m_paperHeightHu - h) / 2; p->SetRect(CRect(x, y, x + w, y + h)); CTextPropsDlg dlg(this); dlg.m_text = p->m_text; dlg.m_fontName = p->m_fontName; dlg.m_pointSize = p->m_pointSize; dlg.m_align = p->m_align; dlg.m_bold = p->m_bold; if (dlg.DoModal() == IDOK) { p->m_text = dlg.m_text; p->m_fontName = dlg.m_fontName; p->m_pointSize = dlg.m_pointSize; p->m_align = dlg.m_align; p->m_bold = dlg.m_bold; AddAndSelect(p); } else { delete p; } } // 矩形枠アイテムを追加(用紙の約6割×4割サイズで中央配置)。 void CCanvas::InsertRect() { if (!m_pLayout) return; CShapeItem* p = new CShapeItem(); p->m_shape = 0; int w = m_pLayout->m_paperWidthHu * 6 / 10; int h = m_pLayout->m_paperHeightHu * 4 / 10; int x = (m_pLayout->m_paperWidthHu - w) / 2; int y = (m_pLayout->m_paperHeightHu - h) / 2; p->SetRect(CRect(x, y, x + w, y + h)); AddAndSelect(p); } // 直線アイテムを追加(用紙中央の水平線)。 void CCanvas::InsertLine() { if (!m_pLayout) return; CShapeItem* p = new CShapeItem(); p->m_shape = 1; int y = m_pLayout->m_paperHeightHu / 2; p->SetRect(CRect(m_pLayout->m_paperWidthHu / 10, y, m_pLayout->m_paperWidthHu * 9 / 10, y)); AddAndSelect(p); } // 画像アイテムを追加。ファイル選択→読み込み成功なら用紙中央に半分サイズで配置。 void CCanvas::InsertImage() { if (!m_pLayout) return; CFileDialog dlg(TRUE, nullptr, nullptr, OFN_FILEMUSTEXIST | OFN_HIDEREADONLY, _T("画像ファイル (*.png;*.jpg;*.jpeg;*.bmp;*.gif)|*.png;*.jpg;*.jpeg;*.bmp;*.gif|すべて (*.*)|*.*||"), this); if (dlg.DoModal() != IDOK) return; CImageItem* p = new CImageItem(); if (!p->LoadFromFile(dlg.GetPathName())) { delete p; AfxMessageBox(_T("画像を読み込めませんでした。")); return; } int w = m_pLayout->m_paperWidthHu / 2; int h = m_pLayout->m_paperHeightHu / 2; int x = (m_pLayout->m_paperWidthHu - w) / 2; int y = (m_pLayout->m_paperHeightHu - h) / 2; p->SetRect(CRect(x, y, x + w, y + h)); AddAndSelect(p); } // 選択アイテムのプロパティを編集する。現状はテキストのみ編集ダイアログを持つ。 void CCanvas::EditSelectedProperties() { if (!m_pSelected) return; if (m_pSelected->Type() == ItemType::Text) { CTextItem* t = static_cast(m_pSelected); CTextPropsDlg dlg(this); dlg.m_text = t->m_text; dlg.m_fontName = t->m_fontName; dlg.m_pointSize = t->m_pointSize; dlg.m_align = t->m_align; dlg.m_bold = t->m_bold; if (dlg.DoModal() == IDOK) { t->m_text = dlg.m_text; t->m_fontName = dlg.m_fontName; t->m_pointSize = dlg.m_pointSize; t->m_align = dlg.m_align; t->m_bold = dlg.m_bold; m_dirty = TRUE; Invalidate(); } } else { AfxMessageBox(_T("この種類のアイテムには編集可能なプロパティがありません。")); } } // 選択中アイテムを削除する。 void CCanvas::DeleteSelected() { if (!m_pSelected || !m_pLayout) return; m_pLayout->DeleteItem(m_pSelected); m_pSelected = nullptr; m_dirty = TRUE; Invalidate(); } // ---- コピー / 複製 ------------------------------------------------------- // 選択アイテムをクリップボード(自前保持)へ複製して控える。 void CCanvas::CopySelected() { if (!m_pSelected) return; delete m_pClipboard; m_pClipboard = m_pSelected->Clone(); } // クリップボードの内容を 1mm ずらして貼り付け、選択状態にする。 void CCanvas::Paste() { if (!m_pClipboard || !m_pLayout) return; CPrintItem* p = m_pClipboard->Clone(); CRect r = p->Rect(); r.OffsetRect(100, 100); // 1mm ずらして重なりを避ける p->SetRect(r); AddAndSelect(p); } // 選択アイテムをその場で複製し(1mm ずらし)、複製を選択状態にする。 void CCanvas::DuplicateSelected() { if (!m_pSelected || !m_pLayout) return; CPrintItem* p = m_pSelected->Clone(); CRect r = p->Rect(); r.OffsetRect(100, 100); p->SetRect(r); AddAndSelect(p); } // ---- z オーダー ---------------------------------------------------------- // 選択アイテムを最前面へ。 void CCanvas::BringSelectedToFront() { if (!m_pSelected || !m_pLayout) return; m_pLayout->BringToFront(m_pSelected); m_dirty = TRUE; Invalidate(); } // 選択アイテムを最背面へ。 void CCanvas::SendSelectedToBack() { if (!m_pSelected || !m_pLayout) return; m_pLayout->SendToBack(m_pSelected); m_dirty = TRUE; Invalidate(); } // 選択アイテムを1つ前面へ。 void CCanvas::BringSelectedForward() { if (!m_pSelected || !m_pLayout) return; m_pLayout->BringForward(m_pSelected); m_dirty = TRUE; Invalidate(); } // 選択アイテムを1つ背面へ。 void CCanvas::SendSelectedBackward() { if (!m_pSelected || !m_pLayout) return; m_pLayout->SendBackward(m_pSelected); m_dirty = TRUE; Invalidate(); } // ---- 整列(用紙/余白の内側矩形を基準) ------------------------------------ // 選択アイテムを基準矩形(余白があれば余白内側、無ければ用紙)に対して整列する。 // how: 0=左 1=水平中央 2=右 3=上 4=垂直中央 5=下。サイズは変えず位置だけ移動。 void CCanvas::AlignSelected(int how) { if (!m_pSelected || !m_pLayout) return; CRect b = m_pLayout->ContentRect(); CRect r = m_pSelected->Rect(); int w = r.Width(), h = r.Height(); switch (how) { case 0: r.left = b.left; r.right = r.left + w; break; // 左 case 1: { int cx = (b.left + b.right) / 2; r.left = cx - w / 2; r.right = r.left + w; } break; // 水平中央 case 2: r.right = b.right; r.left = r.right - w; break; // 右 case 3: r.top = b.top; r.bottom = r.top + h; break; // 上 case 4: { int cy = (b.top + b.bottom) / 2; r.top = cy - h / 2; r.bottom = r.top + h; } break; // 垂直中央 case 5: r.bottom = b.bottom; r.top = r.bottom - h; break; // 下 } m_pSelected->SetRect(r); m_dirty = TRUE; Invalidate(); } // 「新規」「開く」でレイアウトが差し替わった後に呼ぶ。選択解除して再描画する。 void CCanvas::OnLayoutReplaced() { m_pSelected = nullptr; m_dirty = FALSE; Invalidate(); }