2.1 创建工具条 ....................................................................................................................... 2
2.1.1 添加新工具条 ......................................................................................................... 2 2.1.2 在应用程序中显示工具条 ..................................................................................... 6 2.1.3 连接工具条按钮处理函数 ..................................................................................... 8 2.2 使用鼠标绘图 ................................................................................................................. 10
2.2.1 鼠标消息 ............................................................................................................... 10 2.2.2 用鼠标绘制直线段 ............................................................................................... 13 2.2.3 用鼠标绘制椭圆和椭圆区域 ............................................................................... 19 2.2.4 用鼠标绘制矩形区域 ........................................................................................... 20 2.3 图元定义及重画 ............................................................................................................. 22
2.3.1 图元基类CMapElement ....................................................................................... 22 2.3.2 直线段图元子类CLine ......................................................................................... 26 2.3.3 椭圆图元子类CEllipse ......................................................................................... 27 2.3.4 椭圆区域图元子类CEllipseRegion ...................................................................... 27 2.3.5 矩形区域图元子类CRectangleRegion ................................................................. 28 2.3.6 图元重画 ............................................................................................................... 28 2.4 设置线型和区域填充方式.............................................................................................. 33
2.4.1 添加对话框资源 ................................................................................................... 33 2.4.2 创建设置线型和区域填充方式对话框类CSetStyleDlg...................................... 35 2.4.3 完成颜色和示例的实时显示 ............................................................................... 39 2.4.4 在下拉框中绘图 ................................................................................................... 46 2.4.5 初始化对话框 ....................................................................................................... 53 2.4.6 使用用户选择的线型和区域填充方式绘制图元 ............................................... 55 2.5 选中图元 ......................................................................................................................... 62
2.5.1 图元的包围盒 ....................................................................................................... 63 2.5.2 图元的选中判断 ................................................................................................... 64 2.5.3 绘制图元的选中标识 ........................................................................................... 67 2.5.4 用鼠标选中单个图元 ........................................................................................... 69 2.5.5 用鼠标和键盘配合选择多个图元 ....................................................................... 72 2.5.6 选择矩形区域内的多个图元 ............................................................................... 73 2.6 编辑图元 ......................................................................................................................... 77
2.6.1 修改图元的形状 ................................................................................................... 77 2.6.2 移动图元 ............................................................................................................... 84 2.6.3 放大或缩小图元 ................................................................................................... 88 2.6.4 删除图元 ............................................................................................................... 93 2.6.5 图元的剪切、复制和粘贴 ................................................................................... 95 2.6.6 在状态条中显示鼠标光标位置坐标值 ............................................................. 101 2.6.7 撤销图元的绘制和编辑 ..................................................................................... 102 2.7 图元的持久化 ............................................................................................................... 112 2.8 解决闪屏现象 ............................................................................................................... 117 2.9 小结 ............................................................................................................................... 119
第一章 MFC交互绘图基础
在上一章我们所创建的应用程序中,通过添加的菜单项实现了简单的用户和应用程序的交互。用户可以通过选择菜单项,定义使用的画笔和画刷,并通过选择菜单项执行相应的绘图代码来看绘制的图形。但是该应用程序有很多缺点,比如绘制的图形有限,想要绘制新的图形必须修改代码;通过菜单处理函数执行的绘图代码因为没有将图形的信息存储起来,导致图形在窗口进行视图重画时不能够正确显示等等。通常情况下,用户需要使用更灵活的方式来绘制图形。比如像Windows中的“画图”程序一样,用户使用鼠标绘制图形,可以更灵活方便的设置绘图使用的画笔和画刷的类型,并且希望绘制完的图形可以保存起来,以后可以再次打开以前所绘制的图形并进行编辑。
本章将以编写一个简单的绘图应用程序为例,介绍如何在MFC中实现鼠标绘图,如何定义图元的结构以保证应用程序可以正确的重画用户绘制的图形,如何选择和编辑已有的图形,如何保存图形到永久存储介质中等等的编程方法。
这个简单的绘图应用程序将实现以下基本功能:用户使用鼠标绘制图形;通过对话框设置绘制图形使用的线型和颜色以及填充封闭区域的模式和颜色;用户可以选择已经绘制的图形,并可以对该图形进行编辑;可以保存绘制完的图形到永久存储介质(这里是硬盘)中,以便以后可以读取以前绘制的图形,并再次进行编辑。
2.1 创建工具条
创建一个新的MFC项目,项目名称为DrawMap。创建该项目时各步的设置与上一章中创建DrawTest项目时相同,只是在“MFC AppWizard – Step 4 of 6”对话框中不选择Printing and print preview复选框。
在上一章的应用程序中,用户需要通过选择菜单项来选择要执行的功能。当菜单项的层数比较多的时候,用户需要点击的次数较多。对于一些常用的功能,用户会希望能够更容易的选择到,此时就可以使用工具条。
对于本章中要创建的绘图应用程序来说,绘图功能是常用功能,所以可以将这些功能的选择做成工具条。用户通过点击工具条按钮,就能选择要绘制的图形的类型,然后用鼠标进行绘图。 2.1.1 添加新工具条
我们创建应用程序项目时,在“MFC AppWizard – Step 4 of 6”对话框中选择了Docking toolbar复选框,此时系统会在应用程序中创建一个默认的初始工具条。该工具条的样式如图2.1所示。
我们可以修改此工具条,在该工具条中添加新的按钮来对应绘图功能。不过,通常情况下,因为一个应用程序窗口可以有多个工具条,为了把相类似的功能放
在同一个工具条中,我们准备在绘图应用程序中添加一个新的工具条,把绘图功能按钮放在该工具条中。在已有的工具条中添加新的按钮和在新建的工具条中添加按钮是一样的,所以读者只需要学会如何添加新的工具条,也就学会了如何修改已有的工具条。
选择资源面板,用鼠标右键点击“Toolbar”节点,弹出快捷菜单,如图2.2所示。
在快捷菜单中选择“Insert…”,出现“Insert Resource”对话框,如图2.3所示。
该对话框用于在项目中添加各种资源。对话框左边的列表框中列出了可添加的资源种类。选择“Toolbar”,添加一个新的工具条资源,然后单击“New”(新建)按钮,系统会在项目中添加一个新的工具条。也可以在图2.2的快捷菜单中选择“Insert Toolbar”直接插入一个工具条。
此时,在资源面板的“Toolbar”节点下我们会看到两个节点。一个是“IDR_MAINFRAME”,该工具条是默认的初始工具条。另一个是“IDR_TOOLBAR1”,它是我们新添加的工具条,名称是系统起的默认名称。用鼠标右键点击该节点。在弹出的快捷菜单中(图2.2所示快捷菜单)选择“Properties”,会出现“Toolbar
Properties”(工具条属性对话框),如图2.4所示。
在“ID”下拉框中,我们可以修改当前工具条的ID,该ID用于标识工具条。此处我们将此ID修改为IDR_DRAW。
添加新工具条完毕,现在需要在工具条中添加工具条按钮。在资源面板中选中“IDR_DRAW”节点,我们可以在右侧的工具条编辑区中编辑此工具条,如图2.5所示。
在编辑区的上端是完成后工具条的样式,现在工具条中只有一个空白的按钮,是系统自动添加的。下部的左侧是选中的工具条按钮的样式预览。中间是按钮的绘制区,用户在该区域中绘制工具条按钮的图形样式。右侧是绘图工具条,该工具条提供给用户简单的绘图工具,可以用于绘制工具条按钮。
现在我们来绘制工具条按钮。在此之前需要确定该工具条中有几个按钮,每个按钮都是什么功能。我们现在要绘制的是用于选择绘图类型的工具条按钮。在本章要创建的绘图应用程序中准备让用户可以绘制四种类型的图形:直线段,椭
圆,椭圆区域,矩形区域。其中椭圆指只有边界线的椭圆,而椭圆区域除了边界线之外,还要对内部进行填充。因为本应用程序只是用来学习如何用MFC进行交互绘图,所以没有提供更多的绘图类型。工具条按钮的图形样式最好能够直观的表现出该按钮的功能。
在工具条编辑区的绘图工具条中选择绘制直线,然后在中间的绘图区中画一条直线段,如图2.6所示。
此工具条按钮可以直观的表明该按钮用于绘制直线段。同时系统在该工具条按钮右侧自动添加一个空白按钮。用鼠标左键双击我们刚刚绘制的工具条按钮,会出现“Toolbar Button Properties”(工具条按钮属性)对话框,如图2.7所示。
在“ID”下拉框中输入该工具条按钮的ID为ID_DRAWLINE。在“Prompt”输入框中输入说明“绘制直线段”,该说明为按钮的提示。
按照相同的方法可以绘制其他三个工具条按钮,并设置相应的属性,具体数据如下表所示:
工具条按钮 ID ID_DRAWLINE ID_DRAWELLIPSE ID_DRAWELLIPSEREGION ID_DRAWRECTANGLE 绘制完的工具条如图2.8所示。 Prompt 绘制直线段 绘制椭圆 绘制椭圆区域 绘制矩形区域
2.1.2 在应用程序中显示工具条
新的工具条创建完毕,此时如果我们运行应用程序,会发现该工具条并没有显示出来,这是因为我们还没有编写代码将该工具条加入到应用程序窗口中。下面我们来看一下如何将工具条加入到应用程序窗口中。
首先,选择类面板,双击CMainFrame节点,在右侧的编辑区中将打开CMainFrame类的头文件。在头文件中我们可以找到如下代码:
protected: // control bar embedded members CStatusBar m_wndStatusBar; CToolBar m_wndToolBar;
这里声明了一个CStatusBar类对象变量m_wndStatusBar和一个CToolBar类对象变量m_wndToolBar。它们分别对应了系统自动添加的默认状态栏和默认的初始工具条。CStatusBar是MFC封装的一个状态栏类,而CToolBar类是一个工具条类。想要操作工具条就必须首先声明一个工具条的对象。这里我们添加如下代码:
CToolBar m_DrawToolBar;//绘图工具条对象
该对象将用于与绘图工具条对应。在类面板中双击CMainFrame节点下的OnCreate节点,在编辑区打开CMainFrame类的CPP文件,并定位到该类的OnCreate成员函数处。该成员函数在主窗口创建的时候调用,在此函数中可以给主窗口添加工具条和状态栏。此时该成员函数的代码如下:
int CMainFrame::OnCreate(LPCREATESTRUCT lpCreateStruct) {
if (CFrameWnd::OnCreate(lpCreateStruct) == -1) return -1;
//创建默认初始工具条
if (!m_wndToolBar.CreateEx(this, TBSTYLE_FLAT, WS_CHILD | WS_VISIBLE | CBRS_TOP | CBRS_GRIPPER | CBRS_TOOLTIPS | CBRS_FLYBY | CBRS_SIZE_DYNAMIC) || !m_wndToolBar.LoadToolBar(IDR_MAINFRAME))
{ TRACE0(\"Failed to create toolbar\\n\"); return -1; // fail to create }
//创建默认状态栏
if (!m_wndStatusBar.Create(this) || !m_wndStatusBar.SetIndicators(indicators, sizeof(indicators)/sizeof(UINT))) { TRACE0(\"Failed to create status bar\\n\"); return -1; // fail to create }
// TODO: Delete these three lines if you don't want the toolbar to // be dockable
m_wndToolBar.EnableDocking(CBRS_ALIGN_ANY); EnableDocking(CBRS_ALIGN_ANY); DockControlBar(&m_wndToolBar);
return 0; }
看一下此函数中创建默认初始工具条的代码,会发现分别调用了工具条类CToolBar的CreateEx函数和LoadToolBar函数来生成和初始化工具条。
LoadToolBar函数,用于加载指定的工具条资源,其函数声明如下: BOOL LoadToolBar(LPCTSTR lpszResourceName); BOOL LoadToolBar(UINT nIDResource);
其中第一个函数的参数lpszResourceName为指向要加载的工具条资源名称的指针,第二个函数的参数nIDResource是要加载的工具条资源的ID,通常都使用第二个函数来加载工具条。在当前函数中就是通过默认初始工具条的ID(IDR_MAINFRAME)来加载的。如果加载成功,函数返回TRUE,否则返回FALSE。
CreateEx函数,用于初始化工具条,其函数声明如下:
BOOL CreateEx(CWnd* pParentWnd, DWORD dwCtrlStyle = TBSTYLE_FLAT, DWORD dwStyle = WS_CHILD | WS_VISIBLE | CBRS_ALIGN_TOP, CRect rcBorders = CRect(0, 0, 0, 0), UINT nID = AFX_IDW_TOOLBAR);
其中参数pParentWnd为指向包含工具条的父窗口的指针。参数dwCtrlStyle指定了工具条的附加风格,值TBSTYLE_FLAT指定了工具条为一个水平风格的工具条;参数dwStyle指定了工具条所具有的各种风格,该参数可以设为多个可选值的组合值,各值之间用“|”连接。WS_CHILD指定工具条为一个子工具条,WS_VISIBLE指定工具条可见,CBRS_TOP指定工具条在窗口的顶端出现,CBRS_GRIPPER指定工具条最左端有一凸起的竖条并且使工具条可移动,CBRS_TOOLTIPS使工具条按钮具有提示特性,CBRS_FLYBY使光标在工具条按钮上时显示按钮提示(如果没有此风格,则只有在实际按下鼠标键时才显示提示),CBRS_SIZE_DYNAMIC指定了工具条大小为动态的。参数rcBorders指定了工具条的边框,默认的值为没有边框。参数nID为工具条的子窗口ID。通常后两个参数使用默认值即可,在调用函数时不用传入。如果工具条初始化成功,函数返回TRUE,否则返回FALSE。
我们在创建默认初始工具条的代码下添加如下代码: //创建绘图工具条
if (!m_DrawToolBar.CreateEx(this, TBSTYLE_FLAT, WS_CHILD | WS_VISIBLE | CBRS_TOP | CBRS_GRIPPER | CBRS_TOOLTIPS | CBRS_FLYBY | CBRS_SIZE_DYNAMIC) || !m_DrawToolBar.LoadToolBar(IDR_DRAW))
{ TRACE0(\"Failed to create toolbar\\n\"); return -1; // fail to create }
该段代码在m_DrawToolBar工具条对象中加载IDR_DRAW工具条,并初始化该对象,如果失败则返回窗口创建失败。
初始化工具条完成后,可以设置工具条的停放能力。看OnCreate函数中的如下代码:
m_wndToolBar.EnableDocking(CBRS_ALIGN_ANY); EnableDocking(CBRS_ALIGN_ANY); DockControlBar(&m_wndToolBar);
该段代码首先调用CToolBar的成员函数EnableDocking来设置工具条本身的停放,参数值CBRS_ALIGN_ANY指定工具条可以停放在窗口的四个边框的任意一边(也可选CBRS_ALIGN_TOP、CBRS_ALIGN_BOTTOM、CBRS_ALIGN_LEFT、CBRS_ALIGN_RIGHT等值,指定具体停放在哪一边,也可以是可选值的组合)。然后调用窗口类的EnableDocking函数指定主窗口允许的停放,参数值CBRS_ALIGN_ANY与上一个函数中的参数值意义相同,即主窗口允许工具条停放在窗口的四个边框的任意一边。最后调用窗口类的DockControlBar函数,将指定的工具条放在初始位置(窗口的视图区的左上方边框)。如果省略这三个函数,则工具条变成标准工具条,固定在窗口的上方。这里需要注意的是,因为DockControlBar函数要将工具条放在窗口的上边框处,所以EnableDocking函数指定的窗口允许停放位置必须包含CBRS_ALIGN_TOP(或者使用CBRS_ALIGN_ANY),否则运行将出错。我们可以指定新添加的工具条的停放状态,修改上面的三行代码如下:
m_wndToolBar.EnableDocking(CBRS_ALIGN_ANY); //设置绘图工具条的停放状态
m_DrawToolBar.EnableDocking(CBRS_ALIGN_ANY); EnableDocking(CBRS_ALIGN_ANY); DockControlBar(&m_wndToolBar); //在主窗口中放置绘图工具条 DockControlBar(&m_DrawToolBar); 添加了如上代码之后,我们就将刚才新建的工具条加入到了主窗口中,运行应用程序,我们将在默认的初始工具条下面看到我们新添加的绘图工具条。该工具条与初始工具条一样,可以移动位置,并可以停放在窗口的四个边框中的任意一边上。但是此时该工具条中的按钮都处于不可用状态,这是因为还没有为工具条按钮连接处理函数。
2.1.3 连接工具条按钮处理函数
连接工具条按钮处理函数类似于给菜单项连接处理函数。用Ctrl+W打开类向导对话框,在类下拉框中选择CDrawMapView类,在资源ID列表中选择工具条按钮的ID,如ID_DRAWLINE,在消息列表中列出了工具条按钮支持的消息(与菜单项相同)。双击COMMAND消息,在出现的添加处理函数对话框中直接选择
OK按钮,使用默认的函数名称。如图2.9所示。
此时可以双击成员函数列表中的对应成员函数来进行编辑,也可以一次把所有的工具条按钮的处理函数(总共四个)都创建出来再统一编辑。
我们在这四个工具条按钮的处理函数中要确定的是绘图的类型,即需要知道用户想要用鼠标绘制什么样的图形。可以采用如下的方法:在CDrawMapView类中添加一个成员变量,声明如下:
int m_DrawType;//绘图类型
因为在本章的绘图应用程序中除了可以绘制图形之外,还可以选择已绘制的图形并进行编辑,所以要增加一个变量来标识当前是否处于绘图状态。在CDrawMapView类中添加一个成员变量,声明如下:
BOOL m_isDraw;//是否正在绘图
该变量为true,表示当前正处于绘图状态,为false,则表示没有处于绘图状态。在构造函数中将此变量初始化为true。
然后在工具条按钮的处理函数中分别给m_DrawType设置不同的值来代表绘制不同的图形,并设置当前处于绘图状态。在鼠标绘图时,通过判断m_DrawType的值来完成不同的图形的绘制。编写工具条按钮处理函数如下:
//绘制直线段工具条按钮处理函数 void CDrawMapView::OnDrawline() {
// TODO: Add your command handler code here m_DrawType = 1;//1表示绘制直线段 m_isDraw = true;//初始状态为绘图状态 }
//绘制椭圆工具条按钮处理函数
void CDrawMapView::OnDrawellipse() {
// TODO: Add your command handler code here m_DrawType = 2;//2表示绘制椭圆 m_isDraw = true;//当前处于绘图状态 }
//绘制椭圆区域工具条按钮处理函数
void CDrawMapView::OnDrawellipseregion() {
// TODO: Add your command handler code here m_DrawType = 3;//3表示绘制椭圆区域 m_isDraw = true;//当前处于绘图状态 }
//绘制矩形区域工具条按钮处理函数 void CDrawMapView::OnDrawrectangle() {
// TODO: Add your command handler code here m_DrawType = 4;//4表示绘制矩形区域 m_isDraw = true;//当前处于绘图状态 }
m_DrawType变量分别用1,2,3和4表示绘制直线段,椭圆,椭圆区域和矩形区域。同时需要在CDrawMapView类的构造函数中添加如下代码:
m_DrawType = 1;//默认初始绘图状态为绘制直线段 m_isDraw = true;//当前处于绘图状态 即应用程序的初始状态为绘制直线段。
2.2 使用鼠标绘图
在编写鼠标绘图的代码之前,首先要确定如何用鼠标完成绘图。以用鼠标绘制直线段为例:首先将鼠标的光标移动到直线段的一个端点处,按下鼠标左键,然后按住鼠标左键不放,移动鼠标光标到直线段的另一个端点处,此时松开鼠标左键,就完成了用鼠标绘制直线段,应用程序会在两个端点之间绘制一条直线段。绘制椭圆和椭圆区域比较类似,先后确定的是椭圆的外接矩形的两个对角点。而对于绘制矩形区域,则确定的是矩形区域的两个对角点。
为了在应用程序中响应用户的鼠标动作,就需要在编写应用程序时选择响应鼠标消息并编写其对应的处理函数。 2.2.1 鼠标消息
针对用户使用鼠标的一些基本操作,比如鼠标的单击、双击、移动等,Windows提供了相应的通用消息。这些鼠标消息按照鼠标动作发生的区域可以分为两大类:视图区鼠标消息和非视图区鼠标消息。
非视图区鼠标消息指鼠标光标在应用程序窗口视图区外的非视图区发生动作时产生的鼠标消息。非视图区包括标题栏、最小化和最大化按钮、关闭窗口按钮、系统菜单栏和窗口框架等。非视图区鼠标消息虽然用得比较少,但对于应用程序窗口的管理是有用的。通过非视图区鼠标消息可以知道窗口何时进行移动、关闭或改变大小。下表中列出了常用的非视图区鼠标消息及其含义: 非视图区鼠标消息 含义 WM_NCMOUSEMOVE 非视图区鼠标移动 WM_NCLBUTTONUP 非视图区鼠标左键抬起 WM_NCLBUTTONDBLCLK 非视图区鼠标左键双击 WM_NCLBUTTONDOWN 非视图区鼠标左键按下 WM_NCRBUTTONUP 非视图区鼠标右键抬起 WM_NCRBUTTONDBLCLK 非视图区鼠标右键双击 WM_NCRBUTTONDOWN 非视图区鼠标右键按下 视图区鼠标消息指鼠标光标在应用程序窗口视图区内发生动作时产生的鼠标消息。视图区鼠标消息比较常用,用鼠标绘图就要使用视图区鼠标消息。下表列出了常用的视图区鼠标消息及其含义: 视图区鼠标消息 含义 WM_MOUSEMOVE 视图区鼠标移动 WM_LBUTTONUP 视图区鼠标左键抬起 WM_LBUTTONDBLCLK 视图区鼠标左键双击 WM_LBUTTONDOWN 视图区鼠标左键按下 WM_RBUTTONUP 视图区鼠标右键抬起 WM_RBUTTONDBLCLK 视图区鼠标右键双击 WM_RBUTTONDOWN 视图区鼠标右键按下 实现对鼠标消息的处理要完成以下工作: (1) 定义鼠标消息处理函数;
(2) 使用消息映像宏实现鼠标消息和消息处理函数间的消息映像; (3) 编写鼠标消息处理函数的代码。
下表列出了视图区鼠标消息对应的消息映像宏及消息处理函数: 视图区鼠标消息 消息映像宏 消息处理函数 afx_msg void OnMouseMove WM_MOUSEMOVE ON_WM_MOUSEMOVE() (UINT nFlags, CPoint point); WM_LBUTTONUP WM_LBUTTONDBLCLK WM_LBUTTONDOWN WM_RBUTTONUP ON_WM_LBUTTONUP() afx_msg void OnLButtonUP (UINT nFlags, CPoint point); (UINT nFlags, CPoint point); ON_WM_LBUTTONDBLCLK() afx_msg void OnLButtonDblClk ON_WM_LBUTTONDOWN() ON_WM_RBUTTONUP() afx_msg void OnLButtonDown (UINT nFlags, CPoint point); afx_msg void OnRButtonUp (UINT nFlags, CPoint point); (UINT nFlags, CPoint point); WM_RBUTTONDBLCLK ON_WM_RBUTTONDBLCLK() afx_msg void OnRButtonDblClk WM_RBUTTONDOWN ON_WM_RBUTTONDOWN() afx_msg void OnRButtonDown (UINT nFlags, CPoint point); 其中消息处理函数的参数point是CPoint对象,它存储了产生鼠标消息时鼠标光标所处位置的坐标,如鼠标左键按下的处理函数中传入的point参数中存放了鼠标左键按下位置的坐标。参数nFlags是一个无符号数,它表明了在鼠标消息产生的时候鼠标按钮及部分键盘按键的状态,其值可为下表中值的任意组合: nFlags参数值 说明 MK_CONTROL Ctrl键按下 MK_LBUTTON 鼠标左键按下 MK_MBUTTON 鼠标中键按下 MK_RBUTTON 鼠标右键按下 MK_SHIFT Shift键按下 比如在鼠标左键按下的处理函数中,如果参数nFlags传入的值为MK_CONTROL,则表示在鼠标左键按下的同时键盘上的Ctrl键也被按下。
我们可以使用类向导来添加鼠标消息处理函数,应用程序框架将会自动填写代码完成鼠标消息和其处理函数之间的映像。打开类向导,在类列表中选择CDrawMapView类,在消息列表框中选择WM_LBUTTONDOWN消息并用鼠标左键双击,此时类向导自动在成员函数列表框中添加该消息的处理函数。因为该处理函数的名称不能修改,所以不会出现增加成员函数对话框。用同样方法添加WM_MOUSEMOVE消息和WM_LBUTTONUP消息的处理函数,因为前面我们制定的鼠标绘图方法中将要用到这三种鼠标消息的处理函数。我们打开CDrawMapView类的头文件,可以在其中看到如下代码:
// Generated message map functions protected:
//{{AFX_MSG(CDrawMapView) afx_msg void OnDrawline(); afx_msg void OnDrawellipse();
afx_msg void OnDrawellipseregion(); afx_msg void OnDrawrectangle();
afx_msg void OnLButtonDown(UINT nFlags, CPoint point); afx_msg void OnMouseMove(UINT nFlags, CPoint point); afx_msg void OnLButtonUp(UINT nFlags, CPoint point); //}}AFX_MSG
DECLARE_MESSAGE_MAP()
前面四句代码声明了工具条按钮的处理函数,而后三句代码则声明了鼠标消息的处理函数。
打开CDrawMapView类的类文件(CPP文件),在文件的开始部分可以看到如下代码:
BEGIN_MESSAGE_MAP(CDrawMapView, CView) //{{AFX_MSG_MAP(CDrawMapView)
ON_COMMAND(ID_DRAWLINE, OnDrawline)
ON_COMMAND(ID_DRAWELLIPSE, OnDrawellipse)
ON_COMMAND(ID_DRAWELLIPSEREGION, OnDrawellipseregion) ON_COMMAND(ID_DRAWRECTANGLE, OnDrawrectangle) ON_WM_LBUTTONDOWN()
ON_WM_MOUSEMOVE() ON_WM_LBUTTONUP() //}}AFX_MSG_MAP END_MESSAGE_MAP()
该段代码完成了消息及对应的处理函数之间的映像。其中前四句代码是工具条按钮与处理函数的映像,而后三句代码是鼠标消息和处理函数间的映像。之所以鼠标消息的映像中没有指定处理函数名,是因为鼠标消息处理函数的名称是固定的,不能修改。以上代码是类向导自动添加的,如果我们不使用类向导来创建鼠标消息的处理函数,也可以手动添加以上代码,效果是一样的。
现在看一下应用程序框架创建的原始的鼠标消息处理函数,代码如下: //鼠标左键按下处理函数
void CDrawMapView::OnLButtonDown(UINT nFlags, CPoint point) {
// TODO: Add your message handler code here and/or call default
CView::OnLButtonDown(nFlags, point); }
//鼠标移动处理函数
void CDrawMapView::OnMouseMove(UINT nFlags, CPoint point) {
// TODO: Add your message handler code here and/or call default
CView::OnMouseMove(nFlags, point); }
//鼠标左键抬起处理函数
void CDrawMapView::OnLButtonUp(UINT nFlags, CPoint point) {
// TODO: Add your message handler code here and/or call default
CView::OnLButtonUp(nFlags, point); }
我们发现在函数中已经添加了一句代码,这行代码是调用对应的父类的鼠标消息处理函数进行一些默认处理。我们所添加的代码都必须添加到该句代码之前,并且此句代码不能删除,否则将会出错。
同时需要注意的是,Windows的鼠标消息发生的间隔是一秒钟,并不是所有的鼠标动作都会产生鼠标消息。假设你按鼠标左键的速度足够快,在一秒钟内可以按鼠标左键多次,则并不是每次按键都会产生鼠标消息,只有第一次按键以及后面按键与前一次按键的时间间隔在一秒钟以上的那些按键才会产生鼠标消息。 2.2.2 用鼠标绘制直线段
现在看一下如何编码实现用鼠标绘制直线段。既然鼠标消息分为视图区鼠标消息和非视图区鼠标消息,那么如果在鼠标绘图过程中,鼠标在视图区内按下左键,然后移动到视图区外才把鼠标左键抬起,应用程序窗口就得不到视图区的鼠
标左键抬起消息。因此在用鼠标绘制图形之前应该首先捕捉鼠标,使当前视图区接受所有的鼠标操作引起的鼠标消息。 2.2.2.1 捕捉鼠标
我们可以用SetCapture函数和ReleaseCapture函数捕捉和释放鼠标。 SetCapture函数,用于捕捉鼠标,其函数声明如下: CWnd* SetCapture();
该函数是窗口基类CWnd的成员函数,因为CView类是从CWnd类派生而来的,所以它也拥有该函数。该函数返回捕捉了鼠标的窗口指针。执行该函数后,视图类就捕捉了鼠标,此后的鼠标动作都将产生视图区鼠标消息。
ReleaseCapture函数,用于释放鼠标,其函数声明如下: BOOL ReleaseCapture();
该函数用于释放被捕捉的鼠标。在用鼠标绘图完毕后,需要调用该函数释放鼠标,否则窗口将不能正确接受鼠标消息。
除了在鼠标绘图开始时捕捉鼠标之外,也可以通过调用ClipCursor函数将鼠标限制在指定区域中以避免在鼠标绘图过程中出现不同类型的鼠标消息。
ClipCursor函数,用于将鼠标限制在指定的矩形区域中,其函数声明如下: BOOL ClipCursor(CONST RECT lpRect);
参数lpRect指向一个RECT结构体,该结构体定义了一个矩形区域,该函数将鼠标限制在此矩形区域中。如果鼠标光标要移动到矩形区域外,系统将自动调正鼠标光标位置,使其始终在指定的矩形区域内。通常用如下代码把鼠标限制在应用程序窗口的视图区内:
CRect rect;//矩形区域对象
GetClientRect(&rect);//获得并保存窗口视图区区域坐标
ClientToScreen(&rect);//用视图区区域坐标重新计算屏幕坐标 ClipCursor(&rect);//限制鼠标在窗口视图区中 上面的代码将鼠标限制在窗口的视图区中,这样鼠标只能在视图区中产生动作,也就只会产生视图区鼠标消息。其中用到的GetClientRect函数和ClientToScreen函数都是CWnd类的成员函数。
GetClientRect函数,用于获得窗口视图区的矩形区域坐标,其函数声明如下:
void GetClientRect(LPRECT lpRect) const;
该函数将窗口的视图区的左上角和右下角坐标存放在lpRect指针指向的矩形区域结构中。
ClientToScreen函数,用于将传入的矩形区域坐标或点坐标转化成实际的屏幕坐标,其函数声明如下:
void ClientToScreen(LPPOINT lpPoint) const; void ClientToScreen(LPRECT lpRect) const;
参数lpPoint和lpRect分别指向点结构和矩形区域结构,该函数将传入的点的坐标或矩形区域的坐标(左上角点和右下角点坐标)转换成实际的屏幕坐标,这样调用ClipCursor函数时才能将鼠标限制在正确的区域中。
被限制的鼠标在绘图完毕后应该取消限制,采用如下语句来完成取消对鼠标的限制:
ClipCursor(NULL);
限制鼠标移动范围会加大系统负担,所以通常不采用此种方法。
2.2.2.2 设置鼠标光标形状
在用鼠标绘制图形时,我们希望修改鼠标光标形状,而不是使用默认的斜箭头光标。鼠标的光标形状由专门的光标(Cursor)资源所决定。我们可以在资源面板上向项目中添加光标资源(在添加资源对话框中有光标资源类型,如图2.3所示),每个光标资源对应一个唯一的资源ID,应用程序框架通过该ID来识别光标资源。要使用光标资源作为鼠标的光标形状,需要首先将光标资源加载到系统中,然后再设置鼠标光标形状为加载的光标资源的形状。
LoadCursor函数,用于加载光标资源,其函数声明如下: HCURSOR LoadCursor(LPCTSTR lpszResourceName) const; HCURSOR LoadCursor(UINT nIDResource) const;
参数lpszResourceName和nIDResouce分别为光标资源的名称和ID号,函数将指定的光标资源加载到系统内存中。函数返回光标资源句柄(HCURSOR)。
LoadStandardCursor函数,用于加载Windows预定义的光标资源,其函数声明如下:
HCURSOR LoadStandardCursor(LPCTSTR lpszCursorName) const;
参数lpszCursorName是由一些以IDC_开头的光标资源名称,用来指定Windows预定义的光标资源,下表中列出了预定义的光标资源名称和对应的光标形状: 预定义的光标资源名称 光标形状 IDC_ARROW 标准的斜箭头光标 IDC_IBEAM 标准的插入文本光标 IDC_WAIT 沙漏形状的光标 IDC_CROSS 标准的十字光标 IDC_UPARROW 向上方向的箭头 IDC_SIZEALL 带有四个方向箭头的光标 IDC_SIZENSWE 带有左上和右下方向箭头的光标 IDC_SIZENESW 带有右上和左下方向箭头的光标 IDC_SIZEWE 带有左右方向箭头的光标 IDC_SIZENS 带有上下方向箭头的光标 该函数同样返回光标资源句柄。 上面两个加载光标资源的函数都是应用程序基类CWinApp的成员函数,在视图类中要使用该函数,需要调用AfxGetApp()函数获得应用程序基类的指针,然后再调用加载光标资源函数。
加载完光标资源后,调用SetCursor函数设置使用光标资源。
SetCursor函数,用于设置当前使用的光标资源,其函数声明如下: HCURSOR SetCursor(HCURSOR hCursor);
参数hCursor为要设置的光标资源句柄。函数返回原来使用的光标资源的句柄。
在我们的绘图应用程序中,使用鼠标绘图时,设置鼠标光标形状为标准的十字光标。在CDrawMapView类中加入下面的成员变量:
HCURSOR m_Cursor;//光标资源句柄
该变量存放应用程序当前使用的光标资源句柄。在绘图工具条按钮的处理函数中设置对应的光标资源句柄,添加如下代码到四个绘图工具条按钮的处理函数
中:
//设置鼠标光标形状为标准十字光标
m_Cursor = AfxGetApp()->LoadStandardCursor(IDC_CROSS); 因为应用程序初始状态就是绘图状态(绘制直线段),所以此代码也需加入到CDrawMapView类的构造函数中。然后只需在鼠标消息处理函数中加入如下代码设置使用光标资源:
SetCursor(m_Cursor);
三个鼠标消息的处理函数中都要加入该行代码。假设在鼠标左键按下的处理函数中不设置使用鼠标资源,则当鼠标左键按下时,鼠标光标将变回到默认的斜箭头状态。
2.2.2.3 使用橡皮线绘图
在使用鼠标绘图的时候,当鼠标左键按下时表示绘图开始,此时随着鼠标光标的移动,希望实时的把图形绘制出来,这样用户可以随时看到自己要绘制的图形是什么样的,而不是只有到最后鼠标左键抬起的时候才把图形绘制出来。为了实现这种效果可以在鼠标移动消息处理函数中就把当前图形绘制出来,这样每当鼠标移动消息处理函数被调用的时候都会将当前鼠标光标所处位置和鼠标左键按下位置所确定的图形绘制出来。但是如果一直绘图的话,每次绘制的图形都留在视图区中,会产生许多根本不需要的图形。所以正确的做法是每次绘制图形时都先擦除上次所绘制的图形,然后再绘制新的图形。这种绘图方法就称为使用橡皮线绘图(意指绘图线像橡皮一样可以擦除以前绘制的图形)。
因为在本章的绘图应用程序中除了要用鼠标绘制图形之外,还要用鼠标选择图形并进行编辑,所以我们单独编写三个函数,分别应用在鼠标绘图时、鼠标移动、鼠标左键抬起三个鼠标消息的处理上。在CDrawMapView类中添加下面三个成员函数:
//鼠标绘图时鼠标左键按下消息处理函数
void DrawLButtonDown(UINT nFlags, CPoint point); //鼠标绘图时鼠标移动消息处理函数
void DrawMouseMove(UINT nFlags, CPoint point); //鼠标绘图时鼠标左键抬起消息处理函数 void DrawLButtonUp(UINT nFlags, CPoint point);
这三个函数的参数与系统的鼠标消息处理函数参数相同,我们分别编写这三个函数,编写完的代码如下:
//鼠标绘图时鼠标左键按下处理函数
void CDrawMapView::DrawLButtonDown(UINT nFlags, CPoint point) {
SetCursor(m_Cursor);//设置使用光标资源
this->SetCapture();//捕捉鼠标
//设置开始点和终止点,此时为同一点 m_StartPoint = point; m_EndPoint = point;
m_LButtonDown = true;//设置鼠标左键按下 }
//鼠标绘图时鼠标移动处理函数
void CDrawMapView::DrawMouseMove(UINT nFlags, CPoint point) {
SetCursor(m_Cursor);//设置使用光标资源 CClientDC dc(this);//构造设备环境对象
//判断鼠标移动的同时鼠标左键按下,并且要绘制的是直线段 if (m_LButtonDown && m_DrawType == 1) { dc.SetROP2(R2_NOT);//设置绘图模式为R2_NOT //重新绘制前一个鼠标移动消息处理函数绘制的直线段 //因为绘图模式的原因,结果是擦除了该线段 dc.MoveTo(m_StartPoint); dc.LineTo(m_EndPoint); //绘制新的直线段 dc.MoveTo(m_StartPoint); dc.LineTo(point); //保存新的直线段终点 m_EndPoint = point; } }
//鼠标绘图时鼠标左键抬起处理函数
void CDrawMapView::DrawLButtonUp(UINT nFlags, CPoint point) {
SetCursor(m_Cursor);//设置使用光标资源 ReleaseCapture();//释放鼠标
CClientDC dc(this);//构造设备环境对象
//绘制的是直线段 if (m_DrawType == 1) { //绘制最终要绘制的直线段 dc.MoveTo(m_StartPoint); dc.LineTo(m_EndPoint); } }
在处理函数中用到的变量m_StartPoint和m_EndPoint用于存放所要绘制的直线段的起始点坐标和终止点坐标。这两个变量是CDrawMapView的成员变量,所以我们要在CDrawMapView类中添加这两个变量,其类型是CPoint:
CPoint m_EndPoint;//鼠标绘图终止点坐标 CPoint m_StartPoint;//鼠标绘图开始点坐标
变量m_LButtonDown是一个布尔型变量,该变量用于标识绘图的时候鼠标左键是否按下,因为要求绘图过程中要一直按住鼠标左键。在CDrawMapView
类中要添加这个变量:
BOOL m_LButtonDown;//鼠标左键是否按下
并且在CDrawMapView类的构造函数中初始化该变量,添加如下代码: m_LButtonDown = false;
现在分别看一下在三个鼠标消息处理函数中我们都做了哪些工作。
(1) 鼠标左键按下处理函数。在函数中先设置了鼠标使用的光标资源,并捕捉鼠标。然后设置了m_StartPoint和m_EndPoint的初始值,此时鼠标左键刚刚按下,所以这两个点相同。最后设置m_LButtonDown为true,表示鼠标左键已经按下。
(2) 鼠标移动处理函数,主要在该函数中完成橡皮线的绘制。先在函数中设置鼠标使用的光标资源,再构造设备环境对象,以便进行绘图。if条件判断在鼠标左键按下并且要绘制的图形是直线段时,执行绘制直线段橡皮线的代码。绘制直线段橡皮线的代码就是if条件句内的代码。该代码设置绘图模式为R2_NOT,就是在这个绘图模式下才产生了橡皮线的效果。接下来,首先绘制m_StartPoint和m_EndPoint之间的直线段,再绘制m_StartPoint和产生鼠标移动消息时鼠标光标所在位置point之间的直线段,最后将point赋值给m_EndPoint。因为m_EndPoint中存放的一直是上次调用鼠标移动消息处理函数时鼠标光标所处的位置,所以绘制m_StartPoint和m_EndPoint之间的直线段时,该直线段已经存在了,因为绘图模式的关系,本次绘图就起到了将原来的线段擦除的功能。在m_StartPoint和point之间绘制直线段,此时point点是新的位置,所以原来视图区中不会有该直线段存在,则此时绘图实际在视图区中绘制了一条从m_StartPoint到point的直线段。最后将m_EndPoint赋值为point,保证下一次执行鼠标移动消息处理函数时可以正确的将本次执行时绘制的直线段擦除掉。可以看到m_StartPoint点在鼠标左键按下时进行赋值之后就一直没有改变,因为在当前鼠标绘图方式中,起始点是一直不变的,鼠标移动所改变的是终止点。
(3) 鼠标左键抬起处理函数,此时表示本次鼠标绘制图形完毕。在函数中设置了鼠标使用的光标资源,并释放鼠标。然后构造设备环境对象,用于绘制最终的图形。if条件句判断当前绘制的是直线段,就调用相应的绘图函数将直线段绘制出来。实际上此处应该使用用户设定的画笔和画刷来绘制图形,并且需要将绘制的图形储存起来以便应用程序可以进行重画,在后面介绍到相关部分时将修改此处代码,现在只是把图形简单地绘制出来。代码最后将m_LButtonDown设置为false,表示鼠标左键处于抬起状态。
现在我们在系统的鼠标消息处理函数中调用我们所编写的函数,代码如下: //鼠标左键按下处理函数
void CDrawMapView::OnLButtonDown(UINT nFlags, CPoint point) {
// TODO: Add your message handler code here and/or call default //处于绘图状态时调用鼠标绘图时鼠标左键按下处理函数 if (m_isDraw) this->DrawLButtonDown(nFlags,point); CView::OnLButtonDown(nFlags, point); }
//鼠标移动处理函数
void CDrawMapView::OnMouseMove(UINT nFlags, CPoint point) {
// TODO: Add your message handler code here and/or call default //处于绘图状态时调用鼠标绘图时鼠标移动处理函数 if (m_isDraw) this->DrawMouseMove(nFlags,point); CView::OnMouseMove(nFlags, point); }
//鼠标左键抬起处理函数
void CDrawMapView::OnLButtonUp(UINT nFlags, CPoint point) {
// TODO: Add your message handler code here and/or call default //处于绘图状态时调用鼠标绘图时鼠标左键抬起处理函数 if (m_isDraw) this->DrawLButtonUp(nFlags,point); CView::OnLButtonUp(nFlags, point); }
调用我们编写的函数之前先判断当前是否处于绘图状态,如果是则调用我们编写的绘图状态下对应的鼠标消息处理函数。现在运行应用程序,就可以使用鼠标绘制直线段了,我们可以看一下橡皮线的具体效果。 2.2.3 用鼠标绘制椭圆和椭圆区域
使用鼠标绘制椭圆和椭圆区域类似于绘制直线段,差别在于要绘制的橡皮线是不同的,而且绘图完毕时最终绘制的图形分别为椭圆和椭圆区域。对于绘制椭圆和椭圆区域来说,使用的橡皮线都是相同的,即为椭圆的边界线。这里要注意不能使用设备环境的Ellipse函数来绘制椭圆边界线,因为Ellipse函数绘制的是填充的椭圆区域,填充的区域内部将产生覆盖效果,不能满足橡皮线的要求。这里我们调用设备环境的Arc函数来分别绘制椭圆的两段首尾相接的椭圆弧,从而合成一个椭圆。
DrawLButtonDown函数不需要修改,因为在鼠标左键按下时进行的初始设置是相同的。在DrawMouseMove中添加如下代码,完成椭圆和椭圆区域的橡皮线绘制:
//判断鼠标移动的同时鼠标左键按下,并且要绘制的是椭圆或椭圆区域 if (m_LButtonDown && (m_DrawType == 2 || m_DrawType ==3)) {
dc.SetROP2(R2_NOT);//设置绘图模式为R2_NOT //擦除前一次函数调用时绘制的椭圆边界线
dc.Arc(m_StartPoint.x,m_StartPoint.y,m_EndPoint.x,m_EndPoint.y, m_StartPoint.x,m_StartPoint.y,m_EndPoint.x,m_EndPoint.y); dc.Arc(m_StartPoint.x,m_StartPoint.y,m_EndPoint.x,m_EndPoint.y, m_EndPoint.x,m_EndPoint.y,m_StartPoint.x,m_StartPoint.y); //绘制新的椭圆边界线
dc.Arc(m_StartPoint.x,m_StartPoint.y,point.x,point.y, m_StartPoint.x,m_StartPoint.y,point.x,point.y);
dc.Arc(m_StartPoint.x,m_StartPoint.y,point.x,point.y, point.x,point.y,m_StartPoint.x,m_StartPoint.y); //保存新的终止点 m_EndPoint = point; }
将该段代码添加到绘制直线段橡皮线代码后面。代码中Arc函数以m_StartPoint和m_EndPoint点(或者point点)为椭圆弧所在椭圆的外接矩形的左上角点和右下角点。然后先以m_StartPoint和m_EndPoint点(或者point点)作为椭圆弧的起始点和终止点,再以m_StartPoint和m_EndPoint点(或者point点)作为椭圆弧的终止点和起始点,就正好画出一个完整的椭圆。
在DrawLButtonUp函数中添加如下代码,完成椭圆或椭圆区域的绘制工作: //绘制的是椭圆 if (m_DrawType == 2) {
//绘制椭圆边界线
dc.Arc(m_StartPoint.x,m_StartPoint.y,m_EndPoint.x,m_EndPoint.y, m_StartPoint.x,m_StartPoint.y,m_EndPoint.x,m_EndPoint.y); dc.Arc(m_StartPoint.x,m_StartPoint.y,m_EndPoint.x,m_EndPoint.y, m_EndPoint.x,m_EndPoint.y,m_StartPoint.x,m_StartPoint.y); }
//绘制的是椭圆区域 if (m_DrawType == 3) {
//绘制椭圆区域
dc.Ellipse(m_StartPoint.x,m_StartPoint.y, m_EndPoint.x,m_EndPoint.y); }
绘制椭圆因为绘制的只是椭圆的边界线,所以采用了与绘制椭圆橡皮线相同的方法来完成。而绘制椭圆区域时则直接调用了Ellipse函数进行绘制。
现在运行应用程序,我们可以通过点击工具条按钮来选择绘制直线段、椭圆或者椭圆区域。
2.2.4 用鼠标绘制矩形区域
绘制矩形区域橡皮线时,和绘制椭圆和椭圆区域的橡皮线一样,不能使用Rectangle函数来进行绘制。同样,DrawLButtonDown函数不需要修改。在DrawMouseMove函数中添加如下代码,完成矩形区域的橡皮线的绘制:
//判断鼠标移动的同时鼠标左键按下,并且要绘制的是矩形区域 if (m_LButtonDown && m_DrawType == 4) {
dc.SetROP2(R2_NOT);//设置绘图模式为R2_NOT //擦除前一次函数调用时绘制的矩形边界线 dc.MoveTo(m_StartPoint);
dc.LineTo(m_StartPoint.x,m_EndPoint.y); dc.LineTo(m_EndPoint);
dc.LineTo(m_EndPoint.x,m_StartPoint.y); dc.LineTo(m_StartPoint); //绘制新的矩形边界线 dc.MoveTo(m_StartPoint);
dc.LineTo(m_StartPoint.x,point.y); dc.LineTo(point);
dc.LineTo(point.x,m_StartPoint.y); dc.LineTo(m_StartPoint); //保存新的终止点 m_EndPoint = point; }
矩形边界线是通过分别绘制矩形的四个边界完成的。
在鼠标绘制矩形完成后,要绘制最终的矩形区域时,处理稍微有些复杂。这是因为用Rectangle绘制矩形区域的特性决定的。在绘制椭圆区域时,调用Arc函数绘制的椭圆边界线和调用Ellipse函数绘制的椭圆区域的边界线是完全重合的。因为不论m_StartPoint点和m_EndPoint点是否是椭圆所在的外接矩形的左上角点和右下角点(因为鼠标移动的关系,可能导致这两点并不是最终绘制的椭圆的外接矩形的左上角点和右下角点),Arc函数和Ellipse函数绘制出的图形都是相同的,因为真正起作用的是两点的x坐标和y坐标,它们分别指定了椭圆外接矩形的上边界、下边界、左边界和右边界。所以在绘制最终的椭圆区域时,直接调用Ellipse函数即可,无需对m_StartPoint点和m_EndPoint点的坐标进行修正。但是在绘制矩形区域时就需要对这两点的坐标进行修改,以保证m_StartPoint点和m_EndPoint确实是绘制的矩形区域的左上角点和右下角点,即在默认逻辑坐标系中,m_StartPoint的x坐标值应小于m_EndPoint的x坐标值,m_StartPoint的y坐标值应小于m_EndPoint的y坐标值。并且在最后绘制的时候要给m_EndPoint的x和y坐标值分别加1,Rectangle函数绘制出的矩形区域的边界才与通过分别绘制矩形区域边界的四个直线段所形成的矩形区域边界线重合。在DrawLButtonUp函数中添加如下代码:
//绘制的是矩形区域 if (m_DrawType == 4) {
int c;
//确保m_StartPoint确实为矩形区域的左上角 //m_EndPoint确实是矩形区域的右下角 if (m_StartPoint.x > m_EndPoint.x) { c = m_StartPoint.x; m_StartPoint.x = m_EndPoint.x; m_EndPoint.x = c; }
if (m_StartPoint.y > m_EndPoint.y) { c = m_StartPoint.y; m_StartPoint.y = m_EndPoint.y;
m_EndPoint.y = c; }
//绘制矩形区域
dc.Rectangle(m_StartPoint.x,m_StartPoint.y, m_EndPoint.x+1,m_EndPoint.y+1); }
代码中先修改m_StartPoint和m_EndPoint的坐标值,然后在调用Rectangle函数绘制矩形区域时,将传入的m_EndPoint的x和y坐标值分别加1。
运行应用程序,现在我们可以任意绘制提供的四种图形了。但是如果绘制完图形后,将应用程序窗口最小化,然后再恢复,我们会发现刚才绘制的图形已经没有了。这是因为我们没有在OnDraw函数中把这些图形重画出来。为了能够重画图形,就需要将绘制的图形的信息存储起来。下面我们将介绍如何定义图形的存储结构,以及如何在OnDraw函数中将它们重画出来。
2.3 图元定义及重画
基于面向对象的程序设计方法,可以将用户绘制的每一个图形都看作一个对象,我们可以称之为图元。在面向对象编程中,对象用类进行定义。我们可以根据图形类型的不同定义不同的类。在本章的绘图应用程序中,提供了四种可绘制的图形:直线段,椭圆,椭圆区域和矩形区域。可以定义四个类分别与之对应,即直线段类,椭圆类,椭圆区域类和矩形区域类。这样每一个图元(用户绘制的图形)都是其对应的类的实例。同时,不同类型的图元之间又有相同的部分,比如图元都是用指定的点坐标来控制其形状与位置的。所以我们可以定义一个图元类,作为具体的图形类的基类。在其父类中定义图元共有的成员变量和成员函数,而在每个子类中定义具有自己特色的成员变量和成员函数。 2.3.1 图元基类CMapElement
我们将图元基类的名称定义为CMapElement。类名以大写的字母C开头是MFC对类名的要求。
打开类面板,用鼠标右键单击类面板中树形列表的根节点“DrawMap Classes”(Classes前面是项目名称),会弹出一个快捷菜单,如图2.10所示。在快捷菜单中选择“New Classs…”,将会出现“New Class”(创建新类)对话框,如图2.11所示。该对话框用于添加新类到项目中。
对话框中的“Class type”下拉框用于选择要创建的类的类型,可选的类型有三种:MFC Class,创建一个以MFC类为基类的类;Generic Class,创建一个普通类;Form Class,创建一个窗口类。图2.11是选择MFC Class时的对话框样式。此时“Name”输入框输入类的名称,根据输入的类的名称,系统自动生成类文件的名称,并显示在“File name”标签框中。在“Base class”下拉框中可以选择一个MFC类作为要创建的类的基类,该处必须选择一个类作为基类。如果要创建的是一个对话框类,可以在“Dialog ID”下拉框中选择一个对话框资源ID。系统将自动把创建的对话框类和对话框资源进行关联。
如果在“Class type”中选择Form class,则对话框样式如图2.12所示。此时可以选择的基类只有四个:CFormView(默认),CDaoRecordView,CRecordView,CDialog。
创建CMapElement类,我们选择“Class type”为Generic Class,此时对话框样式如图2.13所示。此时新建类的基类在“Base class(es)”表格中输入,因为C++中子类的基类可以有多个。我们在“Name”输入框中输入类名为CMapElement,此时系统默认的类文件名为MapElement.cpp,我们可以点击“Change…”按钮来修改类文件名和头文件名,但是建议采用系统默认的文件名,这里我们采用系统
默认的文件名。我们输入CMapElement类的基类为CObject类,该类是大多数MFC类的最根本的基类。我们选择它作为图元类的基类,是想要利用该类所提供的序列化能力,这在以后我们持久化图元时是非常有用的。点击OK按钮确认创建CMapElement类,此时可能会出现消息框,提示使用CObject类作为基类需要自己添加头文件,点击消息框的“确定”按钮后,系统创建CMapElement类并将该类加入到当前项目中。我们在类面板中可以看到该类。
在类向导对话框中也有一个“Add class…”按钮,点击后在出现的快捷菜单中选择“New…”,也可以打开一个新建类的对话框,该对话框只用于新建MFC类,不能选择创建其它两种类。
在类面板中,用鼠标左键双击CMapElement类节点,打开头文件。在类声明之前添加如下代码:
#include CObject类是在afxtempl.h头文件中定义的,所以需要包含该头文件。我们可以看到系统自动在CMapElement类中添加了构造函数和析构函数。 现在来看一下在图元基类中应该有哪些成员变量和成员函数。在CMapElement类中添加如下私有成员变量: private: CPoint m_StartPoint;//图元起始控制点 CPoint m_EndPoint;//图元终止控制点 这两个CPoint变量就可以控制图元的位置了。现在用户可以绘制的四种图形都可以用这两个点来确定图形所在的位置。 在CMapElement类中添加如下公有成员函数: public: //设置图元起始控制点 void SetStartPoint(CPoint point); //设置图元终止控制点 void SetEndPoint(CPoint point); //获得图元起始控制点 CPoint GetStartPoint(); //获得图元终止控制点 CPoint GetEndPoint(); //绘制图元,由具体的图元子类覆盖实现 virtual void draw(CDC* pDC); //获得图元类型,由具体的图元子类覆盖实现 virtual int GetType(); 各个函数的具体实现代码如下: void CMapElement::SetStartPoint(CPoint point) { m_StartPoint = point; } void CMapElement::SetEndPoint(CPoint point) { m_EndPoint = point; } CPoint CMapElement::GetStartPoint() { return m_StartPoint; } CPoint CMapElement::GetEndPoint() { return m_EndPoint; } void CMapElement::draw(CDC *pDC) { } int CMapElement::GetType() { return 0; } 其中成员函数SetStartPoint和SetEndPoint用于设置图元的起始控制点和终止控制点,而函数GetStartPoint和GetEndPoint用于获得图元的起始控制点和终止控制点。 成员函数draw用于完成图元的绘制。该函数定义为虚函数,即由图元的子类来完成具体的实现,也就是说由每个图元子类自己来决定如何进行绘制。这样做的好处是在重画的时候不需要区分图元是哪种图元,只需要调用draw方法进行绘制即可。同时绘图代码在一个函数中,如果绘图方式发生改变,只需要修改此函数即可。该函数传入设备环境对象指针。 成员函数GetType用于返回代表图元类型的整型值。本函数也定义成虚函数,由图元子类来完成具体实现。在编辑图元的时候,需要知道要编辑的图元的种类,以便绘制相应的橡皮线。 图元基类其实还需要其它的成员变量和成员函数,我们将在后面用到时逐步进行介绍。 2.3.2 直线段图元子类CLine 我们定义直线段图元子类的类名为CLine,其基类为CMapElement。该子类需要实现基类定义的两个虚函数draw和GetType。在子类中添加公有成员函数: public: void draw(CDC* pDC);//绘制图元 int GetType();//返回图元类型 具体的实现代码如下: void CLine::draw(CDC *pDC) { //绘制直线段 pDC->MoveTo(GetStartPoint()); pDC->LineTo(GetEndPoint()); } int CLine::GetType() { //返回图元类型为直线段 return 1; } 2.3.3 椭圆图元子类CEllipse 我们定义椭圆图元子类的类名为CEllipse,其基类为CMapElement。同样该子类需要添加如下公有成员函数: public: void draw(CDC* pDC);//绘制图元 int GetType();//返回图元类型 具体的实现代码如下: void CEllipse::draw(CDC *pDC) { //获得椭圆的控制点 CPoint sp = GetStartPoint(); CPoint ep = GetEndPoint(); //绘制椭圆边界线 pDC->Arc(sp.x,sp.y,ep.x,ep.y,sp.x,sp.y,ep.x,ep.y); pDC->Arc(sp.x,sp.y,ep.x,ep.y,ep.x,ep.y,sp.x,sp.y); } int CEllipse::GetType() { //返回图元类型为椭圆 return 2; } draw函数中绘制椭圆边界线的方法和前面我们介绍的在DrawLButtonUp函数中绘制椭圆边界线的方法相同。 2.3.4 椭圆区域图元子类CEllipseRegion 我们定义椭圆区域图元子类的类名为CEllipseRegion,其基类为CMapElement。同样需要添加如下公有成员函数: public: void draw(CDC* pDC);//绘制图元 int GetType();//返回图元类型 具体的实现代码如下: void CEllipseRegion::draw(CDC *pDC) { //获得椭圆区域的控制点 CPoint sp = GetStartPoint(); CPoint ep = GetEndPoint(); //绘制椭圆区域 pDC->Ellipse(sp.x,sp.y,ep.x,ep.y); } int CEllipseRegion::GetType() { //返回图元类型为椭圆区域 return 3; } 2.3.5 矩形区域图元子类CRectangleRegion 我们定义矩形区域图元子类的类名为CRectangleRegion,其基类为CMapElement。添加如下公有成员函数: public: void draw(CDC* pDC);//绘制图元 int GetType();//返回图元类型 具体的实现代码如下: void CRectangleRegion::draw(CDC *pDC) { //获得矩形区域控制点 CPoint sp = GetStartPoint(); CPoint ep = GetEndPoint(); //绘制矩形区域 pDC->Rectangle(sp.x,sp.y,ep.x,ep.y); } int CRectangleRegion::GetType() { //返回图元类型为矩形区域 return 4; } 2.3.6 图元重画 现在我们已经定义了图元基类和对应不同图形的图元子类,在用户绘制完一个图形后,我们可以实例化一个对应的图元子类对象,该对象实例就对应了用户所画的图形。在重画图元时,只需调用该对象实例的draw成员函数即可。现在我们需要一个存放这些对象实例的地方。 因为我们定义的图元基类CMapElement的基类是CObject。MFC提供了存放CObject对象实例指针的列表对象类CObArray。列表类实现了对类对象的按顺序存取,可以像对象数组一样通过指定类对象在列表中的序号来访问类对象。列表类优于数组的地方在于不用先声明大小以申请空间,它可以动态的改变列表的大小。下面简单介绍一下CObArray中常用的成员函数。 Add函数,用于向列表中添加CObject对象指针,其函数声明如下: int Add(CObject* newElement) throw(CMemoryException); 参数newElement为要添加的CObject对象指针。如果添加成功,函数返回添加的对象指针在列表中的序号。新添加的对象添加到列表的尾部,并且列表的序号从0开始,即如果当前列表中已经有4个对象指针,则添加了新的对象指针后,函数返回4。如果添加失败,函数抛出CMemoryException异常。 GetAt函数,用于获得指定序号的列表中的CObject对象指针,其函数声明如下: CObject* GetAt(int nIndex) const; 参数nIndex指定了要获得对象指针在列表中的序号,列表序号从0开始,即如果想获得列表中的第5个对象指针,需要传入参数值4。函数返回CObject对象指针。传入的序号要确保是列表的有效序号,假设列表中当前有5个对象指针,如果传入的参数值大于4将导致错误。 GetSize函数,用于获得当前列表的大小,即存放的CObject对象指针的数量,其函数声明如下: int GetSize() const; SetAt函数,用于将指定序号的列表中的CObject对象指针替换为传入的CObject对象指针,要确保指定的序号是有效的,其函数声明如下: void SetAt(int nIndex, CObject* newElement); InsertAt函数,用于在指定序号的位置插入传入的CObject对象指针或CObArray,其函数声明如下: void InsertAt(int nIndex, CObject* newElement, int nCount = 1) throw(CMemoryException); void InsertAt(int nStartIndex, CObArray* pNewArray) throw(CMemoryException); 第一个函数的参数nIndex指定了要插入的位置序号,该序号可以比列表的实际大小要大;参数newElement为要插入的CObject对象指针;参数nCount指定了传入的对象指针要插入多少次,默认值为1。第二个函数的参数nIndex与第一个函数中的含义相同,也可以大于列表的实际大小;参数pNewArray为指向一个CObArray列表的指针。该函数将指定列表中的所有CObject对象指针插入到当前列表中的指定位置。如果函数执行失败,将抛出CMemoryException异常。 RemoveAt函数,用于移除列表中指定序号的CObject对象指针,其函数声明如下: void RemoveAt(int nIndex, int nCount = 1); 参数nIndex指定了开始移除的位置序号,该序号要确保有效;参数nCount指定了要移除的CObject对象指针的数量,默认为1。如果指定移除的数量多于从指定的移除位置开始的列表中实际的CObject对象指针的数量,则函数将把从nIndex开始的列表中所有的CObject对象指针移除。这里需要注意的是移除CObject对象指针只是从列表中移除,而实际的CObject对象仍然存在,只是不 能再通过列表访问到。 RemoveAll函数,移除列表中所有的CObject对象指针,其函数声明如下: void RemoveAll(); 以上是CObArray中比较常用的成员函数。在MFC中还有很多列表类,其主要差别是存放的对象不同,但是基本上都提供了以上功能的成员函数,只是参数类型会有所不同。 在我们的绘图应用程序中不直接使用CObArray类,而是创建一个新类CMapList,该类从CObArray类继承。这样如果我们需要,可以添加成员函数或覆盖已有的成员函数来满足我们的要求。在项目中添加新类CMapList,其基类为CObArray。同创建CMapElement类时一样,我们需要在CMapList类的头文件中包含afxtempl.h头文件。编写CMapList类的析构函数,输入如下代码: CMapList::~CMapList() { //销毁列表中所有指针所指向的对象 for (int i=0;i 我们在CDrawMapDoc类的头文件中包含CMapList的头文件MapList.h,然后在CDrawMapDoc类中添加下面的成员变量: CMapList m_MapList;//当前绘制的图元的列表 图元列表放在CDrawMapDoc类中是因为在文档视图体系中文档用于存储数据。现在我们需要在用户绘制完图元时,实例化对应的图元子类,设置控制点,然后调用draw函数绘制图元,最后将图元子类的指针存入m_MapList列表中。修改DrawLButtonUp成员函数,输入如下代码: //鼠标绘图时鼠标左键抬起处理函数 void CDrawMapView::DrawLButtonUp(UINT nFlags, CPoint point) { SetCursor(m_Cursor);//设置使用光标资源 ReleaseCapture();//释放鼠标 CDC* pDC = this->GetDC();//获得设备环境对象 //获得文档指针 CDrawMapDoc* pDoc = GetDocument(); ASSERT_VALID(pDoc); //绘制的是直线段 if (m_DrawType == 1) { //构造直线段图元对象指针 CLine* line = new CLine(); //设置控制点 line->SetStartPoint(m_StartPoint); line->SetEndPoint(m_EndPoint); //绘制直线段图元 line->draw(pDC); //添加直线段图元对象指针到图元列表中 pDoc->m_MapList.Add(line); } //绘制的是椭圆 if (m_DrawType == 2) { //构造椭圆图元对象指针 CEllipse* ellipse = new CEllipse(); //设置控制点 ellipse->SetStartPoint(m_StartPoint); ellipse->SetEndPoint(m_EndPoint); //绘制椭圆图元 ellipse->draw(pDC); //添加椭圆图元对象指针到图元列表中 pDoc->m_MapList.Add(ellipse); } //绘制的是椭圆区域 if (m_DrawType == 3) { //构造椭圆区域对象指针 CEllipseRegion* ellipseRegion = new CEllipseRegion(); //设置控制点 ellipseRegion->SetStartPoint(m_StartPoint); ellipseRegion->SetEndPoint(m_EndPoint); //绘制椭圆区域图元 ellipseRegion->draw(pDC); //添加椭圆区域图元对象指针到图元列表中 pDoc->m_MapList.Add(ellipseRegion); } //绘制的是矩形区域 if (m_DrawType == 4) { int c; //确保m_StartPoint确实为矩形区域的左上角 //m_EndPoint确实是矩形区域的右下角 if (m_StartPoint.x > m_EndPoint.x) { c = m_StartPoint.x; m_StartPoint.x = m_EndPoint.x; m_EndPoint.x = c; } if (m_StartPoint.y > m_EndPoint.y) { c = m_StartPoint.y; m_StartPoint.y = m_EndPoint.y; m_EndPoint.y = c; } //终止控制点坐标值加1 m_EndPoint.x++;m_EndPoint.y++; //构造矩形区域图元对象指针 CRectangleRegion* rectangle = new CRectangleRegion(); //设置控制点 rectangle->SetStartPoint(m_StartPoint); rectangle->SetEndPoint(m_EndPoint); //绘制矩形区域 rectangle->draw(pDC); //添加矩形区域图元对象指针到图元列表中 pDoc->m_MapList.Add(rectangle); } //释放设备环境对象 this->ReleaseDC(pDC); m_LButtonDown = false;//设置鼠标左键抬起 } 这样每次用户绘制完图元,都有相应的图元子类对象指针存入了m_MapList图元列表中。我们只需要在OnDraw函数中将m_MapList图元列表中的每个图元一一绘制出来即可完成图元的重画。修改OnDraw函数,输入如下代码: void CDrawMapView::OnDraw(CDC* pDC) { CDrawMapDoc* pDoc = GetDocument(); ASSERT_VALID(pDoc); // TODO: add draw code for native data here //循环图元列表 for (int i=0;i 在代码中我们通过图元基类指针调用draw函数,因为基类中draw函数声明为虚函数,所以系统会调用子类实现的draw函数来完成图元绘制。 现在我们运行应用程序,绘制完图形后将应用程序窗口最小化后再恢复,此 时我们仍然可以看到刚才所绘制的图形。 2.4 设置线型和区域填充方式 前面我们一直都是使用设备环境对象默认的画笔和画刷在绘制图元,实际使用中用户有时可能想要绘制出不同线型、不同填充方式的图形。本节中,我们将创建一个对话框,让用户在此对话框中设置想要使用的线型以及填充方式,我们在绘制图元的时候将使用用户所设置的线型和填充方式。 2.4.1 添加对话框资源 要使用自定义的对话框,首先需要有一个对话框资源。该对话框资源决定了对话框的表现形式,包括对话框的大小,对话框中有哪些控件等等。现在我们在当前项目中插入一个新的对话框资源。 选择资源面板,用鼠标右键点击Dialog节点,在弹出的快捷菜单中选择“Insert…”来打开插入资源对话框(如图2.3所示),然后选择添加对话框资源,或者直接选择“Insert Dialog”来插入一个对话框资源。插入对话框资源后,在右侧的编辑区中可以对该资源进行编辑,如图2.14所示。 新建的对话框资源的系统默认ID为IDD_DIALOG1。用鼠标右键点击资源面板中该节点,在弹出的快捷菜单中选择“Properties”,在弹出的对话框属性对话框中将ID改为图2.14中所示的IDD_SETSTYLE。在编辑区中间的就是新建的对话框资源的默认样式。在对话框资源中默认添加了两个按钮OK和Cancel。在编辑区右侧是控件工具条,该工具条中提供了可以用在对话框中的控件。我们可以用鼠标选择我们想用的控件,然后在对话框中想要放置该控件的位置用鼠标左键单击,此控件将放置在该位置。每个空间和对话框资源本身都可以用鼠标右键点击,然后在弹出的快捷菜单中选择“Properties”来打开对应的属性设置对话框来设置属性。添加完控件后,我们可以用鼠标移动它们的位置和修改它们的大小。 我们将要创建如图2.15所示的对话框资源。该对话框样式就是用户在设置线型和填充模式时所看到的对话框。 我们修改该对话框的标题为“设置线型及区域填充方式”。将OK按钮和Cancel按钮的文本改成了“确定”和“取消”(按钮的Caption属性)。“线型:”、“线宽:”、“颜色:”、“填充方式:”,“前景色:”和“背景色:”是标签控件,显示的文本是标签控件的Caption属性。“示例”是一个分组框控件,该控件类似于标签控件,只是用线围出了一个矩形区域,显示的文本是控件的Caption属性。在“线型:”和“填充方式:”两个标签右侧是两个下拉框,我们分别修改它们的ID为IDC_LINESTYLE和IDC_FILLSTYLE。用户将通过下拉框来选择线型和填充方式。对话框中还添加了三个显示文本为“...”的按钮,这三个按钮用于调用颜色设置对话框来设置颜色。我们分别修改“颜色:”、“前景色:”和“背景色:”右侧的“...”按钮的ID为IDC_LINECOLOR、IDC_FILLFORECOLOR和IDC_FILLBACKCOLOR。在对话框中还有四个黑色边框的矩形,实际上这也是按钮,是通过设置按钮的属性使之显示成如此样式的。属性设置如图2.16所示: 在按钮属性对话框中的Styles分页上选择“Owner draw”和“Flat”复选框, 同时清空按钮的Caption属性(General分页上),即可使按钮显示为图2.15中所显示的样式。添加这四个按钮是为了在选择完颜色后,在颜色标签右侧的矩形框中可以看到选择的颜色;在示例中的按钮上可以显示当前选择的线型和填充方式绘制出的图形的样式。我们分别修改“颜色:”、“前景色:”、“背景色:”和“示例”的示例按钮的ID为IDC_LCSAMPLE、IDC_FFCSAMPLE、IDC_FBCSAMPLE和IDC_SAMPLE。需要注意,添加的下拉框控件要用鼠标选中控件的下拉按钮,然后调整下拉框的下拉部分的大小。 对话框资源创建完毕后,还需要创建对话框类来加载该资源。在应用程序中是通过对话框类来完成对于对话框的各种操作的。 2.4.2 创建设置线型和区域填充方式对话框类CSetStyleDlg 创建对话框类很简单,我们只需要在对话框编辑区双击对话框(不能是对话框上的控件,否则将是创建对应控件的类),系统会自动打开类向导,并出现添加新类对话框,如图2.17所示。 此时单击“Adding a Class”对话框中的OK按钮,将出现如图2.18所示的新建类对话框。在“Name”输入框中输入我们定义的对话框类名CSetStyleDlg。该类的基类为CDialog,该类为MFC提供的对话框基类。在“Dialog ID”下拉框中选择创建的对话框类所对应的对话框资源ID,这里选择我们刚刚创建的对话框资源IDD_SETSTYLE。然后单击OK按钮,系统将创建对话框类CSetStyleDlg。我们关闭类向导,然后选择类面板,此时可以看到我们所创建的对话框类。在该类中默认添加了一个构造函数和一个DoDataExchange函数。DoDataExchange函数用于完成对话框中的控件与控制变量或数据变量的连接。该函数的内容在我们调用类向导进行设置后由系统自动添加,一般不需要我们来修改此函数。 对话框类创建完毕后,我们需要为对话框中的控件连接数据变量或者控制变量,并为按钮编写处理函数,才能使对话框的控件完成我们所需要的功能。其中两个下拉框控件我们要做特殊的处理,我们将在后面进行介绍,这里先看一下其它的控件。 首先看用于输入线宽的输入框控件,为了获得用户在该输入框中输入的数值,我们需要为该控件连接一个数据变量。打开类向导,选择“Member Variables”分页,在“Class name”下拉框中选择我们创建的对话框类CSetStyleDlg,此时在“Control IDs”列表框中显示当前对话框中所有控件的资源ID和对应的数据或控制成员变量,如图2.19所示。列表框中Type表示成员变量的值类型,而Member是成员变量名。因为当前还没有添加任何与控件对应的成员变量,所以Type和Member还都为空。 在“Control IDs”列表框中选择ID为IDC_LINEWIDTH,该ID即为输入线宽的输入框,然后单击右侧的“Add Variable…”按钮,会出现“Add Member Variable”(添加成员变量)对话框,如图2.20所示。对话框中的“Member variable name”输入框用于输入成员变量名,默认会有一个“m_”的前缀。这里我们输入m_LineWidth。在“Category”下拉框中选择成员变量的类型,可选Value(数据成员变量)或Control(控制成员变量),这里选择Value,表示该成员变量用于获得控件的数据。在“Variable type”下拉框中用于选择成员变量的值类型,根据成员变量类型的不同,此处可选择的值类型会不同。这里因为我们选择的类型是Value,所以可以选择的值类型包括CString(字符串,输入框的默认值类型),int(整型值),long(长整型值)等,我们选择int作为线宽变量m_LineWidth的 值类型。单击OK按钮,系统创建该成员变量,此时的“Control IDs”列表框如图2.21所示。 我们看到已经为ID为IDC_LINEWIDTH的输入框资源连接了值类型为int的数据成员变量m_LineWidth。因为当前选择的是该变量,所以可以在下面的“Minimum Value”和“Maximum Value”输入框中输入当前变量的可输入的最小 值和最大值,应用程序框架将确保用户输入值不会超出设定的范围。此处我们分别输入1和20,即最小线宽为1,最大线宽为20。单击OK按钮关闭类向导,即可以在类面板中看到CSetStyleDlg类已经增加了成员变量m_LineWidth。 我们看一下代码是如何实现的,打开CSetStyleDlg类的头文件,我们会发现如下代码: // Dialog Data //{{AFX_DATA(CSetStyleDlg) enum { IDD = IDD_SETSTYLE }; int m_LineWidth; //}}AFX_DATA 此段代码第一句完成对话框类与对话框资源的连接。第二句声明了int类型的成员变量m_LineWidth。然后看DoDataExchange函数,里面添加了如下代码: //{{AFX_DATA_MAP(CSetStyleDlg) DDX_Text(pDX, IDC_LINEWIDTH, m_LineWidth); DDV_MinMaxInt(pDX, m_LineWidth, 1, 20); //}}AFX_DATA_MAP DDX_Text完成了成员变量和输入框资源的连接,而DDV_MinMaxInt完成了成员变量取值范围的设定。 同时在对话框类的构造函数中对成员变量m_LineWidth进行了初始化,代码如下: //{{AFX_DATA_INIT(CSetStyleDlg) m_LineWidth = 0; //}}AFX_DATA_INIT 以上代码都是系统自动添加的,只要使用类向导创建对话框类和添加成员变量,就不需要手动输入这些代码。不过我们可以根据需要修改这些代码,比如修 改成员变量的初始值。 在本对话框中除了创建上面的变量m_LineWidth,还需要创建对应下拉框的控制成员变量,这些我们将在后面介绍下拉框时介绍。 2.4.3 完成颜色和示例的实时显示 在设置线型和区域填充模式对话框中,我们希望点击“...”按钮后可以显示“颜色”设置对话框,让用户选择想要使用的颜色,然后可以在对应的按钮上显示出用户选择的颜色,并且可以在示例中的按钮上用当前用户选择的线型和填充模式绘制一个示例图形。下面我们介绍如何实现这些功能。 首先,为了记录用户选择的线型,颜色,填充方式等信息,需要在对话框类中增加如下的成员变量: public: int m_LineStyle;//线型 COLORREF m_LineColor;//画线颜色 int m_FillStyle;//区域填充方式 COLORREF m_FillForeColor;//区域填充前景色 COLORREF m_FillBackColor;//区域填充背景色 以上成员变量不连接对话框中的控件资源,不用像添加m_LineWidth成员变量那样添加。这些成员变量(包括m_LineWidth)的值需要在调用该对话框时设置为当前用户使用的线型和填充方式,所以添加成员函数SetStyle,用于设置相应的数值,其函数声明如下: //设置线型和区域填充方式为当前使用的 void SetStyle(int lineStyle, int lineWidth, COLORREF lineColor, int fillStyle, COLORREF ffColor,COLORREF fbColor); 函数代码如下: void CSetStyleDlg::SetStyle(int lineStyle, int lineWidth, COLORREF lineColor, int fillStyle, COLORREF ffColor, COLORREF fbColor) { m_LineStyle = lineStyle;//设置线型 m_LineWidth = lineWidth;//设置线宽 m_LineColor = lineColor;//设置画线颜色 m_FillStyle = fillStyle;//设置区域填充方式 m_FillForeColor = ffColor;//设置区域填充前景色 m_FillBackColor = fbColor;//设置区域填充背景色 } 打开类向导,选择为CSetStyleDlg类添加WM_DRAWITEM消息的处理函数,处理函数名为系统的固定默认的OnDrawItem。该函数用于完成对话框中各个控件的绘制,其函数声明如下: afx_msg void OnDrawItem(int nIDCtl, LPDRAWITEMSTRUCT lpDrawItemStruct); 其中参数nIDCtl为要绘制的控件的ID;参数lpDrawItemStruct指向LPDRAWITEMSTRUCT结构体,该结构体中存放了传入ID对应的控件的相关信息,我们需要用到的是其中的hDC成员变量,该变量是用户绘制控件的设备环境句柄。我们使用该函数的思想是:该函数用于绘制传入的ID所指定的控件资源,我们在函数中判断传入的ID是我们要用于显示颜色或示例的控件的ID,则自行编码完成绘制,并不调用系统默认的绘制该种控件的函数,而其它控件则仍然调 用系统的默认绘制控件函数。根据以上思想,我们在OnDrawItem函数中输入如下代码: void CSetStyleDlg::OnDrawItem(int nIDCtl, LPDRAWITEMSTRUCT lpDrawItemStruct) { // TODO: Add your message handler code here and/or call default //设备环境对象 CDC dc; //根据ID判断是显示画线颜色的控件,实时显示画线颜色 if (nIDCtl == IDC_LCSAMPLE) { //将控件的设备环境句柄设置到设备环境对象中 dc.Attach(lpDrawItemStruct->hDC); //获得控件指针 CWnd* pWnd=GetDlgItem(IDC_LCSAMPLE); //获得控件所处位置的矩形区域 CRect rect; pWnd->GetClientRect(&rect); //构造画笔和画刷对象 CPen Pen; CPen *OldPen; CBrush Brush; CBrush *OldBrush; //创建空画笔,即不绘制矩形边界 Pen.CreatePen(PS_NULL,0,RGB(0,0,0)); //创建以指定画线颜色为填充颜色的画刷 Brush.CreateSolidBrush(m_LineColor); //选择画笔和画刷并返回原有画笔和画刷 OldPen=dc.SelectObject(&Pen); OldBrush=dc.SelectObject(&Brush); //在控件的位置绘制填充矩形 dc.Rectangle(&rect); //选回原有的画笔和画刷 dc.SelectObject(OldPen); dc.SelectObject(OldBrush); //删除自定义的画笔和画刷 Pen.DeleteObject(); Brush.DeleteObject(); } //根据ID判断是显示区域填充前景色颜色的控件,实时显示区域填充前景色 else if (nIDCtl == IDC_FFCSAMPLE) { //将控件的设备环境句柄设置到设备环境对象中 景色 dc.Attach(lpDrawItemStruct->hDC); //获得控件指针 CWnd* pWnd=GetDlgItem(IDC_FFCSAMPLE); //获得控件所处位置的矩形区域 CRect rect; pWnd->GetClientRect(&rect); //构造画笔和画刷对象 CPen Pen; CPen *OldPen; CBrush Brush; CBrush *OldBrush; //创建空画笔,即不绘制矩形边界 Pen.CreatePen(PS_NULL,0,RGB(0,0,0)); //创建以指定区域填充前景色为填充颜色的画刷 Brush.CreateSolidBrush(m_FillForeColor); //选择画笔和画刷并返回原有画笔和画刷 OldPen=dc.SelectObject(&Pen); OldBrush=dc.SelectObject(&Brush); //在控件的位置绘制填充矩形 dc.Rectangle(&rect); //选回原有的画笔和画刷 dc.SelectObject(OldPen); dc.SelectObject(OldBrush); //删除自定义的画笔和画刷 Pen.DeleteObject(); Brush.DeleteObject(); } //根据ID判断是显示区域填充背景色颜色的控件,实时显示区域填充背else if (nIDCtl == IDC_FBCSAMPLE) { //将控件的设备环境句柄设置到设备环境对象中 dc.Attach(lpDrawItemStruct->hDC); //获得控件指针 CWnd* pWnd=GetDlgItem(IDC_FBCSAMPLE); //获得控件所处位置的矩形区域 CRect rect; pWnd->GetClientRect(&rect); //构造画笔和画刷对象 CPen Pen; CPen *OldPen; CBrush Brush; CBrush *OldBrush; //创建空画笔,即不绘制矩形边界 Pen.CreatePen(PS_NULL,0,RGB(0,0,0)); //创建以指定区域填充背景色为填充颜色的画刷 Brush.CreateSolidBrush(m_FillBackColor); //选择画笔和画刷并返回原有画笔和画刷 OldPen=dc.SelectObject(&Pen); OldBrush=dc.SelectObject(&Brush); //在控件的位置绘制填充矩形 dc.Rectangle(&rect); //选回原有的画笔和画刷 dc.SelectObject(OldPen); dc.SelectObject(OldBrush); //删除自定义的画笔和画刷 Pen.DeleteObject(); Brush.DeleteObject(); } //根据ID判断是示例控件,实时显示当前示例 else if (nIDCtl == IDC_SAMPLE) { //将控件的设备环境句柄设置到设备环境对象中 dc.Attach(lpDrawItemStruct->hDC); //获得控件指针 CWnd* pWnd=GetDlgItem(IDC_SAMPLE); //获得控件所处位置的矩形区域 CRect rect; pWnd->GetClientRect(&rect); //构造画笔和画刷对象 CPen Pen; CPen *OldPen; CBrush Brush; CBrush *OldBrush; //构造LOGBRUSH结构 LOGBRUSH lb; //设置画线颜色 lb.lbColor = m_LineColor; lb.lbStyle = BS_SOLID; //用第三种初始化函数初始化画笔 Pen.CreatePen(PS_GEOMETRIC|m_LineStyle,m_LineWidth,&lb,0,NULL); //选择画笔并返回原有画笔 OldPen=dc.SelectObject(&Pen); //用当前线型绘制直线 dc.MoveTo(rect.left + 10,rect.top+20); dc.LineTo(rect.right - 10,rect.top+20); //填充方式为-1,则是实心填充 if (m_FillStyle == -1) //创建以指定区域填充背景色为填充颜色的画刷 Brush.CreateSolidBrush(m_FillForeColor); else { //创建阴影线画刷,并设置背景色 Brush.CreateHatchBrush(m_FillStyle,m_FillForeColor); dc.SetBkColor(m_FillBackColor); } //选择画刷并返回原有画刷 OldBrush=dc.SelectObject(&Brush); //用指定的线型和填充方式绘制填充矩形 rect.top = rect.top + 50; rect.bottom = rect.bottom -10; rect.left = rect.left + 10; rect.right = rect.right - 10; dc.Rectangle(&rect); //选回原有的画笔和画刷 dc.SelectObject(OldPen); dc.SelectObject(OldBrush); //删除自定义的画笔和画刷 Pen.DeleteObject(); Brush.DeleteObject(); } else //其它控件仍然需要使用系统默认的绘制方法进行绘制 CDialog::OnDrawItem(nIDCtl, lpDrawItemStruct); } 看如上代码的注释即可知道编写的代码的含义,这里就不再重复了。需要说明的是,为了在选用非实线线型时仍然可以绘制线宽大于1的线,初始化画笔采用了第三种初始化函数。示例的时候显示了用当前线型绘制的直线段和用当前线型和区域填充方式绘制的矩形区域两种图形。以上代码将IDC_LCSAMPLE、IDC_FFCSAMPLE、IDC_FBCSAMPLE和IDC_SAMPLE四个控件资源使用编写的代码进行绘制,其它控件仍然使用默认的绘制方法进行绘制。 下面为三个“...”按钮添加处理函数,来调用“颜色”对话框选择颜色,并引起显示颜色和显示示例的控件重画,来达到实时显示的目的。为按钮添加处理函数很简单,只需在对话框资源编辑区中用鼠标双击要添加处理函数的按钮,系统就会弹出成员函数添加对话框,并设置一个默认的函数名,单击对话框的OK按钮就可以创建按钮的处理函数。我们用以上方法添加三个“...”按钮的处理函数,函数名称均使用默认名称,然后输入代码如下: //设置画线颜色 void CSetStyleDlg::OnLinecolor() { // TODO: Add your control notification handler code here CColorDialog dlg;//构造系统提供的颜色设置的对话框 //调用对话框 if (dlg.DoModal()==IDOK) //用户选择“确定”来关闭颜色对话框,则获得用户选择的颜色 m_LineColor=dlg.GetColor(); //获得画线颜色显示按钮的指针,并使该按钮重画 CWnd* pWnd=GetDlgItem(IDC_LCSAMPLE); pWnd->Invalidate(); pWnd->UpdateWindow(); //获得示例按钮指针,并使该按钮重画 CWnd* pWnd2=GetDlgItem(IDC_SAMPLE); pWnd2->Invalidate(); pWnd2->UpdateWindow(); } //设置区域填充前景色 void CSetStyleDlg::OnFillforecolor() { // TODO: Add your control notification handler code here CColorDialog dlg;//构造系统提供的颜色设置的对话框 //调用对话框 if (dlg.DoModal()==IDOK) //用户选择“确定”来关闭颜色对话框,则获得用户选择的颜色 m_FillForeColor=dlg.GetColor(); //获得画线颜色显示按钮的指针,并使该按钮重画 CWnd* pWnd=GetDlgItem(IDC_FFCSAMPLE); pWnd->Invalidate(); pWnd->UpdateWindow(); //获得示例按钮指针,并使该按钮重画 CWnd* pWnd2=GetDlgItem(IDC_SAMPLE); pWnd2->Invalidate(); pWnd2->UpdateWindow(); } //设置区域填充背景色 void CSetStyleDlg::OnFillbackcolor() { // TODO: Add your control notification handler code here CColorDialog dlg;//构造系统提供的颜色设置的对话框 //调用对话框 if (dlg.DoModal()==IDOK) //用户选择“确定”来关闭颜色对话框,则获得用户选择的颜色 m_FillBackColor=dlg.GetColor(); //获得画线颜色显示按钮的指针,并使该按钮重画 CWnd* pWnd=GetDlgItem(IDC_FBCSAMPLE); pWnd->Invalidate(); pWnd->UpdateWindow(); //获得示例按钮指针,并使该按钮重画 CWnd* pWnd2=GetDlgItem(IDC_SAMPLE); pWnd2->Invalidate(); pWnd2->UpdateWindow(); } 其中的CColorDialog对话框类是MFC封装的一个颜色对话框,其样式如图2.22所示。 该对话框是我们很熟悉的Windows中用来设置颜色的对话框。显示对话框要调用对话框类的DoModal成员函数,该函数以模态方式显示对话框。当对话框关闭后,该函数才返回,即对下面的代码来说,只有显示的对话框关闭了,才会继续执行条件判断,以及之后的语句。 if (dlg.DoModal()==IDOK) 当对话框是单击“确定”(或者是OK)按钮关闭时,DoModal函数返回值为IDOK,此时表示用户选用了一个颜色,所以要设置到相应的颜色变量中。最后获得显示颜色和示例的控件指针,引起控件重画,应用程序框架会调用OnDrawItem函数来完成控件绘制,这样就实现了实时显示。 现在完成了设置颜色后的实时显示,我们还需要完成修改线宽后的实时显示。在类向导中,选择CSetStyleDlg类,然后在ID列表框中选择IDC_LINEWIDTH(线宽输入框),再在消息列表框中选择EN_UPDATE消息,该消息在输入框数据发生变化时产生。创建该消息的处理函数,并输入如下代码: //线宽输入框数据发生改变消息处理函数 void CSetStyleDlg::OnUpdateLinewidth() { // TODO: If this is a RICHEDIT control, the control will not // send this notification unless you override the CDialog::OnInitDialog() } 代码中首先调用UpdateData(TRUE)来设置输入框中输入的数据到对应的数据变量m_LineWidth中,然后对超出范围的值进行修改,再将修正的值设置到输入框中(调用UpdateData(FALSE),将数据变量的值设置到控件中)。最后引发示例按钮的重画。 2.4.4 在下拉框中绘图 通常是在下拉框中输入文本,让用户通过文本信息来指导选择的内容。但是对于绘图来说,用文字描述的线型,填充方式等不够直观,如果能够在下拉框中直接看到使用线型绘制的直线段,使用填充方式填充的区域则要直观得多。所以本节介绍如何实现在下拉框中进行绘图。 要完成在下拉框中绘图首先需要设置下拉框控件资源的对应属性。在对话框编辑区中用鼠标右键点击下拉框,在弹出的快捷菜单中选择“Properties”,在出现的下拉框属性对话框中选择Styles分页进行如图2.23所示的设置。其中“Owner draw”下拉框中选择Variable,表示下拉框中的每一项由程序绘制,不采用默认的形式。两个下拉框都要进行相同的设置。 现在来看如何在线型下拉框中绘制使用特定线型绘制的直线段。为了能够在下拉框中进行绘图,需要创建一个下拉框类,该类的基类为CComboBox(MFC提供的下拉框类的基类),通过覆盖其中的函数来实现绘图。在项目中创建一个新类CLineStyleCmb,其基类为CComboBox,因为此类是一个MFC类,所以创建新类时选择创建类的类型为MFC Class。 // function to send the EM_SETEVENTMASK message to the control // with the ENM_UPDATE flag ORed into the lParam mask. // TODO: Add your control notification handler code here //更新控件数据到对应的成员变量中 UpdateData(TRUE); //如果输入数值超出范围,则修正数值 if (m_LineWidth > 20) { m_LineWidth = 20; //更新数据变量m_LineWidth的值到输入框中 UpdateData(FALSE); } if (m_LineWidth < 1) { m_LineWidth = 1; //更新数据变量m_LineWidth的值到输入框中 UpdateData(FALSE); } //获得示例按钮指针,并使该按钮重画 CWnd* pWnd2=GetDlgItem(IDC_SAMPLE); pWnd2->Invalidate(); pWnd2->UpdateWindow(); 同时我们需要创建一个新类CLineStyleData,该类没有基类。此类作为下拉框中每一项存放的数据的模型,在用户选择了下拉框中的一项的时候,程序通过该项对应的数据对象来指导用户选择什么样的数据。在该类中添加如下成员变量,我们在后面的代码中将看到此类是如何使用的。 public: int m_LineStyle ;//线型 现在CLineStyleCmb类中添加了成员函数AddItem,该函数用于向下拉框中添加项目,其函数声明如下: void AddItem(int lineStyle); 参数lineStyle为线型值,在函数中输入如下代码: //在下拉框中添加项目 void CLineStyleCmb::AddItem(int lineStyle) { //构造线型数据类对象指针 CLineStyleData* pData = new CLineStyleData(); //设置线型 pData->m_LineStyle = lineStyle; //添加线型数据类对象到下拉框中,与下拉框中的一项相对应 //函数返回插入的项在下拉框中的序号 int nRet = AddString((LPCSTR) pData); //序号为LB_ERR,表示插入出错 if (nRet == LB_ERR) //删除创建的对象 delete pData; } 代码中构造了CLineStyleData类对象,然后调用AddString(CComboBox类提供的增加项目的函数)函数将此对象加入到下拉框中作为新的一项,该对象作为新加项的数据存在。 现在添加我们要覆盖的函数,打开类向导,选择CLineStyleCmb类,在ID列表中也选择该类,然后分别在消息列表中选择MeasureItem消息、DrawItem消息和DeleteItem消息并分别创建它们的处理函数。这三个处理函数分别用于调整下拉框中项目的高度、绘制下拉框中的项目和删除下拉框中的项目。它们对应的函数的声明如下: void CLineStyleCmb::MeasureItem (LPMEASUREITEMSTRUCT lpMeasureItemStruct) void CLineStyleCmb::DrawItem (LPMEASUREITEMSTRUCT lpMeasureItemStruct) void CLineStyleCmb::DeleteItem (LPMEASUREITEMSTRUCT lpMeasureItemStruct) 三个函数传入的都是LPMEASUREITEMSTRUCT结构对象。在这三个函数中输入如下代码: void CLineStyleCmb::MeasureItem (LPMEASUREITEMSTRUCT lpMeasureItemStruct) { // TODO: Add your code to determine the size of specified item //设置每个下拉框中项目的高度 lpMeasureItemStruct->itemHeight = 24 ; } //绘制下拉框项目 void CLineStyleCmb::DrawItem(LPDRAWITEMSTRUCT lpDrawItemStruct) { // TODO: Add your code to draw the specified item //获得用于绘制下拉框项目的设备环境对象指针 CDC* pDC = CDC::FromHandle(lpDrawItemStruct->hDC) ; //获得项目对应的线型数据对象指针 CLineStyleData* pData = (CLineStyleData*)(lpDrawItemStruct->itemData) ; ASSERT(pData) ; //获得项目所处位置的矩形区域 CRect rc(lpDrawItemStruct->rcItem) ; //如果传入的项目有错,则直接返回 if(lpDrawItemStruct->itemID == LB_ERR) return ; //项目处于绘制状态或者选择状态 if(lpDrawItemStruct->itemAction&(ODA_DRAWENTIRE|ODA_SELECT)) { //构造画笔对象 CPen pen; CPen* oldpen; //当前项目处于选择状态 if (lpDrawItemStruct->itemState&ODS_SELECTED) //初始化蓝色画笔 pen.CreatePen(PS_SOLID,2,RGB(0,0,255)); //当前项目没有处于选择状态 else //初始化白色画笔 pen.CreatePen(PS_SOLID,2,RGB(255,255,255)); //选择画笔 oldpen = pDC->SelectObject(&pen); //绘制项目的边框 pDC->MoveTo(rc.left,rc.top); pDC->LineTo(rc.right,rc.top); pDC->LineTo(rc.right,rc.bottom-2); pDC->LineTo(rc.left,rc.bottom-2); pDC->LineTo(rc.left,rc.top); //构造LOGBRUSH结构 LOGBRUSH lb; lb.lbColor = RGB(0,0,0); lb.lbStyle = BS_SOLID; //构造当前项目对应的线型的画笔 CPen pen2(PS_GEOMETRIC|pData->m_LineStyle,3,&lb,0,NULL); //选择该画笔 pDC->SelectObject(&pen2); //在项目中绘制直线段 pDC->MoveTo(rc.left+5,(rc.top+rc.bottom )/2); pDC->LineTo(rc.right-5,(rc.top+rc.bottom )/2); //选择原来的画笔 pDC->SelectObject(oldpen); //删除画笔 pen.DeleteObject(); pen2.DeleteObject(); } } //删除项目 void CLineStyleCmb::DeleteItem(LPDELETEITEMSTRUCT lpDeleteItemStruct) { // TODO: Add your specialized code here and/or call the base class //获得要删除项目对应的线型数据对象指针 CLineStyleData* pData = (CLineStyleData*)(lpDeleteItemStruct->itemData) ; //确保指针正确 ASSERT(pData) ; //删除线型数据对象 delete pData ; } 上面代码中的注释已经说明了每句代码的含义,这里不再重复。需要说明的是,在DrawItem函数中绘制直线段之前,首先判断项目是否在选择状态是指用户在点击了下拉框的下拉按钮,显示出下拉列表后,鼠标在下拉列表中的各项上移动,此时鼠标光标所在的项目处于选择状态,如果此时点击鼠标左键就表示要选择该项。为了突出要选择的项,在该项的四边用蓝色画笔绘制了一个边框,而不处于选择状态的项不用绘制该边框。 在用户选择了下拉框中一项后,也需要实时地在示例按钮中显示用户选择的线型所绘制的直线段。为了实现此功能需要在CLineStyleCmb类中添加相应的处理函数。打开类向导,选择CLineStyleCmb类,添加对=CBN_SELCHANGE消息的处理函数,该消息在用户选择了下拉框中的项目后产生,其函数名为OnSelchange,在该函数中输入如下代码: void CLineStyleCmb::OnSelchange() { // TODO: Add your control notification handler code here //获得当前选中的项目的序号 int nIndex = GetCurSel() ; //选中的项目没有错误则开始绘制示例 if(nIndex != LB_ERR) { //获得选中项目的线型数据对象指针 CLineStyleData* pData = (CLineStyleData*)GetItemDataPtr(nIndex) ; //获得下拉框的父窗口指针,即CSetStyleDlg对话框的指针 CSetStyleDlg* dlg = (CSetStyleDlg*)this->GetParent(); //设置对话框中当前的线型为选择的线型 dlg->m_LineStyle = pData->m_LineStyle; //引起示例按钮的重画 CWnd* pWnd = dlg->GetDlgItem(IDC_SAMPLE); pWnd->Invalidate(); pWnd->UpdateWindow(); } } 以上我们就完成了在下拉框中绘图的工作,并且用户选择完线型后,会在示例按钮中实时显示用户选择的线型所绘制的图形。其中代码用到了CLineStyleData类和CSetStyleDlg类,要注意包含头文件。 在填充方式下拉框中绘制使用特定填充方式填充的区域的方法与上面介绍的类似,主要差别在于下拉框项目中绘制的图形不同。我们这里直接给出要添加的类和相应函数的实现。 添加填充方式数据对象类CFillStyleData,该类也没有基类。在该类中添加如下成员变量: public: int m_FillStyle;//区域填充方式 添加下拉框MFC类CFillStyleCmb,该类的基类是CComboBox。在该类中添加成员函数AddItem,该函数的实现代码如下: //添加项目 void CFillStyleCmb::AddItem(int fillStyle) { //构造填充方式数据类对象指针 CFillStyleData* pData = new CFillStyleData(); //设置填充方式 pData->m_FillStyle = fillStyle; //添加填充方式数据类对象到下拉框中,与下拉框中的一项相对应 //函数返回插入的项在下拉框中的序号 int nRet = AddString((LPCSTR) pData); //序号为LB_ERR,表示插入出错 if (nRet == LB_ERR) //删除创建的对象 delete pData; } 在CFillStyleCmb类中添加对MeasureItem消息、DrawItem消息和DeleteItem消息的处理函数,其实现代码如下: void CFillStyleCmb::MeasureItem (LPMEASUREITEMSTRUCT lpMeasureItemStruct) { // TODO: Add your code to determine the size of specified item //设置每个下拉框中项目的高度 lpMeasureItemStruct->itemHeight = 24 ; } //绘制下拉框项目 void CFillStyleCmb::DrawItem(LPDRAWITEMSTRUCT lpDrawItemStruct) { // TODO: Add your code to draw the specified item //获得用于绘制下拉框项目的设备环境对象指针 CDC* pDC = CDC::FromHandle(lpDrawItemStruct->hDC) ; //获得项目对应的填充方式数据对象指针 CFillStyleData* pData = (CFillStyleData*)(lpDrawItemStruct->itemData) ; ASSERT(pData) ; //获得项目所处位置的矩形区域 CRect rc(lpDrawItemStruct->rcItem) ; //如果传入的项目有错,则直接返回 if(lpDrawItemStruct->itemID == LB_ERR) return ; //项目处于绘制状态或者选择状态 if(lpDrawItemStruct->itemAction&(ODA_DRAWENTIRE|ODA_SELECT)) { //构造画笔对象 CPen pen; CPen* oldpen; //当前项目处于选择状态 if (lpDrawItemStruct->itemState&ODS_SELECTED) //初始化蓝色画笔 pen.CreatePen(PS_SOLID,2,RGB(0,0,255)); //当前项目没有处于选择状态 else //初始化白色画笔 pen.CreatePen(PS_SOLID,2,RGB(255,255,255)); //选择画笔 oldpen = pDC->SelectObject(&pen); //绘制项目的边框 pDC->MoveTo(rc.left,rc.top); pDC->LineTo(rc.right,rc.top); pDC->LineTo(rc.right,rc.bottom-2); pDC->LineTo(rc.left,rc.bottom-2); pDC->LineTo(rc.left,rc.top); //构造空画笔 CPen pen2(PS_NULL,1,RGB(0,0,0)); //构造画刷对象 CBrush brush; //构造使用当前填充方式的画刷 //填充方式值为-1,表示实心填充 if (pData->m_FillStyle == -1) //构造实心填充画刷,使用黑色填充 brush.CreateSolidBrush(RGB(0,0,0)); else //构造阴影线画刷 brush.CreateHatchBrush(pData->m_FillStyle,RGB(0,0,0)); //选择该画笔 pDC->SelectObject(&pen2); //选择画刷,并返回原有画刷 CBrush* oldbrush = pDC->SelectObject(&brush); //在项目中绘制填充的矩形区域 pDC->Rectangle(rc.left+5,rc.top+5,rc.right -5,rc.bottom-5); //选择原来的画笔和画刷 pDC->SelectObject(oldpen); pDC->SelectObject(oldbrush); //删除画笔和画刷 pen.DeleteObject(); pen2.DeleteObject(); brush.DeleteObject(); } } //删除项目 void CFillStyleCmb::DeleteItem(LPDELETEITEMSTRUCT lpDeleteItemStruct) { // TODO: Add your specialized code here and/or call the base class //获得要删除项目对应的线型数据对象指针 CFillStyleData* pData = (CFillStyleData*)(lpDeleteItemStruct->itemData) ; //确保指针正确 ASSERT(pData) ; //删除线型数据对象 delete pData ; } 其中在绘制下拉框项目时,绘制了使用选定的填充方式进行填充的矩形区域。 在CFillStyleCmb类中添加对=CBN_SELCHANGE消息的处理函数,其实现代码如下: void CFillStyleCmb::OnSelchange() { // TODO: Add your control notification handler code here // TODO: Add your control notification handler code here //获得当前选中的项目的序号 int nIndex = GetCurSel() ; //选中的项目没有错误则开始绘制示例 if(nIndex != LB_ERR) { //获得选中项目的填充方式数据对象指针 CFillStyleData* pData = (CFillStyleData*)GetItemDataPtr(nIndex) ; //获得下拉框的父窗口指针,即CSetStyleDlg对话框的指针 CSetStyleDlg* dlg = (CSetStyleDlg*)this->GetParent(); //设置对话框中当前的填充方式为选择的填充方式 dlg->m_FillStyle = pData->m_FillStyle; //引起示例按钮的重画 CWnd* pWnd = dlg->GetDlgItem(IDC_SAMPLE); pWnd->Invalidate(); pWnd->UpdateWindow(); } } 以上我们就完成了在两个下拉框中进行绘图。 2.4.5 初始化对话框 上面介绍了如何在下拉框中绘图,我们还需要在下拉框中添加初始的项目,此工作可以在对话框初始化时完成。打开类向导,选择CSetStyleDlg类,在消息列表中选择WM_INITDIALOG消息,该消息在对话框初始化时产生。添加该消息的处理函数,其处理函数名为OnInitDialog。在该函数类中输入如下代码,完成对话框的初始化: BOOL CSetStyleDlg::OnInitDialog() { CDialog::OnInitDialog(); // TODO: Add extra initialization here //在线型下拉框中添加项目 m_LineStyleCmb.AddItem(PS_SOLID); m_LineStyleCmb.AddItem(PS_DASH); m_LineStyleCmb.AddItem(PS_DOT); m_LineStyleCmb.AddItem(PS_DASHDOT); m_LineStyleCmb.AddItem(PS_DASHDOTDOT); //设置当前选择的线型为正在使用的绘制图元的线型 if (m_LineStyle == PS_SOLID) m_LineStyleCmb.SetCurSel(0); else if(m_LineStyle == PS_DASH) m_LineStyleCmb.SetCurSel(1); else if(m_LineStyle == PS_DOT) m_LineStyleCmb.SetCurSel(2); else if(m_LineStyle == PS_DASHDOT) m_LineStyleCmb.SetCurSel(3); else m_LineStyleCmb.SetCurSel(4); //在填充方式下拉框中添加项目 m_FillStyleCmb.AddItem(-1);//实心填充 m_FillStyleCmb.AddItem(HS_BDIAGONAL); m_FillStyleCmb.AddItem(HS_CROSS); m_FillStyleCmb.AddItem(HS_DIAGCROSS); m_FillStyleCmb.AddItem(HS_FDIAGONAL); m_FillStyleCmb.AddItem(HS_HORIZONTAL); m_FillStyleCmb.AddItem(HS_VERTICAL); //设置当前选择的填充方式为正在使用的绘制图元的填充方式 if (m_FillStyle == -1) m_FillStyleCmb.SetCurSel(0); else if(m_FillStyle == HS_BDIAGONAL) m_FillStyleCmb.SetCurSel(1); else if(m_FillStyle == HS_CROSS) m_FillStyleCmb.SetCurSel(2); else if(m_FillStyle == HS_DIAGCROSS) m_FillStyleCmb.SetCurSel(3); else if(m_FillStyle == HS_FDIAGONAL) m_FillStyleCmb.SetCurSel(4); else if(m_FillStyle == HS_HORIZONTAL) m_FillStyleCmb.SetCurSel(5); else m_FillStyleCmb.SetCurSel(6); //更新对话框控件数据 UpdateData(false); UpdateData(); //绘制示例 CWnd* pWnd=GetDlgItem(IDC_SAMPLE); pWnd->Invalidate(); pWnd->UpdateWindow(); return TRUE; // return TRUE unless you set the focus to a control // EXCEPTION: OCX Property Pages should return FALSE } 代码主要是向线型下拉框和填充方式下拉框中添加项目,并设置当前选择的项目。初始化的最后引发示例按钮的重画,使用户可以看到当前正在使用的线型和填充方式的效果。 2.4.6 使用用户选择的线型和区域填充方式绘制图元 为了能够使用用户选择的线型和区域填充方式绘制图元,首先需要在应用程序中记录当前正在使用的线型和填充模式等信息。在CDrawMapView类中添加如下成员变量: public: int m_LineStyle;//线型 int m_LineWidth;//线宽 COLORREF m_LineColor;//画线颜色 int m_FillStyle;//区域填充方式 COLORREF m_FillForeColor;//区域填充前景色 COLORREF m_FillBackColor;//区域填充背景色 并在构造函数中添加如下代码为这些变量设置初始值: //设置线型和填充方式等初始值 m_LineStyle = PS_SOLID; m_LineWidth = 1; m_LineColor = RGB(0,0,0); m_FillStyle = -1; m_FillForeColor = RGB(255,255,255); m_FillBackColor = RGB(255,255,255); 然后我们在绘图工具条中添加一个按钮,用户点击该按钮来调用设置线型和区域填充方式对话框,该按钮的ID为ID_SETSTYLE,添加完按钮的工具条如图2.24所示。 用鼠标移动按钮,使按钮之间产生距离,工具条在显示时将在空白处产生一条竖线。为该按钮添加处理函数,代码如下: void CDrawMapView::OnSetstyle() { // TODO: Add your command handler code here //构造设置线型和填充方式对话框对象 CSetStyleDlg dlg; //设置线型和填充方式的当前值 dlg.SetStyle(m_LineStyle,m_LineWidth,m_LineColor, m_FillStyle,m_FillForeColor,m_FillBackColor); //调用对话框 if (dlg.DoModal() == IDOK) { //当对话框按“确定”按钮关闭时,设置用户选择的线型等数据值 m_LineStyle = dlg.m_LineStyle; m_LineWidth = dlg.m_LineWidth; m_LineColor = dlg.m_LineColor; m_FillStyle = dlg.m_FillStyle; m_FillForeColor = dlg.m_FillForeColor; m_FillBackColor = dlg.m_FillBackColor; } } 代码中先构造CSetStyleDlg对象,然后调用SetStyle成员函数将当前使用的线型等相关数据值设置到对话框中。当对话框是按“确定”按钮关闭时,将用户在对话框中选中的相关数据值设置到CDrawMapView类的相应成员变量中。 现在需要在绘制图元时使用当前的线型和填充方式。在图元基类CMapElement中添加如下成员变量: public: int m_LineStyle;//线型 int m_LineWidth;//线宽 COLORREF m_LineColor;//画线颜色 int m_FillStyle;//区域填充方式 COLORREF m_FillForeColor;//区域填充前景色 COLORREF m_FillBackColor;//区域填充背景色 并添加如下成员函数: CPen* GetPen(); CBrush* GetBrush(); 这两个成员函数用于获得使用当前线型和区域填充方式的画笔和画刷指针。函数的实现代码如下: CPen* CMapElement::GetPen() { //构造LOGBRUSH结构 LOGBRUSH lb; lb.lbColor = m_LineColor; lb.lbStyle = BS_SOLID; //返回构造的画笔指针 return new CPen(PS_GEOMETRIC|m_LineStyle,m_LineWidth,&lb,0,NULL); } CBrush* CMapElement::GetBrush() { if (m_FillStyle == -1) //填充方式值为-1,返回实心画刷指针 return new CBrush(m_FillForeColor); else //否则返回阴影线画刷指针 return new CBrush(m_FillStyle,m_FillForeColor); } 然后在每个图元子类的draw函数中使用上面获得的画笔和画刷。CLine类的draw函数修改如下: void CLine::draw(CDC *pDC) { //设置绘图模式为使用画笔定义的颜色 pDC->SetROP2(R2_COPYPEN); //获得当前线型画笔 CPen* pen = GetPen(); //选择画笔,并返回原有画笔 CPen* oldpen = pDC->SelectObject(pen); //绘制直线段 pDC->MoveTo(GetStartPoint()); pDC->LineTo(GetEndPoint()); //选择回原有画笔 pDC->SelectObject(oldpen); //删除创建的画笔 pen->DeleteObject();delete pen; } CEllipse类的draw函数修改如下: void CEllipse::draw(CDC *pDC) { //设置绘图模式为使用画笔定义的颜色 pDC->SetROP2(R2_COPYPEN); //获得当前线型画笔 CPen* pen = GetPen(); //选择画笔,并返回原有画笔 CPen* oldpen = pDC->SelectObject(pen); //获得椭圆的控制点 CPoint sp = GetStartPoint(); CPoint ep = GetEndPoint(); //绘制椭圆边界线 pDC->Arc(sp.x,sp.y,ep.x,ep.y,sp.x,sp.y,ep.x,ep.y); pDC->Arc(sp.x,sp.y,ep.x,ep.y,ep.x,ep.y,sp.x,sp.y); //选择回原有画笔 pDC->SelectObject(oldpen); //删除创建的画笔 pen->DeleteObject();delete pen; } CEllipseRegion类的draw函数修改如下: void CEllipseRegion::draw(CDC *pDC) { //设置绘图模式为使用画笔定义的颜色 pDC->SetROP2(R2_COPYPEN); //获得当前线型画笔 CPen* pen = GetPen(); //获得当前填充模式的画刷 CBrush* brush = GetBrush(); //如果是阴影线画刷,设置背景色 if (m_FillStyle != -1) pDC->SetBkColor(m_FillBackColor); //选择画笔,并返回原有画笔 CPen* oldpen = pDC->SelectObject(pen); //选择画刷,并返回原有画刷 CBrush* oldbrush = pDC->SelectObject(brush); //获得椭圆区域的控制点 CPoint sp = GetStartPoint(); CPoint ep = GetEndPoint(); //绘制椭圆区域 pDC->Ellipse(sp.x,sp.y,ep.x,ep.y); //选择回原有画笔和画刷 pDC->SelectObject(oldpen); pDC->SelectObject(oldbrush); //删除创建的画笔和画刷 pen->DeleteObject();delete pen; brush->DeleteObject();delete brush; } CRectangleRegion类的draw函数修改如下: void CRectangleRegion::draw(CDC *pDC) { //设置绘图模式为使用画笔定义的颜色 pDC->SetROP2(R2_COPYPEN); //获得当前线型画笔 CPen* pen = GetPen(); //获得当前填充模式的画刷 CBrush* brush = GetBrush(); //如果是阴影线画刷,设置背景色 if (m_FillStyle != -1) pDC->SetBkColor(m_FillBackColor); //选择画笔,并返回原有画笔 CPen* oldpen = pDC->SelectObject(pen); //选择画刷,并返回原有画刷 CBrush* oldbrush = pDC->SelectObject(brush); //获得矩形区域控制点 CPoint sp = GetStartPoint(); CPoint ep = GetEndPoint(); //绘制矩形区域 pDC->Rectangle(sp.x,sp.y,ep.x,ep.y); //选择回原有画笔和画刷 pDC->SelectObject(oldpen); pDC->SelectObject(oldbrush); //删除创建的画笔和画刷 pen->DeleteObject();delete pen; brush->DeleteObject();delete brush; } 其实以上四个draw函数修改的部分就是选择使用画笔和画刷。同时DrawLButtonUp函数也需要修改,修改后的DrawLButtonUp函数代码如下: //鼠标绘图时鼠标左键抬起处理函数 void CDrawMapView::DrawLButtonUp(UINT nFlags, CPoint point) { SetCursor(m_Cursor);//设置使用光标资源 ReleaseCapture();//释放鼠标 CDC* pDC = this->GetDC();//获得设备环境对象 pDC->SetROP2(R2_NOT);//设置绘图模式为了擦除多余橡皮线 //获得文档指针 CDrawMapDoc* pDoc = GetDocument(); ASSERT_VALID(pDoc); //绘制的是直线段 if (m_DrawType == 1) { //擦除多余的橡皮线 pDC->MoveTo(m_StartPoint); pDC->LineTo(m_EndPoint); //构造直线段图元对象指针 CLine* line = new CLine(); //设置控制点 line->SetStartPoint(m_StartPoint); line->SetEndPoint(m_EndPoint); //设置线型 line->m_LineStyle = m_LineStyle; line->m_LineWidth = m_LineWidth; line->m_LineColor = m_LineColor; //绘制直线段图元 line->draw(pDC); //添加直线段图元对象指针到图元列表中 pDoc->m_MapList.Add(line); } //绘制的是椭圆 if (m_DrawType == 2) { //擦除多余的橡皮线 pDC->Arc(m_StartPoint.x,m_StartPoint.y,m_EndPoint.x,m_EndPoint.y, m_StartPoint.x,m_StartPoint.y,m_EndPoint.x,m_EndPoint.y); pDC->Arc(m_StartPoint.x,m_StartPoint.y,m_EndPoint.x,m_EndPoint.y, m_EndPoint.x,m_EndPoint.y,m_StartPoint.x,m_StartPoint.y); //构造椭圆图元对象指针 } CEllipse* ellipse = new CEllipse(); //设置控制点 ellipse->SetStartPoint(m_StartPoint); ellipse->SetEndPoint(m_EndPoint); //设置线型 ellipse->m_LineStyle = m_LineStyle; ellipse->m_LineWidth = m_LineWidth; ellipse->m_LineColor = m_LineColor; //绘制椭圆图元 ellipse->draw(pDC); //添加椭圆图元对象指针到图元列表中 pDoc->m_MapList.Add(ellipse); //绘制的是椭圆区域 if (m_DrawType == 3) { //擦除多余的橡皮线 pDC->Arc(m_StartPoint.x,m_StartPoint.y,m_EndPoint.x,m_EndPoint.y, m_StartPoint.x,m_StartPoint.y,m_EndPoint.x,m_EndPoint.y); pDC->Arc(m_StartPoint.x,m_StartPoint.y,m_EndPoint.x,m_EndPoint.y, m_EndPoint.x,m_EndPoint.y,m_StartPoint.x,m_StartPoint.y); //构造椭圆区域对象指针 CEllipseRegion* ellipseRegion = new CEllipseRegion(); //设置控制点 ellipseRegion->SetStartPoint(m_StartPoint); ellipseRegion->SetEndPoint(m_EndPoint); //设置线型和填充方式 ellipseRegion->m_LineStyle = m_LineStyle; ellipseRegion->m_LineWidth = m_LineWidth; ellipseRegion->m_LineColor = m_LineColor; ellipseRegion->m_FillStyle = m_FillStyle; ellipseRegion->m_FillForeColor = m_FillForeColor; ellipseRegion->m_FillBackColor = m_FillBackColor; //绘制椭圆区域图元 ellipseRegion->draw(pDC); //添加椭圆区域图元对象指针到图元列表中 pDoc->m_MapList.Add(ellipseRegion); } //绘制的是矩形区域 if (m_DrawType == 4) { //擦除多余的橡皮线 pDC->MoveTo(m_StartPoint); pDC->LineTo(m_StartPoint.x,m_EndPoint.y); pDC->LineTo(m_EndPoint); pDC->LineTo(m_EndPoint.x,m_StartPoint.y); pDC->LineTo(m_StartPoint); int c; //确保m_StartPoint确实为矩形区域的左上角 //m_EndPoint确实是矩形区域的右下角 if (m_StartPoint.x > m_EndPoint.x) { c = m_StartPoint.x; m_StartPoint.x = m_EndPoint.x; m_EndPoint.x = c; } if (m_StartPoint.y > m_EndPoint.y) { c = m_StartPoint.y; m_StartPoint.y = m_EndPoint.y; m_EndPoint.y = c; } //终止控制点坐标值加1 m_EndPoint.x++;m_EndPoint.y++; //构造矩形区域图元对象指针 CRectangleRegion* rectangle = new CRectangleRegion(); //设置控制点 rectangle->SetStartPoint(m_StartPoint); rectangle->SetEndPoint(m_EndPoint); //设置线型和填充方式 rectangle->m_LineStyle = m_LineStyle; rectangle->m_LineWidth = m_LineWidth; rectangle->m_LineColor = m_LineColor; rectangle->m_FillStyle = m_FillStyle; rectangle->m_FillForeColor = m_FillForeColor; rectangle->m_FillBackColor = m_FillBackColor; //绘制矩形区域 rectangle->draw(pDC); //添加矩形区域图元对象指针到图元列表中 pDoc->m_MapList.Add(rectangle); } //释放设备环境对象 this->ReleaseDC(pDC); m_LButtonDown = false;//设置鼠标左键抬起 } 主要修改的是两部分,一是创建图元子类对象后需要设置当前的线型和填充 模式(直线段和椭圆不需要设置填充模式),二是要将多余的橡皮线擦除。 到此,我们的绘图应用程序终于可以让用户定义绘制图元的线型和区域填充方式了。现在可以运行应用程序看看具体的效果,图2.25是设置线型和区域填充方式的一个截图。 2.5 选中图元 为了能够编辑某个想要进行编辑的图元,首先应该能够选择该图元,本节将介绍如何用鼠标选中已经绘制的图元。因为选择图元也属于绘图应用程序提供的一种功能,所以我们也在绘图工具条中增加一个按钮与此功能对应。我们将添加的工具条按钮的ID设置为ID_SELECT,同时把该按钮移动到工具条的最左端(可以在工具条编辑区中用鼠标移动工具条)。添加完按钮的工具条如图2.26所示。 为该按钮添加处理函数,处理函数代码如下: void CDrawMapView::OnSelect() { // TODO: Add your command handler code here //设置当前不处于绘图状态,即处于选择图元状态 this->m_isDraw = false; } 该代码将当前状态设置为选择图元。 2.5.1 图元的包围盒 用鼠标选择图元的简单思想如下:用鼠标在视图区中点击一点,然后取得一个图元,判断该点是否在图元上。非区域图元判断点是否落在边界线上,区域图元需要判断在边界线上或区域内。因为针对不同图元,判断点是否在边界线上(或区域内)的方法可能会比较复杂,而鼠标点击的点可能离图元非常远,此时如果再判断该点是否在图元的边界线上(或区域内)就显得没有必要。所以这里引入最小包围盒的概念,一个图元的最小包围盒是包含此图元的最小矩形区域。在判断点是否在图元上时,先判断点是否落在图元的包围盒内,如果在包围盒内,再判断是否在图元上,如果点在图元上则表示该图元被选中。 在图元基类CMapElement中添加如下四个成员变量: public: //包围盒坐标值 int m_left,m_right,m_top,m_bottom; 它们分别代表了图元包围盒的左边界、右边界、上边界和下边界的坐标值。在图元基类CMapElement中添加如下成员函数: public: //设置图元包围盒 void SetBound(); 该成员函数用于设置图元的包围盒,其实现代码如下: //设置图元包围盒 void CMapElement::SetBound() { if (m_StartPoint.x < m_EndPoint.x) { m_left = m_StartPoint.x; m_right = m_EndPoint.x; } else { m_left = m_EndPoint.x; m_right = m_StartPoint.x; } if (m_StartPoint.y < m_EndPoint.y) { m_top = m_StartPoint.y; m_bottom = m_EndPoint.y; } else { m_top = m_EndPoint.y; m_bottom = m_StartPoint.y; } } 此段代码的含义很好理解,这里就不再说明了。每次创建图元对象的时候需 要调用此函数来设置图元的包围盒。我们是在CDrawMapView类中的DrawLButtonUp成员函数中创建图元对象的,所以需要该成员函数中每个创建图元子类的对象指针,然后在设置了图元的起始和终止控制点后,调用SetBound函数来设置图元的包围盒。这些代码很容易添加,读者可自行添加,只需要注意SetBound函数的调用要在SetStartPoint函数和SetEndPoint函数调用之后即可。 2.5.2 图元的选中判断 在图元基类CMapElement中添加下面的成员变量: public: //图元是否选中标识,为true表示图元被选中 BOOL m_isSelected; 在该类的构造函数中设置该变量的初始值为false,即初始图元不处于选中状态。 在图元基类CMapElement中添加下面的虚函数: public: //判断传入的位置坐标是否可以选中图元 //如果选中则返回true virtual BOOL IsSelected(CPoint point); 每个图元子类实现该成员函数来完成自身是否选中的判断,下面分别介绍四个图元子类的该成员函数的实现。 直线段图元类CLine的IsSelected成员函数代码实现如下: //判断直线段是否被选中 BOOL CLine::IsSelected(CPoint point) { //首先判断是否在包围盒内 if (point.x < m_left || point.x > m_right || point.y < m_top || point.y > m_bottom) //在包围盒外,返回没有选中 return false; //利用点到直线的距离判断是否选中 //获得直线段的两个端点 CPoint sp = GetStartPoint(); CPoint ep = GetEndPoint(); //根据直线段的两个端点计算直线方程Ax+By+C=0中的A、B、C值 int A = sp.y - ep.y; int B = ep.x - sp.x; int C = sp.x * ep.y - ep.x * sp.y; //利用点到直线距离方程式计算传入点到直线段的距离 double s = (double) ((A* point.x + B*point.y + C)/sqrt(A*A+B*B)); //容许用户点选有一定的误差 if (fabs(s) < (double)(m_LineWidth/2) + 2.0) //选中 return true; else //没有选中 return false; } 代码首先判断传入的点是否在包围盒内,如果不在则返回没有选中。判断传入点是否在直线段上时,采用了计算该点到直线段的距离的方法。因为用户很难恰好点选在直线段上,所以在判断距离时没有判断该距离为0,而是给出了一定的误差范围,只要用户点中的点与直线段的距离在误差范围内都算选中了直线段。同时这个误差范围考虑了直线段的宽度。代码中用到的函数sqrt和fabs分别是求开方和取绝对值函数,要使用这两个函数需要包含math.h头文件。 椭圆图元类CEllipse的IsSelected成员函数代码实现如下: //判断椭圆是否被选中 BOOL CEllipse::IsSelected(CPoint point) { //首先判断是否在包围盒内 if (point.x < m_left || point.x > m_right || point.y < m_top || point.y > m_bottom) //在包围盒外,返回没有选中 return false; //获得椭圆的控制点 CPoint sp = GetStartPoint(); CPoint ep = GetEndPoint(); //计算椭圆的长短轴长度 double a = fabs((double)((ep.x - sp.x)/2.0)); double b = fabs((double)((ep.y - sp.y)/2.0)); //计算椭圆的中心点坐标 double xx = (double)((sp.x + ep.x)/2.0); double yy = (double)((sp.y + ep.y)/2.0); //获得传入点的坐标值 double x = (double)point.x; double y = (double)point.y; //计算传入点代入椭圆方程的值 double d = (x-xx)*(x-xx)/(a*a)+(y-yy)*(y-yy)/(b*b); //判断传入的点是否在椭圆上,考虑点选误差 if (d > 0.8 && d < 1.2 ) //选中椭圆 return true; else //没有选中椭圆 return false; } 代码采用检查传入点是否满足椭圆方程的方法判断该点是否在椭圆上。 椭圆区域图元类CEllipseRegion的IsSelected成员函数代码实现如下: //判断是否选中椭圆区域 BOOL CEllipseRegion::IsSelected(CPoint point) { //首先判断是否在包围盒内 if (point.x < m_left || point.x > m_right || point.y < m_top || point.y > m_bottom) //在包围盒外,返回没有选中 return false; //获得椭圆区域的控制点 CPoint sp = GetStartPoint(); CPoint ep = GetEndPoint(); //计算椭圆的长短轴长度 double a = fabs((double)((ep.x - sp.x)/2.0)); double b = fabs((double)((ep.y - sp.y)/2.0)); //计算椭圆的中心点坐标 double xx = (double)((sp.x + ep.x)/2.0); double yy = (double)((sp.y + ep.y)/2.0); //获得传入点的坐标值 double x = (double)point.x; double y = (double)point.y; //计算传入点代入椭圆方程的值 double d = (x-xx)*(x-xx)/(a*a)+(y-yy)*(y-yy)/(b*b); //判断传入的点是否在椭圆边界上或椭圆区域内 if (d <= 1 ) //选中椭圆 return true; else //没有选中椭圆 return false; } 判断方法和椭圆一样,只要传入点在椭圆区域内部就表示椭圆被选中。 矩形区域图元类CRectangleRegion的IsSelected成员函数代码实现如下: //判断矩形区域是否被选中 BOOL CRectangleRegion::IsSelected(CPoint point) { //判断是否在包围盒内 if (point.x < m_left || point.x > m_right || point.y < m_top || point.y > m_bottom) //在包围盒外,没有选中 return false; else //在包围盒内,被选中 return true; } 矩形区域的判断非常简单,只要传入点落在包围盒内就表明矩形区域被选中。 2.5.3 绘制图元的选中标识 利用上面编写的函数,我们已经可以判断图元是否被选中。我们还需要给被选中的图元绘制选中标识,否则用户将无法知道自己选择的图元是否已经被选中。我们想要在图元的可编辑节点处绘制一个小的黑色矩形,这样用户不但可以知道图元已经被选中,同时也可以通过移动可编辑节点来编辑图元。对于直线段来说,可编辑节点就是直线段的两个端点。对于椭圆和椭圆区域来说,可编辑节点是椭圆的外接矩形的四个角点。而对于矩形区域来说就是矩形区域的四个角点。 在图元基类CMapElement中添加下面的虚函数: public: //绘制图元的选择标识 virtual void drawSelected(CDC* pDC); 因为椭圆图元子类CEllipse、椭圆区域图元子类CEllipseRegion和矩形区域图元子类CRectangleRegion绘制的选中标识是一样的,只有直线段图元子类CLine要绘制的选中标识不同。所以在基类中缺省实现绘制椭圆、椭圆区域和矩形区域的选中标识。而CLine则覆盖基类的该函数,完成自己的选中标识的绘制。 图元基类CMapElement的drawSelected函数实现如下: //绘制图元的选择标识 void CMapElement::drawSelected(CDC *pDC) { //创建空画笔 CPen pen(PS_NULL,1,RGB(0,0,0)); //创建黑画刷 CBrush brush(RGB(0,0,0)); //选择画笔和画刷,并返回原有的画笔和画刷 CPen* oldpen = pDC->SelectObject(&pen); CBrush* oldbrush = pDC->SelectObject(&brush); //设置绘图模式并返回原有的绘图模式 int mode = pDC->SetROP2(R2_NOTXORPEN); //获得图元的控制点 CPoint sp = GetStartPoint(); CPoint ep = GetEndPoint(); //用矩形区域绘制可编辑节点 pDC->Rectangle(sp.x-3,sp.y-3,sp.x+3,sp.y+3); pDC->Rectangle(sp.x-3,ep.y-3,sp.x+3,ep.y+3); pDC->Rectangle(ep.x-3,ep.y-3,ep.x+3,ep.y+3); pDC->Rectangle(ep.x-3,sp.y-3,ep.x+3,sp.y+3); //选择原有的绘图模式 pDC->SetROP2(mode); //选择原来的画笔和画刷 pDC->SelectObject(oldpen); pDC->SelectObject(oldbrush); //删除创建的画笔和画刷 pen.DeleteObject(); brush.DeleteObject(); } 使用R2_NOTXORPEN绘图模式是为了在线宽比较大的时候,仍可以看到选中标识。 直线段子类CLine实现的drawSelected函数代码如下: //绘制直线段的选中标识 void CLine::drawSelected(CDC *pDC) { CPen pen(PS_NULL,1,RGB(0,0,0)); //创建空画笔 CBrush brush(RGB(0,0,0)); //创建黑画刷 //选择画笔和画刷,并返回原有的画笔和画刷 CPen* oldpen = pDC->SelectObject(&pen); CBrush* oldbrush = pDC->SelectObject(&brush); //设置绘图模式并返回原有的绘图模式 int mode = pDC->SetROP2(R2_NOTXORPEN); //获得直线段的控制点 CPoint sp = GetStartPoint(); CPoint ep = GetEndPoint(); //用矩形区域绘制可编辑节点 pDC->Rectangle(sp.x-3,sp.y-3,sp.x+3,sp.y+3); pDC->Rectangle(ep.x-3,ep.y-3,ep.x+3,ep.y+3); //选择原有的绘图模式 pDC->SetROP2(mode); //选择原来的画笔和画刷 pDC->SelectObject(oldpen); pDC->SelectObject(oldbrush); //删除创建的画笔和画刷 pen.DeleteObject(); brush.DeleteObject(); } 为了在重画时正确显示图元的选中标识,需要修改OnDraw函数,输入如下代码: void CDrawMapView::OnDraw(CDC* pDC) { CDrawMapDoc* pDoc = GetDocument(); ASSERT_VALID(pDoc); // TODO: add draw code for native data here //循环图元列表 for (int i=0;i< pDoc->m_MapList.GetSize();i++) { //获得图元列表中的图元子类指针并将其造型成图元基类指针 CMapElement* pMap = (CMapElement*) pDoc->m_MapList.GetAt(i); pMap->draw(pDC); //调用draw函数绘制图元 } } 为了避免先绘制的图元选中标识被后绘制的图元覆盖掉,图元的选中标识是最后统一绘制的。其中用到的m_SelectedList列表用于存放选中的图元的指针,在后面我们会介绍如何将选中的图元加入到该列表中。 2.5.4 用鼠标选中单个图元 在CDrawMapView类添加如下成员函数: public: //编辑状态下鼠标左键按下消息处理函数 void EditLButtonDown(UINT nFlags, CPoint point); //编辑状态下鼠标移动消息处理函数 void EditMouseMove(UINT nFlags, CPoint point); //编辑状态下鼠标抬起消息处理函数 void EditLButtonUp(UINT nFlags, CPoint point); 这三个函数用于在编辑图元状态时(包括了选择图元和编辑图元)对应的鼠标消息的处理。 修改CDrawMapView类的默认鼠标消息处理函数,调用上面三个函数,代码如下: //鼠标左键按下处理函数 void CDrawMapView::OnLButtonDown(UINT nFlags, CPoint point) { // TODO: Add your message handler code here and/or call default //处于绘图状态时调用鼠标绘图时鼠标左键按下处理函数 if (m_isDraw) this->DrawLButtonDown(nFlags,point); //处于编辑状态时调用编辑状态鼠标左键按下处理函数 else this->EditLButtonDown(nFlags,point); CView::OnLButtonDown(nFlags, point); } //鼠标移动处理函数 void CDrawMapView::OnMouseMove(UINT nFlags, CPoint point) { // TODO: Add your message handler code here and/or call default //处于绘图状态时调用鼠标绘图时鼠标移动处理函数 //循环选中图元列表,绘制图元的选中标识 for (int j= pDoc->m_SelectedList.GetSize()-1;j>=0;j--) { //获得列表中的图元子类指针并将其造型成图元基类指针 CMapElement* pMap = (CMapElement*) pDoc->m_SelectedList.GetAt(j); //调用drawSelected函数绘制图元选中标识 pMap->drawSelected(pDC); } if (m_isDraw) this->DrawMouseMove(nFlags,point); //处于编辑状态时调用编辑状态鼠标移动处理函数 else this->EditMouseMove(nFlags,point); CView::OnMouseMove(nFlags, point); } //鼠标左键抬起处理函数 void CDrawMapView::OnLButtonUp(UINT nFlags, CPoint point) { // TODO: Add your message handler code here and/or call default //处于绘图状态时调用鼠标绘图时鼠标左键抬起处理函数 if (m_isDraw) this->DrawLButtonUp(nFlags,point); //处于编辑状态时调用编辑状态鼠标左键抬起处理函数 else this->EditLButtonUp(nFlags,point); CView::OnLButtonUp(nFlags, point); } 在CDrawMapDoc类中添加下面的成员变量: public: //选中图元列表 CObArray m_SelectedList; 将选中的图元指针存放在该列表中,这样查询该列表就可以知道哪些图元被选中,不需要通过循环所有的图元,判断图元的m_isSelected成员变量来知道哪些图元被选中。 我们在编辑状态下鼠标左键抬起处理函数中来完成图元是否选中的判断,在EditLButtonUp函数中输入如下代码: //编辑状态下鼠标左键抬起处理函数 void CDrawMapView::EditLButtonUp(UINT nFlags, CPoint point) { //获得文档指针 CDrawMapDoc* pDoc = GetDocument(); ASSERT_VALID(pDoc); //循环图元列表中的所有图元 for (int i= pDoc->m_MapList.GetSize()-1;i>=0;i--) { //获得图元指针 CMapElement* pMap = (CMapElement*) pDoc->m_MapList.GetAt(i); //调用图元的IsSelected函数,判断该图元是否被选中 if (pMap->IsSelected(point)) { //循环已选中的图元列表,修改图元的选中标识 for (int j=0;j< pDoc->m_SelectedList.GetSize();j++) { CMapElement* pSMap = (CMapElement*) pDoc->m_SelectedList.GetAt(j); pSMap->m_isSelected = false; //设置图元不再被选中 } pDoc->m_SelectedList.RemoveAll();//清空选中图元列表 //设置当前选中的图元的m_isSelected变量为真 pMap->m_isSelected = true; //将选中的图元添加到选中图元列表中 pDoc->m_SelectedList.Add(pMap); this->Invalidate();//重画 return; //已选中一个图元,不需要再继续检查 } } } 该段代码有几点需要说明:一是循环图元列表时是从最后一个图元开始,这是因为图元列表中图元的顺序就是绘制它们时的顺序,而当绘制的两个图元位置重叠时,后绘制的图元将覆盖先绘制的图元,那么选择的时候就应该是后绘制的图元被选中,所以应按绘制顺序的逆序来判断图元是否被选中;二是因为是选择单个图元,所以新选中一个图元后,原来选中的图元的选中状态就应该取消,代码在新选中一个图元后,清除原来选中图元的选中状态并清空选中图元列表,再把新选中的图元添加到该列表中;三是除了要绘制新选中图元的选中标识,还需要清除原来处于选中状态的图元的选中标识,所以直接调用Invalidate函数引起视图重画,这样图元的选中标识将被正确绘制;四是当选择了一个新图元后,就不需要再判断其它图元是否被选中,所以直接调用return结束函数的执行,如果本次选择没有图元被选中,则不更改以前选中的图元的选中状态。 现在我们运行应用程序,就可以用鼠标每次选中一个已绘制的图元,如图2.27所示,选中了一个椭圆区域,在椭圆的外接矩形的四个角点绘制了选中标识。 2.5.5 用鼠标和键盘配合选择多个图元 有时我们需要能够同时选择多个图元。选择多个图元的方法可以有多种,这里介绍两种比较常用的:一种是在使用鼠标点选图元时,如果按住了Ctrl键,则表示要选择多个图元,第二种是使用鼠标画出一个矩形区域,在该区域内的图元将都被选中。 要完成用鼠标和键盘配合选择多个图元,只需如下修改EditLButtonUp函数即可: //编辑状态下鼠标左键抬起处理函数 void CDrawMapView::EditLButtonUp(UINT nFlags, CPoint point) { //获得文档指针 CDrawMapDoc* pDoc = GetDocument(); ASSERT_VALID(pDoc); //循环图元列表中的所有图元 for (int i= pDoc->m_MapList.GetSize()-1;i>=0;i--) { //获得图元指针 CMapElement* pMap = (CMapElement*) pDoc->m_MapList.GetAt(i); //调用图元的IsSelected函数,判断该图元是否被选中 if (pMap->IsSelected(point)) { //发生鼠标消息的同时Ctrl键被按下 if (nFlags == MK_CONTROL) { //如果图元原来没有处于选中状态 if (!pMap->m_isSelected) { //设置图元为被选中 pMap->m_isSelected = true; //添加选中图元到选中图元列表中 pDoc->m_SelectedList.Add(pMap); } //如果图元原来已经处于选中状态则不需要重复加入到 //选中图元列表中 //重画 this->Invalidate(); //已选中一个图元,不需要再继续检查 return; } //以下处理选择图元时没有按Ctrl键 //循环已选中图元列表,修改图元的选中状态 for (int j=0;j< pDoc->m_SelectedList.GetSize();j++) { CMapElement* pSMap = (CMapElement*) pDoc->m_SelectedList.GetAt(j); //设置图元不再被选中 pSMap->m_isSelected = false; } //清空选中图元列表 pDoc->m_SelectedList.RemoveAll(); //设置当前选中的图元的m_isSelected变量为真 pMap->m_isSelected = true; //将选中的图元添加到选中图元列表中 pDoc->m_SelectedList.Add(pMap); //重画 this->Invalidate(); //已选中一个图元,不需要再继续检查 return; } } } if (nFlags == MK_CONTROL)条件句体是我们新增加的代码,该段代码判断用鼠标点选的同时是否按住了Ctrl键,并进行相应的处理。其实选择多个图元只需要在每选择一个图元时,不修改以前选中的图元的状态即可。需要注意的是图元原来可能已经处于被选中状态,此时不要将此图元的指针重复加入到选中图元列表中。 2.5.6 选择矩形区域内的多个图元 选择多个图元的另一种方法是,用鼠标画出一个矩形区域,该区域内的所有图元都将被选中。这时三个鼠标消息处理函数都要用到,因为除了判断图元是否在矩形区域内,还需要完成选择区域的橡皮线绘制。修改EditLButtonDown、EditMouseMove和EditLButtonUp三个成员函数的代码如下: //编辑状态下鼠标左键按下处理函数 void CDrawMapView::EditLButtonDown(UINT nFlags, CPoint point) { this->SetCapture();//捕捉鼠标 //设置开始点和终止点,此时为同一点 m_StartPoint = point; m_EndPoint = point; m_LButtonDown = true;//设置鼠标左键按下 } //编辑状态下鼠标移动处理函数 void CDrawMapView::EditMouseMove(UINT nFlags, CPoint point) { //鼠标按下状态,此时处于用鼠标画矩形选择多个图元时 if (m_LButtonDown) { //构造设备环境对象 CClientDC dc(this); //设置绘图模式,用于绘制橡皮线 dc.SetROP2(R2_NOTXORPEN); //创建蓝色虚线画笔 CPen pen(PS_DASH,1,RGB(0,0,255)); //选择画笔 dc.SelectObject(&pen); //擦除前一次函数调用时绘制的矩形边界线 dc.MoveTo(m_StartPoint); dc.LineTo(m_StartPoint.x,m_EndPoint.y); dc.LineTo(m_EndPoint); dc.LineTo(m_EndPoint.x,m_StartPoint.y); dc.LineTo(m_StartPoint); //绘制新的矩形边界线 dc.MoveTo(m_StartPoint); dc.LineTo(m_StartPoint.x,point.y); dc.LineTo(point); dc.LineTo(point.x,m_StartPoint.y); dc.LineTo(m_StartPoint); //保存新的终止点 m_EndPoint = point; } } //编辑状态下鼠标左键抬起处理函数 void CDrawMapView::EditLButtonUp(UINT nFlags, CPoint point) { //获得文档指针 CDrawMapDoc* pDoc = GetDocument(); ASSERT_VALID(pDoc); //设置鼠标状态为抬起 m_LButtonDown = false; //释放鼠标 ReleaseCapture(); //如果开始点和终止点之间的差距小于允许误差,表示鼠标是点选状态 //允许有误差是因为有时用户单击鼠标时也无法保证鼠标按下和抬起是发生在同一点 if (abs(m_StartPoint.x - m_EndPoint.x) < 2 && abs(m_StartPoint.y - m_EndPoint.y) < 2) { ……. //原来的用于处理鼠标单选(或配合Ctrl键多选)的代码 //为了节约篇幅这里不再重复列出 //该段代码放于此条件体内 ……. } //起始点和终止点距离大于允许范围,处于用鼠标画矩形多选状态 else { CClientDC dc(this); //构造设备环境对象 //设置绘图模式,用于擦除橡皮线 dc.SetROP2(R2_NOTXORPEN); CPen pen(PS_DASH,1,RGB(0,0,255)); //创建蓝色虚线画笔 dc.SelectObject(&pen); //选择画笔 //擦除最后绘制的橡皮线 dc.MoveTo(m_StartPoint); dc.LineTo(m_StartPoint.x,m_EndPoint.y); dc.LineTo(m_EndPoint); dc.LineTo(m_EndPoint.x,m_StartPoint.y); dc.LineTo(m_StartPoint); //调整用于选择的矩形区域的坐标 int c; if (m_StartPoint.x > m_EndPoint.x) { c = m_StartPoint.x; m_StartPoint.x = m_EndPoint.x; m_EndPoint.x = c; } if (m_StartPoint.y > m_EndPoint.y) { c = m_StartPoint.y; m_StartPoint.y = m_EndPoint.y; m_EndPoint.y = c; } //如果选择的时候没有按住Ctrl键 if (nFlags != MK_CONTROL) { //清空选中图元列表,并修改原有选中图元的状态 for (int j=0;j< pDoc->m_SelectedList.GetSize();j++) { CMapElement* pSMap = (CMapElement*) pDoc->m_SelectedList.GetAt(j); //设置图元不再被选中 pSMap->m_isSelected = false; } pDoc->m_SelectedList.RemoveAll(); } //循环图元列表 } } 在EditMouseMove函数中完成了橡皮线的绘制。在EditLButtonDown函数中设置了m_StartPoint和m_EndPoint的值。EditLButtonUp函数根据这两个点的位置距离来判断当前是用鼠标进行点选还是用鼠标画出一个矩形区域进行多选。如果此两点距离在允许范围内,表示用鼠标进行点选,否则表示进行多选。本段代码还允许在用鼠标多选时的同时按Ctrl键。上面的代码并不难理解,这里就不再详细说明了。运行应用程序,现在用户可以选中单个或多个图元,如图2.28所示,图中选中了两个图元。 for (int i=0;i< pDoc->m_MapList.GetSize();i++) { //获得图元指针 CMapElement* pMap = (CMapElement*) pDoc->m_MapList.GetAt(i); //判断图元的包围盒是否都在矩形区域内 //并且图元原来没有处于选中状态 if (pMap->m_left >= m_StartPoint.x && pMap->m_right <= m_EndPoint.x && pMap->m_top >= m_StartPoint.y && pMap->m_bottom <= m_EndPoint.y && !pMap->m_isSelected) { //设置图元被选中,并添加到选中图元列表中 pMap->m_isSelected = true; pDoc->m_SelectedList.Add(pMap); } } this->Invalidate();//重画 2.6 编辑图元 编辑图元包括修改图元的形状,移动图元的位置,放大缩小图元等等。其中修改图元的形状通常只能对一个图元进行,而移动图元位置或放大缩小图元可以同时对多个图元进行。 2.6.1 修改图元的形状 在只选中一个图元的情况下,可以用鼠标左键按住图元的选中标识(即可编辑节点)进行移动来修改图元的形状。在修改图元形状时,我们需要完成如下工作:一、当鼠标光标移动到图元的选中标识上时,修改鼠标光标形状为IDC_SIZEALL,表示此时可以修改图元形状;二、在用户修改图元形状时显示橡皮线;三、修改图元形状完毕后修改图元的控制点数据。 在CDrawMapView类中添加下面的成员变量: public: BOOL m_ifCanEdit;//是否可以修改图元形状 该成员变量为true时,表示可以修改图元形状。在CDrawMapView类的构造函数中初始化该成员变量的值为false。 为了能够在鼠标光标移动到图元的选中标识上时修改光标形状,需要知道鼠标光标的当前位置是否在图元的选中标识上。在图元基类CMapElement中添加下面的虚函数: public: //判断传入的点是否在图元的选中标识上 virtual BOOL IsOnSelected(CPoint point); 在图元基类CMapElement中缺省实现对椭圆、椭圆区域和矩形区域的选中标识的判断,实现代码如下: //判断传入的点是否在图元的选中标识上 //缺省实现椭圆、椭圆区域和矩形区域的判断 BOOL CMapElement::IsOnSelected(CPoint point) { //获得图元的控制点 CPoint sp = GetStartPoint(); CPoint ep = GetEndPoint(); //分别判断传入点是否在四个选中标识其中之一上 if (point.x >= sp.x - 3 && point.x <= sp.x + 3 && point.y >= ep.y - 3 && point.y <= ep.y + 3) return true; if (point.x >= ep.x - 3 && point.x <= ep.x + 3 && point.y >= sp.y - 3 && point.y <= sp.y + 3) return true; if (point.x >= ep.x - 3 && point.x <= ep.x + 3 && point.y >= ep.y - 3 && point.y <= ep.y + 3) return true; if (point.x >= sp.x - 3 && point.x <= sp.x + 3 && point.y >= sp.y - 3 && point.y <= sp.y + 3) return true; //不在任何一个选中标识上 return false; } 直线段图元子类CLine的选中标识与其它图元子类不同,该类覆盖基类的IsOnSelected成员函数,实现自己的判断,输入代码如下: //判断传入点是否在直线段的选中标识上 BOOL CLine::IsOnSelected(CPoint point) { //获得直线段的控制点 CPoint sp = GetStartPoint(); CPoint ep = GetEndPoint(); //分别判断传入点是否在两个选中标识其中之一上 if (point.x >= ep.x - 3 && point.x <= ep.x + 3 && point.y >= ep.y - 3 && point.y <= ep.y + 3) return true; if (point.x >= sp.x - 3 && point.x <= sp.x + 3 && point.y >= sp.y - 3 && point.y <= sp.y + 3) return true; //不在选中标识上 return false; } 因为在修改图元形状时要绘制橡皮线,而根据用户选择的修改图元形状的选中标识的不同,橡皮线初始的起始点和终止点也不同,所以在图元类中还应该提供设置橡皮线初始的起始点和终止点的成员函数。在图元基类CMapElement中添加如下的虚函数: public: //根据传入点坐标,设置修改图元时橡皮线初始的起始点和终止点 virtual void SetEditPoint(CPoint point,CPoint* startPoint,CPoint* endPoint); 和IsOnSelected成员函数一样,在图元基类中缺省实现椭圆、椭圆区域和矩形区域的设置,实现代码如下: //根据传入点坐标,设置修改图元时橡皮线初始的起始点和终止点 void CMapElement::SetEditPoint(CPoint point, CPoint *startPoint, CPoint *endPoint) { //获得图元的控制点 CPoint sp = GetStartPoint(); CPoint ep = GetEndPoint(); //判断传入点在哪个选中标识上,再设置橡皮线的起始点和终止点 if (point.x >= sp.x - 3 && point.x <= sp.x + 3 && point.y >= ep.y - 3 && point.y <= ep.y + 3) { startPoint->x = sp.x;startPoint->y = ep.y; endPoint->x = ep.x;endPoint->y = sp.y; } else if (point.x >= ep.x - 3 && point.x <= ep.x + 3 && point.y >= sp.y - 3 && point.y <= sp.y + 3) { startPoint->x = ep.x;startPoint->y = sp.y; endPoint->x = sp.x;endPoint->y = ep.y; } else if (point.x >= ep.x - 3 && point.x <= ep.x + 3 && point.y >= ep.y - 3 && point.y <= ep.y + 3) { startPoint->x = ep.x;startPoint->y = ep.y; endPoint->x = sp.x;endPoint->y = sp.y; } else if (point.x >= sp.x - 3 && point.x <= sp.x + 3 && point.y >= sp.y - 3 && point.y <= sp.y + 3) { startPoint->x = sp.x;startPoint->y = sp.y; endPoint->x = ep.x;endPoint->y = ep.y; } } 当传入点在某一个选中标识上时,该选中标识就是终止点,而与该选中标识对角的点就是起始点。直线段图元子类CLine覆盖基类的SetEditPoint方法,输入代码如下: void CLine::SetEditPoint(CPoint point, CPoint *startPoint, CPoint *endPoint) { //获得图元的控制点 CPoint sp = GetStartPoint(); CPoint ep = GetEndPoint(); //判断传入点在哪个选中标识上,再设置橡皮线的起始点和终止点 if (point.x >= ep.x - 3 && point.x <= ep.x + 3 && point.y >= ep.y - 3 && point.y <= ep.y + 3) { endPoint->x = ep.x;endPoint->y = ep.y; startPoint->x = sp.x;startPoint->y = sp.y; } else if (point.x >= sp.x - 3 && point.x <= sp.x + 3 && point.y >= sp.y - 3 && point.y <= sp.y + 3) { endPoint->x = sp.x;endPoint->y = sp.y; startPoint->x = ep.x;startPoint->y = ep.y; } } 对直线段来说,当传入点在直线段其中一个端点上时(选中标识),该端点是终止点,而另一个端点是起始点。以上做好了修改图元形状的准备,下面分别 修改EditLButtonDown函数、EditMouseMove函数和EditLButtonUp函数,完成修改图元的工作。 EditLButtonDown函数修改如下: //编辑状态下鼠标左键按下处理函数 void CDrawMapView::EditLButtonDown(UINT nFlags, CPoint point) { this->SetCapture();//捕捉鼠标 m_LButtonDown = true;//设置鼠标左键按下 //可以修改图元形状 if (m_ifCanEdit) { //获得文档指针 CDrawMapDoc* pDoc = GetDocument(); ASSERT_VALID(pDoc); //设置鼠标光标的形状 SetCursor(AfxGetApp()->LoadStandardCursor(IDC_SIZEALL)); //获得选中的图元的指针 CMapElement* pMap = (CMapElement*) pDoc->m_SelectedList.GetAt(0); //设置橡皮线的初始起始点和终止点 pMap->SetEditPoint(point,&m_StartPoint,&m_EndPoint); } //不可以修改图元形状 else { //设置开始点和终止点,不是修改图元形状时为同一点 m_StartPoint = point; m_EndPoint = point; } } 鼠标左键按下时,如果m_ifCanEdit成员变量为真,表示可以修改图元,此时需要调用选中图元的SetEditPoint函数设置修改图元时的橡皮线的初始起始点和终止点,并且设置鼠标光标形状。如果m_ifCanEdit成员变量为假,则正常设置起始点和终止点,此时这两点用于选择图元(点选或者矩形区域多选)。 EditMouseMove函数修改如下: //编辑状态下鼠标移动处理函数 void CDrawMapView::EditMouseMove(UINT nFlags, CPoint point) { //鼠标左键按下,并且可以修改图元形状 if (m_LButtonDown && m_ifCanEdit) { //获得文档指针 CDrawMapDoc* pDoc = GetDocument(); ASSERT_VALID(pDoc); //获得选中的图元的指针 CMapElement* pMap = (CMapElement*) pDoc->m_SelectedList.GetAt(0); CClientDC dc(this);//构造设备环境对象 CPen pen(PS_SOLID,1,RGB(200,200,200)); dc.SelectObject(&pen); dc.SetROP2(R2_NOTXORPEN);//设置绘图模式为R2_NOT //修改形状的图元是直线段 if (pMap->GetType() == 1) { //擦除前一个直线段 dc.MoveTo(m_StartPoint); dc.LineTo(m_EndPoint); //绘制新的直线段 dc.MoveTo(m_StartPoint); dc.LineTo(point); //保存新的直线段终点 m_EndPoint = point; } //修改形状的图元是椭圆或椭圆区域 if (pMap->GetType() == 2 || pMap->GetType() == 3) { //擦除前一次函数调用时绘制的椭圆边界线 dc.Arc(m_StartPoint.x,m_StartPoint.y,m_EndPoint.x,m_EndPoint.y, m_StartPoint.x,m_StartPoint.y,m_EndPoint.x,m_EndPoint.y); dc.Arc(m_StartPoint.x,m_StartPoint.y,m_EndPoint.x,m_EndPoint.y, m_EndPoint.x,m_EndPoint.y,m_StartPoint.x,m_StartPoint.y); //绘制新的椭圆边界线 dc.Arc(m_StartPoint.x,m_StartPoint.y,point.x,point.y, m_StartPoint.x,m_StartPoint.y,point.x,point.y); dc.Arc(m_StartPoint.x,m_StartPoint.y,point.x,point.y, point.x,point.y,m_StartPoint.x,m_StartPoint.y); //保存新的终止点 m_EndPoint = point; } //修改形状的图元是矩形区域 if (pMap->GetType() == 4) { //擦除前一次矩形边界线 dc.MoveTo(m_StartPoint); dc.LineTo(m_StartPoint.x,m_EndPoint.y); dc.LineTo(m_EndPoint); dc.LineTo(m_EndPoint.x,m_StartPoint.y); dc.LineTo(m_StartPoint); //绘制新的矩形边界线 dc.MoveTo(m_StartPoint); dc.LineTo(m_StartPoint.x,point.y); dc.LineTo(point); dc.LineTo(point.x,m_StartPoint.y); dc.LineTo(m_StartPoint); //保存新的终止点 m_EndPoint = point; } } //处于鼠标按下状态并不能修改图元,此时处于用鼠标画矩形选择多个图元时 else if (m_LButtonDown) { //构造设备环境对象 CClientDC dc(this); //设置绘图模式,用于绘制橡皮线 dc.SetROP2(R2_NOTXORPEN); //创建蓝色虚线画笔 CPen pen(PS_DASH,1,RGB(0,0,255)); //选择画笔 dc.SelectObject(&pen); //擦除前一次函数调用时绘制的矩形边界线 dc.MoveTo(m_StartPoint); dc.LineTo(m_StartPoint.x,m_EndPoint.y); dc.LineTo(m_EndPoint); dc.LineTo(m_EndPoint.x,m_StartPoint.y); dc.LineTo(m_StartPoint); //绘制新的矩形边界线 dc.MoveTo(m_StartPoint); dc.LineTo(m_StartPoint.x,point.y); dc.LineTo(point); dc.LineTo(point.x,m_StartPoint.y); dc.LineTo(m_StartPoint); //保存新的终止点 m_EndPoint = point; } //选中的图元只有一个,并且鼠标左键没有按下 else if (((CDrawMapDoc*)GetDocument())->m_SelectedList.GetSize() == 1) { //获得选中的图元的指针 CMapElement* pMap = (CMapElement*) ((CDrawMapDoc*)GetDocument())->m_SelectedList.GetAt(0); //判断当前鼠标光标是否在选中图元的选中标识上 if (pMap->IsOnSelected(point)) { //在选中标识上,设置鼠标光标形状为IDC_SIZEALL SetCursor(AfxGetApp()->LoadStandardCursor(IDC_SIZEALL)); //设置可以修改图元形状 m_ifCanEdit = true; } else { //没在选中标识上,设置鼠标光标形状为IDC_ARROW SetCursor(AfxGetApp()->LoadStandardCursor(IDC_ARROW)); //设置不可以修改图元形状 m_ifCanEdit = false; } } } 此函数中增加了两个条件模块:一个是m_LbuttonDown和m_ifCanEdit都为真时,表示鼠标左键按下,并且可以修改图元。在该条件模块中完成修改图元的橡皮线绘制。在绘制橡皮线时,调用了图元的GetType函数来获得图元的类型,根据不同的图元类型绘制不同的橡皮线。另一个是m_LbuttonDown为false,并且选中的图元只有一个时,不需要绘制橡皮线(橡皮线包括了修改图元时的橡皮线和区域选择多个图元时的橡皮线)。之所以要判断选中的图元只有一个是因为只有仅选中一个图元时,才能够修改该图元的形状。在该条件模块中,调用选中图元的IsOnSelected成员函数,判断当前鼠标所在位置是否是选中图元的选中标识。如果是,则修改鼠标光标形状为IDC_SIZEALL,并且将m_ifCanEdit设为true,表示可以修改图元;否则,则修改光标形状为标准的斜箭头光标,并且设置m_ifCanEdit为false,表示不可以修改图元。 EditLButtonUp函数修改如下: void CDrawMapView::EditLButtonUp(UINT nFlags, CPoint point) { //获得文档指针 CDrawMapDoc* pDoc = GetDocument(); ASSERT_VALID(pDoc); //设置鼠标状态为抬起 m_LButtonDown = false; //释放鼠标 ReleaseCapture(); //修改图元形状结束 if (m_ifCanEdit) { CMapElement* pMap = (CMapElement*) pDoc->m_SelectedList.GetAt(0); if (pMap->GetType() == 4) { int c; //确保m_StartPoint确实为矩形区域的左上角 //m_EndPoint确实是矩形区域的右下角 if (m_StartPoint.x > m_EndPoint.x) { c = m_StartPoint.x; m_StartPoint.x = m_EndPoint.x; m_EndPoint.x = c; } if (m_StartPoint.y > m_EndPoint.y) { c = m_StartPoint.y; m_StartPoint.y = m_EndPoint.y; m_EndPoint.y = c; } //终止控制点坐标值加1 m_EndPoint.x++;m_EndPoint.y++; } //设置图元控制点 pMap->SetStartPoint(m_StartPoint); pMap->SetEndPoint(m_EndPoint); //重新设置图元包围盒 pMap->SetBound(); //设置修改图元形状完毕 m_ifCanEdit = false; //重画 this->Invalidate(); return; } …….. //原有处理选择图元的代码,为节约篇幅不再重复列出 …….. } 此函数增加了if (m_ifCanEdit)条件模块。m_ifCanEdit为true时,表示修改图元完毕,此时修改图元的控制点为修改形状后的控制点,并重新设置包围盒。然后将m_ifCanEdit设为false,并且让视图重画。现在我们的绘图应用程序可以修改选中图元的形状了。 2.6.2 移动图元 本章的绘图应用程序使用键盘按键的方式来移动图元,这里的图元可以是一个或多个。移动图元我们作如下的默认:如果有选中的图元,则只移动选中的图元,如果没有选中的图元,则移动所有的图元。 为了使用键盘按键移动图元,需要为应用程序添加处理键盘消息的成员函数。常用的Windows键盘消息为WM_KEYDOWN和WM_KEYUP,它们分别在键盘上的按键被按下和抬起时产生。对应的消息处理函数是OnKeyDown和 OnKeyUp,其函数声明如下: afx_msg void OnKeyDown(UINT nChar, UINT nRepCnt, UINT nFlags); afx_msg void OnKeyUp(UINT nChar, UINT nRepCnt, UINT nFlags); 其中参数nFlags为无符号整数,其对应的二进制数的各位含义如下表: 位 说明 15 按键的转换状态,若键刚释放为1,刚按下为0 14 前一键的状态,前一键被按下为1,释放为0 13 描述表代码,若Alt键被按下为1,其他为0 11-12 保留作扩展 9-10 保留,不用 8 标志使用了扩展键(数码键盘上的键和功能键),使用了扩展键为1,否则为0 0-7 8位的OEM扫描码,可以是键的任意合法组合 参数nRepCnt为重复计数,对于一般的键击,该值为1。 参数nChar指明了产生键盘消息的按键,其值是键盘上按键对应的ASCII码键值,比如键盘上向左、向上、向右和向下方向键的键值分别为37、38、39和40。MFC中定义了一些虚拟键码,也可以通过这些虚拟键码来判断键盘消息是由什么按键产生的。下表中列出了常用的一些虚拟键码: 虚拟键码 对应按键 虚拟键码 对应按键 VK_0 – VK_9 键盘顶端数字键0-9 VK_LEFT 向左方向键 VK_A – VK_Z VK_MENU 字母键A-Z Alt键 VK_ADD VK_MULTIPLY 小数字键盘“+” 小数字键盘“*” VK_BACK VK_NEXT Backspace键 PageDown键 VK_CANCEL VK_NUMLOCK Numlock键 Ctrl+Break组合键 VK_CAPITAL VK_NUMPAD0 - 小数字键盘0-9 Capslock键 VK_NUMPAD9 VK_CLEAR VK_PAUSE Clear键 Pause键 VK_CONTROL VK_PRIOR Ctrl键 PageUp键 VK_DECIMAL VK_RETURN 小数点键“.” 回车键 VK_DELETE VK_RIGHT Delete键 向右方向键 VK_DIVIDE VK_SCROLL 小数字键盘“/” ScrollLock键 VK_DOWN VK_SHIFT 向下方向键 Shift键 VK_END VK_SNAPSHOT PrintScreen键 End键 VK_ESCAPE VK_SPACE Esc键 空格键 VK_F1 - VK_F12 功能键F1-F12 VK_SUBTRACT 小数字键盘“-” VK_HOME VK_TAB Home键 Tab键 VK_INSERT VK_UP Insert键 向上方向键 使用虚拟键码,可以让我们无需知道按键的具体ASCII码键值。 打开类向导,选择在CDrawMapView类添加对WM_KEYDOWN消息的处理函数。本应用程序只需处理按键按下消息即可。 在图元基类CMapElement中添加下面的四个成员函数: public: //向下移动图元指定步长 void MoveDown(int step); //向右移动图元指定步长 void MoveRight(int step); //向上移动图元指定步长 void MoveUp(int step); //向左移动图元指定步长 void MoveLeft(int step); 这四个函数用于移动图元,参数step指定了移动的步长,实现代码如下: //向左移动图元指定步长 void CMapElement::MoveLeft(int step) { //获得图元控制点 CPoint sp = GetStartPoint(); CPoint ep = GetEndPoint(); //向左移动指定步长 sp.x = sp.x - step; ep.x = ep.x - step; //重新设置控制点 SetStartPoint(sp); SetEndPoint(ep); //重新设置包围盒 SetBound(); } //向上移动图元指定步长 void CMapElement::MoveUp(int step) { //获得图元控制点 CPoint sp = GetStartPoint(); CPoint ep = GetEndPoint(); //向上移动指定步长 sp.y = sp.y - step; ep.y = ep.y - step; //重新设置控制点 SetStartPoint(sp); SetEndPoint(ep); //重新设置包围盒 SetBound(); } //向右移动图元指定步长 void CMapElement::MoveRight(int step) { //获得图元控制点 CPoint sp = GetStartPoint(); CPoint ep = GetEndPoint(); //向右移动指定步长 sp.x = sp.x + step; ep.x = ep.x + step; //重新设置控制点 SetStartPoint(sp); SetEndPoint(ep); //重新设置包围盒 SetBound(); } //向下移动图元指定步长 void CMapElement::MoveDown(int step) { //获得图元控制点 CPoint sp = GetStartPoint(); CPoint ep = GetEndPoint(); //向下移动指定步长 sp.y = sp.y + step; ep.y = ep.y + step; //重新设置控制点 SetStartPoint(sp); SetEndPoint(ep); //重新设置包围盒 SetBound(); } 现在编写键盘按键按下的处理函数,输入如下代码: //键盘按键按下处理函数 void CDrawMapView::OnKeyDown(UINT nChar, UINT nRepCnt, UINT nFlags) { // TODO: Add your message handler code here and/or call default //不处在绘制图元、区域选择图元或者修改图元状态 //并且按键是我们要处理的按键 if (!m_LButtonDown && (nChar == VK_LEFT || nChar == VK_DOWN || nChar == VK_RIGHT || nChar == VK_UP)) { //获得文档指针 CDrawMapDoc* pDoc = GetDocument(); ASSERT_VALID(pDoc); //定义指向CObArray列表的指针 CObArray* pArray = NULL; if (pDoc->m_SelectedList.GetSize() == 0) //当前没有选中的图元,使pArray指针指向图元列表 //因为CMapList的基类是CObArray,所以可以进行下面的造型 pArray = (CObArray*)(& pDoc->m_MapList); else //当前有选中的图元,使pArray指针指向选中图元列表 pArray = & pDoc->m_SelectedList; //循环列表(图元列表或选中图元列表) for (int i=0;i CView::OnKeyDown(nChar, nRepCnt, nFlags); } 在m_LButtonDown为false时处理移动是为了避免在绘制图元、区域选择图元或者修改图元状态下进行移动导致错误。本段代码为了避免编码的重复,定义了指向CObArray对象的指针,根据是否当前有选中的图元,让该指针分别指向选中图元列表(有选中图元)或图元列表(没有选中图元)。然后从pArray指针指向的列表中读取图元指针,根据按键的不同进行相应的移动。移动的步长为一个像素。 2.6.3 放大或缩小图元 和移动图元一样,我们设计当按键盘上的PageUp键时放大图元,而按键盘上的PageDown键时缩小图元。同时我们也做如下的默认:如果有选中图元,则只放大或缩小选中的图元,如果没有选中的图元,则放大或缩小所有的图元。 在图元基类CMapElement中添加如下虚函数: public: //按指定比例缩小图元 virtual void ZoomOut(int blc); //按指定比例放大图元 virtual ZoomIn(int blc); 这两个函数用于放大或缩小图元。在图元基类中缺省实现对椭圆、椭圆区域和矩形区域的放大和缩小,实现代码如下: //按指定比例放大图元 void CMapElement::ZoomIn(int blc) { //获得图元控制点 CPoint sp = GetStartPoint(); CPoint ep = GetEndPoint(); //按传入比例放大图元 ep.x = (ep.x - sp.x)*blc + sp.x; ep.y = (ep.y - sp.y)*blc + sp.y; //重新设置控制点 SetStartPoint(sp); SetEndPoint(ep); //重新设置包围盒 SetBound(); } //按指定比例缩小图元 void CMapElement::ZoomOut(int blc) { //获得图元控制点 CPoint sp = GetStartPoint(); CPoint ep = GetEndPoint(); //按传入比例缩小图元 ep.x = (ep.x - sp.x)/blc + sp.x; ep.y = (ep.y - sp.y)/blc + sp.y; //重新设置控制点 SetStartPoint(sp); SetEndPoint(ep); //重新设置包围盒 SetBound(); } 因为直线段图元相对于其它三种图元的特殊性,这里也需要用覆盖基类的方法来完成自己的放大和缩小,直线段图元子类CLine的ZoomIn和ZoomOut函数的实现代码如下: //按比例放大直线段 void CLine::ZoomIn(int blc) { //获得直线段控制点 CPoint sp = GetStartPoint(); CPoint ep = GetEndPoint(); //按比例放大直线段 ep.x = (ep.x - sp.x) * blc + sp.x; ep.y = (ep.y - sp.y) * blc + sp.y; //重新设置控制点 SetStartPoint(sp); SetEndPoint(ep); //重新设置包围盒 SetBound(); } //按比例缩小直线段 void CLine::ZoomOut(int blc) { //获得直线段控制点 CPoint sp = GetStartPoint(); CPoint ep = GetEndPoint(); //按比例缩小直线段 ep.x = (ep.x - sp.x) / blc + sp.x; ep.y = (ep.y - sp.y) / blc + sp.y; //重新设置控制点 SetStartPoint(sp); SetEndPoint(ep); //重新设置包围盒 SetBound(); } 图元的放大和缩小属于图形的比例变换,图形的比例变换是针对一个参考点进行的。参考点的不同会导致比例变换的结果不同。我们上面的代码对图元进行比例变换时,对于椭圆、椭圆区域和矩形区域,以每个图元的包围盒的左上角点为参考点,而对于直线段则以绘制直线段的起始点为参考点。修改键盘按键按下处理函数,修改后代码如下: //键盘按键按下处理函数 void CDrawMapView::OnKeyDown(UINT nChar, UINT nRepCnt, UINT nFlags) { // TODO: Add your message handler code here and/or call default //不处在绘制图元、区域选择图元或者修改图元状态 //并且按键是我们要处理的按键 if (!m_LButtonDown && (nChar == VK_LEFT || nChar == VK_DOWN || nChar == VK_RIGHT || nChar == VK_UP || nChar == VK_NEXT || nChar == VK_PRIOR)) { //获得文档指针 CDrawMapDoc* pDoc = GetDocument(); ASSERT_VALID(pDoc); } } //定义指向CObArray列表的指针 CObArray* pArray = NULL; if (pDoc->m_SelectedList.GetSize() == 0) //当前没有选中的图元,使pArray指针指向图元列表 //因为CMapList的基类是CObArray,所以可以进行下面的造型 pArray = (CObArray*)(& pDoc->m_MapList); else //当前有选中的图元,使pArray指针指向选中图元列表 pArray = & pDoc->m_SelectedList; //循环列表(图元列表或选中图元列表) for (int i=0;i pArray = NULL; //释放指针变量 this->Invalidate();//重画 CView::OnKeyDown(nChar, nRepCnt, nFlags); 处理函数中添加的代码不多,在一开始的条件判断中添加了对PageUp和PageDown键的判断。在循环体内,判断按键是PageUp或PageDown时,分别调用了图元的ZoomIn和ZoomOut函数。传入参数值为2,表示图元放大或缩小一倍。现在运行应用程序,实际看一下图元放大或缩小的效果。我们会发现使用我们采用的放大和缩小的方法,椭圆、椭圆区域和矩形区域无论是放大还是缩小,包围盒的左上角点的位置都不会改变,而直线段则是其绘制时的起始点位置不会 改变。图2.29中给出了一个图元放大前后对比的例子。关于比例变换的知识请读者阅读计算机图形学书中关于图形变换的章节。 2.6.4 删除图元 我们可以将选中的图元删除。删除图元通常有以下两种方式:一是给图元对象添加一个布尔型成员变量,比如m_IsDeleted,该变量为true表示图元已被删除,而为false表示图元没有被删除,在删除图元时设置该变量为真,而在重画时不绘制那些该变量值为true的图元对象,这样也达到了删除图元的效果(因为我们再看不到该图元了),而实际上图元对象仍然在内存中存在,并没有被删除;第二种方法就是将要删除的图元对应的图元对象销毁,这种方法真正将图元从内存中删除了。这两种方法各有利弊。第一种方法因为并没真正删除图元对象,所以可以恢复已删除的图元,即撤销删除,可是如果用户多次绘制图元并删除,就会造成内存中的图元对象远远多于用户能够看到的图元,过多的占用系统资源。极限情况下甚至会造成窗口中一个图元也没有,可是所有的系统资源却已经被以前删除的图元占用光了!第二种方法无法实现撤销删除,但是它保证了窗口中显示的图元与内存中的图元是一致的,不会占用额外的内存空间。第一种方法很容易实现,第二种方法相对来说实现起来复杂一些。本章中的绘图应用程序采用第二种方法来删除图元。 为了实现真正删除图元,我们在CDrawMapDoc类中添加如下两个成员函数: public: //将选中图元对象的指针从图元列表中移除 void RemoveSelected(); //清空传入的列表指针指定的列表 //并且将列表中原有指针指向的对象销毁 void DeleteList(CObArray* pArray); 第一个函数用于将要销毁的图元对象的指针从图元列表中移除,否则将造成图元列表中指针仍然存在,但对应的对象已经被销毁的现象,此时将导致错误。第二个函数用于将传入的列表中指针指向的对象销毁并清空列表。这两个函数的实现代码如下: //将选中图元对象的指针从图元列表中移除 void CDrawMapDoc::RemoveSelected() { //循环选中图元列表 for (int i=0;i //清空传入的列表指针指定的列表 //并且将列表中原有指针指向的对象销毁 void CDrawMapDoc::DeleteList(CObArray *pArray) { //查看列表中是否还有对象指针存在 while (pArray->GetSize() != 0) { //还有对象指针,取出列表中的第一个对象指针 CMapElement* pMap = (CMapElement*)pArray->GetAt(0); //从列表中将该指针移除 pArray->RemoveAt(0); //销毁该指针指向的对象 delete pMap; } } 我们在键盘上按Delete键来删除选中的对象,修改键盘按键按下消息处理函数,修改后代码如下: void CDrawMapView::OnKeyDown(UINT nChar, UINT nRepCnt, UINT nFlags) { // TODO: Add your message handler code here and/or call default //不处在绘制图元、区域选择图元或者修改图元状态 //并且按键是我们要处理的按键 if (!m_LButtonDown && (nChar == VK_LEFT || nChar == VK_DOWN || nChar == VK_RIGHT || nChar == VK_UP || nChar == VK_NEXT || nChar == VK_PRIOR)) { ……. //移动图元和图元放大或缩小的处理代码 …… } //鼠标左键没有按下,按了Delete键,并且有选中的图元 if (!m_LButtonDown && nChar == VK_DELETE && ((CDrawMapDoc*)GetDocument())->m_SelectedList.GetSize() != 0) { } CView::OnKeyDown(nChar, nRepCnt, nFlags); } 以上我们就实现了图元的真正删除。 2.6.5 图元的剪切、复制和粘贴 //将选中的图元指针从图元列表中移除 ((CDrawMapDoc*)GetDocument())->RemoveSelected(); //销毁选中图元列表中图元指针指向的图元对象并清空该列表 ((CDrawMapDoc*)GetDocument())->DeleteList (&((CDrawMapDoc*)GetDocument())->m_SelectedList); //重画 this->Invalidate(); 本绘图应用程序对图元的剪切、复制和粘贴的实现思想如下:对被选中的图元可以进行剪切或复制,然后把剪切或复制的图元指针存入到一个剪贴板列表中,然后在粘贴时将剪贴板列表中的图元添加到图元列表中。 在CDrawMapDoc类中添加下面的成员变量: public: //剪贴板列表 CMapList m_ClipboardList; 该列表存放被剪切或复制的图元对象指针,使用CMapList作为该列表的类型是因为我们编写的该类的析构函数可以销毁列表中指针指向的对象。 在应用程序默认的系统工具条中有剪切、复制、粘贴的按钮,只是原来没有相应的处理函数,所以在应用程序运行时是灰色不可用状态。我们通过资源面板选择系统默认工具条IDR_MAINFRAME,在工具条编辑区中双击这三个按钮可以知道它们的ID,然后在类向导中创建它们的处理函数。 编写剪切按钮的处理函数,代码如下: //剪切选中图元 void CDrawMapView::OnEditCut() { // TODO: Add your command handler code here //获得文档指针 CDrawMapDoc* pDoc = GetDocument(); ASSERT_VALID(pDoc); //有选中的图元,开始剪切工作 if (pDoc->m_SelectedList.GetSize() != 0) { //将选中图元的指针从图元列表中移除 pDoc->RemoveSelected(); //清空原有的剪贴板列表,并销毁列表中指针指向的对象 pDoc->DeleteList(&pDoc->m_ClipboardList); //循环选中图元列表 for (int i=0;i //获得选中图元指针 CMapElement* pMap = (CMapElement*)pDoc->m_SelectedList.GetAt(i); //修改图元的选中状态 pMap->m_isSelected = false; //添加选中图元到剪贴板列表中 pDoc->m_ClipboardList.Add(pMap); } //清空选中图元列表 pDoc->m_SelectedList.RemoveAll(); //重画 this->Invalidate(); } } 编写复制按钮的处理函数,代码如下: //复制选中的图元 void CDrawMapView::OnEditCopy() { // TODO: Add your command handler code here //获得文档指针 CDrawMapDoc* pDoc = GetDocument(); ASSERT_VALID(pDoc); //有选中的图元,开始复制工作 if (pDoc->m_SelectedList.GetSize() != 0) { //清空原有的剪贴板列表,并销毁列表中指针指向的对象 pDoc->DeleteList(&pDoc->m_ClipboardList); //循环选中图元列表 for (int i=0;i //设置绘图属性 line->m_LineStyle = pMap->m_LineStyle; line->m_LineWidth = pMap->m_LineWidth; line->m_LineColor = pMap->m_LineColor; //添加图元到剪贴板列表中 pDoc->m_ClipboardList.Add(line); } //选中的是椭圆 else if (pMap->GetType() == 2) { //创建新的椭圆对象 CEllipse* ellipse = new CEllipse(); //设置控制点并将控制点向后和向下移动10个像素 ellipse->SetStartPoint(pMap->GetStartPoint()); ellipse->SetEndPoint(pMap->GetEndPoint()); ellipse->MoveRight(10); ellipse->MoveDown(10); //设置绘图属性 ellipse->m_LineStyle = pMap->m_LineStyle; ellipse->m_LineWidth = pMap->m_LineWidth; ellipse->m_LineColor = pMap->m_LineColor; //添加图元到剪贴板列表中 pDoc->m_ClipboardList.Add(ellipse); } //选中的是椭圆区域 else if (pMap->GetType() == 3) { //创建新的椭圆区域对象 CEllipseRegion* ellipseRegion = new CEllipseRegion(); //设置控制点并将控制点向后和向下移动10个像素 ellipseRegion->SetStartPoint(pMap->GetStartPoint()); ellipseRegion->SetEndPoint(pMap->GetEndPoint()); ellipseRegion->MoveRight(10); ellipseRegion->MoveDown(10); //设置绘图属性 ellipseRegion->m_LineStyle = pMap->m_LineStyle; ellipseRegion->m_LineWidth = pMap->m_LineWidth; ellipseRegion->m_LineColor = pMap->m_LineColor; ellipseRegion->m_FillStyle = pMap->m_FillStyle; ellipseRegion->m_FillForeColor = pMap->m_FillForeColor; ellipseRegion->m_FillBackColor = pMap->m_FillBackColor; //添加图元到剪贴板列表中 pDoc->m_ClipboardList.Add(ellipseRegion); } //选中的是矩形区域 else { //创建新的矩形区域对象 CRectangleRegion* rectangle = new CRectangleRegion(); //设置控制点并将控制点向后和向下移动10个像素 rectangle->SetStartPoint(pMap->GetStartPoint()); rectangle->SetEndPoint(pMap->GetEndPoint()); rectangle->MoveRight(10); rectangle->MoveDown(10); //设置绘图属性 rectangle->m_LineStyle = pMap->m_LineStyle; rectangle->m_LineWidth = pMap->m_LineWidth; rectangle->m_LineColor = pMap->m_LineColor; rectangle->m_FillStyle = pMap->m_FillStyle; rectangle->m_FillForeColor = pMap->m_FillForeColor; rectangle->m_FillBackColor = pMap->m_FillBackColor; //添加图元到剪贴板列表中 pDoc->m_ClipboardList.Add(rectangle); } } } } 代码中,在剪切或复制新的选中图元到剪贴板列表之前,把原来的剪贴板列表中图元指针指向的图元对象销毁并清空列表,即如果在剪贴板列表中有图元时进行剪切或复制,原有的图元将被删除。复制图元时将复制的图元向右向下移动了10个像素单位,这是为了避免粘贴的时候,新粘贴的图元与原有图元重合。同时因为剪切图元后,被剪切的图元将不能再看到,所以需要重画,而复制图元则不需重画。 编写粘贴图元按钮处理函数,代码如下: //粘贴图元 void CDrawMapView::OnEditPaste() { // TODO: Add your command handler code here //获得文档指针 CDrawMapDoc* pDoc = GetDocument(); ASSERT_VALID(pDoc); //剪贴板列表不为空,开始粘贴工作 if (pDoc->m_ClipboardList.GetSize() != 0) { //将剪贴板列表中的图元指针加入到图元列表中 pDoc->m_MapList.InsertAt(pDoc->m_MapList.GetSize(), &pDoc->m_ClipboardList); //清空剪贴板列表 pDoc->m_ClipboardList.RemoveAll(); //重画 this->Invalidate(); } } 运行应用程序,我们可以通过点击工具条中的剪切、复制、粘贴按钮来完成对图元的剪切、复制和粘贴。我们也可以按快捷键来完成上述功能。剪切的快捷键是Ctrl+X,复制的快捷键是Ctrl+C,而粘贴的快捷键是Ctrl+V。现在我们简单介绍一下如何为应用程序添加快捷键。 先看一下如何为菜单项连接快捷键。选择资源面板,打开系统默认的菜单IDR_MAINFRAME,如图2.30所示,在“编辑(E)”菜单项下有“剪切(T) Ctrl+X”、“复制(C) Ctrl+C”和“粘贴(P) Ctrl+V”菜单项。 其中菜单项右侧就显示了该菜单项对应的快捷键。用鼠标右键点击“剪切(T) Ctrl+X”菜单项,在弹出的快捷菜单中选择Properties,打开菜单项属性对话框,如图2.31所示。 我们看到在“Caption”输入框中输入了如下内容: 剪切(&T)\Ctrl+X 其中(&T)产生了菜单项中“(T)”部分,它使我们可以通过按住Alt键加上&符号后面指定的键来逐层访问菜单,比如访问“剪切(T) Ctrl+X”菜单项,可以先按Alt+E(先访问“编辑(E)”菜单项),再按Alt+T。其中的\Ctrl+X则产生了“剪切(T) Ctrl+X”菜单项中右侧的“Ctrl+X”部分,\使其后的文字离开前面的文字一段后显示。同时我们注意到此菜单项的ID和工具条中剪切按钮的ID相同,因为系统调用菜单项或工具条的处理函数是通过ID来识别的,所以点击此菜单项和工具条将调用同一个处理函数。 在Caption中的内容只是完成在菜单项上的显示,快捷键并没有和菜单项实 际连接起来。为了将快捷键和菜单项真正连接起来还需要完成下面的工作。在资源面板中打开“Accelerator”节点,然后双击其下的IDR_MAINFRAME节点,在右侧的编辑区中会打开资源ID和对应快捷键的设置表,如图2.32所示。 表中列出了当前应用程序中已经定义的快捷键和对应的资源ID。因为剪切等菜单项是系统自动创建的,所以在表中已经有了其快捷键和对应的资源ID的连接定义。我们现在为绘图工具条中的六个按钮定义快捷键。用鼠标左键双击表尾端的空白行(图2.32中的深色部分),会出现快捷键属性设置对话框,如图2.33所示。 我们可以在“ID”下拉框中选择资源ID,或者直接输入ID,当该下拉框失去焦点时,系统会自动在ID后面加上其值,此处我们选择ID_SELECT(选择图元工具条按钮)。在“Key”下拉框中我们可以选择按键的虚拟键码,也可以直接输入键名,此处我们输入E。右侧的上部的三个复选框Ctrl,Alt和Shift分别代表快捷键组合为Ctrl键,或Alt键,或Shift,或三键的组合加上“Key”中指定的键。如果都不选,则表示快捷键为“Key”中指定的键。这里我们选择Ctrl。下面的两个单选钮中,如果选中了“ASCII”,则上面可选的复选框只有Alt。在输入“Key”的时候,也可以点击“Next Key Typed”按钮,此时系统让用户按下要设置的快捷键组合,然后系统自动设置“Key”下拉框的值和选择右侧上部的相应复选框。关闭对话框,设置的资源ID和快捷键连接将添加到资源ID和对应快捷键的设置 表中。这样我们就为绘图工具条中的选择图元按钮设置了快捷键Ctrl+E,运行应用程序时可以通过按快捷键Ctrl+E来进入选择(编辑)图元状态。用相同方法可以为绘图工具条中的其它五个按钮设置快捷键,读者可自己决定使用什么样的快捷键组合,只需注意快捷键不要重复即可。 2.6.6 在状态条中显示鼠标光标位置坐标值 在状态条中实时显示鼠标光标位置坐标值可以给用户绘制或编辑图元提供很大帮助。下面将介绍如何实现。 因为系统默认的状态条只有一个分栏,而该分栏用于显示菜单项或按钮等的帮助信息,所以需要给状态条增加一个分栏。方法很简单,只需要打开MainFrame.cpp文件(CMainFrame类的类文件),在文件开始的地方找到indicators的定义,在第一个ID_SEPARATOR后面再添加一个ID_SEPARATOR即可。最终代码如下: static UINT indicators[] = { ID_SEPARATOR, // status line indicator ID_SEPARATOR, // 显示坐标值分栏 ID_INDICATOR_CAPS, ID_INDICATOR_NUM, ID_INDICATOR_SCRL, }; 因为在绘图状态和编辑状态时都需要显示坐标值,所以修改CDrawMapView类的OnMouseMove函数,代码如下: //鼠标移动处理函数 void CDrawMapView::OnMouseMove(UINT nFlags, CPoint point) { // TODO: Add your message handler code here and/or call default //获得状态条指针 CStatusBar* pStatus = (CStatusBar*)AfxGetApp()->m_pMainWnd ->GetDescendantWindow(ID_VIEW_STATUS_BAR); //获得指针成功 if (pStatus) { //格式化坐标值输出到字符数组中 char tbuf[40]; sprintf(tbuf,\"(%d,%d)\ //在状态条的第二个分栏中显示坐标值 pStatus->SetPaneText(1,tbuf); } //处于绘图状态时调用鼠标绘图时鼠标移动处理函数 if (m_isDraw) this->DrawMouseMove(nFlags,point); //处于编辑状态时调用编辑状态鼠标移动处理函数 else this->EditMouseMove(nFlags,point); CView::OnMouseMove(nFlags, point); } 其中CStatusBar是MFC中封装的状态条类,而ID_VEW_STATUS_BAR是状态条的ID。SetPaneText是状态条类的成员函数,用于在指定的状态条分栏中输出文本信息。 运行应用程序,我们现在可以随时在状态条中看到鼠标光标的当前位置坐标了。 2.6.7 撤销图元的绘制和编辑 运行应用程序时,在“编辑(E)”菜单下我们会看到除了剪切、复制、粘贴等菜单项之外,还有一个“撤销(U) Ctrl+Z”菜单项,撤销是指取消用户的上一次操作。因为并没有编写该菜单项的处理函数,所以此菜单项还处于不可用状态。我们现在来看一下如何撤销用户绘制图元和编辑图元的动作。 撤销操作的基本思想是:定义一个操作结构,该结构可以描述所作的操作,然后每进行一个操作后都构造一个对应的操作结构放在一个列表中,在执行撤销操作的时候,取出列表中的最后一个操作结构,判断其是一个什么样的操作,然后执行相应的撤销动作。如果操作是一个绘制图元操作,就将该图元删除;如果操作是一个编辑图元操作,则修改图元信息使之回到编辑前的状态。 基于以上思想,我们在项目中添加一个新类CActionList,此类没有基类。我们在该类中存放操作列表,同时提供操作该列表的成员函数和撤销操作的成员函数。然后在该类头文件中添加用于定义操作的结构体Action,具体定义如下: //操作结构体 struct Action{ //操作类型: //1,绘制图元 //2,修改图元形状 //3,移动图元 int actionType; //图元的控制节点,用于修改图元形状动作 CPoint startPoint; CPoint endPoint; //确定移动方向,或者是放大或缩小 UINT direction; //图元指针 CMapElement* pMap; //图元指针列表 CObArray* pMapList; }; 此结构体每一个对应一个图元对象的操作,而在移动和缩放时可能操作的图元是多个,此时用pMapList列表成员存储图元指针。我们将此结构体定义写在CActionList类定义的外面。 在CActionList类中添加如下成员变量: public: CMapList* pMapList;//图元列表指针 CObArray* pSelectedList;//选中图元列表指针 CList AddHead函数,用于将传入的新列表元素加到队列头,并作为队列新的头元素,返回加入的元素的位置值,其函数声明如下: POSITION AddHead(ARG_TYPE newElement); AddTail函数,用于将传入的新列表元素加到队列尾,并作为队列新的尾元素,返回加入的元素的位置值,其函数声明如下: POSITION AddTail(ARG_TYPE newElement); GetHead函数,返回队列的头元素,其函数声明如下: TYPE GetHead() const; GetTail函数,返回队列的尾元素,其函数声明如下: TYPE GetTail() const; RemoveHead函数,移除队列的头元素,其函数声明如下: TYPE RemoveHead(); RemoveTail函数,移除队列的尾元素,其函数声明如下: TYPE RemoveTail(); GetCount函数,返回队列的元素数量,其函数声明如下: int GetCount() const; 现在为CActionList类添加成员函数,实现绘制图元的撤销,添加函数声明如下: public: void CancelAction();//撤销上一次操作 //添加一个绘图操作到操作列表中 void AddDrawAction(CMapElement* pMap); //撤销一个绘图操作,传入操作结构对象 BOOL CancelDrawAction(Action a); //添加操作结构对象到操作列表中 void AddActionList(Action a); 其中CancelAction函数为撤销操作的入口函数,在该函数中取出操作列表中的最后一个操作,然后根据操作的类型判断执行哪个具体的撤销操作函数。其代码实现如下: //撤销操作 void CActionList::CancelAction() { //操作列表不为空 while (m_ActionList.GetCount() != 0) { //获得操作列表中的最后一个操作结构对象 Action a = (Action)m_ActionList.GetTail(); //定义一个布尔变量,存储具体撤销操作函数的返回值 BOOL r; 作 //取出的操作是一个绘图操作 if (a.actionType == 1) //调用CancelDrawAction函数撤销该次绘图 r = CancelDrawAction(a); //移除操作列表中最后一个操作对象 m_ActionList.RemoveTail(); //如果撤销操作成功就返回,否则继续撤销操作列表中的再前一个操 if (r) return; } } 在函数中,取出操作列表中的最后一个操作结构对象,如果该操作是一个绘图操作,则调用CancelDrawAction函数撤销此次绘图。CancelDrawAction函数的实现代码如下: //撤销传入的操作结构对象指定的绘图操作 BOOL CActionList::CancelDrawAction(Action a) { //循环图元列表 for (int i=0;i } } } //销毁撤销绘制的图元 delete pMap; //返回撤销绘制操作成功 return true; } } //返回撤销绘制操作失败 return false; } 上面代码需要说明的有两点:一是如果要撤销绘制的图元当前处于选中状态,则除了要将其从图元列表中移除外,也需要从选中图元列表中移除,否则将出错;二是如果要撤销绘制操作的图元在撤销前已经被删除或剪切掉了,这样在图元列表中将找不到要撤销的图元,此时返回撤销失败,让CancelAction函数可以继续撤销前一次的操作。 AddDrawAction函数用于添加一个绘图操作对象到操作列表中,实现代码如下: //添加绘图操作对象到操作列表中 void CActionList::AddDrawAction(CMapElement *pMap) { //构造操作结构对象 Action a; //设置操作类型为绘图操作 a.actionType = 1; //设置绘图指针 a.pMap = pMap; //调用AddActionList函数将操作对象加入到操作对象列表中 AddActionList(a); } 绘图操作对象只需设置类型值为1,并设置绘制的图元指针即可。添加操作对象到操作对象列表中的工作由AddActionList函数完成,该函数的实现代码如下: //添加操作结构对象到操作列表中 void CActionList::AddActionList(Action a) { //如果当前操作列表中已经有100个操作了 if (m_ActionList.GetCount() == 100) { //移除操作列表的第一个操作 m_ActionList.RemoveHead(); } //添加传入的操作对象到操作列表的队尾 m_ActionList.AddTail(a); } 此段代码对操作列表的大小进行了限制,这样做是为了避免保存操作对象占用太多的内存,而用户绘图时实际上并不经常需要撤销掉很多步之前的操作。 现在还需要做三个工作。一是在CDrawMapDoc类中添加下面的成员变量: public: //操作列表,撤销操作用 CActionList m_ActionList; 二是在绘制图元结束时要添加绘制操作到操作列表中,这个需要修改CDrawMapView类的DrawLButtonUp函数。修改方法很简单,只需在每个将创建的图元子类对象指针加入到图元列表中的语句后面加上一个语句调用文档类m_ActionList成员变量的AddDrawAction函数,并将图元子类对象指针传入。总共需要加入四句代码: 在pDoc->m_MapList.Add(line); 语句后加上pDoc->m_ActionList.AddDrawAction(line); 在pDoc->m_MapList.Add(ellipse); 语句后加上pDoc->m_ActionList.AddDrawAction(ellipse); 在pDoc->m_MapList.Add(ellipseRegion); 语句后加上pDoc->m_ActionList.AddDrawAction(ellipseRegion); 在pDoc->m_MapList.Add(rectangle); 语句后加上pDoc->m_ActionList.AddDrawAction(rectangle); 三是需要编写“撤销”菜单项的处理函数。选择资源面板,打开系统菜单后,选择打开“撤销”菜单项的属性对话框,可以知道该菜单项的ID。然后通过类向导创建该菜单项的处理函数,并编写如下代码: //撤销菜单项的处理函数 void CDrawMapView::OnEditUndo() { // TODO: Add your command handler code here //获得文档指针 CDrawMapDoc* pDoc = GetDocument(); ASSERT_VALID(pDoc); //设置图元列表指针和选中图元列表指针到操作列表对象中 pDoc->m_ActionList.pMapList = &pDoc->m_MapList; pDoc->m_ActionList.pSelectedList = &pDoc->m_SelectedList; //执行撤销 pDoc->m_ActionList.CancelAction(); //重画 this->Invalidate(); } 到此,我们可以撤销绘图操作了。运行应用程序,用鼠标绘制几个图元,然后选择“撤销”菜单项或者按Ctrl+Z,我们会看到绘制的图元被一个个撤销。 现在来编写撤销修改图元操作的代码,在CActionList类中添加如下成员函数: public: //添加修改图元操作对象到操作列表中 void AddChangeAction(CMapElement* pMap,CPoint sp,CPoint ep); //撤销一个修改图元操作,传入操作结构对象 BOOL CancelChangeAction(Action a); AddChangeAction函数实现代码如下: //添加修改操作对象到操作列表中 void CActionList::AddChangeAction(CMapElement *pMap, CPoint sp, CPoint ep) { //构造操作结构对象 Action a; //设置操作类型为修改图元 a.actionType = 2; //设置图元指针 a.pMap = pMap; //设置原有图元控制点 a.startPoint = sp; a.endPoint = ep; //添加操作结构对象到操作列表中 AddActionList(a); } CancelChangeAction函数实现代码如下: //撤销一个修改图元操作,传入操作结构对象 BOOL CActionList::CancelChangeAction(Action a) { //循环图元列表 for (int i=0;i //返回撤销修改操作失败 return false; } 因为操作结构对象中保存了图元修改前的控制点,所以撤销时将保存的控制 点设置回去即可。因为被修改的图元也可能已被删除或剪切,所以撤销可能会失败。 在CancelAction函数中调用CancelDrawAction函数的语句后面加入如下代码: //取出的操作是一个修改图元操作 else if (a.actionType == 2) //调用CancelChangeAction函数撤销该次修改 r = CancelChangeAction(a); 在取出的要撤销的操作是修改图元操作时,调用CancelChangeAction函数进行撤销。 此时我们还需要在修改图元结束时调用AddChangeAction函数来添加修改图元操作。需要修改的函数是CDrawMapView类的EditLButtonUp函数,在函数中的CMapElement* pMap = (CMapElement*)pDoc->m_SelectedList.GetAt(0);语句后加入下面的语句: pDoc->m_ActionList.AddChangeAction(pMap,pMap->GetStartPoint(), pMap->GetEndPoint()); 到目前为止我们也可以撤销修改图元操作了。下面来编写撤销移动或缩放图元操作的代码。因为移动和缩放可以针对多个图元进行,所以处理起来相对复杂一些。类似的,在CActionList类中添加如下成员函数: public: //添加移动或缩放图元操作对象到操作列表中 void AddMoveZoomAction(UINT nChar); //撤销移动或缩放图元操作,传入操作结构对象 BOOL CancelMoveZoomAction(Action a); AddMoveZoomAction函数实现如下: //添加移动或缩放图元操作对象到操作列表中 void CActionList::AddMoveZoomAction(UINT nChar) { //构造列表指针 CObArray* pArray; if (pSelectedList->GetSize() != 0) //当前有选中的图元,移动或缩放的是选中图元 pArray = pSelectedList; else //当前没有选中图元,移动或缩放的是所有图元 pArray = pMapList; //构造移动或缩放操作结构对象 Action a; //设置操作类型为移动或缩放图元 a.actionType = 3; //设置按键的键值,通过它可以知道具体是什么操作 a.direction = nChar; //创建列表 a.pMapList = new CObArray(); //将被移动或缩放的所有图元指针存入到操作对象的列表成员中 a.pMapList->InsertAt(0,pArray); //添加操作结构对象到操作列表中 AddActionList(a); } CancelMoveZoomAction函数实现代码如下: //撤销移动或缩放图元操作,传入操作结构对象 BOOL CActionList::CancelMoveZoomAction(Action a) { //撤销移动或缩放操作的图元计数 int count = 0; //循环操作对象中保存的图元列表 for (int j=0;j //图元进行的是缩小操作 else if (a.direction == VK_NEXT) //撤销操作为放大 pMap->ZoomIn(2); //撤销操作计数加1 count++; //跳出图元列表循环 break; } } } //清空操作对象的图元指针列表 a.pMapList->RemoveAll(); //销毁操作对象的图元指针列表,避免出现内存泄漏 delete a.pMapList; //如果执行了撤销操作,返回撤销成功,否则返回撤销失败 return count != 0; } 函数根据操作对象direction成员指定的具体操作类型(移动的方向、放大或者缩小)对图元进行方向的移动或缩放操作。因为当前进行移动或缩放操作的图元可能已经被删除或剪切,撤销操作可能会失败。需要特别注意的是,因为此类型操作对象中的图元指针列表成员是通过new创建的,所以为了避免造成内存泄漏,在将操作对象从操作列表中移除时需要销毁pMapList指针对象。基于同样的原因,需要修改AddActionList函数,修改后的代码如下: //添加操作结构对象到操作列表中 void CActionList::AddActionList(Action a) { //如果当前操作列表中已经有100个操作了 if (m_ActionList.GetCount() == 100) { //获得操作列表中第一个操作对象 Action a = (Action)m_ActionList.GetHead(); //如果该操作类型为移动或缩放 if (a.actionType == 3) { //清空操作对象的图元指针列表 a.pMapList->RemoveAll(); //销毁操作对象的图元指针列表 delete a.pMapList; } //移除操作列表的第一个操作 m_ActionList.RemoveHead(); } //添加传入的操作对象到操作列表的队尾 m_ActionList.AddTail(a); } 在CActionList类中添加如下成员函数: public: //清空操作列表并销毁其中的移动或缩放操作对象中的图元指针列表对象 void Clear(); 该函数实现代码如下: void CActionList::Clear() { //循环操作列表 while (m_ActionList.GetCount() != 0) { //获得操作列表中最后一个操作对象 Action a = (Action)m_ActionList.GetTail(); //如果该操作为移动或缩放操作 if (a.actionType == 3) { //清空操作对象的图元指针列表 a.pMapList->RemoveAll(); //销毁操作对象的图元指针列表 delete a.pMapList; } //移除操作列表中最后一个操作对象 m_ActionList.RemoveTail(); } } 同时编写CActionList类的析构函数,调用Clear函数完成析构时的对象清理工作,代码如下: CActionList::~CActionList() { Clear(); } 以上修改可以确保不会造成内存泄漏问题。现在修改CancelAction函数,在调用CancelChangeAction函数的语句后面加入下面的代码: //取出的操作是一个移动或缩放图元操作 else if (a.actionType == 3) //调用CancelChangeAction函数撤销该次移动或缩放 r = CancelMoveZoomAction(a); 在取出的要撤销的操作是移动或缩放图元操作时,调用CancelMoveZoomAction函数进行撤销。最后修改CDrawMapView类的OnKeyDown函数,在pArray = NULL;语句后加入如下代码: //设置图元列表指针和选中图元列表指针到操作列表对象中 pDoc->m_ActionList.pMapList = &pDoc->m_MapList; pDoc->m_ActionList.pSelectedList = &pDoc->m_SelectedList; //添加移动或缩放操作对象到操作列表中 pDoc->m_ActionList.AddMoveZoomAction(nChar); 现在的绘图应用程序可以撤销所有的绘制图元和编辑图元的操作了。使用类似的方法,我们也可以撤销删除图元、剪切、复制、粘贴等操作,只需要编写相应的添加操作对象到操作列表和撤销操作的函数即可。这里就不再介绍了,读者感兴趣的话可以自己试着编码完成。 2.7 图元的持久化 我们的应用程序中所操作的图元对象都是存放在内存中的,一旦应用程序关闭,用户绘制的图形就会丢失。为了让应用程序能够显示以前用户绘制的图形,就需要对图元进行持久化,即把图元的数据信息以文件的形式存储到硬盘这样的永久性存储媒介中。 文件的存储格式主要有两种:文本格式和二进制格式。MFC对这两种格式文件的存储都支持。文本格式的文件操作比较方便,可以用文本编辑软件(如记事本)打开,直接观察到存储的内容。MFC中封装了Cfile类来完成文本格式文件的存取。虽然我们可以通过CFile类对象将图元数据存储成文本格式文件,但是编写存储和读取图元数据的代码会非常复杂。因为MFC应用程序框架本身没有提供对文本格式文件更多的支持,所以无论是将图元数据转换成文本字符串进行存储,还是从文件中读取字符数据再转换成图元数据并重构每个图元对象都需要程序员自己编写。同时文本格式文件的读取速度也要比二进制格式慢很多。而二进制格式文件虽然不能简单地被打开编辑,但是存取速度快,同时利用MFC提供的序列化功能,可以大大降低存取数据编程的复杂度。所以我们的绘图应用程序采用序列化的方式将图元数据存储成二进制格式文件。 在MFC中,只要类是从CObject直接或间接派生的,就可以进行序列化。这也是当初我们选择CObject类作为图元基类CMapElement的基类的原因之一。要完成类的序列化除了要从CObject类直接或间接继承下来以外,还需要满足下面三个条件: 条件1:类声明中包含DECLARE_SERIAL宏; 条件2:类文件(通常为cpp文件)中包含IMPLEMENT_SERIAL宏; 条件3:类实现序列化函数Serialize。 现在我们以图元基类CMapElement为例,说明如何让类满足上面的三个条件。首先在CMapElement的类声明(头文件)中添加如下代码: DECLARE_SERIAL(CMapElement) 这样就满足了第一个条件,其中宏传递的参数是当前进行序列化的类的类名。 然后在CMapElement的类文件(MapElement.cpp)中添加如下代码: IMPLEMENT_SERIAL(CMapElement, CObject, 1) 这样就满足了第二个条件,其中宏传递的第一个参数是类名。此代码可以添加在类文件开始的#include语句之后的任何位置,只要不在成员函数内部即可。 最后在CMapElement类中添加下面的成员函数: public: void Serialize(CArchive& ar);//序列化函数 该函数完成具体的序列化工作,我们在函数中输入如下代码: //序列化函数 void CMapElement::Serialize(CArchive &ar) { //存储 if (ar.IsStoring()) { //序列化方式存储控制点坐标和包围盒 ar< 其中,传入参数ar用于向文件中以序列的方式写入或读取数据,它的IsStoring函数在存储数据时返回true,读取数据时返回false。“<<”符号表示向ar中写入数据,而“>>”符号表示从ar中读出数据。这里要注意读出数据和写入数据的顺序必须是相同的,这样才能正确的读取保存的数据。因为区域填充格式相关的数据对线形的图元类CLine和Cellipse没有意义,所以在基类中只序列化了控制点坐标和包围盒,线型数据和区域填充数据由各个子类来序列化。现在图元基类的序列化代码就编写完毕了。用同样的方法我们可以很容易的编写图元子类的序列化代码。 在CLine类的类声明中添加如下代码: DECLARE_SERIAL(CLine) 在类文件中添加如下代码: IMPLEMENT_SERIAL(CLine, CObject, 1) 在CLine类中添加序列化函数并实现如下: void CLine::Serialize(CArchive &ar) { //调用父类的序列化函数 CMapElement::Serialize(ar); //存储 if (ar.IsStoring()) { //序列化存储线型数据 ar< 在CEllipse类的类声明中添加如下代码: DECLARE_SERIAL(CEllipse) 在类文件中添加如下代码: IMPLEMENT_SERIAL(CEllipse, CObject, 1) 在CEllipse类中添加序列化函数并实现如下: void CEllipse::Serialize(CArchive &ar) { //调用父类的序列化函数 CMapElement::Serialize(ar); //存储 if (ar.IsStoring()) { //序列化存储线型数据 ar< 在CEllipseRegion类的类声明中添加如下代码: DECLARE_SERIAL(CEllipseRegion) 在类文件中添加如下代码: IMPLEMENT_SERIAL(CEllipseRegion, CObject, 1) 在CEllipseRegion类中添加序列化函数并实现如下: void CEllipseRegion::Serialize(CArchive &ar) { //调用父类的序列化函数 CMapElement::Serialize(ar); //存储 if (ar.IsStoring()) { //序列化存储线型和填充方式数据 ar< //读取 else { //序列化读取线型和填充方式数据 ar>>m_LineStyle>>m_LineWidth>>m_LineColor; ar>>m_FillStyle>>m_FillForeColor>>m_FillBackColor; } } 在CRectangleRegion类的类声明中添加如下代码: DECLARE_SERIAL(CRectangleRegion) 在类文件中添加如下代码: IMPLEMENT_SERIAL(CRectangleRegion, CObject, 1) 在CRectangleRegion类中添加序列化函数并实现如下: void CRectangleRegion::Serialize(CArchive &ar) { //调用父类的序列化函数 CMapElement::Serialize(ar); //存储 if (ar.IsStoring()) { //序列化存储线型和填充方式数据 ar< 以上就完成了所有图元类的序列化代码编写。现在编写CDrawMapDoc类中的序列化函数Serialize,代码如下: void CDrawMapDoc::Serialize(CArchive& ar) { m_MapList.Serialize(ar);//序列化图元列表 if (ar.IsStoring()) { // TODO: add storing code here } else { // TODO: add loading code here //读取新的图元数据时需要处理以下列表 //清空选中图元列表并销毁列表中图元对象 DeleteList(&m_SelectedList); //清空剪贴板列表并销毁列表中对象 DeleteList(&m_ClipboardList); //清理操作列表 m_ActionList.Clear(); } } 文档类作为文档视图体系中存储数据的类,该类的序列化函数在保存或读取数据文件的时候会被应用程序框架自动调用。在该函数中我们可以编写存取当前所有图元对象的代码。因为在图元列表m_MapList中存放了当前系统中的所有图元,所以存储时只需要存储该列表中的所有对象,同样读取时将所有图元对象读取到该列表中即可。MFC类中所有的列表类都是从CObject派生而来的,都满足序列化的条件,所以我们只需要调用m_MapList的序列化函数,列表类自身就会自动调用列表中的每一个对象的序列化函数进行序列化。所以只需要这一句代码就可以完成所有图元的存储和读取工作。因为读取文件时很有可能用户已经完成了一些绘制工作,这时候读取以前存储的图元数据,虽然图元列表因为执行序列化而进行了更新,但是其它三个列表,包括选中图元列表m_SelectedList、剪贴板列表m_ClipboardList和操作列表m_ActionList中的数据仍然存在,所以在读取数据的时候需要对三个列表进行清理,以保证编辑读取的图元时不会出错。 现在运行应用程序,在视图区绘制一些图形,然后点击系统工具条中的保存文件按钮或者按Ctrl+S,系统会弹出一个保存文件对话框让用户选择保存文件的位置和输入文件名称,然后点击“保存”按钮,就会在指定位置保存一个二进制格式的文件。关闭应用程序,点击系统工具条中的打开文件按钮或者按Ctrl+O,系统会弹出一个打开文件对话框,选择刚才我们保存的文件,按“打开”按钮后,我们会发现刚才绘制的图形又重新出现在视图区中,我们可以继续绘制图元或对已有的图元进行编辑。 在系统工具条中还有一个新建按钮,点击该按钮将执行CDrawMapDoc类的OnNewDocument函数。在多文档应用程序中会创建一个新的文档,而对于单文档应用程序,则会重新使用当前文档。对于用户来说,选择新建是为了开始一个全新的绘制,此时应该不存在任何图元,所以我们修改此函数,输入如下代码: BOOL CDrawMapDoc::OnNewDocument() { if (!CDocument::OnNewDocument()) return FALSE; // TODO: add reinitialization code here // (SDI documents will reuse this document) //清空选中图元列表 m_SelectedList.RemoveAll(); //清空图元列表并销毁列表中图元对象 DeleteList(&m_MapList); //清空剪贴板列表并销毁列表中对象 DeleteList(&m_ClipboardList); //清理操作列表 m_ActionList.Clear(); return TRUE; } 这样用户在使用应用程序时如果选择新建,则所有已有的图元都将被删除,所有的列表都将被清空,让用户可以开始新的绘制。 2.8 解决闪屏现象 在使用我们编写的这个绘图应用程序的时候会发现,当修改窗口大小或者移动图元的时候会出现很严重的闪屏现象,本来应该被区域图元覆盖的图元在修改窗口大小或移动图元时会被看到。这是因为在这两种情况下会多次进行重画,如果重画过程中绘制了前面的图元后,遮挡该图元的图元还没有绘制,这时应该被遮挡的图元就会被看到。而当多个重画连续进行时,就会出现被遮挡的图元时隐时现的闪屏现象。为了解决这个问题,我们可以在重画的时候不是将每个图元直接绘制到屏幕上,而是先绘制到内存中,最后再统一绘制到屏幕上。 首先在CDrawMapView类中添加如下两个成员变量: public: //缓存绘图用的设备对象 CDC m_MemDC; //缓存位图 CBitmap m_MemBitmap; m_MemDC是一个CDC类对象,我们将用该对象作为绘图缓存。而CBitmap是MFC封装的一个位图类。在最终显示图形时,先将缓存的图形添加到位图中,再显示到显示设备上。这两个对象需要进行初始化。打开类向导,选择CDrawMapView类,然后选择OnInitialUpdate消息,添加该消息的处理函数,其函数名为OnInitialUpdate。该函数在视图类初始化时调用。在该函数中输入如下代码: void CDrawMapView::OnInitialUpdate() { CView::OnInitialUpdate(); // TODO: Add your specialized code here and/or call the base class //判断缓存设备环境对象是否已经初始化 if (m_MemDC.m_hDC == NULL) { //获得设备环境对象 CDC* pDC = this->GetDC(); //创建对应于传入设备环境对象的内存设备描述表 m_MemDC.CreateCompatibleDC(pDC); //创建对应的传入设备环境对象的位图 m_MemBitmap.CreateCompatibleBitmap(pDC,1200,800); } } 其中CDC类的CreateCompatibleDC成员函数创建了对应于传入的设备环境对象的内存设备描述表。这个设备描述表是代表了显示设备的一块内存区域,在图形实际显示在显示设备上之前,它被用来在内存中对图形进行准备。而位图类的CreateCompatibleBitmap则创建了一个对应于传入的设备环境对象的位图,后面两个参数指定了位图的大小。因为单文档应用程序的文档类会被重用,所以此处先要判断缓存设备环境对象是否已经初始化过了。否则我们在应用程序中选择创建新文档时将产生错误。需要注意的是,此处的代码不能够放在构造函数中,那个时候对象并没有真正创建,this指针是无效的。 现在我们需要编写代码将图元先绘制到内存中。打开类向导,选择CDrawMapView类,然后选择WM_PAINT消息,创建该消息的处理函数,函数名称为OnPaint。WM_PAINT消息在窗口需要重画时产生。如果应用程序选择响应WM_PAINT消息,则OnDraw函数将不被自动调用,而是调用WM_PAINT消息的处理函数OnPaint。在该函数中输入如下代码: void CDrawMapView::OnPaint() { //绘图用设备环境对象 CPaintDC dc(this); // device context for painting // TODO: Add your message handler code here //获得窗口的剪切区 CRect rectUpdate; dc.GetClipBox(&rectUpdate); //选择使用定义的绘图位图 CBitmap* pOldBitmap = m_MemDC.SelectObject(&m_MemBitmap); //设置窗口的剪切区 m_MemDC.SelectClipRgn(NULL); m_MemDC.IntersectClipRect(&rectUpdate); //获得窗口背景色画刷 CBrush backgroundBrush((COLORREF) ::GetSysColor(COLOR_WINDOW)); //选择画刷并返回原有的画刷 CBrush* pOldBrush = m_MemDC.SelectObject(&backgroundBrush); //用选择的画刷填充窗口剪切区 m_MemDC.PatBlt(rectUpdate.left, rectUpdate.top, rectUpdate.Width(), rectUpdate.Height(), PATCOPY); //调用OnDraw函数在内存中绘制图元 OnDraw(&m_MemDC); //在窗口剪切区中显示缓存设备环境对象中的图形 dc.BitBlt(rectUpdate.left, rectUpdate.top, rectUpdate.Width(), rectUpdate.Height(), &m_MemDC, rectUpdate.left, rectUpdate.top, SRCCOPY); //选择原有的位图和画刷 m_MemDC.SelectObject(pOldBitmap); m_MemDC.SelectObject(pOldBrush); // Do not call CView::OnPaint() for painting messages } 首先获得一个用于在显示器上显示图形的设备环境对象,然后调用它的GetClipBox成员函数获得窗口剪切区。窗口剪切区是窗口实际能够显示图形的区域。例如我们在应用程序窗口中绘制一些图形,然后将应用程序窗口变小,此时可能导致一些图形的部分或全部在实际窗口之外,这部分图形是在窗口中看不到的,而窗口实际上能够看到的区域就是窗口的剪切区,它是相对于图形而言的。在剪切区外的图形实际上没有必要绘制,因为在窗口中是看不到的。用缓存设备对象选择用于显示的位图,并将获得的窗口剪切区设置到缓存设备对象中。然后创建一个以窗口的背影色为填充色的实心画刷,并选入到缓存设备对象中,并用该画刷在缓存设备对象中填充窗口剪切区。这是因为我们最后显示图形时是通过位图来显示的,而位图就需要知道每个像素点的颜色,绘制图形时使用的像素颜色由绘制图形时使用的画笔和画刷所决定,而其它的像素的颜色还没有决定。所以这里先设定窗口剪切区中所有的像素颜色为窗口的背景色,然后在绘图时再由绘图函数去修改那些用到的像素的颜色,就像是绘画之前先准备好画布一样。在此之后调用OnDraw函数重画图元,因为传入的设备环境对象为缓存设备环境对象,所以那些绘制的图元并没有绘制到显示屏幕上,而是绘制到了内存中。缓存设备对象选择了位图,所以在窗口剪切区中的图形也绘制到了位图中。最后调用最开始获得的用于在显示屏幕上绘图的设备环境对象的BitBlt成员函数将内存中的图形绘制到显示屏幕上。BitBlt函数的作用是将指定的源设备环境对象区域中的像素进行位块(bit_block)转换,以传送到目标设备环境对象中。在这里就是将缓存设备对象中的位图中位于窗口剪切区中的像素传送到显示屏幕设备环境对象中。这样就把内存中位于窗口剪切区中的图形在窗口中显示出来了。此时我们再运行应用程序,在修改窗口或移动图元时就不会出现前面提到的闪屏现象了。 2.9 小结 在本章中,我们通过编写一个简单的绘图应用程序,学习了在MFC中如何利用鼠标消息,键盘消息,对话框等交互手段进行图形的绘制。同时介绍了一种图元的定义方式,以及在这种图元定义方式下如何绘制、选中以及编辑图元。我们还学习了如何以序列化的方式将图元进行持久化。并在最后给出了一种解决闪屏的方法。 在我们学习的内容中,鼠标消息、键盘消息、对话框等都是编写MFC应用程序中经常会用到的,读者应该熟练掌握。因为本书的重点是绘图,所以关于对话框并没有进行详细的介绍。而关于对话框和控件的知识是非常多的,感兴趣的读者可以阅读相关的书籍。本章的绘图应用程序因为主要是为了学习之用,所以提供的图元种类较少,读者可以仿照图元类定义的方法来增加新的图元,让该应 用程序可以绘制更多的图形。 因篇幅问题不能全部显示,请点此查看更多更全内容