第 10 章 Windows用户界面 10.1 Windows消息与命令 10.2 单文档与多文档程序 10.3 对话框与常用组件 10.2 单文档与多文档程序 10.3 对话框与常用组件 10.4 通用对话框与Windows 95控件 返回主目录
10.1 Windows消息与命令 10.1.1 消息驱动机制 消息是一种报告有关事件发生的通知,类似于DOS下的用户输入。Windows应用程序是由消息驱动的。Windows操作系统允许多个任务同时运行,应用程序的输入输出由Windows统一管理;Windows系统下每一个窗口都维护一个消息队列,操作系统接收和管理所有输入消息、系统消息,并把它们发送给相应窗口的消息队列。应用程序初始化完成后,进入消息循环,维护自己的消息队列,从中取出消息,对其进行处理。编写消息处理函数是Windows编程的主要工作之一。
Windows应用程序的消息来源有以下四种:
(3) 系统消息:对程序化的事件或系统时钟中断作出反应。一些系统消息,像DDE消息(动态数据交换消息)要通过Windows的系统消息队列,而有的则不通过系统消息队列而直接送入应用程序的消息队列,如创建窗口消息。 (4) 用户消息:这是程序员自己定义并在应用程序中主动发出的,一般由应用程序的某一部分内部处理。 基于MFC类库的应用程序完成初始化后,调用Run( )函数进入消息循环。在CWnd类(所有窗口类的基类)中预定义标准Windows消息的处理函数,处理函数的名称以“On”开始,用户可以根据需要重写这些函数。
在CWnd类中,标准Windows消息的处理函数声明都带有afx_msg关键字,例如消息 WM_PAINT的处理函数被声明为: afx_msg void OnPaint( ); 关键字afx_msg用于把消息处理函数与其它CWnd成员函数分开,这些函数是通过消息映射实现的,依赖于标准的预处理宏。预处理之后,关键字afx_msg的位置就变成了空白。以全局对象theApp为例,其中的消息映射如下: BEGIN_MESSAGE_MAP(CEx01App, CWinApp) //{{AFX_MSG_MAP(CEx01App)
ON_COMMAND(ID_APP_ABOUT, OnAppAbout) // 注意:ClassWizard(类向导)将可能添加或删除消息映射宏 //}}AFX_MSG_MAP // 标准的基于文档的文件命令 ON_COMMAND(ID_FILE_NEW, CWinApp::OnFileNew) ON_COMMAND(ID_FILE_OPEN, CWinApp::OnFileOpen) // 标准的打印设置命令 ON_COMMAND(ID_FILE_PRINT_SETUP, CWinApp::OnFilePrintSetup) END_MESSAGE_MAP( )
其中的BEGIN_MESSAGE_MAP和END_MESSAGE_MAP都是预处理宏,用于声明消息映射的开始和结束。而在类中重新修改的消息处理函数声明形式为: //Generated message map functions protected: //{{AFX_MSG(CEx01Ciew) afx_msg void OnPaint( ); //}}AFX_MSG DECLARE_MESSAGE_MAP( ) 最后的DECLARE_MESSAGE_MAP宏,声明在该类中使用消息映射。
10.1.2 应用程序菜单 Windows菜单是应用程序命令项列表,菜单项可以是文字或位图。通过选择菜单项使应用程序完成与菜单项相关的命令。 下面通过一个例子来介绍如何在应用程序中建立新菜单、如何在菜单中加入新的菜单项、如何为菜单项建立相应的命令处理函数以实现消息映射。 首先看看如何在已有的下拉菜单中增加一个菜单项。 第一步,编辑菜单项资源,步骤如下: (1) 运行AppWizard创建Ex02(参看9.5.2 节的“利用AppWizard建立一个新项目”),选中“Single document”(单文档SDI)单选项后,直接按下Finish按钮。
(2) 在项目工作区窗口中打开ResourceView(资源视图)。 (3) 双击该视图中的Menu图标,展开菜单资源。 (4) 双击IDR_MAINFRAME,打开菜单编辑器,如图10.1所示。 (5) 可以看到,在“文件”菜单的最下方有一个空白菜单项,用户可以编辑这个菜单项来添加菜单项。也可以单击某个菜单项,然后按键盘上的Insert键,在选定的菜单项前面插入一个新的菜单项。 (6) 双击空白菜单项,打开Menu Item Properties对话框,如图10.2所示。
图10.1 菜单编辑器
图10.2 Menu Item Properties对话框
(7) 在Caption文本框中输入新菜单项名,输入的名字是“Demo”。在ID组合框中输入新菜单项的ID。注意,这个ID必须是唯一的,不能和别的ID重名。一般菜单项ID的命令规则是:ID_菜单名_菜单项名。因此将该菜单项命名为ID_FILE_DEMO。 (8) 关闭该对话框,新的菜单项已经添加到原有的菜单中。运行这个程序可以看到,在“文件”菜单的最下方有一个“Demo”菜单项,如图10.3所示。 注意:这里选SDI类型,一是为了简化程序设计,二是想让读者对比SDI应用程序(如本例的Ex02)和MDI应用程序(如第9章的Ex01)在外观和使用上的一些区别,对SDI和MDI有一个简单了解。关于SDI和MDI应用程序将在后续章节中向读者详细介绍。
第二步,进行消息映射。图10.3中Demo菜单项是灰色的,不可用,因为它没有命令处理函数。为此必须要对菜单项进行消息映射,为新的菜单项建立处理函数。操作步骤如下: (1)在View菜单中选择Class Wizard选项或使用加速键Ctrl+W,打开ClassWizard对话框,选中其中的Message Maps选项卡,如图10.4所示。 (2)在Class Name下拉列表框中选择CEx02View。 (3)在Object IDs列表框中选择菜单项Demo的ID,即ID_FILE_DEMO。 (4)在Messages列表框中单击COMMAND,此时Add Function按钮变亮,表示可以
图10.3 添加了Demo菜单项的应用程序
图10.4 ClassWizard 的Message Maps选项卡
添加处理函数。单击Add Function按钮或双击COMMAND,弹出Add Member Function对话框,如图10.5所示。 在Member function name编辑框中给出了系统推荐使用的函数名是OnFileDemo。通常可以使用这个缺省的名字,如果有特殊需要也可以修改它。单击OK按钮,这时可以在Member functions列表框中看到Demo菜单项的处理函数已经创建,如图10.6所示。也就是说,当用户选择Demo菜单项时,应用程序将调用OnFileDemo函数实现消息映射。 在图10.6中单击OK按钮完成消息映射。
图10.5 Add Member Function对话框
图10.6 创建Demo的处理函数OnFileDemo
第三步,编写响应程序代码。现在为OnFileDemo函数编写实际的处理程序。比如,当用户选择菜单项Demo时,在窗口中显示“您选择了Demo菜单项。”信息。操作步骤如下: (1) 在文档类CEx02Doc中声明一个CString对象m_String。从上一章可以知道,程序中的文档对象用于存储在视图中的数据,因此这里选择在文档类中创建字符串对象。 在项目工作区中选ClassView类视图,双击CEx02Doc,打开类编辑器,如图10.7所示。对CEx02Doc.h头文件中修改CEx02Doc类的定义: class CEx02Doc : public CDocument {
protected: // create from serialization only CEx02Doc( ); DECLARE_DYNCREATE(CEx02Doc) CString m_String; //输入本行内容 …… } 程序中的黑体字部分表示需要输入的内容,本书下面章节的片段中均使用此方法。“m_”前缀声明成员变量是Visual C++的标准,读者应该习惯于使用这种变量声明方法。
(2) 在CEx02Doc的构造函数中初始化m_String。在类视图中单击文档类CEx02Doc前面的“+”,将其展开。双击CEx02Doc( )函数,将会在正文窗口内打开代码编辑器。对CEx02Doc( )函数编辑如下: CEx02Doc::CEx02Doc( ) { // TODO: add one-time construction code here m_String=""; //输入本行内容 }
(3) 编辑OnFileDemo( )函数,将m_String赋值为“您选择了Demo菜单项。”。在图10 (3) 编辑OnFileDemo( )函数,将m_String赋值为“您选择了Demo菜单项。”。在图10.6(若在屏幕上看不到它,按下Ctrl+W加速键)的Member function列表框中单击OnFileDemo,然后按下Edit Code按钮或双击OnFileDemo, 出现Ex02View.cpp代码编辑窗口,并且光标已经处于需要编辑的函数OnFileDemo中。编辑OnFileDemo( )函数: void CEx02View::OnFileDemo( ) {
// TODO: Add your command handler code here CEx02Doc* pDoc=GetDocument( ); //输入以下四行内容 ASSERT_VALID(pDoc); pDoc->m_String="您选择了Demo菜单项。"; Invalidate( ); }
由于对象m_String处于文档类CEx02Doc中,要在视图类中对这一对象进行存取,必须先获得一个指向文档对象的指针,这可通过GetDocument函数完成。ASSERT_VALID( )函数则用于确保一定能获取该指针,这是Visual C++的技术,如果不使用这一函数,可能会产生错误信息。Invalidate( )函数使客户区内容失效,强迫程序调用OnDraw( )函数,重新绘制视图。 (4) 用OnDraw()函数重新绘制客户区。视图对象管理程序客户区,对客户区的绘制工作由成员函数OnDraw( )完成。几乎所有应用程序的绘制工作都通过这一函数实现,编程时必须修改这个函数。
在项目工作区中单击类视图中CEx02View前面的“+”将其展开,双击OnDraw( )函数,打开CEx02View 在项目工作区中单击类视图中CEx02View前面的“+”将其展开,双击OnDraw( )函数,打开CEx02View.cpp编辑窗口,光标位于OnDraw( )函数中。按下述内容对OnDraw函数进行编辑: void CEx02View::OnDraw(CDC* pDC) { CEx02Doc* pDoc = GetDocument( ); ASSERT_VALID(pDoc); // TODO: add draw code for native data here pDC->TextOut(0, 0, pDoc->m_String); //输入本行
} 这里使用了CDC类的成员函数TextOut( )函数,该函数用于在CDC设备类中显示字符串。它有三个参数,前两个参数用于指示字符串显示的相对位置,第三个参数用于传递要显示的字符串。 现在编译运行这个程序。在“文件”菜单中选择Demo菜单项,窗口中显示“您选择了Demo菜单项。”的信息,如图10.8所示。 建立新菜单的方法与添加菜单项类似,首先编辑菜单资源,然后为相应的菜单项建立消息映射,编写消息处理函数。在资源编辑器中建立新菜单的一般步骤如下:
图10.8 Ex02程序运行的结果
(1) 打开菜单编辑器,可以看到在菜单的最右边有一个空白的菜单项,如图10.9所示。 (2) 编辑该空白菜单项(双击它,出现对话框),将其命名为NewMenu,如图10.10所示。可以看到,Pop-up复选框是被选中的,表示本菜单带有弹出式子菜单,因此无须定义本菜单的ID。 (3) 关闭Menu Item Properties对话框,NewMenu菜单下方已经自动增加了一个空白的菜单项,如图10.11所示。用户可以通过编辑该空白菜单项,向NewMenu菜单添加菜单项。
图10.9 菜单编辑器
图10.10 New Item Properties对话框
图10.11 为NewMenu添加菜单项
(4) 向NewMenu菜单中添加两个菜单项New1和New2,并将其ID分别命名为ID_NEWMENU_NEW1和ID_NEWMENU_NEW2,如图10.12所示。 在Windows中常常可以看到,点击某个菜单项还可以显示下一级子菜单。例如为NewMenu的New2添加子菜单Sub1和Sub2的步骤如下: (1) 在菜单编辑器中双击New2菜单项,打开Menu ItemProperties对话框,如图10.13所示。 (2) 选中对话框中的Pop-up复选框,此时ID下拉列表框中的ID_NEWMENU_ NEW2自动消失。关闭该对话框后,在菜单项New2右边出现一个黑色的三角和一个空的子菜单,如图10.14所示。
图10.12 新菜单建立完成
图10.13 Menu Item Properties对话框
图10.14 New2带有下一级子菜单
(3) 编辑New2的空白子菜单,添加Sub1和Sub2两个菜单项,如图10 (3) 编辑New2的空白子菜单,添加Sub1和Sub2两个菜单项,如图10.15所示。并将它们的ID分别命名为ID_NEWMENU_NEW2_SUB1和ID_NEWMENU_ NEW2_SUB2。 无论菜单项在哪一级菜单中,建立它们的命令处理函数的方法都是一样的。因为所有的菜单项都有一个唯一的ID标识,菜单项所处的位置不影响它们的处理函数的编写。
图10.15 为NewMenu添加子菜单
10.1.3 快捷键和加速键 1. 快捷键 “文件”菜单中的“F”带有下划线,这表示用户可以通过按下Alt+F打开“文件”菜单,而无须用鼠标选取。F称为“文件”菜单的快捷键。 增加快捷键非常简单,只要在给定菜单名或菜单项名中的某个位置多写一个“&”字符,就把该字符后面的那个字符定义成了快捷键。比如把NewMenu中的字母Mp定义为快捷键,只需将菜单名设置为New&Menu即可,如图10.16所示。
图10.16 增加快捷键
2.加速键 加速键是用于某个菜单项的控制键,用户可以通过按这些键直接打开相应的菜单选项,而无须打开菜单选取。例如,在Windows中,按Ctrl+V与“编辑”菜单中的“粘贴”选择是相同的。 注意,只有菜单项才有加速键。 下面是向NewMenu菜单中的New1菜单项增加加速键Ctrl+F1的步骤: (1) 在项目工作区的资源视图中双击Accelerator图标。 (2) 双击IDR_MAINFRAME图标,打开加速键编辑器,如图10.17所示。 (3) 双击加速键编辑器中的最后一个空白项,打开Accel Properties对话框,如图10.18所示。
图10.17 加速键编辑器
图10.18 Accel Properties对话框
(4) 在ID下拉列表框中选择New1菜单项的ID,即“ID_NEWMENU_NEW1”。 (5) 在Key下拉列表框中选择VK_F1,关闭对话框,把Ctrl+F1加入到New1菜单项中。 (6) 最后还要将New1的标题改为“New1 Ctrl+F1”。
10.1.4 工具栏和状态栏 1.工具栏 工具栏显示一组按钮,每个按钮对应一个菜单项。工具栏按钮是方便直观的用户界面,用户可以直接点击某个按钮,其作用和选取相应菜单选项是相同的。 可以把任何菜单项连接到工具栏中的按钮。以“文件”菜单中的Demo菜单项为例,将其添加到工具栏的步骤如下: (1) 在项目工作区的资源视图中,双击Toolbar图标。 (2) 双击IDR_MAINFRAME图标,打开工具栏编辑器,同时在它的右边出现一组工具栏绘图工具,如图10.19所示。
图10.19 工具栏编辑器
(3) 选取工具栏编辑器中工具栏最右边的空白按钮,使用绘图工具在新按钮中画出如图10.19所示的图形。 (4) 双击工具栏编辑器中编辑好的新按钮,打开Toolbar Button Properties对话框,如图10.20所示。 (5) 在ID下拉列表框中选择ID_FILE_DEMO,新的按钮已经连接到Demo菜单项上。运行程序,单击新按钮,可以看到窗口中显示出与选择Demo菜单项相同的信息,如图10.21所示。 2.状态栏 状态栏用于显示某些提示信息。当用户把鼠标移到某个菜单项或工具栏按钮上时,状态栏中通常会显示一些简单的信息,提示用户该选项可以做哪些操作。
图10.20 Toolbar Properties对话框
图10.21 添加了工具栏按钮的应用程序
增加工具栏按钮的状态栏提示,只需在图10.20所示的Toolbar Properties对话框的Prompt文本框中放置相应信息即可。其操作步骤如下: (1) 打开需要增加提示的按钮的Toolbar Properties对话框。这里为Demo按钮增加提示信息,如图10.22所示。 (2) 在Prompt文本框中输入这个按钮的说明:“在窗口显示相关的信息\n演示信息”,这意味着当用户将鼠标移到Demo按钮上时,状态栏中将显示“在窗口显示相关信息”,而在鼠标旁边将显示“演示信息”。 再次运行程序,看一看当鼠标移到Demo按钮上时,是否会出现如图10.23所示的提示信息。
图10.22 为工具栏按钮增加提示信息
图10.23 添加了提示信息的工具栏按钮
10.2 单文档与多文档程序 10.2.1 文档—视图结构 使用MFC AppWizard创建应用程序,首先要求用户确定应用程序的基本结构:单文档(SDI)、多文档(MDI)或基于对话框的应用程序界面。其中单文档或多文档创建的应用程序是基于文档的应用程序。 创建基于文档的应用程序一般需要经过以下几步:① 为每一种类型的文档定义一个派生自CDocument类的派生类;② 添加用于存储文档数据的成员变量;③ 编写用于实现读取和修改文档数据的函数成员。
在MFC中,用文档—视图结构可将数据从用户对数据的观察中分离出来。文档用来管理应用程序的数据,视图用于显示文档并管理与用户的交互过程。实际上,视图在用户与文档之间起桥梁作用,如图10.24所示。 MFC AppWizard创建的文档是从CDocument类派生的,视图类是从CView类派生的。 CDocument类为应用程序定义的文档类提供基本功能,而CView类为应用程序定义的视图类提供基本功能。视图和文档连接在一起,在文档与用户间起中介作用。视图在屏幕上显示文档数据并把用户输入转换成对文档的操作。
图10. 24 文档—视图之间的关系
10.2.2 单文档应用程序的建立 上一节的例子通过菜单显示了一行字符,是通过菜单完成的,本节直接在应用程序的窗口中显示一些信息,如“Visual C++的单文档应用程序很简单”,按如下步骤进行操作: (1) 利用MFC AppWizard创建单文档的应用程序Ex03。 (2) 在项目工作区的类视图中单击CEx03View类前面“+”,展开该类。 (3) 双击OnDraw( )函数名,打开Ex03View.cpp编辑窗口。 (4) 对OnDraw( )函数按如下内容编辑:
void CEx03View::OnDraw(CDC* pDC) { CEx03Doc* pDoc = GetDocument( ); ASSERT_VALID(pDoc); CString m_Message=" Visual C++的单文档应用程序很简单"; pDC->TextOut(30, 30, m_Message); }
现在编译运行程序。“Visual C++的单文档应用程序很简单”显示在程序窗口中,但是,一个好的程序必然少不了与用户之间的交互。在Windows中,用户最常用的输入工具无非是键盘和鼠标,如果要显示由键盘输入的字符串,应该如何处理呢? 下面演示如何处理键盘和鼠标输入以及存储和读取文档数据。 首先,建立键盘输入存储区,用于存储键盘输入的字符串。程序中的文档对象用于存储在视图中显示的数据,因此下面在文档类中创建键盘输入存储区。当用户击键时,把键入的数据加到一个字符串中,并在视图中显示这个字符串。下面修改CEx03Doc类,增加字符串数据成员m_Message:
(1) 在类视图中双击CEx03Doc图标, 在Ex03Doc.h编辑窗口修改CEx03Doc的类 声明: class CEx03Doc : public CDocument { protected: // create from serialization only CEx03Doc( ); DECLARE_DYNCREATE(CEx03Doc) CString m_Message; //在此加入变量声明 …… }
(2) 在文档类CEx03Doc的构造函数中,把这个字符串变量初始化为空字符串。在类视图中单击CEx03Doc图标前的“+”,展开该类。双击CEx03Doc( )函数,在Ex03Doc.cpp编辑窗口修改CEx03Doc的构造函数: CEx03Doc::CEx03Doc( ) { // TODO: add one-time construction code here m_Message=" "; //对m_Message变量初始化 }
现在来处理键盘输入。键盘输入产生WM_CHAR消息,处理键盘输入就必须编写相应的消息处理函数。首先按照下面的步骤使用Class Wizard来完成消息的映射: (1) 按下Ctrl+W打开MFC ClassWizard对话框,并选择Message Maps选项卡。由于需要添加的消息处理函数属于CEx03View类,在Class name下拉列表框中选择CEx03View,如图10.25所示。 (2) 在Messages列表框中选择WM_CHAR消息,并双击它(或单击App Function按钮),这时可以在Member functions列表框中看到添加了这一OnChar函数。
图10.25 MFC ClassWizard对话框的Message Maps选项卡
现在可以通过OnChar函数来读取键盘输入并将读取的字符存入到m_Message变量中。在图10 现在可以通过OnChar函数来读取键盘输入并将读取的字符存入到m_Message变量中。在图10.25中按下Edit Code按钮,编辑OnChar函数如下: void CEx03View::OnChar(UINT nChar, UINT nRepCnt, UINT nFlags) { CEx03Doc* pDoc=GetDocument( ); //获取文档对象指针 ASSERT_VALID(pDoc); //确保一定能获取该指针 pDoc->m_Message += nChar; //将读取的字符加到m_Message中 Invalidate( ); //强行调用OnDraw函数,重绘视图 CView::OnChar(nChar, nRepCnt, nFlags); }
最后,对OnDraw函数进行修改,去掉对m_Message的声明(已经在CEx03Doc类中声明过),使之成为: void CEx03View::OnDraw(CDC* pDC) { CEx03Doc* pDoc = GetDocument( ); ASSERT_VALID(pDoc); // TODO: add draw code for native data here pDC->TextOut(30, 30, pDoc->m_Message); } 编译运行该程序并键入某些字符,相应的字符将在窗口中显示出来。
视图中显示的文档数据通过文档的串行化方法来实现磁盘的存取。所谓文档的串行化,是指在文档打开时,能够自动把文档中的数据转换为文档类所支持的对象;在保存文档时,能够自动将文档类对象转换为文档数据格式。 文档串行化方法是由CEx03Doc类中的Serialize函数完成的。在项目工作区的类视图中单击CEx03Doc前面的“+”,展开该类。双击Serialize函数,在代码编辑窗口对其编辑如下:
void CEx03Doc::Serialize(CArchive& ar) { if (ar.IsStoring( )) ar << m_Message; //存入文档 else ar >> m_Message; //读入文档 } 现在运行这个程序。在窗口中输入“这是第一次的输入”,再选择“文件”菜单中的“保存”,在“文件名”编辑框中输入“1”。再运行一次,输入“这是第二次的输入”,将其存为“2”,如图10.26所示。
图10.26 在Ex03程序中存储文件
现在可以使用“文件”菜单中的“打开”菜单项分别打开这两个文件,可以看到程序窗口中显示的字符串与存储的字符串完全一致。 接下来看看如何处理鼠标输入。按照下面的步骤修改上面的程序,将允许用户通过在客户区的某个位置单击鼠标来确定新文本输入的位置。当用户输入文本时,将会在单击鼠标的位置开始输入;当用户用鼠标选择新位置时,清除字符串对象m_Message中的字符,以便接收新的字符串。 由于鼠标左键被按下时,文本显示位置也随之变化,必须首先建立一个新的存储区,用以记录鼠标单击的位置。鼠标位置可以通过鼠标消息处理函数传递的参数获得。所有鼠标消息处理函数都传递两个参数:
UINT nFlags,CPoint point 其中,point参数是CPoint类的一个对象,用于存入鼠标当前位置。可以在程序视图类的头文件中创建一个CPoint类的对象,命名为m_Point,用于记录point参数。编辑Ex03View.h,修改CEx03View类的声明: class CEx03View : public CView { protected: // create from serialization only CEx03View( ); DECLARE_DYNCREATE(CEx03View) CPoint m_Point; //用户加入本行 …… }
与m_Message对象一样,需要对m_Point做初始化工作,这一工作在视图类的构造函数CEx03View( )中完成: CEx03View::CEx03View( ) { // TODO: add construction code here m_Point.x=0; //用户加入以下两行// m_Point.y=0; }
现在使用ClassWizard创建WM_LBUTTONDOWN消息(被鼠标左键单击)的处理函数,该函数记录鼠标新位置,并在该位置显示新输入的文本。步骤如下: (1) 按下Ctrl+W加速键,启动ClassWizard对话框,选择Message Maps选项卡。 (2) 在Class name下拉列表框中选择CEx03View,在Messages列表框中选择WM_LBUTTONDOWN消息。 (3) 按下Add Function按钮,这时可以在Member functions列表框中看到WM_LBUTTONDOWN的处理函数已被创建,函数名为OnLButtonDown。编辑OnLButtonDown函数如下:
void CEx03View::OnLButtonDown(UINT nFlags, CPoint point) { // TODO: Add your message handler code here and/or call default m_Point=point; //记录鼠标位置 CEx03Doc* pDoc=GetDocument( ); //定义pDoc使用其指向CEx03Doc对象 ASSERT_VALID(pDoc); pDoc->m_Message.Empty(); //清空字符串 Invalidate(); //强迫重新绘图 CView::OnLButtonDown(nFlags, point); }
新位置已经记录在m_Point中了。改写OnDraw函数,在新位置显示文本: void CEx03View::OnDraw(CDC* pDC) { CEx03Doc* pDoc = GetDocument( ); ASSERT_VALID(pDoc); // TODO: add draw code for native data here pDC->TextOut(m_Point.x, m_Point.y, pDoc->m_Message); }
运行程序。在客户区的任何位置单击鼠标左键,然后输入文本,新文本将会在该位置出现。程序中读者可能发现一个问题:输入字符或单击鼠标的时候,无法确认当前字符的输入位置。在Windows中,通常使用光标指示用户输入字符时显示的位置。下面介绍如何在程序中建立和使用光标。 要建立一个新光标,必须确定光标的大小。通常,光标与当前字符高度相同,宽度为平均字符宽度的1/8。文本字符串大小的相关信息由MFC提供的结构类型TEXTMETRIC记录,只要说明该类型的一个变量,然后使用CDC类中的GetTextMetrics方法对其进行填充即可。TEXTMETRIC类型中包括的成员有:tmHeight(字符高度)、tmAveCharWidth(平均字符宽度)、thMaxCharWidth(最大字符宽度)、thWeight(磅数)、tmItalic(斜体)等等。上面程序建立和使用光标的步骤如下:
(1) 在视图类中建立指示光标是否已经建立的布尔变量m_Created和光标位置m_Pos: class CEx03View : public CView { protected: // create from serialization only CEx03View( ); DECLARE_DYNCREATE(CEx03View) CPoint m_Point; BOOL m_Created; //光标是否建立// CPoint m_Pos; //光标位置// …… }
(2) 在视图构造函数中将m_Created初始化为False,表示尚未创建;将m_Pos初始化为0: CEx03View::CEx03View( ) { // TODO: add construction code here m_Point.x=0; m_Point.y=0; m_Created=FALSE; m_Pos=m_Point; }
(3) 使用GetTextMetrics方法获取当前字符的大小,并用CreateSolidCaret方法实际建立光标,编辑OnDraw函数: void CEx03View::OnDraw(CDC* pDC) { CEx03Doc* pDoc = GetDocument( ); ASSERT_VALID(pDoc); // TODO: add draw code for native data here pDC->TextOut(m_Point.x, m_Point.y, pDoc->m_Message); if(!m_Created){
TEXTMETRIC metric; pDC->GetTextMetrics(&metric); CreateSolidCaret(metric.tmAveCharWidth/8, metric.tmHeight); } 创建光标后,使用SetCarePos设置光标位置,并使用ShowCaret在视图中显示光标,还需要将m_Created设置为TRUE, 表示光标已被创建。重新修改OnDraw函数:
void CEx03View::OnDraw(CDC* pDC) { CEx03Doc* pDoc = GetDocument( ); ASSERT_VALID(pDoc); // TODO: add draw code for native data here pDC->TextOut(m_Point.x, m_Point.y, pDoc->m_Message); if(!m_Created){ TEXTMETRIC metric; pDC->GetTextMetrics(&metric);
CreateSolidCaret(metric.tmAveCharWidth/8, metric.tmHeight); SetCaretPos(m_Pos); ShowCaret( ); m_Created=TRUE; } 现在编译并运行程序。可以看到光标在窗口的左上角闪动。但在新位置点下鼠标左键时,光标并没有跟着移动。
光标需要指示下一个字符的输入位置。因此,当用户输入字符时,光标应该随之移动而出现在字符串的尾部。可按下面的方法处理: (1) 首先计算字符串结尾的位置。通过使用GetTextExtent函数可以获得字符串的高度和长度。需要注意,这里的长度不是指字符串中字符的个数,而是字符串在视图中以像素为单位计算的长度。 (2) 隐藏光标,将光标移动到字符串的尾部并再次显示它。修改OnDraw函数如下: void CEx03View::OnDraw(CDC* pDC) { CEx03Doc* pDoc = GetDocument( ); ASSERT_VALID(pDoc);
// TODO: add draw code for native data here pDC->TextOut(m_Point.x, m_Point.y, pDoc->m_Message); if(!m_Created){ TEXTMETRIC metric; pDC->GetTextMetrics(&metric); CreateSolidCaret(metric.tmAveCharWidth/8, metric.tmHeight); SetCaretPos(m_Pos); ShowCaret( ); m_Created=TRUE; }
CSize StringSize=pDC->GetTextExtent(pDoc->m_Message); //字符串长度 m_Pos.x=m_Point.x+StringSize.cx; //计算光标位置 m_Pos.y=m_Point.y; HideCaret( ); //隐藏光标 SetCaretPos(m_Pos); //移动光标到新位置 ShowCaret( ); //显示光标 }
(3) 最后还应该考虑到,在程序窗口失去输入焦点时,需要隐藏光标,当窗口重新获得焦点时再次显示光标。程序窗口失去焦点和获得焦点的消息分别是WM_KILLFOCUS和WM_SETFOCUS。使用ClassWizard分别创建这两个消息的处理函数,并编辑它们如下: void CEx03View::OnKillFocus(CWnd* pNewWnd) { CView::OnKillFocus(pNewWnd); HideCaret( ); }
void CEx03View::OnSetFocus(CWnd* pOldWnd) { CView::OnSetFocus(pOldWnd); ShowCaret( ); } 再次编译运行这个程序,可以看到闪动的光标指示下一个字符的输入位置。当用户用鼠标在其它位置点击时,所输入的字符串消失,光标又在新位置闪动。
10.2.3 多文档应用程序的建立 单文档应用程序与多文档应用程序在外观上是有区别的。可以再次运行Ex01程序,将它与单文档应用程序相比较(如Ex02、Ex03)。多文档应用程序多出一个“窗口”菜单,并且在主窗口中可以同时打开多个子窗口。这些子窗口中的内容可以是不同的。 利用MFC AppWizard生成单文档(SDI)应用程序时,只派生文档类、视图类、主窗口类和应用程序类。而生成多文档(MDI)应用程序时,它有一个主窗口和嵌在该窗口中的子窗口,所以具有CChildFrame类,而且每个窗口可以使用不同的文档模板。 单文档应用程序的主窗口类派生自CFrameWnd类,而多文档应用程序的主窗口类则是CMDIFrameWnd类的派生类。
在文档模板方面,单文档应用程序的文档模板是CSingleDocTemplate类,多文档应用程序的文档模板则是CMultiDocTemplate类。CSingleDocTemplate类构造函数的用法与CMultiDocTemplate类相同,但从建立文档模板所给定的参数不难发现,单文档应用程序文档模板的框架窗口与其主框架窗口使用相同的类(CMainFrame),而多文档应用程序文档模板的文件框架窗口使用的是子窗口类(CChildFrame)。多文档应用程序中可以有多个子窗口,因此多文档应用程序中可以有多个文档模板。 通过观察单文档和多文档应用程序的InitInstance( )函数,可以明显看出它们之间的差别:
//单文档应用程序的InitInstance( )函数 BOOL CEx02App::InitInstance( ) { …… //建立文档模板 CSingleDocTemplate* pDocTemplate; pDocTemplate = new CSingleDocTemplate( IDR_MAINFRAME, RUNTIME_CLASS(CEx02Doc), RUNTIME_CLASS(CMainFrame), // main SDI frame window
RUNTIME_CLASS(CEx02View)); AddDocTemplate(pDocTemplate); …… // 只有一个窗口 m_pMainWnd->ShowWindow(SW_SHOW); m_pMainWnd->UpdateWindow( ); return TRUE; } //多文档应用程序的InitInstance( )函数 BOOL CEx01App::InitInstance( ) { //建立文档模板
CMultiDocTemplate* pDocTemplate; pDocTemplate = new CMultiDocTemplate( IDR_EX01TYPE, RUNTIME_CLASS(CEx01Doc), RUNTIME_CLASS(CChildFrame), // custom MDI child frame RUNTIME_CLASS(CEx01View)); AddDocTemplate(pDocTemplate); // 建立MDI主窗口框架
CMainFrame* pMainFrame = new CMainFrame; if (!pMainFrame->LoadFrame(IDR_MAINFRAME)) return FALSE; m_pMainWnd = pMainFrame; …… // 显示主窗口 pMainFrame->ShowWindow(m_nCmdShow); pMainFrame->UpdateWindow( ); return TRUE; }
下面是建立一个多文档应用程序的例子。操作如下: (1) 利用MFC Appwizard创建多文档的应用程序Ex04。 (2) 在项目工作区的类视图中双击CEx04Doc图标,出现类编辑器。在CEx04Doc的类声明中加入对m_Message的声明: class CEx04Doc : public CDocument { protected: // create from serialization only CEx04Doc( ); DECLARE_DYNCREATE(CEx04Doc) CString m_Message;
…… } (3) 修改CEx04Doc( )构造函数,对m_Message变量初始化: CEx04Doc::CEx04Doc( ) { // TODO: add one-time construction code here m_Message=""; (4) 按下Ctrl+W,打开ClassWizard的Message Maps选项卡,对CEx04View类的WM_CHAR消息添加消息处理函数OnChar,对其编辑如下:
void CEx04View::OnChar(UINT nChar, UINT nRepCnt, UINT nFlags) { // TODO: Add your message handler code here and/or call default CEx04Doc* pDoc=GetDocument( ); ASSERT_VALID(pDoc); pDoc->m_Message += nChar; Invalidate( ); CView::OnChar(nChar, nRepCnt, nFlags); }
(5) 对OnDraw函数进行修改: void CEx04View::OnDraw(CDC* pDC) { CEx04Doc* pDoc = GetDocument( ); ASSERT_VALID(pDoc); // TODO: add draw code for native data here pDC->TextOut(0, 0, pDoc->m_Message); } 编译并运行该程序,选择“文件”菜单中的“新建”,或直接在工具栏中按下“新建”按钮,创建多个文档窗口,在不同的窗口中可以输入并显示不同的字符串。
10.3 对话框与常用组件 10.3.1 对话框 1.有模式对话框和无模式对话框 10.3 对话框与常用组件 10.3.1 对话框 1.有模式对话框和无模式对话框 在Windows应用程序中,对话框无处不在。在打开文件、查询以及其它数据交互时,都会用到对话框。从最简单的消息框,到复杂的数据处理框,都可以用对话框来完成。其实对话框是一个真正的窗口,它不但可以接受消息,而且还可以被移动和关闭,甚至可以在它的客户区中进行绘图操作。更为便利的是,在设计时可以把控件直接粘到对话框上去,以实现各种操作。
对话框按其动作模式分为“有模式”和“无模式”两大类。有模式对话框在被关闭之前,用户无法再进行其它工作;无模式对话框被关闭之前,用户可以在应用程序的其它窗口中进行工作。当有模式对话框被打开之后,它就接管了父窗口的输入控制权,只有当用户关闭了该对话框之后,控制权才交给父窗口。而无模式对话框则与父窗口共享控制权,用户可以在主窗口和对话框之间来回切换。 根据有模式对话框的特点,它通常被用作输入数据,如常见的打开文件对话框、保存文件对话框、显示程序信息对话框等等。而无模式对话框常用来提供更多的选择功能,如工具箱和调色板等。
2.在Visual C++程序中使用有模式对话框 首先建立一个SDI界面的应用程序Ex06,然后编辑对话框资源,步骤如下: (1) 在Insert菜单中选择Resource菜单项或直接按下Ctrl+R加速键,打开Insert Resource对话框,如图10.27所示。 (2) 在对话框中选取Dialog图标,然后单击New按钮,这时将建立一个新的对话框,并打开对话框编辑器,如图10.28所示。
图10.27 Insert Resource对话框
图10.28 对话框编辑器
也可以选中项目工作区的资源视图中的Dialog,并单击右键,在出现的快捷菜单中选择Insert Dialog菜单项,直接出现图10.28。 在图10.28左边的资源视图中可以看到,Visual C++ 6.0已经自动添加了一个对话框,ID为IDD_DIALOG1。在对话框中预设了两个按钮:OK和Cancel。这两个按钮是对话框的缺省按钮,OK按钮接受用户对对话框的动作,Cancel按钮取消用户的动作。运行时如果用户单击OK按钮,对话框关闭并返回IDOK值;如果用户单击Cancel按钮,对话框关闭并返回IDCANCEL值。 对话框资源建立后还不能直接使用,通常需要为对话框建立一个新类,然后创建对话框类的对象,才能使用这个资源。建立对话框类的步骤如下:
(1) 启动ClassWizard,弹出Adding a Class对话框,如图10.29所示。
(2) 单击OK,打开New Class对话框,如图10.30所示。
(3) 在Name文本框中输入新类的名字,如“NewDialog”,单击OK按钮。这时对话框的新类已经创建。读者可以回到ClassWizard对话框,检查Class name下拉列表框中应该出现NewDialog类,如图10.31所示。也可以单击项目工作区中的ClassView页面,检查该对话框类,它已经被加入到项目中了,如图10.32所示。 新建的对话框类的基类为CDialog类,该类所提供的函数成员见表10.1。
图10.31 生成新类后的ClassWizard对话框
图10.32 增加一个新类之后的Class View
表10.1 CDialog类提供的函数成员 函数成员名 功 能 Cdialog 构造Cdialog对象 Create 初始化CDialog对象 CreateIndirect 从内存中的对话框模板建立无模式对话框 DoModal 显示有模式对话框 EndDialog 关闭有模式对话框 GotoDlgCtrl 把焦点移到对话框中指定的控件 InitModalIndirect 从内存中的对话框建立有模式对话框;存储 参数,直到调用DoMoal函数 NextDlgCtrl 把焦点移到下一个控件 OnCancel 按下Cancel按钮或ESC键 OnInitDialog 优先完成对话框的初始化 OnOK 按下OK按钮 OnSetFont 在显示文本时,指定对话框使用的字体 PrevDlgCtrl 把焦点移到以前的对话框 SetHelpID 设置上下文相关的帮助ID
现在创建了新的对话框类,声明一个该对话框类对象,然后可以显示该对话框。首先在“文件”菜单中新建一个“显示对话框…”菜单项(图10 现在创建了新的对话框类,声明一个该对话框类对象,然后可以显示该对话框。首先在“文件”菜单中新建一个“显示对话框…”菜单项(图10.33),通过该菜单项来激活上面建立的对话框。 设置该菜单项的ID为ID_FILE_SHOW。按下Ctrl+W启动ClassWizard,为这个菜单项创建处理函数OnFileShow( )。 由于OnFileShow( )函数处于CEx06View类中,而新的对话类头文件不会自动包含在CEx06View.cpp中,得靠手工完成。双击FileView中的CEx06View.cpp,在代码编辑窗口修改CEx06View.cpp:
图10.33 添加菜单项显示对话框
// Ex06View.cpp : implementation of the CEx06View class #include "stdafx.h" #include "Ex06.h" #include "Ex06Doc.h" #include "Ex06View.h" #include "NewDialog.h" 现在,可以在OnFileShow( )函数中声明NewDialog类的对象,并使用对话框的成员函数DoModal( )以有模式方式将对话框显示在屏幕上了。DoModal( )函数返回一个整数值,用于指示用户是单击OK按钮Cancel按钮关闭对话框。
编辑OnFileShow( )函数: void CEx06View::OnFileShow( ) { // TODO: Add your command handler code here NewDialog NewDlg; int result=NewDlg.DoModal( ); } 现在可以运行程序了。选择“显示对话框…”菜单项,可以看到在屏幕上显示出新的对话框。
3.向对话框加入控件 上面的例子已经可以显示对话框,但其中几乎是一片空白。在对话框资源编辑窗口旁边有一个控件工具箱,如图10.34所示,使用其中的工具可以向对话框增加控件。 (1) 加入静态文本框(Static Text)控件。步骤如下: ① 在工具箱中单击静态文本框图标控件,然后在对话框中拖放,如图10.35所示。 ② 在静态文本框上单击右键,在弹出的快捷菜单中选择Properities菜单项来设置该控件的属性,打开的Text Properties对话框如图10.36所示。 ③ 在Caption文本框中输入“要显示的字符串”,ID采用默认值。
图10.34 Controls工具箱
图10.35 增加了静态文本框的对话框
图10.36 Text Properties对话框
(2) 加入编辑框(Edit Box)控件。步骤如下: 在工具箱中单击编辑框图标控件。用鼠标在对话框中拖放,重新布置OK和Cancel两个按钮的位置。按图10.37所示增加其它控件,它们的控件类型和ID如表10.2所示。 按下Ctrl+T可以测试对话框的具体形式。 在对话框资源中加入控件后,还必须在对应的对话框类中增加数据成员以访问控件的数据,步骤如下: (1) 启动ClassWizard,打开Member Variables选项卡,如图10.38所示。
图10.37 最后完成的对话框
表10.2 对话框的控件类型和ID 控 件 类 型 ID 属 性 静态文本框 IDC_STATIC “要显示的字符串” 静态文本框 IDC_STATIC “显示位置X值” 静态文本框 IDC_STATIC “显示位置Y值” 编辑框 IDC_MYSTRING 取默认值 编辑框 IDC_POSX 取默认值 编辑框 IDC_POSY 取默认值
图10.38 MFC ClassWizard的Member Variables选项卡
(2) 在Class name下拉列表框中选择NewDialog。 (3) 在Control IDs列表框中显示出各个控件的ID值。选择IDC_MYSTRING,单击Add Variable按钮,打开Add Member Variable对话框,如图10.39所示。 (4) 在Member variable name文本框中输入变量名m_Text,Category下拉列表框中选择Value,Variable type下拉列表框中选择CString,单击OK按钮。 (5) 回到ClassWizard对话框,可以看到ID为IDC_MYSTRING的编辑框控件就与变量m_Text关联到一起了,如图10.40所示。
图10.39 Add Member Variable对话框
图10.40 控件的成员变量m_Text
(6) 用相同的方法按表10.3把控件与变量关联在一起。 表10.3 对话框的控件ID与成员变量 对 象 变量类型 成 员 变 量 IDC_MYSTRING Cstring m_Text IDC_POSX int m_xPos IDC_POSY int m_yPos
下面的步骤引用编辑框的文本并显示: (1) 使用ClassWizard创建对话框OK按钮的事件处理函数OnOK( ),OK按钮的ID是IDOK。同时为保证m_Text、m_xPos和m_yPos中的值来自编辑框的最新文本,使用UpdateData( )方法: void NewDialog::OnOK( ) { // TODO: Add extra validation here UpdateData(TRUE);
CDialog::OnOK( ); } MFC类库使用数据交换(DDX)机制在对话框的控件和对话框类的数据成员之间交换数据。UpdateData(TRUE)将数据从对话框的控制传递到对话框对象的数据成员,缺省的调用在对话框的OnOk( )函数中;UpdateData(FALSE)将数据传送到对话框中,缺省的调用发生在对话框的OnInitDialog( )创建一个有模式对话框时。用户可以在任何时候调用UpdateData( )进行数据交换,该函数参数缺省值为TRUE。 (2) 在文档类中,新建对象m_String以存储m_Text的值,新建int类型的变量m_xPosition和m_yPosition以分别存储m_xPos和m_yPos的值。这样就可以在窗口中获取m_Text、m_xPosition 、m_yPosition,并将其存储在文档中供程序使用。
为CEx06Doc类加入成员变量的一般步骤是: 在项目工作区的类视图中选CEx06Doc类并单击右键,在出现的快捷菜单中选Add Member Variable菜单项后,便出现如图10.41所示的对话框。 在VariableType文本框中输入CString, 在VariableName文本框中输入m_String,选中Access组框中的Public单选按钮。确认无误后,按下OK按钮,已经将该对象加入到CEx06Doc类之中。 用同样的方法为CEx06Doc加入表10.4所示的成员变量。
图10.41 Add Member Variable对话框
Cstring m_String public int m_xPosition public int m_yPosition public 表10.4 CEx06Doc类的成员变量 变 量 类 型 变量名 访问权限 Cstring m_String public int m_xPosition public int m_yPosition public
(3) 在文档类的构造函数中将其初始化: (4) 修改OnFileShow( )函数,将对话框中的数据传送到视图中,并更新视图: void CEx06View::OnFileShow( ) CEx06Doc::CEx06Doc( ) { // TODO: add one-time construction code here m_String=" "; m_xPosition=0; m_yPosition=0; }
{ // TODO: Add your command handler code here NewDialog NewDlg; int result=NewDlg.DoModal( ); if(result==IDOK){ CEx06Doc* pDoc=GetDocument( ); ASSERT_VALID(pDoc); pDoc->m_String=NewDlg.m_Text; pDoc->m_xPosition=NewDlg.m_xPos; pDoc->m_yPosition=NewDlg.m_yPos; Invalidate( ); }
(5) 最后要做的工作就是在OnDraw( )函数中显示文本: void CEx06View::OnDraw(CDC* pDC) { CEx06Doc* pDoc = GetDocument( ); ASSERT_VALID(pDoc); // TODO: add draw code for native data here pDC->TextOut(pDoc->m_xPosition, pDoc->m_yPosition, pDoc->m_String); } 编译并运行程序。在编辑框中输入“I can change text”,在x, y编辑框处分别输入20和30,单击OK按钮关闭对话框后,程序窗口将在(20,30)坐标处显示该文本。
10.3.2 常用控件 上面的例子演示了如何在应用程序中使用对话框以及如何在对话框中使用控件。下面通过一个例子对常用控件的使用作简单的介绍。 这个例子是让用户在列表框中选择购买商品的类别,然后在列表框中选择要购买的商品,再选择复选框,单击“总数”按钮后,系统弹出用户要付出的钱数,如图10.42所示。创建该程序的步骤如下: (1) 利用AppWizard(exe)创建应用程序SaleList,并在第一步中选择Dialog based(基于对话框)。
图10.42 SaleList应用程序运行情况
(2) 在对话框上(不是控件)单击右键,设定该对话框的ID为IDD_SALELIST_ DIALOG,Caption为“销售表”,如图10 (3) 利用ResourceView编辑对话框,编辑如图10.44所示的对话框,各控件的信息如表10.5。在“类别”下拉列表框的属性中选择Styles选项卡,不选Sort复选框,如图10.45所示。
图10.43 Dialog Properties对话框
图10.44 SaleList对话框
表10.5 SaleList的控件信息 控 件 ID 属 性 “价格”静态文本 IDC_PRICE “价格:” “商品”分组框 IDC_STATIC “商品” “分类”单选按钮 IDC_RADIO1 “分类” “全部”单选按钮 IDC_RADIO2 “全部” “类别”静态文本 IDC_STATIC “类别:” “类别”下拉列表框 IDC_SALE_TYPE 默认值 “商品”列表框 IDC_SALE_LIST 默认值 “购买”复选框 IDC_IS_BUY “购买” “总数”命令按钮 IDC_CAL_SUM “总数” “退出”命令按钮 IDOK “退出”
图10.45 下拉列表框的Styles选项卡
为使对话框布局美观,需要精确地对齐控件。首先选中需要对齐的控件(用鼠标左键拖放一个矩形,使其包含所需控件;或按住Ctrl键,然后用鼠标左键逐个选取控件),再使用“控件对齐栏”的各个按钮来对齐控件,如图10.46所示。 (4) 利用ClassWizard在CSaleListDlg类中添加消息映射函数,如表10.6所示。 (5) 切换到ClassWizard的Member Variables选项卡,添加如表10.7所示的成员变量。 (6) 在SaleListDlg.cpp文件的顶端添加commodity结构体变量comm:
图10.46 控件对齐栏
IDC_RADIO1 BN_CLICKED OnRadio1 IDC_RADIO2 BN_CLICKED OnRadio2 表10.6 CSaleListDlg类的消息映射函数 对 象 消 息 函 数 IDC_RADIO1 BN_CLICKED OnRadio1 IDC_RADIO2 BN_CLICKED OnRadio2 IDC_SALE_TYPE CBN_SELCHANGE OnSelchangeSaleType IDC_SALE_LIST LBN_SELCHANGE OnSelchangeSaleList IDC_CAL_SUM BN_CLICKED OnCalSum IDC_IS_BUY BN_CLICKED OnIsBuy
IDC_SALE_LIST CListBox m_lstName IDC_SALE_TYPE CComboBox m_cmbType 表10.7 CSaleListDlg类的成员变量 Control Ids 变 量 类 型 变 量 名 称 IDC_SALE_LIST CListBox m_lstName IDC_SALE_TYPE CComboBox m_cmbType IDC_SALE_TYPE CString m_strType IDC_SALE_LIST CString m_strName IDC_IS_BUY BOOL m_bIsBuy IDC_PRICE Cstring m_strPrice
struct commodity { char* strType; char* strName; BOOL bIsBuy; int nPrice; }comm[ ]={ "日常用品", "牙刷", FALSE, 4, "日常用品", "牙膏", FALSE, 5, "日常用品", "毛巾", FALSE, 10, "日常用品", "肥皂", FALSE, 8,
"五金用品", "螺丝刀", FALSE, 16, "五金用品", "铁锤", FALSE, 34, "五金用品", "电锯", FALSE, 230, "五金用品", "扳手", FALSE, 28, "家用电器", "电视机", FALSE, 6000, "家用电器", "冰箱", FALSE, 3000, "家用电器", "电风扇", FALSE, 280, "家用电器", "空调", FALSE, 5800 };
(7) 编写CSaleListDlg::OnSelchangeSaleType( )函数: void CSaleListDlg::OnSelchangeSaleType( ) { // TODO: Add your control notification handler code here int nStart, nLen=4; int nIndex=m_cmbType.GetCurSel( ); m_cmbType.GetLBText(nIndex,m_strType); if(m_strType=="日常用品") nStart=0; else if(m_strType=="五金用品") nStart=4; else if(m_strType=="家用电器") nStart=8; else //全部//
{ nStart=0; nLen=12; } m_lstName.ResetContent( ); for(int i=nStart;i<nStart+nLen;i++) m_lstName.AddString(comm[i].strName); (8) 编写CSaleListDlg::OnSelchangeSaleList( )函数: void CSaleListDlg::OnSelchangeSaleList( ) { // TODO: Add your control notification handler code here UpdateData( ); for(int i=0; i<12; i++)
if(m_strName==comm[i].strName) { char buf[80]; sprintf(buf, "价格: %d 元", comm[i].nPrice); m_strPrice=buf; m_bIsBuy=comm[i].bIsBuy; UpdateData(FALSE); break; }
(9) 编写CSaleListDlg::OnIsBuy( )函数: void CSaleListDlg::OnIsBuy( ) { // TODO: Add your control notification handler code here UpdateData( ); for(int i=0; i<12; i++) if(m_strName==comm[i].strName) { comm[i].bIsBuy=m_bIsBuy; break; }
(10) 编写CSaleListDlg::OnCalSum( )函数: void CSaleListDlg::OnCalSum( ) { // TODO: Add your control notification handler code here int total=0; for(int i=0; i<12; i++) if(comm[i].bIsBuy==TRUE) total+=comm[i].nPrice; char buf[80]; sprintf(buf, "您应当支付 %d 元",total); AfxMessageBox(buf); }
(11) 编写CSaleListDlg::OnInitDialog( )函数: BOOL CSaleListDlg::OnInitDialog( ) { …… // TODO: Add extra initialization here m_cmbType.AddString("日常用品"); m_cmbType.AddString("五金用品"); m_cmbType.AddString("家用电器"); m_cmbType.AddString("全部");
m_cmbType.SetCurSel(0); for(int i=0; i<4; i++) m_lstName.AddString(comm[i].strName); return TRUE; //return TRUE unless you set the focus to a control } (12) 编写CSaleListDlg::OnRadio1( )函数: void CSaleListDlg::OnRadio1( ) { // TODO: Add your control notification handler code here m_cmbType.ResetContent( ); m_cmbType.AddString("日常用品");
m_cmbType.AddString("五金用品"); m_cmbType.SetCurSel(0); UpdateData( ); } (13) 编写CSaleListDlg::OnRadio2( )函数: void CSaleListDlg::OnRadio2( ) { // TODO: Add your control notification handler code here m_cmbType.ResetContent( ); m_cmbType.AddString("日常用品");
m_cmbType.AddString("家用电器"); m_cmbType.SetCurSel(0); } 编译并运行应用程序,选择商品的类别和名称,注意价格值。选“分类”单选按钮或“全部”单选按钮,注意“类别”下拉列表框和“商品”列表框会有哪些不同。在想“购买”该商品时,选择“购买”复选框。单击“总数”命令按钮,可以获得所购买商品的总价格。
10.4 通用对话框与Windows 95控件 10.4.1 通用对话框 10.4.1 通用对话框 Windows系统提供了一些标准的通用对话框,在Visual C++中可以通过MFC类库中下面几个类来使用这些通用对话框。 1.CFileDialog(文件存取对话框类) 几乎所有的Windows应用程序都和文件有关系,尤其在文档—视图结构下,文件更是保存文档的标准媒介,而文件存取对话框就专为打开文件和保存文档操作而设计的。 在MFC中,CFileDialog源于CDialog类,所以它具备对话框的基本行为。如果CFileDialog类所提供的功能无法满足要求,那么可以把CFileDialog作为基类,派生新的文件对话框类,添加新的函数来满足特定的需要。
创建文件对话框对象时,系统执行CFileDialog( )构造函数来建立此对象,可以通过它的参数在构造函数中设定文件对话框的初值,决定是打开文件还是保存文件以及设置过滤器,只把符合要求的文件在列表框中显示。 2.CPrintDialog(打印对话框类) CPrintDialog类为应用程序提供一个标准的打印环境,可以利用它来建立打印(Print)对话框和打印设置(Print Setup)对话框。 在使用AppWizard建立程序的过程中,如果在“MFC AppWizard—Step 5”(参考9.5.2节)中选中Printing and Print Preview功能设定,系统便会产生相关程序。
3.CColorDialog(颜色设定对话框类) 由于每个人对颜色喜好不一样,在图形化操作界面中,大都会提供颜色设定对话框,让用户依个人喜好重新调配桌面环境的颜色。 4.CFontDialog(字体对话框类) 在字处理或排版软件中,或者需要采用不同字体加强显示效果时,经常需要设定字体、字型等属性,这种场合就可以利用字形设定对话框来帮助用户选择文本外型、大小、颜色及字体。 5.CFindReplaceDialog(查找/替换对话框类) 在编辑文本时,经常会使用字符串“查找”、“替换”命令,尤其是在修改大量文本数据时特别有用。
CFindReplaceDialog类提供了标准的字符串查找和替换(Find and Replace)对话框。此类对话框是无模式的,以便在对话框中输入字符串并执行查找/替换命令时,让用户立即从原操作窗口看到该命令执行的结果。 6.COleDialog(OLE对话框类) 它是一种高级的对话框,由此类派生以下几个对话框类: COleInsertDialog, COleConverDialog, COleChangeIconDialog, COleLinksDialog, Cole- BusyDialog, COleUpdateDialog, COlePasteSpecialDialog, COlePropertiesDialog, ColeChange- SourceDialog OLE对话框类与OLE操作有关,这里不再详细介绍。
下面创建的一个程序,用户通过选择命令菜单弹出“字体”对话框,在对话框中选择字体的名称、下划线、样式、大小等属性来改变字体。 (1) 利用MFC AppWizard(exe)创建单文档应用程序TestDialog。 (2) 利用ResourceView编辑如图10.47所示的菜单。 (3) 利用ClassWizard按表10.8中的信息在CTestDialogView类中添加消息映射函数。 (4) 在CTestDialogView类中添加成员变量和成员函数,如表10.9所示。 (5) 编写CTestDialogView( )构造函数:
图10.47 IDR_MAINFRAME菜单
表10.8 CTestDialogView类中添加消息映射函数 对 象 消 息 函 数 ID_TEST_FONTDLG COMMAND OnTestFontdlg
表10.9 CTestDialogView类中的成员变量和成员函数 变量类型 变量名称 访问权限 int m_nColor protected CFont* m_ pCurFont protected 函数类型 函数名称 访问权限 Void Redraw(CDC* pDC)n protected
CTestDialogView::CTestDialogView( ) { // TODO: add construction code here m_pCurFont=new CFont; m_pCurFont->CreateFont(40, 0, 0, 0, 400, FALSE, FALSE, 0, ANSI_CHARSET, OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS, DEFAULT_QUALITY, DEFAULT_PITCH&FF_SWISS, "Aerial"); }
(6) 编写CTestDialogView( )析构函数: CTestDialogView::~CTestDialogView( ) { delete m_pCurFont; } (7) 编写OnDraw( )函数: void CTestDialogView::OnDraw(CDC* pDC) CTestDialogDoc* pDoc = GetDocument( );
ASSERT_VALID(pDoc); // TODO: add draw code for native data here Redraw(pDC); } (8) 编写OnTestFontdlg( )函数: void CTestDialogView::OnTestFontdlg( ) { // TODO: Add your command handler code here CFontDialog dlg; int nRet=dlg.DoModal( ); if(nRet==IDOK){
delete m_pCurFont; m_pCurFont=new CFont; m_nCurFont->CreateFont(dlg.GetSize( ), 0, 0, 0, dlg.GetWeight( ), dlg.IsItalic( ), dlg.IsUnderline( ), dlg.IsStrikeOut( ), ANSI_CHARSET, OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS, DEFAULT_QUALITY, DEFAULT_PITCH&FF_SWISS, dlg.GetFaceName( )); m_nColor=dlg.GetColor( ); } CDC* pDC=GetDC( ); Redraw(pDC);
(9) 编写Redraw( )函数: void CTestDialogView::Redraw(CDC *pDC) { CRect rect; GetClientRect(&rect); CBrush* pOldBrush = (CBrush*)pDC->SelectStockObject(WHITE_BRUSH); pDC->Rectangle(rect); pDC->SelectObject(pOldBrush); CFont* pOldFont; pOldFont=(CFont*)pDC->SelectObject(m_nCurFont); pDC->SetTextColor(m_nChar); pDC->TextOut(40, 40, "我爱MFC编程!"); pDC->SelectObject(pOldFont); }
表10.10 CTestDialogDoc类的消息映射函数 (10) 利用ClassWizard按表10.10中的信息在CTestDialogDoc类中添加消息映射函数。 (11) 编写OnFileOpen( )函数: 表10.10 CTestDialogDoc类的消息映射函数 对 象 消 息 函 数 ID_FILE_OPEN COMMAND OnFileOpen ID_FILE_SAVE_AS COMMAND OnFileSaveAs
void CTestDialogDoc::OnFileOpen( ) { // TODO: Add your command handler code here CFileDialog dlg(TRUE); int ret=dlg.DoModal( ); if(ret==IDOK){ CString pathName=dlg.GetPathName( ); CString fileName=dlg.GetFileName( ); char buf[120]; sprintf(buf, "The %s file in %s is opened!", fileName,pathName); AfxMessageBox(buf); }
(12) 编写OnFileSaveAs( )函数: void CTestDialogDoc::OnFileSaveAs( ) { // TODO: Add your command handler code here CFileDialog dlg(FALSE); int ret=dlg.DoModal( ); if(ret==IDOK){ CString pathName=dlg.GetPathName( ); CString fileName=dlg.GetFileName( ); char buf[120]; sprintf(buf, "The %s file in %s is saved!", fileName,pathName); AfxMessageBox(buf); }
编译并运行应用程序。选择“文件”菜单中的“另存为”菜单项,将弹出如图10 编译并运行应用程序。选择“文件”菜单中的“另存为”菜单项,将弹出如图10.48所示的对话框;选择“测试”菜单中的“字体对话框”,将弹出如图10.49所示的对话框。
图10.48 运行中的“保存为”对话框
图10.49 运行中的“字体”对话框
10.4.2 Windows 95控件 Windows操作系统提供了很多常用的控件,Windows 95又增加了更多的控件,本节介绍几个Windows 95系统提供的控件。 1.spin(微调器) 微调器(也称数字滚选按钮)通常都和另外一个控件相关联,用于对关联的控件增加或减少一个值。微调器控件的类为CSpinButtonCtrl,消息为UDN_DELTAPOS。 2.ProgressConstrol(进度器控件) 在安装Windows应用程序时,经常会看到有一个指示安装进度的指示器,这就是用进度器控件来完成的。进度器控件的类为CProgressCtrl。
进度器控件有几个基本属性:范围、步长和位置。进度器控件的范围由一个操作进度的起始值共同决定,默认的控件范围是0~100。步长指的是进度器控件每前进一步所走的距离,默认值是0。位置指的是进度器控件的当前所在位置,开始位置当然是范围的起始值。可以通过函数成员SetRange( )和SetStep( )设置范围和步长。 3.SliderControl(滑块控件) 在多媒体应用程序中,设定音量大小的控件是最典型的滑块控件。它有滑块、滑道以及刻度。滑块控件的类为CSliderCtrl,消息为NM_CUSTOMDRAW。可以通过函数成员SetRange( )设定滑块控件的范围。
4.TreeControl(树控件) 树控件是一个显示项目层次列表的窗口,其中的项目可以是文档标题、索引项等等。树控件的类是CTreeCtrl。 在树内,每个项目一般由一个标号(也就是一个文本)和图像共同组成。树内的项目是有层次的,每个项目都可以有比自己层次低的子项目,用户可以通过单击某个项目来扩展或收缩其相关联的子项目。 树的外观是多种多样的,可以在树控件的Styles选项卡中直接设定。其主要外观有:在项目之间的连线属性Has lines,在项目前加一个内有正负号的方框来表示该项目是否已展开的属性Has buttons,根项目之间的连线属性Lines at root,等等。
要使用树控件首先要了解两个结构:TV_ITEM和TV_INSERTSTRUCT。 typedef struct TVITEM { UINT mask; / /指示结构中的哪些成员包含有效数据或补充填充的一个标志数组 HTREEITEM hItem; UINT state, tateMask; //当前状态和项目的有效状态, 是状态值的任意有效组合 LPSTR pszText; //存放项目文本的字符串地址
int cchTextMax; //pszText成员指向的缓冲区的大小 int iImage, SelectedImage; //图像列表中的图标图像和已选定的图标图像的索引 int cChildren; //与项目相关联的子项目数 LPARAM lParam; } TV_ITEM; 其中mask可以是下列值的组合并相应地指明哪个成员有效,如
TVIF_CHILDREN、TVIF_HANDLE、TVIF_IMAGE、TVIF_PARAM、VIF_SELECTEDI- MAGE、TVIF_SATE、TVIF_TEXT分别表示 CChildren、hItem、iImage、lParam、iSelectedImage、state、stateMask、pszText及cchTextMax成员有效。例如,让pszText和iImage均有效,可设mask为: mask = TVIF_TEXT|TVIF_IMAGE (2) 用来表示项目在树内的位置的TV_INSERTSTRUCT结构: typedef struct TVINSERTSTRUCT {
HTREEITEM hParent; //父项目句柄,若为树的根结点,成员值为TVI_ROOT或NULL HTREEITEM hInsertAfter; //可以是项目句柄,在此项目之后将插入新项目 //或为TVI_FIRST, VI_LAST, VI_SORT之一 TV_ITEM item; //定义将加入到树控件的项目的TV_ITEM结构 } TV_INSERTSTRUCT; 由于TV_INSERTSTRUCT结构中含有一个TV_ITEM成员,它实际包含了树项目的所有信息,用户将其父项目及位置信息赋给hParent和hInsertAfter,而将项目的文本、图像等信息赋给item。
下面的程序片段利用树控件生成一个树结构,其中m_myTreectrl为树控件对象。 TV_INSERTSTRUCT tvstruct; //用来存放树项目信息 TCHAR rgszItems[ ][18]= { "车","汽车","火车","自行车", "飞机","客机","战斗机","运输机", "家电","电视","冰箱", "微波炉"}; //存放所有树项目的文本数组 HTREEITEM m_rghItem[12]; //存放所有树项目的句柄数组 for(int i=0; <12; ++)
{ tvstruct.hParent=(i%4==0)?NULL:m_rghItem[i/4*4]; //确定项目的父项目 tvstruct.hInsertAfter=TVI_SORT; //按字母顺序插入项目 tvstruct.item.pszText=rgszItems[i]; //确定项目文本 tvstruct.item.mask=TVIF_TEXT; //指示pszText成员包含有效数据 //将项目插入树中,并将项目句柄保存 m_rghItem[i]=m_myTreectrl.InsertItem(&tvstruct); }
5.Control(标签控件) 在Windows应用程序中经常能看到标签控件,CTabCtrl类提供了一种实现标签控件的方法,标签控件在功能上等价于一系列对话框或窗口。但标签控件无法直接在各个标签页上插入控件,只能在选中不同标签页时显示不同的对话框或窗口的视图。当标签切换时,标签控件有两个重要的消息TCN_SELCHANGING和ICN_SELECHANGE,它们分别是在改变当前标签前,以及选择了新的标签后发出的。这样就可以在TCN_SELECHANGING消息响应函数中,将原来的对话框隐藏起来,而在TCN_SELECHANGE消息响应函数中显示新的对话框。限于篇幅,本书不再详细介绍。 Windows 95控件的使用方法与前面介绍的一般控件的使用方法类似,下面是一个使用微调器的例子。
(1) 根据图10.50建立基本对话框的应用程序Test1。 图10.50 使用微调器的对话框示例
(2) 各控件的属性值如表10.11所示。 表10.11 使用微调器示例的各控件的属性值 控 件 ID 属 性 静态文本 IDC_STATIC “成绩” 编辑框 IDC_EDIT 微调器 IDC_SPIN OK按钮 IDOK “确定” CanCel按钮 IDCANCEL “取消”
(3) 利用ClassWizard在该对话框类中增加消息处理函数。对话框类选中后,在Object IDs的列表框中选IDC_SPIN,在Messages列表框中双击UDN_DELTAPOS消息,默认微调器控件中的消息处理函数为OnDeltaposSpin。 (4) 切换到ClassWizard的Member Variables选项卡,为编辑框设置int类型的成员变量m_Score,最小值为0,最大值为100。 (5) 编写OnInitDialog( )函数如下: BOOL CTest1Dlg::OnInitDialog( ) { CDialog::OnInitDialog( ); // TODO: Add extra initialization here
CSpinButtonCtrl* pSpin; pSpin=(CSpinButtonCtrl*)GetDlgItem(IDC_EDIT); //指向该微调器所关联的Edit控件 pSpin->SetRange(0, 100); //设置微调器滚动范围 return TRUE; //return TRUE unless you set the focus to a control } (6) 编写OnDeltaposSpin( )函数如下: void CTest1Dlg::OnDeltaposSpin(NMHDR* pNMHDR, LRESULT* pResult) { NM_UPDOWN* pNMUpDown = (NM_UPDOWN*)pNMHDR; // TODO: Add your control notification handler code here
m_Score += pNmUpDown->iDelta; //使m_Score值变化 char buf[80]; //在Edit控件中显示此变化 sprintf(buf,"%d", m_Score); SetDlgItemText(IDC_EDIT, buf); *pResult = 0; }