上位机MFC增强版画图板源代码下载
例程运行界面如上图。
可以任意的绘制图形,
图形绘制后,也可以通过鼠标拖动更改图形的外观属性。
有很好的参考价值,
可以下载源代码参考学习。
下载地址:
上位机VC MFC程序开发精典实例大全源码与视频讲解配套下载408例 经历1年的编程与录制点击进入查看
以下为作者对例程的使用介绍
下面是其更新日志:
============================================================
v3.0
自画直线,矩形,园
============================================================
v2.8-2.9
完成所有功能,包括全选,拖动等,参见文档HDraw2.9.doc
============================================================
v2.7
修正画布随滚动条滚动的Bug
完善MouseMove的直线,矩形和椭圆识别
============================================================
v2.5
增强版
增加相应菜单
把画图的Toolbar单独分离出来
在画布上对单张图片操作
============================================================
v2.4
完结版
画布,ScrollView, CRectTracker,调色板,保存成位图
加入中间层View的步骤
1.创建MFC类CHDrawPView:CScrollView
2.修改CHDrawApp的InitInstance方法,将CHDrawView改为CHDrawPView,并修改include
加入CRectTracker的步骤
1.增加成员变量CRectTracker m_tracker
2.在CHDrawPView的构造函数中设置Tracker的大小
3.在CHDrawPView的OnDraw中调用Tracker的Draw函数
4.override CHDrawPView的SetCursor方法 GetCursorPos->ScreenToClient->SetCursor
5.override CHDrawPView的LButtonDown方法 HitTest->Track
将CHDrawView加入CHDrawPView的
使用自定义消息防止ActiveView从画布跑到背景View
============================================================
v2.3
增加Bitmap背景功能
============================================================
v2.2
完成颜色和线宽功能
============================================================
v2.1
增加文本图形类型,增加删除功能,增加打开保存功能
============================================================
v2.0
完成所有基本功能
下面是介绍文档:
1. 概述
1.1. 简介
使用VC开发平台,MFC框架实现一个画图程序,尽可能多的实现Windows自带的画图功能,并扩展其功能。
1.2. 功能需求
1.2.1. 基本绘图功能:(必须全部实现)
(1) 能够用鼠标操控方式,绘制直线、矩形、椭圆。
(2) 在绘图时,选择绘制某种图像后(如直线),在画布中按住鼠标左键后移动鼠标,在画布中实时的根据鼠标的移动显示相应的图形。在松开鼠标左键后,一次绘图操作完成。
(3) 能够在绘制一图形(如一条直线)前设置线的粗细、颜色。(以菜单方式)
(4) 可以以矢量图方式保存绘制的图形。
(5) 可以读取保存的矢量图形文件,并显示绘图的结果。
界面友好的要求:
(6) 有画直线、矩形、椭圆的工具箱。
(7) 有颜色选择工具箱。
(8) 对于当前选中的绘图工具,以“下沉”的形式显示。
(9) 在状态栏中显示鼠标的位置。
(10) 在鼠标移向一工具不动时,有工具的功能提示。
(11) 在菜单上有当前选中的菜单项标识(即前面有小钩)
(12) 可以用鼠标操作方式,通过“拖拽”方式,改变画布的大小。
(13) 在画布大而外框小时,应有水平或垂直方向的滚动条。
1.2.2. 高级编辑功能:
(1) 具有Undo功能。
(2) 可以用鼠标选中绘制的某一图形。被选中的图形符号有标识(参见Word,如一直线段,其两端点上加了两个小框;矩形上有8个小框点)。
(3) 当鼠标靠近某一目标时,鼠标的形状发生改变
(4) 修改被选中的图形。通过鼠标的“拖拽”,可以改变图形的位置、或大小。
(5) 修改被选中图形的颜色、笔划的粗细。
(6) 删除被选中的图形。
(7) 可以使用鼠标“拖拽”一个虚矩形框,一次选择多个图形。
(8) 可以使用 Ctrl 或Shift加鼠标左键选择多个图形对象。
1.2.3. 附加功能:
(1) 可选择打开或关闭工具栏。
(2) 应用程序的标题栏上有程序的图标。
(3) 将图形转换成位图文件的形式保存。
(4) 在选择一个图形元素后(如直线),会有进一步选择线型或线宽的界面。
(5) 仿Word,选择“线型”、“粗细”图标后,会出现进一步选择的选项卡。
1.3. 未实现的功能
2. 主要功能描述
上位机MFC增强版画图板源代码下载
右键修改选中图形的颜色,粗细,线型,删除选中图形
右键和鼠标调整图形大小
对话框矢量修改所有图形
3. 技术细节
3.1. 代码结构
3.1.1. 代码文件
MFC自动生成的文件
1个CHDrawPView
1个HStroke
2个Dialog(HStrokeEditDlg+HStrokeTextDlg)
1个ToolBar(HColorBar)
3.1.2. 代码类
HDrawPView文件只有一个类:CHDrawPView,该类集成自MFC的CScrollView,主要实现维护画布类CHDrawView和滚动功能
HStroke文件里包含目前所有的图形类信息,包括集成与MFC的CObject类的基类HStroke,以及集成自HStroke的具体图形类HStrokeLine(直线),HStrokeRect(矩形),HStrokeEllipse(椭圆),HStrokeText(文本),HStrokePoly(曲线)。
HStrokeEditDlg文件只有一个类:HStrokeEditDlg,该类集成自MFC的CDialog类,主要用来编辑已有图形类
HStrokeTextDlg文件只有一个类:HStrokeTextDlg,该类集成自MFC的CDialog类,主要用来画文本时输入文本信息
HColorBar类只有一个类:HColorBar类,该类集成自MFC的CToolBar类,呈现一个颜色框,方便用户在绘图时选择不同的颜色。
3.2. SetROP2实现重绘
在画图状态下,鼠标移动时既要擦除旧图形,又要绘制新图形。这里主要有两种实现方法:一是全部重绘,二是先擦除旧图形。
如果使用矢量图全部重绘,频繁的绘图动作消耗很大,很容易造成屏幕闪动。但是如果将已有图形保存为位图,然后重绘的时候只要绘制位图即可,这样能避免闪动。
第二种方法要考虑的就是擦除旧图形的问题,本程序使用SetROP2函数设置MASK的方式,每次绘图时采用非异或运算的方式擦除旧图形:
pDC->SetROP2(R2_NOTXORPEN); //设置ROP2
DrawStroke(pDC); //画图擦除旧线(自定义函数)
SetCurrentPoint(point); //设置新点的坐标(自定义函数)
DrawStroke(pDC); //画新线(自定义函数)
3.3. 嵌套View实现画布
m_drawView = new CHDrawView();//创建画布View
if (!m_drawView->CreateEx(WS_EX_LEFT | WS_EX_LTRREADING | WS_EX_RIGHTSCROLLBAR,
AfxRegisterWndClass(CS_VREDRAW | CS_HREDRAW,LoadCursor(NULL,IDC_CROSS),
(HBRUSH)GetStockObject(WHITE_BRUSH),NULL),///白色画布
"",WS_CHILDWINDOW | WS_VISIBLE | WS_CLIPCHILDREN | WS_CLIPSIBLINGS,
m_tracker.m_rect.left,m_tracker.m_rect.top,
m_tracker.m_rect.right-1,m_tracker.m_rect.bottom-1,
this->m_hWnd,NULL)){
TRACE0("Failed to create toolbar\n");
return -1; // fail to create
}
m_drawView->SetDocument((CHDrawDoc*)m_pDocument);//传递CDocument给新View
m_drawView->ShowWindow(SW_NORMAL);
m_drawView->UpdateWindow();
//设置背景View颜色为灰色
SetClassLong(m_hWnd,GCL_HBRBACKGROUND,(long)GetStockObject(GRAY_BRUSH));
3.4. 鼠标靠近目标时突出显示
在鼠标移动的时候,OnMouseMove函数会遍历已有图形,判断鼠标所在点是否属于已有图形范围,如果是,则高亮显示该图形。
高亮显示的方法比较简单,只要增加CRectTracker即可,而判断当前点是否属于某图形比较有意思:
3.4.1. 判断一点是否属于矩形HStrokeRect
使用用MFC的CRect类的IsPointIn方法,当鼠标在矩形边框附近时,认为该点属于HStrokeRect。如图,实线矩形表示HStrokeRect。外矩形为外面的虚线矩形,内矩形为里面的虚线矩形:
BOOL HStrokeRect::IsPointIn(const CPoint &point){
//矩形左上角x坐标
int x1 = m_points.GetAt(0).x < m_points.GetAt(1).x ? m_points.GetAt(0).x : m_points.GetAt(1).x;
//矩形左上角y坐标
int y1 = m_points.GetAt(0).y < m_points.GetAt(1).y ? m_points.GetAt(0).y : m_points.GetAt(1).y;
//矩形右下角x坐标
int x2 = m_points.GetAt(0).x > m_points.GetAt(1).x ? m_points.GetAt(0).x : m_points.GetAt(1).x;
//矩形右下角y坐标
int y2 = m_points.GetAt(0).y > m_points.GetAt(1).y ? m_points.GetAt(0).y : m_points.GetAt(1).y;
//构建外矩行和内矩形
CRect rect(x1,y1,x2,y2), rect2(x1+5,y1+5,x2-5,y2-5);
//如果在外矩形内并在内矩形外
if(rect.PtInRect(point) && !rect2.PtInRect(point))
return TRUE;
else
return FALSE;
}
3.4.2. 判断一点是否属于线段
首先判断一点是否属于这条线段所属的直线,根据直线的判定公式y1/x1 = y2/x2得到x1*y2-x2*y1=0,但是在画图中应该在直线附近就能选中,所以在本程序中:|x1*y2-x2*y1| < 偏差,然后判断该点是否属于这条线段。
//计算该点到线段HStrokeLine的两个顶点的线段(x1,y1), (x2,y2)
int x1 = point.x - m_points.GetAt(0).x;
int x2 = point.x - m_points.GetAt(1).x;
int y1 = point.y - m_points.GetAt(0).y;
int y2 = point.y - m_points.GetAt(1).y;
//计算判断量x1*y2 - x2*y1
int measure = x1*y2 - x2*y1;
//误差允许范围,也就是直线的“附近”
int rule = abs(m_points.GetAt(1).x - m_points.GetAt(0).x)
+abs(m_points.GetAt(0).y - m_points.GetAt(1).y);
rule *= m_penWidth;//将线宽考虑进去
//属于直线
if(measure < rule && measure > -rule){
//判断该点是否属于这条线段
if(x1 * x2 < 0)
return TRUE;;
}
return FALSE;
3.4.3. 判断一点是否属于椭圆
根据椭圆的定义椭圆上的点到椭圆的两个焦点的距离之和为2a,首先计算出椭圆的a, b, c,然后计算出椭圆的两个焦点。
针对某个点,首先根据点坐标和两个焦点的坐标计算出该点到椭圆焦点的距离,然后减去2a,如果在“附近”,则认为其属于HStrokeEllipse,否则不属于。
//计算椭圆的a, b, c
int _2a = abs(m_points.GetAt(0).x - m_points.GetAt(1).x);
int _2b = abs(m_points.GetAt(0).y - m_points.GetAt(1).y);
double c = sqrt(abs(_2a*_2a - _2b*_2b))/2;
//计算椭圆的焦点
double x1,y1,x2,y2;
if(_2a > _2b){//横椭圆
x1 = (double)(m_points.GetAt(0).x + m_points.GetAt(1).x)/2 - c;
x2 = x1 + 2*c;
y1 = y2 = (m_points.GetAt(0).y + m_points.GetAt(1).y)/2;
}
else{//纵椭圆
_2a = _2b;
x1 = x2 = (m_points.GetAt(0).x + m_points.GetAt(1).x)/2;
y1 = (m_points.GetAt(0).y + m_points.GetAt(1).y)/2 - c;
y2 = y1 + 2*c;
}
//点到两个焦点的距离之和,再减去2a
//distance(point - p1) + distance(point - p2) = 2*a;
double measure = sqrt((x1 - point.x)*(x1-point.x) + (y1 - point.y)*(y1-point.y) )
+ sqrt( (point.x - x2)*(point.x - x2) + (point.y - y2)*(point.y - y2))
- _2a;
//计算椭圆的“附近”
double rule = 4*m_penWidth;
if(measure < rule && measure > -rule)
return TRUE;
else
return FALSE;
3.5. 文档序列化
MFC提供了良好的序列化机制,只要在类定义时加入DECLARE_SERIAL宏,在类构造函数的实现前加入IMPLEMENT_SERIAL宏,然后实现Serialize方法即可。本程序即使用该方法序列化:
首先在CHDrawDoc类实现Serialize方法,保存画布大小和所有图形信息:
void CHDrawDoc::Serialize(CArchive& ar)
{
if (ar.IsStoring())
{
//保存时,首先保存画布高和宽,然后序列化所有图形
ar<<m_cavasH<<m_cavasW;
m_strokeList.Serialize(ar);
}
else
{
//打开时,首先打开画布高和宽,然后打开所有图形
ar>>m_cavasH>>m_cavasW;
m_strokeList.Serialize(ar);
}
}
m_strokeList.Serialize(ar);这一句很神奇,Debug追踪的时候会发现,容器类会自动序列化容器内的元素数量,并调用每个元素的序列化方法序列化,所以还需要对每个图形元素实现序列化,以HStrokeLine为例:
在HStrokeLine的类声明中:
class HStrokeLine : public HStroke
{
public:
HStrokeLine();
DECLARE_SERIAL(HStrokeLine)
然后在HStrokeLine的构造函数实现前:
IMPLEMENT_SERIAL(HStrokeLine, CObject, 1)
HStrokeLine::HStrokeLine()
{
m_picType = PIC_line;
}
最后实现HStrokeLine的序列化函数,因为这里HStrokeLine集成自HStroke类而且没有特殊的属性,而HStroke类实现了Serialize函数,所以HStrokeLine类不需要实现Serilize方法,看一下HStroke的Serialize方法即可:
void HStroke::Serialize(CArchive& ar)
{
if(ar.IsStoring()){
int enumIndex = m_picType;
ar<<enumIndex<<m_penWidth<<m_penColor;
m_points.Serialize(ar);
}
else{
int enumIndex;
ar>>enumIndex>>m_penWidth>>m_penColor;
m_picType = (enum HPicType)enumIndex;
m_points.Serialize(ar);
}
}
3.6. 打开保存导出
文档序列化实现以后,程序的打开和保存功能就已经完成了。但是从序列化方法可以看出,打开和保存的都是矢量图形,所以这里实现了一个导出为BMP图像的方法,导出:
//保存文件对话框,选择导出路径
CFileDialog dlg(FALSE, "bmp","hjz.bmp");
if(dlg.DoModal() != IDOK){
return ;
}
CString filePath = dlg.GetPathName();
//
CClientDC client(this);//用于本控件的,楼主可以不用此句
CDC cdc;
CBitmap bitmap;
RECT rect;CRect r;
GetClientRect(&rect);
int cx = rect.right - rect.left;
int cy = rect.bottom - rect.top;
bitmap.CreateCompatibleBitmap(&client, cx, cy);
cdc.CreateCompatibleDC(NULL);
//获取BMP对象
CBitmap * oldbitmap = (CBitmap* ) cdc.SelectObject(&bitmap);
//白色画布
cdc.FillRect(&rect, CBrush::FromHandle((HBRUSH)GetStockObject(WHITE_BRUSH)));
//画图
for(int i = 0; i < GetDocument()->m_strokeList.GetSize(); i ++){
GetDocument()->m_strokeList.GetAt(i)->DrawStroke(&cdc);
}
cdc.SelectObject(oldbitmap);
::OpenClipboard(this->m_hWnd);
::EmptyClipboard();
::SetClipboardData(CF_BITMAP, bitmap);
::CloseClipboard();
HBITMAP hBitmap = (HBITMAP)bitmap;
HDC hDC;
int iBits;
WORD wBitCount;
DWORD dwPaletteSize=0, dwBmBitsSize=0, dwDIBSize=0, dwWritten=0;
BITMAP Bitmap;
BITMAPFILEHEADER bmfHdr;
BITMAPINFOHEADER bi;
LPBITMAPINFOHEADER lpbi;
HANDLE fh, hDib, hPal,hOldPal=NULL;
hDC = CreateDC("DISPLAY", NULL, NULL, NULL);
iBits = GetDeviceCaps(hDC, BITSPIXEL) * GetDeviceCaps(hDC, PLANES);
DeleteDC(hDC);
if (iBits <= 1) wBitCount = 1;
else if (iBits <= 4) wBitCount = 4;
else if (iBits <= 8) wBitCount = 8;
else wBitCount = 24;
GetObject(hBitmap, sizeof(Bitmap), (LPSTR)&Bitmap);
bi.biSize = sizeof(BITMAPINFOHEADER);
bi.biWidth = Bitmap.bmWidth;
bi.biHeight = Bitmap.bmHeight;
bi.biPlanes = 1;
bi.biBitCount = wBitCount;
bi.biCompression = BI_RGB;
bi.biSizeImage = 0;
bi.biXPelsPerMeter = 0;
bi.biYPelsPerMeter = 0;
bi.biClrImportant = 0;
bi.biClrUsed = 0;
dwBmBitsSize = ((Bitmap.bmWidth * wBitCount + 31) / 32) * 4 * Bitmap.bmHeight;
hDib = GlobalAlloc(GHND,dwBmBitsSize + dwPaletteSize + sizeof(BITMAPINFOHEADER));
lpbi = (LPBITMAPINFOHEADER)GlobalLock(hDib);
*lpbi = bi;
hPal = GetStockObject(DEFAULT_PALETTE);
if (hPal)
{
hDC = ::GetDC(NULL);
hOldPal = ::SelectPalette(hDC, (HPALETTE)hPal, FALSE);
RealizePalette(hDC);
}
GetDIBits(hDC, hBitmap, 0, (UINT) Bitmap.bmHeight, (LPSTR)lpbi + sizeof(BITMAPINFOHEADER)
+dwPaletteSize, (BITMAPINFO *)lpbi, DIB_RGB_COLORS);
if (hOldPal)
{
::SelectPalette(hDC, (HPALETTE)hOldPal, TRUE);
RealizePalette(hDC);
::ReleaseDC(NULL, hDC);
}
fh = CreateFile(filePath, GENERIC_WRITE,0, NULL, CREATE_ALWAYS,
FILE_ATTRIBUTE_NORMAL | FILE_FLAG_SEQUENTIAL_SCAN, NULL);
if (fh == INVALID_HANDLE_VALUE)
return ;
bmfHdr.bfType = 0x4D42; // "BM"
dwDIBSize = sizeof(BITMAPFILEHEADER) + sizeof(BITMAPINFOHEADER) + dwPaletteSize + dwBmBitsSize;
bmfHdr.bfSize = dwDIBSize;
bmfHdr.bfReserved1 = 0;
bmfHdr.bfReserved2 = 0;
bmfHdr.bfOffBits = (DWORD)sizeof(BITMAPFILEHEADER) + (DWORD)sizeof(BITMAPINFOHEADER) + dwPaletteSize;
WriteFile(fh, (LPSTR)&bmfHdr, sizeof(BITMAPFILEHEADER), &dwWritten, NULL);
WriteFile(fh, (LPSTR)lpbi, dwDIBSize, &dwWritten, NULL);
GlobalUnlock(hDib);
GlobalFree(hDib);
CloseHandle(fh);
3.7. 友好用户界面
菜单项选中和工具栏图标下沉。该功能的实现非常简单,而且用户体验很好,以当前所画的图形为例:
第一步:增加3个菜单项
名称 ID
直线 ID_DRAW_LINE
椭圆 ID_DRAW_ELLIPSE
矩形 ID_DRAW_RECT
第二步:在工具栏上增加3个工具栏项,注意ID要和上面的三个ID相同。
第三步:在CHDrawDoc类的ClassWizard中增加消息响应函数,分别为以上三个ID增加COMMAND和UPDATE_COMMAND_UI的Handler,COMMAND的Handler就是针对按下工具栏按钮或菜单项的响应函数,而UPDATE_COMMAND_UI则是显示菜单栏时执行的操作,有点类似OnDraw。
以直线为例,ID_DRAW_LINE的COMMAND的Handler为OnDrawLine
void CHDrawDoc::OnDrawLine()
{
//设置当前画图的图形类型为直线
m_picType = PIC_line;
}
ID_DRAW_LINE的UPDATE_COMMAND_UI的Handler为OnUpdateDrawLine:
void CHDrawDoc::OnUpdateDrawLine(CCmdUI* pCmdUI)
{
//如果当前画图类型为直线,设置菜单项前加对号,工具栏项下沉
pCmdUI->SetCheck(PIC_line == m_picType);
}
3.8. 右键菜单修改选中图形的属性
实现方法如下:
第一步:在资源视图中增加一个菜单
第二步:在CHDrawView中增加右键菜单响应函数OnRButtonDown:
void CHDrawView::OnRButtonDown(UINT nFlags, CPoint point)
{
//检查所有处于选中状态的图形,可以有多个
CHDrawDoc *pDoc = GetDocument();
m_strokeSelected.RemoveAll();//首先清空旧数据
for(int i = 0; i < pDoc->m_strokeList.GetSize(); i ++){
if(pDoc->m_strokeList.GetAt(i)->IsHightLight())
m_strokeSelected.Add(pDoc->m_strokeList.GetAt(i));
}
//显示右键菜单
CMenu rmenu;
rmenu.LoadMenu(IDR_MENU_SET);//加载资源中的菜单IDR_MENU_SET
ClientToScreen(&point);//需要坐标转换
rmenu.GetSubMenu(0)->TrackPopupMenu(TPM_LEFTALIGN, point.x, point.y, this);
//因为这里的rmenu是局部变量,所以必须Detach掉
rmenu.Detach();
CView::OnRButtonDown(nFlags, point);
}
第三步:增加菜单响应函数,这里以删除当前所选图形为例:
void CHDrawView::OnPicDelete()
{
//获取存储数据的文档类
CHDrawDoc *pDoc = GetDocument();
//移除所有处于选中状态的图形
int i = 0, j = 0;
for(; i < m_strokeSelected.GetSize(); i ++){
//这里的j没有归0,是有原因的,可以很有效的提高效率
//遍历复杂度为两个数组的和
for(; j < pDoc->m_strokeList.GetSize(); j ++){
if(m_strokeSelected.GetAt(i) == pDoc->m_strokeList.GetAt(j)){
delete pDoc->m_strokeList.GetAt(j);
pDoc->m_strokeList.RemoveAt(j);
break;
}
}
}
//如果没有处于选中状态的图形,则不需要刷新。
if(i > 0)
Invalidate();
}
3.9. 撤销和恢复操作
MFC提供了默认的撤销和恢复的ID,但是并没有提供默认实现,本程序的思路是,定义一个数组和一个数组索引,每执行一个操作,就把当前状态存储到数组中,并把数组索引加1。
撤销时,把索引减一的数组元素恢复到当前文档,恢复时,把索引加一的数组元素恢复到当前文档。
在程序中的步骤为:
第一步:定义数组,数组索引和备份,恢复函数:
CObArray m_backup;
int m_backup_index;
void ReStore(BOOL backward);
void BackUp();
void CHDrawDoc::BackUp()
{
//备份操作,有利有弊。简单,节省内存,序列化有变时不需修改;产生文件占据磁盘
CString fileName;
fileName.Format("hjz%d", m_backup.GetSize());
OnSaveDocument(fileName);
//这里使用Insert而不是Add是因为恢复是并没有删除
m_backup.InsertAt(m_backup_index++, NULL, 1);
}
void CHDrawDoc::ReStore(BOOL backward)
{
m_backup_index -= backward ? 1 : -1;//撤销还是恢复
//…把数组元素恢复到当前文档
OnOpenDocument(m_backup.GetAt(m_backup_index-1));
}
第二步:添加撤销和恢复菜单项,并添加消息句柄:
void CHDrawDoc::OnEditUndo()
{
ReStore(TRUE);
UpdateAllViews(NULL);
}
void CHDrawDoc::OnEditRedo()
{
ReStore(FALSE);
UpdateAllViews(NULL);
}
第三步:在每次对文档的修改操作之前,调用GetDocument()->Backup()
3.10. 使用鼠标拖拽选中多个图形
|