视频演示
自制简易画图软件演示(BV1nk4y1r72y),包含铅笔、直线、矩形、椭圆、橡皮等功能。
开发环境
开发工具为 Visual Studio 2019。画图软件基于 MFC 类库进行设计。
自定义类介绍
CMyButton 是 CDC 的派生类,继承 CDC 的目的是创建位图。一个 CMyButton 的对象只对应一个按钮,按钮有铅笔、直线等。
位图与编号
每一个按钮都有其索引。每个按钮都有一个位图,位图所覆盖的区域在内存中有其对应的颜色作为按钮的映射。
程序启动时,根据编号加载对应的位图资源。
映射
当鼠标左键点击按钮后,可以得到光标所在坐标的颜色,因此可以用按钮所在方形区域的颜色作为按钮的映射,即一种颜色对应一个按钮。
我们在按钮区域填充对应颜色即可构建映射。
1 2 3 4 5
| void CMyButton::DrawButtonBackground(CDC* pdc) { pdc->FillRect(&ButtonRect, &CBrush(ButtonColor)); }
|
状态
按钮从 Windows 画图 3D 中截取,初始状态为第一个状态,当鼠标浮动在按钮上为第二个状态,当选中按钮时为第三个状态。
1 2 3 4 5 6 7 8
| void CMyButton::MouseMove() { if (!ButtonState) ButtonState = 1; }
void CMyButton::LButtonUp() { ButtonState = 2; }
void CMyButton::MouseLeave() { if (ButtonState == 1) ButtonState = 0; }
void CMyButton::ToClear() { ButtonState = 0; }
|
构造函数
综上所述,对一个按钮的描述包括:状态、位图、区域、颜色、索引。
同时,创建一个按钮时应该给予其兼容性 DC,用于在内存中进行操作。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| CMyButton::CMyButton(CRect _rect,COLORREF _color,int _index, CDC* pdc) { ButtonIndex = _index; ButtonRect = _rect; ButtonColor = _color; ButtonState = 0; this->CreateCompatibleDC(pdc); switch (ButtonIndex) { case 1: ButtonBmp.LoadBitmapW(IDB_PENCIL); break; case 2: ButtonBmp.LoadBitmapW(IDB_RECTANGLE); break; case 3: ButtonBmp.LoadBitmapW(IDB_LINE); break; case 4: ButtonBmp.LoadBitmapW(IDB_OVAL); break; case 5: ButtonBmp.LoadBitmapW(IDB_ERASER); default: break; } this->SelectObject(ButtonBmp); }
|
显示按钮
将按钮显示在屏幕上。
1 2 3 4 5 6 7
| void CMyButton::ShowButton(CDC* pdc) { pdc->BitBlt(ButtonRect.left, ButtonRect.top, 50, 50, this, ButtonState * 50, 0, SRCCOPY); }
|
CMouseSelector
**CMouseSelector 用于管理按钮的集合。**利用 STL 中的 vector 储存所有 CMyButton 对象的指针。
OnCreate
OnCreate 是一个消息响应函数,是响应 WM_CREATE
消息的一个函数,而 WM_CREATE
消息是由 Create 函数调用的。一个窗口创建之后,会向操作系统发送 WM_CREATE
消息,因为在MFC里面用一种消息映射的机制来响应消息,也就是可以用函数来响应相应的消息。我们可以在 OnCreate 函数里实现我们要在窗口里面增加的东西,例如按扭,状态栏,工具栏等。OnCreat 不产生窗口,只是在窗口显示前设置窗口的属性如风格、位置等。
(关于 OnCreat 的描述引用自:Windows平台编程之OnCreate函数的说明)
在执行 OnDraw 之前,要把所有按钮的映射在 OnCreat 中设置好,由 OnDraw 将按钮呈现出来。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| int C我的画图软件View::OnCreate(LPCREATESTRUCT lpCreateStruct) { if (CView::OnCreate(lpCreateStruct) == -1) return -1;
CClientDC dc(this); for (int i = 1;i<=5; i++) { CRect rect; rect.top = 5; rect.bottom = 55; if (i == 5) { rect.left = 1080; rect.right = 1160; } else { rect.left = 450 + i * 50; rect.right = 530 + i * 50; } CMyButton* pMyButton = new CMyButton(rect,RGB(i,i,i), i, &dc); MouseSelector.AddButton(pMyButton); } return 0; }
|
设置按钮映射
当新添加一个按钮后,将该按钮的指针存入 CMouseSelector 内的vector 中,并根据按钮的颜色构建映射。由于不在意按钮的顺序,采用 \text{unordered_map} ,查询是 O(1) 的。
1 2 3 4 5 6
| void CMouseSelector::AddButton(CMyButton* pMyButton) { ButtonSet.push_back(pMyButton); ButtonHash.insert(std::pair<COLORREF, CMyButton*>(pMyButton->ButtonColor, pMyButton)); }
|
显示所有按钮
依次调用 vector 中元素的 ShowButton 和 DrawButtonBackground。这样就能显示所有按钮,并填充上按钮对应的颜色。
1 2 3 4 5 6 7 8
| void CMouseSelector::ShowAllButton(CDC* pClientDC,CDC* pMemDC) { for (auto p : ButtonSet) { p->ShowButton(pClientDC); p->DrawButtonBackground(pMemDC); } }
|
管理按钮状态
至多有一个按钮处于被选中的状态,至多有一个按钮处于悬浮状态。
定义两个 CMyButton* 类型的指针,SuspendedButton 和 PressedButton,分别指向悬浮状态的按钮和被点击的按钮。
在构造函数中将两个指针指向 nullptr 。
1 2 3 4 5
| CMouseSelector::CMouseSelector() { SuspendedButton = nullptr; PressedButton = nullptr; }
|
消息响应
当鼠标在按钮的区域上进行操作时,要注意随时改变 SuspendedButton 和 PressedButton 指向的对象。
要时刻注意 SuspendedButton 和 PressedButton 指向空的情况。
选中按钮
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| LocMes CMouseSelector::LButtonUp(COLORREF LocColor) { LocMes res(0, 0); auto it = ButtonHash.find(LocColor); if (it != ButtonHash.end()) { if (PressedButton == it->second) return res; res.NeedUpdate = 1; res.index = it->second->GetButtonIndex(); if (PressedButton) PressedButton->ToClear(); PressedButton = it->second; PressedButton->LButtonUp(); } return res; }
|
鼠标移动
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| bool CMouseSelector::MouseMove(COLORREF LocColor) { bool res=0; auto it = ButtonHash.find(LocColor); if (it == ButtonHash.end() && SuspendedButton) { res = 1; SuspendedButton->MouseLeave(); SuspendedButton = nullptr; } else if (it != ButtonHash.end()) { if (SuspendedButton && SuspendedButton == it->second) return res; res = 1; if (SuspendedButton) SuspendedButton->MouseLeave(); SuspendedButton = it->second; SuspendedButton->MouseMove(); } return res; }
|
CMyTool 作为所有几何图形的基类。
变量
设置所有几何图形的起点,终点,索引,以及开始绘画的标志。
考虑到使用习惯和编程的方便性,图形索引和按钮索引是一致的,如长方形按钮的索引是 2,其图形的索引也是 2。
1 2 3 4 5
| protected: CPoint StartPoint; CPoint EndPoint; int index; bool IsDraw;
|
函数
对所有几何图形的共性进行操作,用虚函数体现其多态性。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| #include "CMyTool.h"
void CMyTool::StartDraw(bool x) { IsDraw = x; }
bool CMyTool::IsDrawing() { return IsDraw; }
void CMyTool::SetIndex(int _id) { index = _id; }
int CMyTool::GetIndex() { return index; };
void CMyTool::ResetPoint(CPoint _s, CPoint _e) { StartPoint = _s; EndPoint = _e; }
CPoint CMyTool::start() { return StartPoint; }
CPoint CMyTool::end() { return EndPoint; }
void CMyTool::Draw() {}
|
CMyPen
CMyTool 的派生类,无需添加任何特性。
1 2 3 4 5 6 7
| #pragma once #include "CMyTool.h" #include "pch.h" class CMyPen : public CMyTool { };
|
CMyLine
CMyTool 的派生类,无需添加任何特性。
1 2 3 4 5 6 7
| #pragma once #include "CMyTool.h" #include "pch.h" class CMyLine : public CMyTool { };
|
CMyEraser
CMyTool 的派生类,无需添加任何特性。
1 2 3 4 5 6 7
| #pragma once #include "CMyTool.h" #include "pch.h" class CMyEraser : public CMyTool { };
|
CMyRectangle
为实现鼠标右键进行正方形和长方形的转换,需要添加一个画正方形的标志,并新设计了两个函数。
CMyRectangle.h
1 2 3 4 5 6 7 8 9 10 11 12
| #pragma once #include "CMyTool.h" #include "pch.h" class CMyRectangle :public CMyTool { private: bool IsDrawSquare; public: bool IsDrawingSquare(); void ToDrawSquare(bool x); };
|
CMyREctangle.cpp
1 2 3 4 5
| #include "CMyRectangle.h"
bool CMyRectangle::IsDrawingSquare() { return IsDrawSquare; }
void CMyRectangle::ToDrawSquare(bool x) { IsDrawSquare = x; }
|
CMyEllipse
为实现鼠标右键进行圆和椭圆的转换,需要添加一个画圆的标志,并新设计了两个函数。
CMyEllipse.h
1 2 3 4 5 6 7 8 9 10 11 12
| #pragma once #include "CMyTool.h" #include "pch.h" class CMyEllipse : public CMyTool { private: bool IsDrawCircle; public: void ToDrawCircle(bool x); bool IsDrawingCircle(); };
|
CMyEllipse.cpp
1 2 3 4 5
| #include "CMyEllipse.h"
void CMyEllipse::ToDrawCircle(bool x) { IsDrawCircle = x; }
bool CMyEllipse::IsDrawingCircle() { return IsDrawCircle; };
|
设置背景
对每个分区创建内存 DC 和位图,创建 CRect 对象设置每个分区的区域范围,用画刷在各个区域内填充对应的颜色。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46
| void C我的画图软件View::DrawBackground() { CClientDC dc(this); CBrush brush; CRect rect; GetWindowRect(&rect); ScreenToClient(&rect); m_BackgroundDC = new CDC; m_BackgroundDC->CreateCompatibleDC(&dc); m_ButtonDC = new CDC; m_ButtonDC->CreateCompatibleDC(&dc); m_BackgroundBit = new CBitmap; m_BackgroundBit->CreateCompatibleBitmap(&dc, rect.right, rect.bottom); m_ButtonBit = new CBitmap; m_ButtonBit->CreateCompatibleBitmap(&dc, rect.right, rect.bottom); m_BackgroundDC->SelectObject(m_BackgroundBit); m_ButtonDC->SelectObject(m_ButtonBit); brush.CreateSolidBrush(RGB(229, 251, 255)); m_BackgroundDC->FillRect(&rect, &brush); brush.DeleteObject(); m_state_rect.bottom = rect.bottom; m_state_rect.left = rect.left; m_state_rect.right = rect.right; m_state_rect.top = rect.bottom - 25; m_tool_rect = rect; m_tool_rect.top = 0; m_tool_rect.bottom = 60; m_paper_rect = rect; m_paper_rect.top = m_tool_rect.bottom; m_paper_rect.bottom =m_state_rect.top ; brush.CreateSolidBrush(RGB(240, 240, 240)); m_BackgroundDC->FillRect(&m_tool_rect, &brush); brush.DeleteObject(); brush.CreateSolidBrush(RGB(101, 101, 101)); m_BackgroundDC->FillRect(&m_state_rect, &brush); brush.DeleteObject(); }
|
在内存中画好图后呈现在客户区。
1 2 3 4 5 6 7 8 9
| void C我的画图软件View::ShowBackground(CDC* _) { CRect rect; GetWindowRect(&rect); ScreenToClient(&rect); _->BitBlt(0, 0, rect.right, rect.bottom, m_BackgroundDC, 0, 0, SRCCOPY); }
|
功能实现
显示坐标
当光标移动时,显示当前点的坐标,因此需要在消息响应函数 OnMouseMove 中设置函数 ShowPosition。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| void C我的画图软件View::ShowPosition(CPoint p) { CDC dc; CClientDC _(this); CBitmap bmp; dc.CreateCompatibleDC(&_); int width = m_state_rect.right - m_state_rect.left; int height = m_state_rect.bottom - m_state_rect.top; bmp.CreateCompatibleBitmap(&_,width,height); dc.SelectObject(&bmp); CString pos; dc.SetTextColor(RGB(255, 255, 255)); dc.SetBkMode(TRANSPARENT); pos.Format(_T("坐标 x: %d y: %d"), p.x, p.y); dc.FillRect(&CRect(0, 0, width, height), &CBrush(RGB(101, 101, 101))); dc.TextOut(600, 1, pos); _.BitBlt(m_state_rect.left, m_state_rect.top, width, height, &dc, 0, 0, SRCCOPY); }
|
交互式绘图
定义 CMyTool* 类型的指针 \text{now_choose},绘制不同图形时指向对应的派生类。定义 CMouseSelector 类型的对象 MouseSelector 展示不同按钮的效果。
左键弹起
当左键弹起时,获取当前点的颜色, 由 CMouseSelector 内部函数 LButtonUp 判断是否选中按钮并返回当前位置的信息,若需要重新显示按钮(选中了新的按钮或按钮的悬浮状态解除),则调用内部函数 ShowAllButtonyiwei ;若内部函数 LButtonUp 返回的按钮索引非零,那么表示选中了按钮,需要将 \text{now_choose} 指向新的对象。
同时,左键弹起也意味着当前选择的几何图形完成绘制,需要将一切绘画的状态初始化。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82
|
void C我的画图软件View::OnLButtonUp(UINT nFlags, CPoint point) { CClientDC dc(this); ShowPosition(point); LocMes res = MouseSelector.LButtonUp(GetPixel(*m_ButtonDC, point.x, point.y)); if (res.NeedUpdate) MouseSelector.ShowAllButton(&dc,m_ButtonDC); if (now_choose) { switch (now_choose->GetIndex()) { case 1: now_choose->StartDraw(0); break; case 2: now_choose->StartDraw(0); dynamic_cast<CMyRectangle*>(now_choose)->ToDrawSquare(0); break; case 3: now_choose->StartDraw(0); break; case 4: now_choose->StartDraw(0); dynamic_cast<CMyEllipse*>(now_choose)->ToDrawCircle(0); case 5: now_choose->StartDraw(0); default: break; } } if (!now_choose||(res.index && res.index != now_choose->GetIndex())) { if (now_choose) { delete now_choose; now_choose = nullptr; } switch (res.index) { case 1: now_choose = new CMyPen; now_choose->StartDraw(0); now_choose->SetIndex(1); break; case 2: now_choose = new CMyRectangle; now_choose->StartDraw(0); now_choose->SetIndex(2); dynamic_cast<CMyRectangle*>(now_choose)->ToDrawSquare(0); break; case 3: now_choose = new CMyLine; now_choose->StartDraw(0); now_choose->SetIndex(3); break; case 4: now_choose = new CMyEllipse; now_choose->StartDraw(0); now_choose->SetIndex(4); dynamic_cast<CMyEllipse*>(now_choose)->ToDrawCircle(0); break; case 5: now_choose = new CMyEraser; now_choose->StartDraw(0); now_choose->SetIndex(5); break; default: break; } } CView::OnLButtonUp(nFlags, point); }
|
左键按下
鼠标左键按下代表开始绘制图形。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
|
void C我的画图软件View::OnLButtonDown(UINT nFlags, CPoint point) { if (now_choose) { now_choose->StartDraw(1); now_choose->ResetPoint(point, point); } CView::OnLButtonDown(nFlags, point); }
|
右键弹起
开启 shift 功能,画折线、正方形、椭圆。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
|
void C我的画图软件View::OnRButtonUp(UINT , CPoint point) { if (now_choose&& now_choose->IsDrawing()) { switch(now_choose->GetIndex()) { case 2: if (dynamic_cast<CMyRectangle*>(now_choose)->IsDrawingSquare()) dynamic_cast<CMyRectangle*>(now_choose)->ToDrawSquare(0); else dynamic_cast<CMyRectangle*>(now_choose)->ToDrawSquare(1); break; case 3: now_choose->ResetPoint(point, point); break; case 4: if (dynamic_cast<CMyEllipse*>(now_choose)->IsDrawingCircle()) dynamic_cast<CMyEllipse*>(now_choose)->ToDrawCircle(0); else dynamic_cast<CMyEllipse*>(now_choose)->ToDrawCircle(1); break; default: break; } } }
|
光标移动
光标移动是绘图操作的主体。
当选择铅笔时,将当前节点置为起点和终点,在这一微小距离内画线,宏观上体现为一支流畅的笔。
当画直线、长方形、椭圆时,采用橡皮筋算法。当光标移动时,对原来位置的图形取反色,消除原有位置图形,显示新的图形。需要注意,画正方形时,边长是光标当前点和起点所形成矩形的长和宽中的最小值;画圆时,其外接正方形边长是光标当前点和起点所形成矩形的长和宽中的最小值。
当选择橡皮时,将其颜色置为背景色,进行铅笔的同样操作。橡皮是一支颜色为背景色的笔。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148
|
void C我的画图软件View::OnMouseMove(UINT nFlags, CPoint point) { ShowPosition(point); CClientDC dc(this); if (MouseSelector.MouseMove(GetPixel(*m_ButtonDC, point.x, point.y))) MouseSelector.ShowAllButton(&dc,m_ButtonDC); CPen pen; if (now_choose&&now_choose->IsDrawing()) { switch (now_choose->GetIndex()) { case 1: pen.CreatePen(PS_SOLID, 3, RGB(0,0,0)); dc.SelectObject(pen); dc.MoveTo(now_choose->start()); dc.LineTo(now_choose->end()); now_choose->ResetPoint(now_choose->end(), point); break; case 2: pen.CreatePen(PS_SOLID, 3, RGB(0, 0, 0)); dc.SelectObject(pen); dc.MoveTo(now_choose->start()); dc.SetROP2(R2_NOTXORPEN); dc.Rectangle(CRect(now_choose->start(),now_choose->end())); dc.MoveTo(now_choose->start()); if (dynamic_cast<CMyRectangle*>(now_choose)->IsDrawingSquare()) { if (point.x < now_choose->start().x) { if (point.y < now_choose->start().y) { int dx = now_choose->start().x - point.x; int dy = now_choose->start().y - point.y; int d = min(dx, dy); point = CPoint(now_choose->start().x - d, now_choose->start().y - d); } else { int dx = now_choose->start().x - point.x; int dy = point.y - now_choose->start().y; int d = min(dx, dy); point = CPoint(now_choose->start().x - d, now_choose->start().y + d); } } else { if (point.y < now_choose->start().y) { int dx = point.x-now_choose->start().x; int dy = now_choose->start().y - point.y; int d = min(dx, dy); point = CPoint(now_choose->start().x + d, now_choose->start().y - d); } else { int dx = point.x - now_choose->start().x; int dy = point.y - now_choose->start().y; int d = min(dx, dy); point = CPoint(now_choose->start().x + d, now_choose->start().y + d); } } } dc.Rectangle(CRect(now_choose->start(),point)); now_choose->ResetPoint(now_choose->start(), point); break; case 3: pen.CreatePen(PS_SOLID, 3, RGB(0, 0, 0)); dc.SelectObject(pen); dc.MoveTo(now_choose->start()); dc.SetROP2(R2_NOTXORPEN); dc.LineTo(now_choose->end()); dc.MoveTo(now_choose->start()); dc.LineTo(point); now_choose->ResetPoint(now_choose->start(),point); break; case 4: pen.CreatePen(PS_SOLID, 3, RGB(0, 0, 0)); dc.SelectObject(pen); dc.MoveTo(now_choose->start()); dc.SetROP2(R2_NOTXORPEN); dc.Ellipse(CRect(now_choose->start(), now_choose->end())); dc.MoveTo(now_choose->start()); if (dynamic_cast<CMyEllipse*>(now_choose)->IsDrawingCircle()) { if (point.x < now_choose->start().x) { if (point.y < now_choose->start().y) { int dx = now_choose->start().x - point.x; int dy = now_choose->start().y - point.y; int d = min(dx, dy); point = CPoint(now_choose->start().x - d, now_choose->start().y - d); } else { int dx = now_choose->start().x - point.x; int dy = point.y - now_choose->start().y; int d = min(dx, dy); point = CPoint(now_choose->start().x - d, now_choose->start().y + d); } } else { if (point.y < now_choose->start().y) { int dx = point.x - now_choose->start().x; int dy = now_choose->start().y - point.y; int d = min(dx, dy); point = CPoint(now_choose->start().x + d, now_choose->start().y - d); } else { int dx = point.x - now_choose->start().x; int dy = point.y - now_choose->start().y; int d = min(dx, dy); point = CPoint(now_choose->start().x + d, now_choose->start().y + d); } } } dc.Ellipse(CRect(now_choose->start(), point)); now_choose->ResetPoint(now_choose->start(), point); break; case 5: pen.CreatePen(PS_SOLID, 20, RGB(229, 251, 255)); dc.SelectObject(pen); dc.MoveTo(now_choose->start()); dc.LineTo(now_choose->end()); now_choose->ResetPoint(now_choose->end(), point); break; default: break; } } CView::OnMouseMove(nFlags, point); }
|