Initial commit: OtozaiPrinter (AveryDennisonUtil) ラベル/レシート レイアウト設計・印刷 MFC アプリ

- 用紙設計キャンバス(1/100mm 基準)・テキスト/画像/矩形/直線アイテム
- コピー/複製/Zオーダー/整列/グリッドスナップ、原紙(下絵)読込と印刷可否
- GDI 直接印刷、.otz 保存/再読込(版数つき)
- MFC/CRT 静的リンクで VS2022 IDE から F5 デバッグ可
This commit is contained in:
yoshikazu
2026-06-05 17:27:29 +09:00
commit 19d1c39370
33 changed files with 3049 additions and 0 deletions
+583
View File
@@ -0,0 +1,583 @@
// 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<CCanvas*>(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<CTextItem*>(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();
}