MFC交互绘图基础

更新时间:2024-03-02 12:55:01 阅读量: 综合文库 文档下载

说明:文章内容仅供预览,部分内容可能不全。下载后的文档,内容与下面显示的完全一致。下载之前请确认下面内容是否您想要的,是否完整无缺。

第二章 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(\ return -1; // fail to create }

//创建默认状态栏

if (!m_wndStatusBar.Create(this) || !m_wndStatusBar.SetIndicators(indicators, sizeof(indicators)/sizeof(UINT))) { TRACE0(\ 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(\ 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;im_MapList.GetSize();i++) { //获得图元列表中的图元子类指针并将其造型成图元基类指针 CMapElement* pMap = (CMapElement*)pDoc->m_MapList.GetAt(i); //调用draw函数绘制图元 pMap->draw(pDC); } }

在代码中我们通过图元基类指针调用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

本文来源:https://www.bwwdw.com/article/elfa.html

Top