基于VC++的画图板程序

person 匿名

发布日期: 2019-07-15 21:47:35 浏览量: 299
评分:
star_border star_border star_border star_border star_border star_border star_border star_border star_border star_border
*转载请注明来自write-bug.com

1. 概述

1.1 简介

使用VC开发平台,MFC框架实现一个画图程序,尽可能多的实现Windows自带的画图功能,并扩展其功能。

1.2 功能需求

1.2.1 基本绘图功能

  • 能够用鼠标操控方式,绘制直线、矩形、椭圆。

  • 在绘图时,选择绘制某种图像后(如直线),在画布中按住鼠标左键后移动鼠标,在画布中实时的根据鼠标的移动显示相应的图形。在松开鼠标左键后,一次绘图操作完成。

  • 能够在绘制一图形(如一条直线)前设置线的粗细、颜色。(以菜单方式)

  • 可以以矢量图方式保存绘制的图形。

  • 可以读取保存的矢量图形文件,并显示绘图的结果。

界面友好的要求:

  • 有画直线、矩形、椭圆的工具箱。

  • 有颜色选择工具箱。

  • 对于当前选中的绘图工具,以“下沉”的形式显示。

  • 在状态栏中显示鼠标的位置。

  • 在鼠标移向一工具不动时,有工具的功能提示。

  • 在菜单上有当前选中的菜单项标识(即前面有小钩)

  • 可以用鼠标操作方式,通过“拖拽”方式,改变画布的大小。

  • 在画布大而外框小时,应有水平或垂直方向的滚动条。

1.2.2 高级编辑功能

  • 具有Undo功能。

  • 可以用鼠标选中绘制的某一图形。被选中的图形符号有标识(参见Word,如一直线段,其两端点上加了两个小框;矩形上有8个小框点)。

  • 当鼠标靠近某一目标时,鼠标的形状发生改变

  • 修改被选中的图形。通过鼠标的“拖拽”,可以改变图形的位置、或大小。

  • 修改被选中图形的颜色、笔划的粗细。

  • 删除被选中的图形。

  • 可以使用鼠标“拖拽”一个虚矩形框,一次选择多个图形。

  • 可以使用 Ctrl 或Shift加鼠标左键选择多个图形对象。

1.2.3 附加功能

  • 可选择打开或关闭工具栏。

  • 应用程序的标题栏上有程序的图标。

  • 将图形转换成位图文件的形式保存。

  • 在选择一个图形元素后(如直线),会有进一步选择线型或线宽的界面。

  • 仿Word,选择“线型”、“粗细”图标后,会出现进一步选择的选项卡。

    2.主要功能描述

右键修改选中图形的颜色,粗细,线型,删除选中图形

右键和鼠标调整图形

对话框矢量修改所有图形

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的方式,每次绘图时采用非异或运算的方式擦除旧图形:

  1. pDC->SetROP2(R2_NOTXORPEN); //设置ROP2
  2. DrawStroke(pDC); //画图擦除旧线(自定义函数)
  3. SetCurrentPoint(point); //设置新点的坐标(自定义函数)
  4. DrawStroke(pDC); //画新线(自定义函数)

3.3 嵌套View实现画布

  1. m_drawView = new CHDrawView();//创建画布View
  2. if (!m_drawView->CreateEx(WS_EX_LEFT | WS_EX_LTRREADING | WS_EX_RIGHTSCROLLBAR,
  3. AfxRegisterWndClass(CS_VREDRAW | CS_HREDRAW,LoadCursor(NULL,IDC_CROSS),
  4. (HBRUSH)GetStockObject(WHITE_BRUSH),NULL),///白色画布
  5. "",WS_CHILDWINDOW | WS_VISIBLE | WS_CLIPCHILDREN | WS_CLIPSIBLINGS,
  6. m_tracker.m_rect.left,m_tracker.m_rect.top,
  7. m_tracker.m_rect.right-1,m_tracker.m_rect.bottom-1,
  8. this->m_hWnd,NULL)){
  9. TRACE0("Failed to create toolbar\n");
  10. return -1; // fail to create
  11. }
  12. m_drawView->SetDocument((CHDrawDoc*)m_pDocument);//传递CDocument给新View
  13. m_drawView->ShowWindow(SW_NORMAL);
  14. m_drawView->UpdateWindow();
  15. //设置背景View颜色为灰色
  16. SetClassLong(m_hWnd,GCL_HBRBACKGROUND,(long)GetStockObject(GRAY_BRUSH));

3.4 鼠标靠近目标时突出显示

在鼠标移动的时候,OnMouseMove函数会遍历已有图形,判断鼠标所在点是否属于已有图形范围,如果是,则高亮显示该图形。

高亮显示的方法比较简单,只要增加CRectTracker即可,而判断当前点是否属于某图形比较有意思:

3.4.1 判断一点是否属于矩形HStrokeRect

使用用MFC的CRect类的IsPointIn方法,当鼠标在矩形边框附近时,认为该点属于HStrokeRect。如图,实线矩形表示HStrokeRect。外矩形为外面的虚线矩形,内矩形为里面的虚线矩形:

  1. BOOL HStrokeRect::IsPointIn(const CPoint &point){
  2. //矩形左上角x坐标
  3. int x1 = m_points.GetAt(0).x < m_points.GetAt(1).x ? m_points.GetAt(0).x : m_points.GetAt(1).x;
  4. //矩形左上角y坐标
  5. int y1 = m_points.GetAt(0).y < m_points.GetAt(1).y ? m_points.GetAt(0).y : m_points.GetAt(1).y;
  6. //矩形右下角x坐标
  7. int x2 = m_points.GetAt(0).x > m_points.GetAt(1).x ? m_points.GetAt(0).x : m_points.GetAt(1).x;
  8. //矩形右下角y坐标
  9. int y2 = m_points.GetAt(0).y > m_points.GetAt(1).y ? m_points.GetAt(0).y : m_points.GetAt(1).y;
  10. //构建外矩行和内矩形
  11. CRect rect(x1,y1,x2,y2), rect2(x1+5,y1+5,x2-5,y2-5);
  12. //如果在外矩形内并在内矩形外
  13. if(rect.PtInRect(point) && !rect2.PtInRect(point))
  14. return TRUE;
  15. else
  16. return FALSE;
  17. }

3.4.2 判断一点是否属于线段

首先判断一点是否属于这条线段所属的直线,根据直线的判定公式y1/x1 = y2/x2得到x1y2-x2y1=0,但是在画图中应该在直线附近就能选中,所以在本程序中:|x1y2-x2y1| < 偏差,然后判断该点是否属于这条线段。

  1. //计算该点到线段HStrokeLine的两个顶点的线段(x1,y1), (x2,y2)
  2. int x1 = point.x - m_points.GetAt(0).x;
  3. int x2 = point.x - m_points.GetAt(1).x;
  4. int y1 = point.y - m_points.GetAt(0).y;
  5. int y2 = point.y - m_points.GetAt(1).y;
  6. //计算判断量x1*y2 - x2*y1
  7. int measure = x1*y2 - x2*y1;
  8. //误差允许范围,也就是直线的“附近”
  9. int rule = abs(m_points.GetAt(1).x - m_points.GetAt(0).x)
  10. +abs(m_points.GetAt(0).y - m_points.GetAt(1).y);
  11. rule *= m_penWidth;//将线宽考虑进去
  12. //属于直线
  13. if(measure < rule && measure > -rule){
  14. //判断该点是否属于这条线段
  15. if(x1 * x2 < 0)
  16. return TRUE;;
  17. }
  18. return FALSE;

3.4.3 判断一点是否属于椭圆

根据椭圆的定义椭圆上的点到椭圆的两个焦点的距离之和为2a,首先计算出椭圆的a, b, c,然后计算出椭圆的两个焦点。

针对某个点,首先根据点坐标和两个焦点的坐标计算出该点到椭圆焦点的距离,然后减去2a,如果在“附近”,则认为其属于HStrokeEllipse,否则不属于。

  1. //计算椭圆的a, b, c
  2. int _2a = abs(m_points.GetAt(0).x - m_points.GetAt(1).x);
  3. int _2b = abs(m_points.GetAt(0).y - m_points.GetAt(1).y);
  4. double c = sqrt(abs(_2a*_2a - _2b*_2b))/2;
  5. //计算椭圆的焦点
  6. double x1,y1,x2,y2;
  7. if(_2a > _2b){//横椭圆
  8. x1 = (double)(m_points.GetAt(0).x + m_points.GetAt(1).x)/2 - c;
  9. x2 = x1 + 2*c;
  10. y1 = y2 = (m_points.GetAt(0).y + m_points.GetAt(1).y)/2;
  11. }
  12. else{//纵椭圆
  13. _2a = _2b;
  14. x1 = x2 = (m_points.GetAt(0).x + m_points.GetAt(1).x)/2;
  15. y1 = (m_points.GetAt(0).y + m_points.GetAt(1).y)/2 - c;
  16. y2 = y1 + 2*c;
  17. }
  18. //点到两个焦点的距离之和,再减去2a
  19. //distance(point - p1) + distance(point - p2) = 2*a;
  20. double measure = sqrt((x1 - point.x)*(x1-point.x) + (y1 - point.y)*(y1-point.y) )
  21. + sqrt( (point.x - x2)*(point.x - x2) + (point.y - y2)*(point.y - y2))
  22. - _2a;
  23. //计算椭圆的“附近”
  24. double rule = 4*m_penWidth;
  25. if(measure < rule && measure > -rule)
  26. return TRUE;
  27. else
  28. return FALSE;

3.5 文档序列化

MFC提供了良好的序列化机制,只要在类定义时加入DECLARE_SERIAL宏,在类构造函数的实现前加入IMPLEMENT_SERIAL宏,然后实现Serialize方法即可。本程序即使用该方法序列化:
首先在CHDrawDoc类实现Serialize方法,保存画布大小和所有图形信息:

  1. void CHDrawDoc::Serialize(CArchive& ar)
  2. {
  3. if (ar.IsStoring())
  4. {
  5. //保存时,首先保存画布高和宽,然后序列化所有图形
  6. ar<<m_cavasH<<m_cavasW;
  7. m_strokeList.Serialize(ar);
  8. }
  9. else
  10. {
  11. //打开时,首先打开画布高和宽,然后打开所有图形
  12. ar>>m_cavasH>>m_cavasW;
  13. m_strokeList.Serialize(ar);
  14. }
  15. }

m_strokeList.Serialize(ar);这一句很神奇,Debug追踪的时候会发现,容器类会自动序列化容器内的元素数量,并调用每个元素的序列化方法序列化,所以还需要对每个图形元素实现序列化,以HStrokeLine为例:
在HStrokeLine的类声明中:

  1. class HStrokeLine : public HStroke
  2. {
  3. public:
  4. HStrokeLine();
  5. DECLARE_SERIAL(HStrokeLine)

然后在HStrokeLine的构造函数实现前:

  1. IMPLEMENT_SERIAL(HStrokeLine, CObject, 1)
  2. HStrokeLine::HStrokeLine()
  3. {
  4. m_picType = PIC_line;
  5. }

最后实现HStrokeLine的序列化函数,因为这里HStrokeLine集成自HStroke类而且没有特殊的属性,而HStroke类实现了Serialize函数,所以HStrokeLine类不需要实现Serilize方法,看一下HStroke的Serialize方法即可:

  1. void HStroke::Serialize(CArchive& ar)
  2. {
  3. if(ar.IsStoring()){
  4. int enumIndex = m_picType;
  5. ar<<enumIndex<<m_penWidth<<m_penColor;
  6. m_points.Serialize(ar);
  7. }
  8. else{
  9. int enumIndex;
  10. ar>>enumIndex>>m_penWidth>>m_penColor;
  11. m_picType = (enum HPicType)enumIndex;
  12. m_points.Serialize(ar);
  13. }
  14. }

3.6 打开保存导出

文档序列化实现以后,程序的打开和保存功能就已经完成了。但是从序列化方法可以看出,打开和保存的都是矢量图形,所以这里实现了一个导出为BMP图像的方法,导出:

  1. //保存文件对话框,选择导出路径
  2. CFileDialog dlg(FALSE, "bmp","hjz.bmp");
  3. if(dlg.DoModal() != IDOK){
  4. return ;
  5. }
  6. CString filePath = dlg.GetPathName();
  7. //
  8. CClientDC client(this);//用于本控件的,楼主可以不用此句
  9. CDC cdc;
  10. CBitmap bitmap;
  11. RECT rect;CRect r;
  12. GetClientRect(&rect);
  13. int cx = rect.right - rect.left;
  14. int cy = rect.bottom - rect.top;
  15. bitmap.CreateCompatibleBitmap(&client, cx, cy);
  16. cdc.CreateCompatibleDC(NULL);
  17. //获取BMP对象
  18. CBitmap * oldbitmap = (CBitmap* ) cdc.SelectObject(&bitmap);
  19. //白色画布
  20. cdc.FillRect(&rect, CBrush::FromHandle((HBRUSH)GetStockObject(WHITE_BRUSH)));
  21. //画图
  22. for(int i = 0; i < GetDocument()->m_strokeList.GetSize(); i ++){
  23. GetDocument()->m_strokeList.GetAt(i)->DrawStroke(&cdc);
  24. }
  25. cdc.SelectObject(oldbitmap);
  26. ::OpenClipboard(this->m_hWnd);
  27. ::EmptyClipboard();
  28. ::SetClipboardData(CF_BITMAP, bitmap);
  29. ::CloseClipboard();
  30. HBITMAP hBitmap = (HBITMAP)bitmap;
  31. HDC hDC;
  32. int iBits;
  33. WORD wBitCount;
  34. DWORD dwPaletteSize=0, dwBmBitsSize=0, dwDIBSize=0, dwWritten=0;
  35. BITMAP Bitmap;
  36. BITMAPFILEHEADER bmfHdr;
  37. BITMAPINFOHEADER bi;
  38. LPBITMAPINFOHEADER lpbi;
  39. HANDLE fh, hDib, hPal,hOldPal=NULL;
  40. hDC = CreateDC("DISPLAY", NULL, NULL, NULL);
  41. iBits = GetDeviceCaps(hDC, BITSPIXEL) * GetDeviceCaps(hDC, PLANES);
  42. DeleteDC(hDC);
  43. if (iBits <= 1) wBitCount = 1;
  44. else if (iBits <= 4) wBitCount = 4;
  45. else if (iBits <= 8) wBitCount = 8;
  46. else wBitCount = 24;
  47. GetObject(hBitmap, sizeof(Bitmap), (LPSTR)&Bitmap);
  48. bi.biSize = sizeof(BITMAPINFOHEADER);
  49. bi.biWidth = Bitmap.bmWidth;
  50. bi.biHeight = Bitmap.bmHeight;
  51. bi.biPlanes = 1;
  52. bi.biBitCount = wBitCount;
  53. bi.biCompression = BI_RGB;
  54. bi.biSizeImage = 0;
  55. bi.biXPelsPerMeter = 0;
  56. bi.biYPelsPerMeter = 0;
  57. bi.biClrImportant = 0;
  58. bi.biClrUsed = 0;
  59. dwBmBitsSize = ((Bitmap.bmWidth * wBitCount + 31) / 32) * 4 * Bitmap.bmHeight;
  60. hDib = GlobalAlloc(GHND,dwBmBitsSize + dwPaletteSize + sizeof(BITMAPINFOHEADER));
  61. lpbi = (LPBITMAPINFOHEADER)GlobalLock(hDib);
  62. *lpbi = bi;
  63. hPal = GetStockObject(DEFAULT_PALETTE);
  64. if (hPal)
  65. {
  66. hDC = ::GetDC(NULL);
  67. hOldPal = ::SelectPalette(hDC, (HPALETTE)hPal, FALSE);
  68. RealizePalette(hDC);
  69. }
  70. GetDIBits(hDC, hBitmap, 0, (UINT) Bitmap.bmHeight, (LPSTR)lpbi + sizeof(BITMAPINFOHEADER)
  71. +dwPaletteSize, (BITMAPINFO *)lpbi, DIB_RGB_COLORS);
  72. if (hOldPal)
  73. {
  74. ::SelectPalette(hDC, (HPALETTE)hOldPal, TRUE);
  75. RealizePalette(hDC);
  76. ::ReleaseDC(NULL, hDC);
  77. }
  78. fh = CreateFile(filePath, GENERIC_WRITE,0, NULL, CREATE_ALWAYS,
  79. FILE_ATTRIBUTE_NORMAL | FILE_FLAG_SEQUENTIAL_SCAN, NULL);
  80. if (fh == INVALID_HANDLE_VALUE)
  81. return ;
  82. bmfHdr.bfType = 0x4D42; // "BM"
  83. dwDIBSize = sizeof(BITMAPFILEHEADER) + sizeof(BITMAPINFOHEADER) + dwPaletteSize + dwBmBitsSize;
  84. bmfHdr.bfSize = dwDIBSize;
  85. bmfHdr.bfReserved1 = 0;
  86. bmfHdr.bfReserved2 = 0;
  87. bmfHdr.bfOffBits = (DWORD)sizeof(BITMAPFILEHEADER) + (DWORD)sizeof(BITMAPINFOHEADER) + dwPaletteSize;
  88. WriteFile(fh, (LPSTR)&bmfHdr, sizeof(BITMAPFILEHEADER), &dwWritten, NULL);
  89. WriteFile(fh, (LPSTR)lpbi, dwDIBSize, &dwWritten, NULL);
  90. GlobalUnlock(hDib);
  91. GlobalFree(hDib);
  92. 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

  1. void CHDrawDoc::OnDrawLine()
  2. {
  3. //设置当前画图的图形类型为直线
  4. m_picType = PIC_line;
  5. }
  6. ID_DRAW_LINEUPDATE_COMMAND_UIHandlerOnUpdateDrawLine:
  7. void CHDrawDoc::OnUpdateDrawLine(CCmdUI* pCmdUI)
  8. {
  9. //如果当前画图类型为直线,设置菜单项前加对号,工具栏项下沉
  10. pCmdUI->SetCheck(PIC_line == m_picType);
  11. }

3.8 右键菜单修改选中图形的属性


实现方法如下:
第一步:在资源视图中增加一个菜单
第二步:在CHDrawView中增加右键菜单响应函数OnRButtonDown:

  1. void CHDrawView::OnRButtonDown(UINT nFlags, CPoint point)
  2. {
  3. //检查所有处于选中状态的图形,可以有多个
  4. CHDrawDoc *pDoc = GetDocument();
  5. m_strokeSelected.RemoveAll();//首先清空旧数据
  6. for(int i = 0; i < pDoc->m_strokeList.GetSize(); i ++){
  7. if(pDoc->m_strokeList.GetAt(i)->IsHightLight())
  8. m_strokeSelected.Add(pDoc->m_strokeList.GetAt(i));
  9. }
  10. //显示右键菜单
  11. CMenu rmenu;
  12. rmenu.LoadMenu(IDR_MENU_SET);//加载资源中的菜单IDR_MENU_SET
  13. ClientToScreen(&point);//需要坐标转换
  14. rmenu.GetSubMenu(0)->TrackPopupMenu(TPM_LEFTALIGN, point.x, point.y, this);
  15. //因为这里的rmenu是局部变量,所以必须Detach掉
  16. rmenu.Detach();
  17. CView::OnRButtonDown(nFlags, point);
  18. }

第三步:增加菜单响应函数,这里以删除当前所选图形为例:

  1. void CHDrawView::OnPicDelete()
  2. {
  3. //获取存储数据的文档类
  4. CHDrawDoc *pDoc = GetDocument();
  5. //移除所有处于选中状态的图形
  6. int i = 0, j = 0;
  7. for(; i < m_strokeSelected.GetSize(); i ++){
  8. //这里的j没有归0,是有原因的,可以很有效的提高效率
  9. //遍历复杂度为两个数组的和
  10. for(; j < pDoc->m_strokeList.GetSize(); j ++){
  11. if(m_strokeSelected.GetAt(i) == pDoc->m_strokeList.GetAt(j)){
  12. delete pDoc->m_strokeList.GetAt(j);
  13. pDoc->m_strokeList.RemoveAt(j);
  14. break;
  15. }
  16. }
  17. }
  18. //如果没有处于选中状态的图形,则不需要刷新。
  19. if(i > 0)
  20. Invalidate();
  21. }

3.9 撤销和恢复操作

MFC提供了默认的撤销和恢复的ID,但是并没有提供默认实现,本程序的思路是,定义一个数组和一个数组索引,每执行一个操作,就把当前状态存储到数组中,并把数组索引加1。
撤销时,把索引减一的数组元素恢复到当前文档,恢复时,把索引加一的数组元素恢复到当前文档。
在程序中的步骤为:
第一步:定义数组,数组索引和备份,恢复函数:

  1. CObArray m_backup;
  2. int m_backup_index;
  3. void ReStore(BOOL backward);
  4. void BackUp();
  5. void CHDrawDoc::BackUp()
  6. {
  7. //备份操作,有利有弊。简单,节省内存,序列化有变时不需修改;产生文件占据磁盘
  8. CString fileName;
  9. fileName.Format("hjz%d", m_backup.GetSize());
  10. OnSaveDocument(fileName);
  11. //这里使用Insert而不是Add是因为恢复是并没有删除
  12. m_backup.InsertAt(m_backup_index++, NULL, 1);
  13. }
  14. void CHDrawDoc::ReStore(BOOL backward)
  15. {
  16. m_backup_index -= backward ? 1 : -1;//撤销还是恢复
  17. //…把数组元素恢复到当前文档
  18. OnOpenDocument(m_backup.GetAt(m_backup_index-1));
  19. }

第二步:添加撤销和恢复菜单项,并添加消息句柄:

  1. void CHDrawDoc::OnEditUndo()
  2. {
  3. ReStore(TRUE);
  4. UpdateAllViews(NULL);
  5. }
  6. void CHDrawDoc::OnEditRedo()
  7. {
  8. ReStore(FALSE);
  9. UpdateAllViews(NULL);
  10. }

第三步:在每次对文档的修改操作之前,调用GetDocument()->Backup()

3.10 使用鼠标拖拽选中多个图形

首先自HStrokeRect类继承一个HStrokeSelect类,实现DrawStroke方法:

  1. void HStrokeSelect::DrawStroke(CDC *pDC)
  2. {
  3. m_penColor = RGB(255,0,0);
  4. m_penWidth = 1;
  5. m_penStyle = PS_DASH;
  6. HStrokeRect::DrawStroke(pDC);
  7. }

然后在LButtonUp时选中区域内的图形,并将HStrokeSelect对象删除:

  1. //Step0.2 选择框
  2. else if(PIC_select == m_stroke->m_picType){
  3. bool refresh = false;//是否需要刷新
  4. CRect rect(m_stroke->m_points.GetAt(0),m_stroke->m_points.GetAt(1));
  5. for(int i = 0; i < pDoc->m_strokeList.GetSize(); i ++){
  6. //是否在所框区域内
  7. if(rect.PtInRect(pDoc->m_strokeList.GetAt(i)->m_points.GetAt(0)) &&
  8. rect.PtInRect(pDoc->m_strokeList.GetAt(i)->m_points.GetAt(1))){
  9. //设置选中状态
  10. pDoc->m_strokeList.GetAt(i)->m_bSelected = true;
  11. refresh = true;//标志需要刷新
  12. }
  13. }
  14. if(refresh)
  15. Invalidate();//刷新
  16. delete m_stroke;//释放内存
  17. }

3.11 直线HStrokeLine的Tracker只显示两个Point

CRectTracker在选中状态下会显示8个点,这对于矩形是合理的,而对于线段来讲,只要显示两个点就可以了,这里重载了CRectTracker类的Draw方法:

  1. void HStrokeTracker::Draw(CDC* pDC) const{
  2. CRect rect;
  3. //一般图形用CRectTracker的方法即可
  4. CRectTracker::Draw(pDC);
  5. //对于直线
  6. if((m_picType == PIC_line) && ((m_nStyle&(resizeInside|resizeOutside))!=0)){
  7. UINT mask = GetHandleMask();
  8. for (int i = 0; i < 8; ++i) {
  9. if (mask & (1<<i)) {
  10. int p1, p2;
  11. //直线斜率小于0,即左上+右下
  12. if(m_picExtra == 0) {
  13. p1 = 1, p2 = 4;
  14. }
  15. //直线斜率大于0,即左下+右上
  16. else{
  17. p1 = 2, p2 = 8;
  18. }
  19. if( ((1<<i) == p1) || ((1<<i) == p2)){
  20. GetHandleRect((TrackerHit)i, &rect);
  21. pDC->FillSolidRect(rect, RGB(0, 0, 0));
  22. }
  23. else{
  24. GetHandleRect((TrackerHit)i, &rect);
  25. pDC->FillSolidRect(rect, RGB(255, 255, 255));
  26. }
  27. }
  28. }
  29. }
  30. }

3.12 键盘控制

重载PreTranslate函数,响应Ctrl+A,Delete,Shift+(UP|DOWN|LEFT|RIGHT)键盘事件,实现全选,删除所选,控制所选多个图形移动功能。

  1. CHDrawDoc *pDoc = GetDocument();
  2. BOOL deleted = FALSE;
  3. int i, x, y;
  4. if (pMsg->message == WM_KEYDOWN) {
  5. switch (pMsg->wParam){
  6. //删除
  7. case VK_DELETE:
  8. for(i = 0; i <pDoc->m_strokeList.GetSize(); i ++){
  9. if(pDoc->m_strokeList.GetAt(i)->m_bSelected){
  10. pDoc->m_strokeList.RemoveAt(i--);
  11. deleted = TRUE;
  12. }
  13. }
  14. if(deleted)
  15. Invalidate();
  16. break;
  17. //全选
  18. case 'A':
  19. case 'a':
  20. if(::GetKeyState(VK_CONTROL) < 0){
  21. for(int i = 0; i <pDoc->m_strokeList.GetSize(); i ++){
  22. pDoc->m_strokeList.GetAt(i)->m_bSelected = TRUE;
  23. }
  24. Invalidate();
  25. }
  26. break;
  27. //移动
  28. case VK_UP:
  29. case VK_DOWN:
  30. case VK_LEFT:
  31. case VK_RIGHT:
  32. x = (pMsg->wParam==VK_RIGHT) - (pMsg->wParam==VK_LEFT);
  33. y = (pMsg->wParam==VK_DOWN) - (pMsg->wParam==VK_UP);
  34. //Shift键加速移动
  35. if(::GetKeyState(VK_SHIFT) < 0){
  36. x *= 8;
  37. y *= 8;
  38. }
  39. for(int i = 0; i <pDoc->m_strokeList.GetSize(); i ++){
  40. if(pDoc->m_strokeList.GetAt(i)->m_bSelected){
  41. pDoc->m_strokeList.GetAt(i)->Move(x,y);
  42. }
  43. }
  44. Invalidate();
  45. break;
  46. }
  47. }

3.13 对话框控制

HStrokeEditDlg对话框,实现对所有图形的矢量化编辑,可以直接修改图形的坐标,颜色,宽度,删除图形等操作。

3.14 动画程序图标

第一步:在资源中增加5个图标资源ICON:IDI_ICON1~IDI_ICON5
第二步:在CMainFrame中增加变量HICON m_icons[5],并在构造函数中加载资源:

  1. m_icons[0] = LoadIcon(AfxGetApp()->m_hInstance, MAKEINTRESOURCE(IDI_ICON1));
  2. m_icons[1] = LoadIcon(AfxGetApp()->m_hInstance, MAKEINTRESOURCE(IDI_ICON2));
  3. m_icons[2] = LoadIcon(AfxGetApp()->m_hInstance, MAKEINTRESOURCE(IDI_ICON3));
  4. m_icons[3] = LoadIcon(AfxGetApp()->m_hInstance, MAKEINTRESOURCE(IDI_ICON4));
  5. m_icons[4] = LoadIcon(AfxGetApp()->m_hInstance, MAKEINTRESOURCE(IDI_ICON5));

第三步:在CMainFrame的OnCreate函数中加载资源,并启动计数器:

  1. SetClassLong(m_hWnd, GCL_HICON, (LONG)m_icons[0]);
  2. SetTimer(WM_ICONALT, 1000, NULL); //设置计数器每秒

第四步:在计数器函数OnTimer中增加修改图标的代码:

  1. static int iconIndex = 1; //静态变量计算第几个图标
  2. if(nIDEvent == WM_ICONALT){
  3. SetClassLong(m_hWnd, GCL_HICON, (LONG)m_icons[iconIndex]);
  4. iconIndex = (++iconIndex) % 5;
  5. }
  6. CFrameWnd::OnTimer(nIDEvent);

3.15 LButtonDown流程

如果Ctrl键没有被按下,遍历图形数组,如果有Track的图形,执行Track操作

  1. Step1:
  2. If Ctrl键没有被按下
  3. For 每个图形
  4. If 该图形被选中
  5. If Track
  6. 移动图形
  7. 标记b_Track为真,表示当前是Track而不是绘图
  8. EndIf
  9. EndIf
  10. EndFor
  11. EndIf
  12. Step2:
  13. For每个图形,
  14. If当前鼠标点在其范围内
  15. If Ctrl键被按下
  16. 该图形的选中状态取反
  17. Else 没有按下Ctrl
  18. 选中当前图形
  19. EndIf
  20. Else 当前鼠标点不再其范围内
  21. If Ctrl键被按下
  22. 无操作,认为是用户多选时的误操作
  23. Else Ctrl键没有被按下
  24. 取消该图形的选中状态
  25. EndIf
  26. EndIf
  27. End For
  28. Step3
  29. If b_Track为假,表示当前是绘图而不是Track
  30. Step3.1. 设置捕获鼠标SetCapture();
  31. Step3.2. 加入新图形m_stroke = pDoc->NewStroke();
  32. Step3.3. 设置起点m_stroke->SetCurrentPoint(point);
  33. Step4. 设置文件已经修改状态
  34. EndIf

3.16 LButtonUp流程

  1. If 用户点下鼠标后就松开,等于没有画
  2. 删除指针,释放内存
  3. ElseIf 画图类型为选择框
  4. For 每个图形
  5. If 该图形在选择框内
  6. 设置状态为选中
  7. EndIf
  8. EndFor
  9. 删除指针,释放内存
  10. Else 画图
  11. 设置当前鼠标坐标
  12. 加入图形
  13. 备份
  14. EndIf

3.17 MouseMove流程

  1. If 当前处于捕获状态(参考LButtonDown
  2. 重画图形
  3. Else
  4. For 每个图形
  5. If 当前鼠标坐标在该图形内
  6. 设置该图形高亮状态为真
  7. Else
  8. 设置该图形高亮状态为假
  9. EndIf
  10. EndFor
  11. EndIf

4. 总结

4.1 Tricks

4.1.1 子View和父View公用一个Doc

本程序使用了两个View,CHDrawPView中创建了CHDrawView,而为了在CHDrawView中使用CHDrawDoc对象,需要将CHDrawPView的Doc传给CHDrawView。这个需求只要为CHDrawView增加一个方法,将m_pDocument指针传过去即可。

但是这样就引来另外一个问题,在CView析构的时候,它会去析构m_pDocument,这样CHDrawDoc就会被CHDrawView和CHDrawPView一共析构两次,出现错误,为了解决这个问题,在传递m_pDocument的时候,用另外一个CDocument指针储存CHDrawView的m_pDocument,然后在CHDrawView析构函数中,再将m_pDocument修改回去。

4.1.2 在类中获取其它类的句柄

在CMainframe中:

  1. SDI中获取View ((CMainFrame*)AfxGetApp()->m_hMainWnd)->GetActiveView();
  2. SDI中获取View CMainFrame::GetActiveView()
  3. SDI中获取Doc
  4. ((CMainFrame*)AfxGetApp()->m_hMainWnd)->GetActiveDocument();
  5. MDI中获取View
  6. AfxGetApp()->m_pMainWnd)->GetActiveFrame()->GetActiveView();
  7. MDI中获取Doc
  8. AfxGetApp()->m_pMainWnd)->GetActiveFrame()->GetActiveDocument();
  9. MDI中获取View GetActiveFrame()->GetActiveView()
  10. MDI中获取View MDIGetActive()->GetActiveView()
  11. CxxxDoc类中:
  12. MDI中获取View GetFirstViewPosition();
  13. MDI中获取View GetNextView()
  14. CxxxView类中:
  15. SDI中获取Frame getMainFrame AfxGetApp()->m_hMainWnd;
  16. SDI中获取Frame getMainFrame CWnd::GetParentFrame()
  17. SDI中获取Frame getMainFrame AfxGetMainWnd()
  18. SDI中获取Doc GetDocument()
  19. MDI中获取Doc GetDocument();

4.1.3 CRectTracker用法

CRectTracker是MFC提供的一个很好用的类,简单易用。使用步骤:
第一步:声明CRectTracker变量

  1. CRectTracker m_tracker;

第二步:初始化CRectTracker的区域m_rect和样式m_style

  1. m_tracker.m_rect.SetRect(0,0,GetDocument()->m_cavasW, GetDocument()->m_cavasH);
  2. m_tracker.m_nStyle=CRectTracker::resizeOutside;

第三步:override OnSetCursor方法:

  1. CPoint point;
  2. //Step1. get cursor position
  3. GetCursorPos(&point);
  4. //Step2. convert point from screen to client
  5. ScreenToClient(&point);
  6. if(m_tracker.HitTest(point) >= 0){
  7. //Step3. set cursor, **notice, use nHitTest instead of return of tracker
  8. m_tracker.SetCursor(pWnd, nHitTest);

第四步:在OnLButtonDown函数中调用HitTest检测并用Track函数跟踪

  1. int hit = m_tracker.HitTest(point);
  2. switch(hit){
  3. case 2:
  4. case 5:
  5. case 6:
  6. if(m_tracker.Track(this,point)){
  7. //step1. cavas reset
  8. GetDocument()->m_cavasH = m_tracker.m_rect.bottom;
  9. GetDocument()->m_cavasW = m_tracker.m_rect.right;
  10. //step2. scroll or not
  11. CRect clientRect;
  12. GetClientRect(&clientRect);
  13. SetScrollSizes(MM_TEXT, CSize(m_tracker.m_rect.Width()+10, m_tracker.m_rect.Height()+10));
  14. m_drawView->MoveWindow(m_tracker.m_rect.left, m_tracker.m_rect.top,
  15. m_tracker.m_rect.right,m_tracker.m_rect.bottom);
  16. GetDocument()->BackUp();//备份
  17. Invalidate();
  18. }
  19. }

使用时容易出现的问题:
如果在调用CRectTracker的Track方法之前调用了SetCapture函数,会发现Track方法失效。因为SetCapture方法会捕获鼠标事件,而Track则需要独立处理鼠标事件,两个函数争夺鼠标活动的处理权,并以Track的失败告终。

3.1.4 内存泄露

内存泄露问题发生的概率非常高,MFC的Debug功能对内存泄露的检测虽然算不上完美,但是基本够用了,使用F5启动调试,然后尽可能多的执行操作,关闭后在Debug窗口显示调试结构,如果有内存泄露,则会出现以下类型的信息:

  1. Detected memory leaks!
  2. Dumping objects ->
  3. afxtempl.h(370) : {1208} normal block at 0x00376880, 40 bytes long.
  4. Data: < . > BE 00 00 00 2E 00 00 00 AC 00 00 00 A1 00 00 00
  5. E:\code\less01\HDraw\HDrawDoc.cpp(131) : {1202} client block at 0x00376770, subtype 0, 48 bytes long.
  6. a HStrokeLine object at $00376770, 48 bytes long
  7. afxtempl.h(370) : {960} normal block at 0x00376708, 40 bytes long.
  8. Data: < ~ > 92 00 00 00 1E 00 00 00 7E 00 00 00 A8 00 00 00
  9. E:\code\less01\HDraw\HDrawDoc.cpp(131) : {954} client block at 0x003765B0, subtype 0, 48 bytes long.
  10. a HStrokeLine object at $003765B0, 48 bytes long
  11. afxtempl.h(370) : {723} normal block at 0x00376548, 40 bytes long.
  12. Data: <Q [ w > 51 00 00 00 5B 00 00 00 07 01 00 00 77 00 00 00
  13. E:\code\less01\HDraw\HDrawDoc.cpp(131) : {717} client block at 0x00377768, subtype 0, 48 bytes long.
  14. a HStrokeLine object at $00377768, 48 bytes long
  15. afxtempl.h(370) : {422} normal block at 0x00377910, 40 bytes long.
  16. Data: << # Y > 3C 00 00 00 23 00 00 00 E4 00 00 00 59 00 00 00
  17. E:\code\less01\HDraw\HDrawDoc.cpp(131) : {419} client block at 0x00377800, subtype 0, 48 bytes long.
  18. a HStrokeLine object at $00377800, 48 bytes long
  19. Object dump complete.

双击相应的信息就能定位到未释放内存的申请地址,然后考虑应该在什么地方释放。

上传的附件 cloud_download 画图板源码,VC++增强版.rar ( 330.75kb, 0次下载 )
error_outline 下载需要10点积分
eject