第五课 对话框
对话框是一种用户界面,它的主要功能是输出信息和接收用户的输入。对话框与控件是密不可分的,在每个对话框内一般都有一些控件,对话框依靠这些控件与用户进行交互.一个典型的对话框例子是选择了File-Open命令后弹出的文件对话框.
对话框是一种复杂的用户界面,本章的讨论将围绕对话框和基本控件进行,主要包括以下几点:
5.1对话框和控件的基本概念
5.2 对话框模板的设计
5.3 对话框类的设计
5.4 非模态对话框
5.5 标签式对话框
5.6 公用对话框
5.7 小结
5.1对话框和控件的基本概念
5.1.1对话框的基本概念
对话框(Dialog)实际上是一个窗口.在MFC中,对话框的功能被封装在了CDialog类中,CDialog类是CWnd类的派生类.
对话框分为模态对话框和非模态对话框两种.大部分读者都会有这样的经历,当你通过File-Open命令打开一个文件对话框后,再用鼠标去选择菜单将只会发出嘟嘟声,这是因为文件对话框是一个模态对话框.模态对话框垄断了用户的输入,当一个模态对话框打开时,用户只能与该对话框进行交互,而其它用户界面对象收不到输入信息.我们平时所遇到的大部分对话框都是模态对话框。非模态对话框的典型例子是Windows95提供的写字板程序中的搜索对话框,搜索对话框不垄断用户的输入,打开搜索对话框后,仍可与其它用户界面对象进行交互,用户可以一边搜索,一边修改文章,这样就大大方便了使用.
本节主要介绍模态对话框,在第四节将介绍非模态对话框.
从MFC编程的角度来看,一个对话框由两部分组成:
对话框模板资源.对话框模板用于指定对话框的控件及其分布,Windows根据对话框模板来创建并显示对话框.
对话框类.对话框类用来实现对话框的功能,由于对话框行使的功能各不相同,因此一般需要从CDialog类派生一个新类,以完成特定的功能.
相应地,对话框的设计包括对话框模板的设计和对话框类的设计两个主要方面.
与对话框有关的消息主要包括WM_INITDIALOG消息和控件通知消息。在对话框创建时,会收到WM_INITDIALOG消息,对话框对该消息的处理函数是OnInitDialog 。
OnInitDialog的主要用处是初始化对话框。对话框的控件会向对话框发送控件通知消息,以表明控件的状态发生了变化。
5.1.2控件的基本概念
图5.1对话框中的控件
控件(Control)是独立的小部件,在对话框与用户的交互过程中,控件担任着主要角色.控件的种类较多,图5.1显示了对话框中的一些基本的控件.MFC的控件类封装了控件的功能,表5.1介绍了一些常用的控件及其对应的控件类.
表5.1
控件 | 功能 | 对应控件类 |
静态正文(Static Text) | 显示正文,一般不能接受输入信息。 | CStatic |
图片(Picture) | 显式位图、图标、方框和图元文件,一般不能接受输入信息. | CStatic |
编辑框(Edit Box) | 输入并编辑正文,支持单行和多行编辑. | CEdit |
命令按钮(Pushbutton) | 响应用户的输入,触发相应的事件. | CButton |
检查框(Check Box) | 用作选择标记,可以有选中、不选中和不确定三种状态。 | CButton |
单选按钮(Radio Button) | 用来从两个或多个选项中选中一项. | CButton |
组框(Group Box) | 显示正文和方框,主要用来将相关的一些控件聚成一组. | CButton |
列表框(List Box) | 显示一个列表,用户可以从该列表中选择一项或多项. | CListBox |
组合框(Combo Box) | 是一个编辑框和一个列表框的组合.分为简易式、下拉式和下拉列表式. | CComboBox |
滚动条(Scroll Bar) | 主要用来从一个预定义范围值中迅速而有效地选取一个整数值. | CScrollBar |
控件实际上都是窗口,所有的控件类都是CWnd类的派生类.控件通常是作为对话框的子窗口而创建的,控件也可以出现在视窗口,工具条和状态条中.
5.2对话框模板的设计
利用Developer Studio提供的可视化设计工具,用户可以方便地设计对话框模板.
请读者按前面章节介绍的方法利用AppWizard建立一个名为Register的MFC应用程序,并在进入MFC AppWizard对话框后按下面几步操作:
在第6步中先选择CRegisterView,然后在Base class栏中选择CEditView,这样CRegisterView将是CEditView的继承类,从而使视图具有了编辑功能.
编译并运行Register,读者会发现Register居然是个编辑器,它可以打开、编辑和保存文本文件. 当然,Register的目的不仅仅是个编辑器。假设要对某一地区的就业情况进行调查,我们希望Register程序能够登录就业情况数据并将数据存储起来.
要登录数据,用对话框是再合适不过了。一个典型的就业情况登录对话框如图5.1所示,本节的任务就是设计如图5.1的中文对话框模板.
切换至资源视图,选择Insert-Resource命令,并在Insert Resource对话框中双击Dialog项。完成后在资源视图中会出现一个名为IDD_DIALOG1的新的对话框模板资源。双击IDD_DIALOG1,则会打开该对话框模板的编辑窗口,如图5.2所示。缺省的对话框模板有OK和Cancel两个按钮,在窗口的旁边有一个控件面板,在控件面板上用鼠标选择一个控件,然后在对话框中点击,则相应的控件就被放置到了对话框模板中。图5.3显示了控件面板上的按钮所代表的控件。读者不用记忆图5.3的内容,如果不能确定控件的类型,可将鼠标在某个控件按钮上停留片刻,则会显示一个工具提示,指出该按钮所代表控件的名称。
图5.2 缺省的对话框模板
图5.3 控件面板
提示:若读者看不到控件面板,请在Developer Studio的工具条的空白处单击鼠标右键,并在随之弹出的菜单中选中Controls。 |
读者可以在对话框模板中随意加几个控件试试看。当用鼠标选择对话框或控件时,会出现一个围绕它的虚框,拖动虚框的边界可以改变对话框或控件的大小,在Developer Studio的状态条中会显示出所选对象的坐标和尺寸。控件可以被拖动,也可以按箭头键来移动选中的控件。在拖动控件时若按住Ctrl键,则控件会被复制。
用户可以一次选择多个控件,选择的方法有两个:1。 在对话框的空白处拖动鼠标,则拖动出来的虚线框内的控件将被选中。2。在选择控件时按住Ctrl键,则可以多重选择。
选中控件或对话框后按回车键,则会弹出一个属性对话框,属性对话框用来设置控件或对话框的各种属性。属性对话框是标签式对话框,第一页是常规属性(General)。一个典型的控件属性对话框如图5.4所示.如果对属性对话框中的选项的意思不明白,可以按F1键获得帮助.
图5.4 控件属性对话框
在控件属性对话框的常规属性中,有一些控件共同的属性:
ID属性。用于指定控件的标识符,Windows依靠ID来区分不同的控件。
Caption(标题)属性。静态正文、组框、按钮、检查框、单选按钮等控件可以显示标题,用来对控件进行文字说明。控件标题中的字符&使紧跟其后的字符有下划线,按Alt+下划线将启动该控件。若控件是一个单选按钮,则Alt+下划线字符将选择该按钮;若是检查框,则相当于对该检查框按空格键;若是按钮,则将激活按钮命令;若控件是一个静态正文,则将激活按tab顺序紧随其后的下一个控件。
Visible属性。用来指定控件是否是可见的。
- Disable属性。使控件允许或禁止,一个禁止的控件呈灰色显示,不能接收任何输入。
- Tabstop属性。用户可以按Tab键移动到具有Tabstop属性的控件上。Tab移动的顺序可以由用户指定。按Ctrl+D则Tab顺序会显示出来,如图5.5,用户可以用鼠标来重新指定Tab顺序。缺省的Tab顺序是控件的创建次序。
- Group属性。用来指定一组控件,用户可以用箭头键在该组控件内移动。在同一组内的单选按钮具有互斥的特性,即在这些单选按钮中只能有一个是选中的。如果一个控件具有Group属性,则这个控件以及按Tab顺序紧随其后的所有控件都属于一组的,直到遇到另一个有Group属性的控件为止。
现在就开始进行对话框模板的设计。首先,用鼠标选中对话框,按回车键,在弹出的属性对话框中将ID改为IDD_REGISTER并指定对话框的标题为“登录数据”。需要注意的是,由于要在对话框中显示汉字,因此必须设定正确的语种和字体。请读者在工作区资源视图的Dialog类型中单击鼠标选中IDD_REGISTER项,然后按Alt+Enter键,并在弹出的属性对话框中的Language栏中选择Chinese(P.R.C.)。接着,打开模板的属性对话框,单击Font...按钮,并选择“宋体”。
接着,请将对话框模板上的所有控件删除,删除的办法是选择控件后按Del键。为了容纳所有需要的控件,需将对话框的尺寸扩大到280×180。然后,请读者按图5.1和表5.2来设计对话框模板。
提示:对话框的尺寸单位不是象素,而是与字体的大小有关。X方向上一个单位等于字符平均宽度的1/4,Y方向上一个单位等于字符平均高度的1/8。这样,随着字体的改变,对话框单位也会改变,对话框本身的总体比例保持不变。 |
表5.2
控件类型 | ID | 标题(Caption) | 其它属性 |
组框(个人情况) | 缺省 | 个人情况 | 缺省 |
组框(单位情况) | 缺省 | 单位情况 | 缺省 |
静态正文(姓名) | 缺省 | 姓名 | 缺省 |
编辑框(姓名) | IDC_NAME | 缺省 | |
检查框(婚否) | IDC_MARRIED | 婚否 | 缺省 |
静态正文(年龄) | 缺省 | 年龄 | 缺省 |
编辑框(年龄) | IDC_AGE | 缺省 | |
组框(性别) | 缺省 | 性别 | 缺省 |
单选按钮(男) | IDC_SEX | 男 | Group、Tabstop |
单选按钮(女) | 缺省 | 女 | 缺省 |
组框(就业状况) | 缺省 | 就业状况 | 缺省 |
单选按钮(在职) | IDC_WORK | 在职 | Group、Tabstop |
单选按钮(下岗) | IDC_WORK1 | 下岗 | 缺省 |
静态正文(工作单位) | 缺省 | 工作单位 | 缺省 |
编辑框(工作单位) | IDC_UNIT | 缺省 | |
静态正文(单位性质) | 缺省 | 单位性质 | 缺省 |
组合框(单位性质) | IDC_KIND | Drop List、不排序(不选中Sort风格)、初始化列表项(见下文说明) | |
静态正文(工资收入) | 缺省 | 工资收入 | 缺省 |
列表框(工资收入) | IDC_INCOME | 不排序(不选中Sort) | |
按钮(确定) | IDOK | 确定(&Y) | 缺省 |
按钮(取消) | IDCANCEL | 取消(&C) | 缺省 |
请注意组合框IDC_KIND的Drop List属性,Drop List属性是在属性对话框的Styles(风格)页的Type栏中选择的,这使得IDC_KIND成为一个下拉列表式组合框。组合框有简易式(Simple)、下拉式(Dropdown)和下拉列表式(Drop List)三种。简易式组合框包含一个编辑框和一个总是显示的列表框。下拉式组合框同简易式组合框的区别在于仅当单击下滚箭头时才出现列表框。下拉列表式组合框也有一个下拉的列表框,但它的编辑框是只读的,不能输入字符。组合框IDC_KIND不要自动排序,因此需在Styles页中使Sort项不被选中。
组合框的列表项可以在设计模板时初始化,而列表框的初始化只能在程序中进行。请读者在组合框IDC_KIND的属性对话框的General页中输入以下几个列表项,以作为单位性质的选项。输入时要注意,换行时不要按回车键,而应按Ctrl+回车键。
国有企事业
集体企业
私有企业
中外合资
外商独资
组合框控件的一个与众不同之处是它有两个尺寸,一个是下拉前的尺寸,一个是下拉后的尺寸。当用鼠标点击组合框上的箭头后,可设定下拉后的尺寸。
控件最好都放在对话框模板的蓝色虚框内,控件之间的距离不要太近,否则有可能造成不正确的显示。
安置好控件之后,下一步的任务是指定Tab顺序。按Ctrl+D键后,会显示当前的Tab顺序,通过用鼠标点击控件可以设定新的Tab顺序,如果想放弃本次修改,在对话框的空白处点击一下即可。请读者按图5.5安排Tab顺序。
图5.5 对话框的Tab顺序
最后,需要测试一下对话框。按Ctrl+T,则会弹出一个当前模板的测试对话框,这个对话框的外观和基本行为与程序中将要弹出的对话框一样。这样,读者不用编译运行程序,通过测试对话框就可以评估对话框是否合乎要求。如果发现了错误或不满意的地方,可按ESC键退出测试对话框并重新修改对话框模板。
至此,对话框模板的设计就完成了。
5.3 对话框类的设计
完成对话框模板的设计后,就需要设计一个对话框类以实现对话框的功能。设计对话框类主要包括下面几步:
创建对话框类。该类应从CDialog类派生。
为对话框类加入与控件相对应的成员变量。
为对话框进行初始化工作。
增加对控件通知消息的处理
5.3.1对话框类的创建
利用ClassWizard,程序员可以十分方便的创建MFC窗口类的派生类,对话框类也不例外。请读者按以下几步操作:
打开IDD_REGISTER对话框模板,然后按Ctrl+W进入ClassWizard。
进入ClassWizard后,ClassWizard发现IDD_REGISTER是一个新的对话框模板,于是它会询问是否要为IDD_REGISTER创建一个对话框类。按OK键确认。
如图5.6在Create New Class对话框中,在Name栏中输入CRegisterDialog,在Base class栏中选择CDialog,在Dialog ID栏中选择IDD_REGISTER。按Create按钮后,对话框类CRegisterDialog即被创建。
图5.6 Create New Class对话框
ClassWizard自动使类CRegesterDialog与IDD_REGISTER模板联系起来。
提示:只要想创建的类是某一MFC窗口类的派生类,一般都可以利用ClassWizard来自动完成创建。创建的一般方法是:打开ClassWizard,选择Add Class->New,然后在Create New Class对话框中输入新类的类名,选择其MFC基类,如果是对话框类,则还要选择对话框的ID。 |
5.3.2为对话框类加入成员变量
对话框的主要功能是输出和输入数据,例子中的登录数据对话框的任务就是输入数据。对话框需要有一组成员变量来存储数据。在对话框中,控件用来表示或输入数据,因此,存储数据的成员变量应该与控件相对应。
与控件对应的成员变量即可以是一个数据,也可以是一个控件对象,这将由具体需要来确定。例如,可以为一个编辑框控件指定一个数据变量,这样就可以很方便地取得或设置编辑框控件所代表的数据,如果想对编辑框控件进行控制,则应该为编辑框指定一个CEdit对象,通过CEdit对象,程序员可以控制控件的行为。需要指出的是,不同类的控件对应的数据变量的类型往往是不一样的,而且一个控件对应的数据变量的类型也可能有多种。表5.3说明了控件的数据变量的类型。
表5.3
控件 | 数据变量的类型 |
编辑框 | CString, int, UINT, long, DWORD, float, double, short, BOOL, COleDateTime, COleCurrency |
普通检查框 | BOOL(真表示被选中,假表示未选中) |
三态检查框 | int(0表示未选中,1表示选中,2表示不确定状态) |
单选按钮(组中的第一个按钮) | int(0表示选择了组中第一个单选按钮,1表示选择了第二个...,-1表示没有一个被选中) |
不排序的列表框 | CString(为空则表示没有一个列表项被选中), int(0表示选择了第一项,1表示选了第二项,-1表示没有一项被选中) |
下拉式组合框 | CString, int(含义同上) |
其它列表框和组合框 | CString(含义同上) |
利用ClassWizard可以很方便地为对话框类CRegisterDialog加入成员变量。请读者按下列步骤操作。
按Ctrl+W进入ClassWizard。
选择ClassWizard上部的Member Variables标签,然后在Class name栏中选择CRegisterDialog。这时,在下面的变量列表中会出现对话框控件的ID,如图5.7所示。
图5.7 ClassWizard对话框
双击列表中的ID_AGE会弹出Add Member Variable对话框,如图5.8所示。在Member variable name栏中输入m_nAge,在Category栏中选择Value,在Variable type栏中选择UINT。按OK按钮后,数据变量m_nAge就会被加入到变量列表中。
图5.8 Add Member Variable对话框
仿照第3步和表5.4,为各个控件加入相应的成员变量。
将m_nAge的值限制在16到65之间。方法是先选择m_nAge,然后在ClassWizard对话框的左下角输入最大和最小值。m_nAge代表年龄,这里规定被调查的人的年龄应在16岁以上,64岁以下。有了这个限制后,对话框会对输入的年龄值进行有效性检查,若输入的值不在限制范围内,则对话框会提示用户输入有效的值。
表5.4
控件ID |
变量类型 |
变量名 |
IDC_AGE |
UINT |
m_nAge |
IDC_INCOME |
CString |
m_strIncome |
IDC_INCOME |
CListBox |
m_ctrlIncome |
IDC_KIND |
CString |
m_strKind |
IDC_MARRIED |
BOOL |
m_bMarried |
IDC_NAME |
CString |
m_strName |
IDC_SEX |
int |
m_nSex |
IDC_UNIT |
CString |
m_strUnit |
IDC_WORK |
int |
m_nWork |
读者会注意到控件IDC_INCOME居然有两个变量,一个是CString型的,一个是CListBox型的,这是完全合法的,不会引起任何冲突。之所以要加入CListBox型的变量,是因为列表框的初始化要通过CListBox对象进行。
提示:在ClassWizard中可分别为一个控件指定一个数据变量和一个控件对象,这样做的好处是即能方便地获得数据,又能方便地控制控件。 |
5.3.3对话框的初始化
对话框的初始化工作一般在构造函数和OnInitDialog函数中完成。在构造函数中的初始化主要是针对对话框的数据成员。读者可以找到CRegisterDialog的构造函数,如清单5.1所示。
清单5.1 CRegisterDialog的构造函数
CRegisterDialog::CRegisterDialog(CWnd* pParent /*=NULL*/)
: CDialog(CRegisterDialog::IDD, pParent)
{
//{{AFX_DATA_INIT(CRegisterDialog)
m_nAge = 0;
m_strIncome = _T("");
m_strKind = _T("");
m_bMarried = FALSE;
m_strName = _T("");
m_nSex = -1;
m_strUnit = _T("");
m_nWork = -1;
//}}AFX_DATA_INIT
}
可以看出,对数据成员的初始化是由ClassWizard自动完成的。若读者对初值的含义还不太清楚,请参看表5.3。
在对话框创建时,会收到WM_INITDIALOG消息,对话框对该消息的处理函数是OnInitDialog。调用OnInitDialog时,对话框已初步创建,对话框的窗口句柄也已有效,但对话框还未被显示出来。因此,可以在OnInitDialog中做一些影响对话框外观的初始化工作。OnInitDialog对对话框的作用与OnCreate对CMainFrame的作用类似。
提示:MFC窗口的初始化工作一般在OnCreate成员函数中进行,但对话框的初始化工作最好在OnInitDialog中进行。 |
OnInitDialog是WM_INITDIALOG消息的处理函数,所以要用ClassWizard为RegisteritDialog类增加一个WM_INITDIALOG消息的处理函数,增加的方法是进入ClassWizard后,先选中MessageMaps标签,然后在Class name中选择CRegisterDialog,在Object IDs栏中选择CRegisterDialog,在Messages栏中找到WM_INITDIALOG并双击之,最后按OK按钮退出ClassWizard。
请读者按清单5.2修改OnInitDialog函数。
清单5.2 OnInitDialog函数
BOOL CRegisterDialog::OnInitDialog()
{
CDialog::OnInitDialog();
// TODO: Add extra initialization here
m_ctrlIncome.AddString("500元以下");
m_ctrlIncome.AddString("500-1000元");
m_ctrlIncome.AddString("1000-2000元");
m_ctrlIncome.AddString("2000元以上");
return TRUE; // return TRUE unless you set the focus to a control
// EXCEPTION: OCX Property Pages should return FALSE
}
CRegisterDialog::OnInitDialog()的主要任务是对工资收入列表框的列表项进行初始化。调用CListBox::AddString可将指定的字符串加入到列表框中。由于该列表是不自动排序的,因此AddString将表项加在列表框的末尾。
5.3.4对话框的数据交换机制
对话框的数据成员变量存储了与控件相对应的数据。数据变量需要和控件交换数据,以完成输入或输出功能。例如,一个编辑框即可以用来输入,也可以用来输出:用作输入时,用户在其中输入了字符后,对应的数据成员应该更新;用作输出时,应及时刷新编辑框的内容以反映相应数据成员的变化。对话框需要一种机制来实现这种数据交换功能,这对对话框来说是至关重要的。
MFC提供了类CDataExchange来实现对话框类与控件之间的数据交换(DDX),该类还提供了数据有效机制(DDV)。数据交换和数据有效机制适用于编辑框、检查框、单选按钮、列表框和组合框。
数据交换的工作由CDialog::DoDataExchange来完成。读者可以找到CRegisterDialog::DoDataExchange函数,如清单5.3所示。
清单5.3 DoDataExchange函数
void CRegisterDialog::DoDataExchange(CDataExchange* pDX)
{
CDialog::DoDataExchange(pDX);
//{{AFX_DATA_MAP(CRegisterDialog)
DDX_Control(pDX, IDC_INCOME, m_ctrlIncome);
DDX_LBString(pDX, IDC_INCOME, m_strIncome);
DDX_CBString(pDX, IDC_KIND, m_strKind);
DDX_Check(pDX, IDC_MARRIED, m_bMarried);
DDX_Text(pDX, IDC_NAME, m_strName);
DDX_Radio(pDX, IDC_SEX, m_nSex);
DDX_Text(pDX, IDC_UNIT, m_strUnit);
DDX_Radio(pDX, IDC_WORK, m_nWork);
DDX_Text(pDX, IDC_AGE, m_nAge);
DDV_MinMaxUInt(pDX, m_nAge, 16, 65);
//}}AFX_DATA_MAP
}
读者可以看出,该函数中的代码是由ClassWizard自动加入的。DoDataExchange只有一个参数,即一个CDataExchange对象的指针pDX。在该函数中调用了DDX函数来完成数据交换,调用DDV函数来进行数据有效检查。
当程序需要交换数据时,不要直接调用DoDataExchange函数,而应该调用CWnd::UpdateData。UpdataData函数内部调用了DoDataExchange。该函数只有一个布尔型参数,它决定了数据传送的方向。调用UpdateData(TRUE)将数据从对话框的控件中传送到对应的数据成员中,调用UpdateData(FALSE)则将数据从数据成员中传送给对应的控件。
在缺省的CDialog::OnInitDialog中调用了UpdateData(FALSE),这样,在对话框创建时,数据成员的初值就会反映到相应的控件上。若用户是按了OK(确定)按钮退出对话框,则对话框认为输入有效,就会调用UpdataData(TRUE)将控件中的数据传给数据成员。图5.9描绘了对话框的这种数据交换机制。
图5.9 对话框的数据交换
5.3.5对话框的运行机制
在程序中运行模态对话框有两个步骤:
在堆栈上以变量的形式构建一个对话框对象。
调用CDialog::DoModal ( )。
DoModal负责对模态话框的创建和撤销。在创建对话框时,DoModal的任务包括载入对话框模板资源、调用OnInitDialog初始化对话框和将对话框显示在屏幕上。完成对话框的创建后,DoModal启动一个消息循环,以响应用户的输入。由于该消息循环截获了几乎所有的输入消息,使主消息循环收不到对对话框的输入,致使用户只能与模态对话框进行交互,而其它用户界面对象收不到输入信息。
若用户在对话框内点击了ID为IDOK的按钮(通常该按钮的标题是“确定”或“OK”),或按了回车键,则CDialog::OnOK将被调用。OnOK首先调用UpdateData(TRUE)将数据从控件传给对话框成员变量,然后调用CDialog::EndDialog关闭对话框。关闭对话框后,DoModal会返回值IDOK。
若用户点击了ID为IDCANCEL的按钮(通常其标题为“取消”或“Cancel”),或按了ESC键,则会导致CDialog::OnCancel的调用。该函数只调用CDialog::EndDialog关闭对话框。关闭对话框后,DoModal会返回值IDCANCEL。
程序根据DoModal的返回值是IDOK还是IDCANCEL就可以判断出用户是确定还是取消了对对话框的操作。
在弄清了对话框的运行机制后,下面让我们来就可以实现Register程序登录数据的功能。
首先,将Register工程的工作区切换至资源视图。打开IDR_MAINFRAME菜单资源,在Edit菜单的底端加入一个名为“登录数据”的新菜单项,并令其ID为ID_EDIT_REGISTER(最好在该项之前加一条分隔线,以便和前面的菜单项分开)。注意不要忘了把菜单资源的语种设置成中文,否则菜单中将显示不出中文来。设置的方法是先在工作区资源视图中选择IDR_MAINFRAME菜单资源,然后按Alt+Enter键,并在弹出的属性对话框中的Language栏中选择Chinese(P.R.C.)。
接着,用ClassWizard为该菜单命令创建命令处理函数CRegisterView::OnEditRegister。注意,OnEditRegister是类CRegisterView的成员函数,这是因为CRegisterView要负责打开和关闭登录数据对话框,并将从对话框中输入的数据在视图中输出。
然后,请读者在RegisterView.cpp文件的开头加入下面一行
#include "RegisterDialog.h"
最后,按清单5.4修改程序。
清单5.4 OnEditRegister函数
void CRegisterView::OnEditRegister()
{
// TODO: Add your command handler code here
CRegisterDialog dlg;
if(dlg.DoModal()==IDOK)
{
CString str;
//获取编辑正文
GetWindowText(str);
//换行
str+="/r/n";
str+="姓名:";
str+=dlg.m_strName;
str+="/r/n";
str+="性别:";
str+=dlg.m_nSex?"女":"男";
str+="/r/n";
str+="年龄:";
CString str1;
//将数据格式输出到字符串对象中
str1.Format("%d",dlg.m_nAge);
str+=str1;
str+="/r/n";
str+="婚否:";
str+=dlg.m_bMarried?"已婚":"未婚";
str+="/r/n";
str+="就业状况:";
str+=dlg.m_nWork?"下岗":"在职";
str+="/r/n";
str+="工作单位:";
str+=dlg.m_strUnit;
str+="/r/n";
str+="单位性质:";
str+=dlg.m_strKind;
str+="/r/n";
str+="工资收入:";
str+=dlg.m_strIncome;
str+="/r/n";
//更新编辑视图中的正文
SetWindowText(str);
}
}
在OnEditRegister函数中,首先构建了一个CRegisterDialog对象,然后调用CDialog::DoModal来实现模态对话框。如果DoModal返回IDOK,则说明用户确认了登录数据的操作,程序需要将录入的数据在编辑视图中输出。程序用一个CString对象来作为编辑正文的缓冲区,CString是一个功能强大的字符串类,它的最大特点在于可以存储动态改变大小的字符串,这样,用户不必担心字符串的长度超过缓冲区的大小, 使用十分方便。
在输出数据时,程序首先调用CWnd::GetWindowText获得编辑正文,这是一个多行的编辑正文。CWnd::GetWindowText用来获取窗口的标题,若该窗口是一个控件,则获取的是控件内的正文。CRegisterView是CEditView的继承类,而CEditView实际上包含了一个编辑控件,因此在CRegisterView中调用GetWindowText获得的是编辑正文。
然后,程序在该编辑正文的末尾加入新的数据。在程序中大量使用了CString类的重载操作符“+=”,该操作符的功能是将操作符右侧字符串添加到操作符左侧的字符串的末尾。注意在多行编辑控件中每行末尾都有一对回车和换行符。在程序中还调用了CString::Format来将数据格式化输出到字符串中,Format的功能与sprintf类似。最后,调用CWnd::SetWindowText来更新编辑视图中的正文。
编译并运行Register,打开登录数据对话框,输入一些数据试试。现在,Register已经是一个简易的数据库应用程序了,它可以将与就业情况有关的数据输出到一个编辑视图中。用户可以编辑视图中的正文,并将结果保存在文本文件中。
5.3.6处理控件通知消息
虽然Register已经可以登录数据了,但读者会很快会发现该程序还有一些不完善的地方:
登录完一个人的数据后,对话框就关闭了,若用户有很多人的数据要输入,则必须频繁地打开对话框,很不方便。在登录数据时,应该使对话框一直处于打开状态。
登录数据对话框分个人情况和单位情况两组,若被调查人是下岗职工,则不必输入单位情况。程序应该能够对用户的输入及时地作出反应,即当用户选择了“下岗”单选按钮时,应使单位情况组中的控件禁止。一个禁止的控件呈灰色示,并且不能接收用户的输入。
要解决上述问题,就必须对控件通知消息进行处理。当控件的状态因为输入等原因而发生变化时,控件会向其父窗口发出控件通知消息。例如,如果用户在登录数据对话框中的某一按钮(包括普通按钮、检查框和单选按钮)上单击鼠标,则该按钮会向对话框发送BN_CLICKED消息。对话框根据按钮的ID激活相应的BN_CLICKED消息处理函数,以对单击按钮这一事件作出反应。通过对按钮的BN_CLICKED消息的处理,我们可以使登录数据对话框的功能达到上述要求。
首先,让我们来解决第一个问题。我们的设想是修改原来的“确定(Y)”按钮,使得当用户点击该按钮后,将数据输出到视图中,并且对话框不关闭,以便用户输入下一个数据。请读者按下面几步进行修改。
修改登录数据对话框的“确定(Y)”按钮,使该按钮的标题变为“添加(&A)”,ID变为IDC_ADD。这样,当用户点击该按钮后,对话框会收到BN_CLICKED消息。由于这个BN_CLICKED消息对应的按钮ID不是IDOK,不会触发OnOK消息处理函数,因此不会关闭对话框。
为按钮IDC_ADD的BN_CLICKED消息创建消息处理函数。创建的方法是进入ClassWizard后,选Message Maps页并在Class name栏中选择CRegisterDialog,然后在Object IDs栏中选择IDC_ADD,在Messages栏中双击BN_CLICKED。在确认使用缺省的消息处理函数名OnAdd后,按回车键退出ClassWizard。
OnAdd要向编辑视图输出正文,就必须获得一个指向CRegisterView对象的指针以访问该对象。为此,请在CRegisterDialog类的说明中加入下面一行
Cwnd* m_pParent;
注意不要加在AFX注释对中。为实现IDC_ADD按钮的功能,请按清单5.5和清单5.6修改程序。主要的改动是把原来由CRegiserView::OnEditRegister完成的在视图中输出数据的任务交给CRegisterDialog::OnAdd来完成。
清单5.5 CRegisterView::OnEditRegister函数
void CRegisterView::OnEditRegister()
{
// TODO: Add your command handler code here
CRegisterDialog dlg(this);
dlg.DoModal();
}
清单5.6 CRegisterDialog类的部分源代码
CRegisterDialog::CRegisterDialog(CWnd* pParent /*=NULL*/)
: CDialog(CRegisterDialog::IDD, pParent)
{
//{{AFX_DATA_INIT(CRegisterDialog)
. . . . . .
//}}AFX_DATA_INIT
m_pParent=pParent;
}
void CRegisterDialog::OnAdd()
{
// TODO: Add your control notification handler code here
//更新数据
UpdateData(TRUE);
//检查数据是否有效
if(m_strName=="" || m_nSex<0 || m_nWork<0 || m_strUnit==""
|| m_strKind=="" || m_strIncome=="")
{
AfxMessageBox("请输入有效数据");
return;
}
CString str;
//获取编辑正文
m_pParent->GetWindowText(str);
//换行
str+="/r/n";
str+="姓名:";
str+=m_strName;
str+="/r/n";
str+="性别:";
str+=m_nSex?"女":"男";
str+="/r/n";
str+="年龄:";
CString str1;
//将数据格式输出到字符串对象中
str1.Format("%d",m_nAge);
str+=str1;
str+="/r/n";
str+="婚否:";
str+=m_bMarried?"已婚":"未婚";
str+="/r/n";
str+="就业状况:";
str+=m_nWork?"下岗":"在职";
str+="/r/n";
str+="工作单位:";
str+=m_strUnit;
str+="/r/n";
str+="单位性质:";
str+=m_strKind;
str+="/r/n";
str+="工资收入:";
str+=m_strIncome;
str+="/r/n";
//更新编辑视图中的正文
m_pParent->SetWindowText(str);
}
CRegisterDialog的构造函数有一个参数pParent,该参数是一个指向CWnd对象的指针,用于指定对话框的父窗口或拥有者窗口。在CRegisterView:: OnEditRegister函数中,在构建CRegisterDialog对象时指定了this参数,this指针指向CRegisterView对象本身。这样在调用CRegisterDialog的构造函数时,this指针值被赋给了CRegisterDialog的成员m_pParent。OnAdd函数可利用m_pParent来访问对话框的拥有者即CRegisterView对象。
提示:术语父窗口(Parent)是相对于子窗口而言。若某一个窗口拥有一个子窗口(Child),则该窗口就被称为子窗口的父窗口。子窗口就是具有WS_CHILD风格的窗口,子窗口依赖于父窗口且完全被限制在父窗口内部。拥有者窗口(owner)相对于被拥有者窗口而言。若某一个窗口拥有一个非子窗口,则该窗口被称为拥有者窗口。被拥有窗口(owned)不具有WS_CHILD风格,可在屏幕上任意移动。 |
当用户用鼠标点击IDC_ADD按钮时,该按钮的BN_CLICKED消息处理函数CRegisterDialog::OnAdd将被调用。在OnAdd中,首先调用了UpdateData(TRUE)以把数据从控件传给对话框的数据成员变量。然后,程序要对数据的有效性进行检查,如果输入的数据不完全有效,则会显示一个消息对话框,提示用户输入有效的数据。接下来进行的工作是在视图中输出数据,这部分代码与清单5.4类似,读者应该比较熟悉了。
完成上述工作后,登录数据对话框就变得较为实用了。打开对话框后,用户可以方便地输入多人的数据,只有按了取消按钮后,对话框才会关闭。
接下来让我们来解决第二个问题。解决该问题的关键在于当用户点击“在职”或“下岗”单选按钮时,程序要对收到的BN_CLICKED消息作出响应。有些读者可能会想到为两个单选按钮分别创建BN_CLICKED消息处理函数,这在只有两个单选按钮的情况下是可以的,但如果一组内有多个单选按钮,则分别创建消息处理函数就比较麻烦了。利用MFC提供的消息映射宏ON_CONTROL_RANGE可以避免这种麻烦,该映射宏把多个ID连续的控件发出的消息映射到同一个处理函数上。这样,我们只要编写一个消息处理函数,就可以对“在职”和“下岗”两个单选按钮的BN_CLICKED消息作出响应。ClassWizard不支持ON_CONTROL_RANGE宏,所以我们必须手工创建单选按钮的消息映射和消息处理函数。
首先,在CRegisterDialog类的头文件中加入消息处理函数的声明,该函数名为OnWorkClicked,如清单5.7所示。
清单5.7 BN_CLICKED消息处理函数OnWorkClicked的声明
. . . . . .
protected:
void OnWorkClicked(UINT nCmdID);
// Generated message map functions
//{{AFX_MSG(CRegisterDialog)
virtual BOOL OnInitDialog();
afx_msg void OnAdd();
//}}AFX_MSG
. . . . . .
然后,在CRegisterDialog类的消息映射中加入ON_CONTROL_RANGE映射,如清单5.8所示。ON_CONTROL_RANGE映射的形式是ON_CONTROL_RANGE
清单5.8 在CRegisterDialog类的消息映射中加入ON_CONTROL_RANGE映射
BEGIN_MESSAGE_MAP(CRegisterDialog, CDialog)
//{{AFX_MSG_MAP(CRegisterDialog)
ON_BN_CLICKED(IDC_ADD, OnAdd)
//}}AFX_MSG_MAP
ON_CONTROL_RANGE(BN_CLICKED, IDC_WORK, IDC_WORK1, OnWorkClicked)
END_MESSAGE_MAP()
ON_CONTROL_RANGE消息映射宏的第一个参数是控件消息码,第二和第三个参数分别指明了一组连续的控件ID中的头一个和最后一个ID,最后一个参数是消息处理函数名。如果读者是按表5.2的顺序放置控件的则IDC_WORK和IDC_WORK1应该是连续的。这样,无论用户是在IDC_WORK还是在IDC_WORK1单选按钮上单击,都会调用OnWorkClicked消息处理函数。
提示:如果不能确定两个ID是否是连续的,请用File->Open命令打开resource.h文件,在该文件中有对控件ID值的定义。如果发现两个ID是不连续的,读者可以改变对ID的定义值使之连续,但要注意改动后的值不要与别的ID值发生冲突。
最后,在CRegisterDialog类所在CPP文件的最后插入消息处理函数CRegisterDialog::OnWorkClicked,如清单5.9所示。
清单5.9 CRegisterDialog::OnWorkClicked消息处理函数
void CRegisterDialog::OnWorkClicked(UINT nCmdID)
{
//判断“在职”单选按钮是否被选中
if(IsDlgButtonChecked(IDC_WORK))
{
//使控件允许
GetDlgItem(IDC_UNIT)->EnableWindow(TRUE);
GetDlgItem(IDC_KIND)->EnableWindow(TRUE);
GetDlgItem(IDC_INCOME)->EnableWindow(TRUE);
}
else
{
//清除编辑框的内容并使之禁止
GetDlgItem(IDC_UNIT)->SetWindowText("");
GetDlgItem(IDC_UNIT)->EnableWindow(FALSE);
//使组合框处于未选择状态并使之禁止
CComboBox *pComboBox=(CComboBox *)GetDlgItem(IDC_KIND);
pComboBox->SetCurSel(-1);
pComboBox->EnableWindow(FALSE);
//使列表框处于未选择状态并使之禁止
m_ctrlIncome.SetCurSel(-1);
m_ctrlIncome.EnableWindow(FALSE);
}
}
OnWorkClicked函数判断“在职”单选按钮是否被选中。若该按钮被选中,则使单位情况组中的控件允许,若该按钮未被选中,则说明“下岗”按钮被选中,这时应使控件禁止,清除编辑框中的正文, 并且使组合框和列表框处于未选中状态。
在OnWorkClicked函数中主要调用了下列函数:
CWnd::IsDlgButtonChecked函数,用来判断单选按钮或检查框是否被选择,该函数的声明为
UINT IsDlgButtonChecked(int nIDButton) const;
参数nIDButton为按钮的ID。若按钮被选择,则函数返回1,否则返回0,若按钮处于不确定状态,则返回值为2。CWnd::GetDlgItem函数,用来获得指向某一控件的指针,该函数的声明为
CWnd* GetDlgItem(int nID) const;
参数nID为控件的ID。该函数返回一个指定控件的CWnd对象指针,通过该指针,程序可以对控件进行控制。CWnd::EnableWindow函数,该函数使窗口允许或禁止,禁止的窗口呈灰色显示,不能接收键盘和鼠标的输入。该函数的声明是
BOOL EnableWindow( BOOL bEnable = TRUE );
若参数bEnable的值为TRUE,则窗口被允许,若bEnable的值为FALSE,则窗口被禁止。CListBox::SetCurSel和CComboBox::SetCurSel函数功能类似,用来使列表中的某一项被选中,选中的项呈高亮度显示。函数的声明是
int SetCurSel(int nSelect);
参数nSelect指定了新选项的索引,第一项的索引值为0,若nSelect的值为-1,那么函数将清除以前的选择,使列表处于未选择状态。
有时,需要将GetDlgItem返回的CWnd指针强制转换成控件对象的指针,以便调用控件对象专有的成员函数对控件进行控制。例如,在程序中GetDlgItem(IDC_KIND)返回的指针被强制转换成CComboBox类型,只有这样,才能调用CComboBox::SetCurSel成员函数。
为了对控件进行查询和控制,在程序中采用了两种访问控件的方法。一种方法是直接利用ClassWizard提供的控件对象,例如m_ctrlIncome列表框对象。另一种方法是利用CWnd类提供的一组管理对话框控件的成员函数,例如程序中用到的GetDlgItem和IsDlgButtonChecked。这两种方法是在对话框内访问控件的常用方法,读者都应该掌握。表5.5列出了管理对话框控件的Cwnd成员函数。
表5.5 用来管理对话框控件的CWnd成员函数
函数名 |
功能 |
CheckDlgButton |
选中或不选中按钮控件。 |
CheckRadioButton |
选择一个指定的单选按钮并使同组内的其它单选按钮不被选择。 |
DlgDirList |
往一个列表框中添加文件、目录或驱动器的列表。 |
DlgDirListComboBox |
往一个组合框中的列表框内添加文件、目录或驱动器的列表。 |
DlgDirSelect |
从一个列表框中获得当前选择的文件、目录或驱动器。 |
DlgDirSelectBomboBox |
从一个组合框中获得当前选择的文件、目录或驱动器。 |
GetCheckedRadioButton |
返回指定的单选按钮组中被选择的单选按钮的ID。 |
GetDlgItem |
返回一个指向一给定的控件的临时对象的指针。 |
GetDlgItemInt |
返回在一个指定的控件中由正文表示的数字值。 |
GetDlgItemText |
获得在一个控件内显示的正文。 |
GetNextDlgGroupItem |
返回一个指向一组控件内的下一个或上一个控件的临时对象的指针。 |
GetNextDlgTabItem |
返回下一个tab顺序的控件的临时对象的指针。 |
IsDlgButtonChecked |
返回一个按钮控件的状态。 |
SendDlgItemMessage |
把一个消息传送给一个控件。 |
SetDlgItemInt |
将一个整数转换为正文,并将此正文赋给控件。 |
SetDlgItemText |
设置一个控件显示的正文。 |
编译并运行Register看看,现在的登录数据对话框已经比较令人满意了.
5.4 非模态对话框
5.4.1 非模态对话框的特点
与模态对话框不同,非模态对话框不垄断用户的输入,用户打开非模态对话框后,仍然可以与其它界面进行交互。
非模态对话框的设计与模态对话框基本类似,也包括设计对话框模板和设计CDialog类的派生类两部分。但是,在对话框的创建和删除过程中,非模态对话框与模态对话框相比有下列不同之处:
非模态对话框的模板必须具有Visible风格,否则对话框将不可见,而模态对话框则无需设置该项风格。更保险的办法是调用CWnd::ShowWindow(SW_SHOW)来显示对话框,而不管对话框是否具有Visible风格。
非模态对话框对象是用new操作符在堆中动态创建的,而不是以成员变量的形式嵌入到别的对象中或以局部变量的形式构建在堆栈上。通常应在对话框的拥有者窗口类内声明一个指向对话框类的指针成员变量,通过该指针可访问对话框对象。
通过调用CDialog::Create函数来启动对话框,而不是CDialog::DoModal,这是模态对话框的关键所在。由于Create函数不会启动新的消息循环,对话框与应用程序共用同一个消息循环,这样对话框就不会垄断用户的输入。Create在显示了对话框后就立即返回,而DoModal是在对话框被关闭后才返回的。众所周知,在MFC程序中,窗口对象的生存期应长于对应的窗口,也就是说,不能在未关闭屏幕上窗口的情况下先把对应的窗口对象删除掉。由于在Create返回后,不能确定对话框是否已关闭,这样也就无法确定对话框对象的生存期,因此只好在堆中构建对话框对象,而不能以局部变量的形式来构建之。
必须调用CWnd::DestroyWindow而不是CDialog::EndDialog来关闭非模态对话框。调用CWnd::DestroyWindow是直接删除窗口的一般方法。由于缺省的CDialog::OnOK和CDialog::OnCancel函数均调用EndDialog,故程序员必须编写自己的OnOK和OnCancel函数并且在函数中调用DestroyWindow来关闭对话框。
因为是用new操作符构建非模态对话框对象,因此必须在对话框关闭后,用delete操作符删除对话框对象。在屏幕上一个窗口被删除后,框架会调用CWnd::PostNcDestroy,这是一个虚拟函数,程序可以在该函数中完成删除窗口对象的工作,具体代码如下
void CModelessDialog::PostNcDestroy
{
delete this; //删除对象本身
}
这样,在删除屏幕上的对话框后,对话框对象将被自动删除。拥有者对象就不必显式的调用delete来删除对话框对象了。
必须有一个标志表明非模态对话框是否是打开的。这样做的原因是用户有可能在打开一个模态对话框的情况下,又一次选择打开命令。程序根据标志来决定是打开一个新的对话框,还是仅仅把原来打开的对话框激活。通常可以用拥有者窗口中的指向对话框对象的指针作为这种标志,当对话框关闭时,给该指针赋NULL值,以表明对话框对象已不存在了。
提示:在C++编程中,判断一个位于堆中的对象是否存在的常用方法是判断指向该对象的指针是否为空。这种机制要求程序员将指向该对象的指针初始化为NULL值,在创建对象时将返回的地址赋给该指针,而在删除对象时将该指针置成NULL值。 |
根据上面的分析,我们很容易把Register程序中的登录数据对话框改成非模态对话框。这样做的好处在于如果用户在输入数据时发现编辑视图中有错误的数据,那么不必关闭对话框,就可以在编辑视图中进行修改。
请读者按下面几步操作:
在登录数据对话框模板的属性对话框的More Styles页中选择Visible项。
在RegisterView.h头文件的CRegisterView类的定义中加入
public:
CRegisterDialog* m_pRegisterDlg;在RegisterView.h头文件的头部加入对CRegisterDialog类的声明
class CRegisterDialog;
加入该行的原因是在CRegisterView类中有一个CRegisterDialog类型的指针,因此必须保证CRegisterDialog类的声明出现在CRegisterView之前,否则编译时将会出错。解决这个问题有两种办法,一种办法是保证在#include “RegisterView.h”语句之前有#include “RegisterDialog.h”语句,这种办法造成了一种依赖关系,增加了编译负担,不是很好;另一种办法是在CRegisterView类的声明之前加上一个对CRegisterDialog的声明来暂时“蒙蔽”编译器,这样在有#include “RegisterView.h”语句的模块中,除非要用到CRegisterDialog类,否则不用加入#include “RegisterDialog.h”语句。在RegisterDialog.cpp文件的头部的#include语句区的末尾添加下面两行
#include "RegisterDoc.h"
#include "RegisterView.h"利用ClassWizard为CRegisterDialog类加入OnCancel和PostNcDestroy成员函数。加入的方法是进入ClassWizard后选择Message Maps页,并在Class name栏中选择CRegisterDialog。然后,在Object IDs栏中选择IDCANCEL后,在Messages栏中双击BN_CLICKED,这就创建了OnCancel。要创建PostNcDestroy,先在Object IDs栏中选择CRegisterDialog,再在Messages栏中双击PostNcDestroy即可。分别按清单5.10和5.11,对CRegisterView类和CRegisterDialog类进行修改。
清单5.10 CRegisterView类的部分代码
CRegisterView::CRegisterView()
{
// TODO: add construction code here
m_pRegisterDlg=NULL; //指针初始化为NULL
}
void CRegisterView::OnEditRegister()
{
// TODO: Add your command handler code here
if(m_pRegisterDlg)
m_pRegisterDlg->SetActiveWindow(); //激活对话框
else
{
//创建非模态对话框
m_pRegisterDlg=new CRegisterDialog(this);
m_pRegisterDlg->Create(IDD_REGISTER,this);
}
}
清单5.11 CRegisterDialog的部分代码
void CRegisterDialog::PostNcDestroy()
{
// TODO: Add your specialized code here and/or call the base class
delete this; //删除对话框对象
}
void CRegisterDialog::OnCancel()
{
// TODO: Add extra cleanup here
((CRegisterView*)m_pParent)->m_pRegisterDlg=NULL;
DestroyWindow(); //删除对话框
}
CRegisterView::OnEditRegister函数判断登录数据对话框是否已打开,若是,就激活对话框,否则,就创建该对话框。该函数中主要调用了下列函数:
调用CWnd::SetActiveWindow激活对话框,该函数的声明为
CWnd* SetActiveWindow( );
该函数使本窗口成为活动窗口,并返回原来活动的窗口。调用CDialog::Create来显示对话框,该函数的声明为
BOOL Create( UINT nIDTemplate, CWnd* pParentWnd = NULL );
参数nIDTemplate是对话框模板的ID。pParentWnd指定了对话框的父窗口或拥有者。
当用户在登录数据对话框中点击“取消”按钮后,CRegisterDialog::OnCancel将被调用,在该函数中调用CWnd::DestroyWindow来关闭对话框,并且将CRegisterView的成员m_pRegisterDlg置为NULL以表明对话框被关闭了。调用DestroyWindow导致了对CRegisterDialog::PostNcDestroy的调用,在该函数中用delete操作符删除了CRegisterDialog对象本身。
编译并运行Register,现在登录数据对话框已经变成一个非模态对话框了。5.4.2 窗口对象的自动清除
一个MFC窗口对象包括两方面的内容:一是窗口对象封装的窗口,即存放在m_hWnd成员中的HWND(窗口句柄),二是窗口对象本身是一个C++对象。要删除一个MFC窗口对象,应该先删除窗口对象封装的窗口,然后删除窗口对象本身。
删除窗口最直接方法是调用CWnd::DestroyWindow或::DestroyWindow,前者封装了后者的功能。前者不仅会调用后者,而且会使成员m_hWnd保存的HWND无效(NULL)。如果DestroyWindow删除的是一个父窗口或拥有者窗口,则该函数会先自动删除所有的子窗口或被拥有者,然后再删除父窗口或拥有者。在一般情况下,在程序中不必直接调用DestroyWindow来删除窗口,因为MFC会自动调用DestroyWindow来删除窗口。例如,当用户退出应用程序时,会产生WM_CLOSE消息,该消息会导致MFC自动调用CWnd::DestroyWindow来删除主框架窗口,当用户在对话框内按了OK或Cancel按钮时,MFC会自动调用CWnd::DestroyWindow来删除对话框及其控件。
窗口对象本身的删除则根据对象创建方式的不同,分为两种情况。在MFC编程中,会使用大量的窗口对象,有些窗口对象以变量的形式嵌入在别的对象内或以局部变量的形式创建在堆栈上,有些则用new操作符创建在堆中。对于一个以变量形式创建的窗口对象,程序员不必关心它的删除问题,因为该对象的生命期总是有限的,若该对象是某个对象的成员变量,它会随着父对象的消失而消失,若该对象是一个局部变量,那么它会在函数返回时被清除。
对于一个在堆中动态创建的窗口对象,其生命期却是任意长的。初学者在学习C++编程时,对new操作符的使用往往不太踏实,因为用new在堆中创建对象,就不能忘记用delete删除对象。读者在学习MFC的例程时,可能会产生这样的疑问,为什么有些程序用new创建了一个窗口对象,却未显式的用delete来删除它呢?问题的答案就是有些MFC窗口对象具有自动清除的功能。
如前面讲述非模态对话框时所提到的,当调用CWnd::DestroyWindow或::DestroyWindow删除一个窗口时,被删除窗口的PostNcDestroy成员函数会被调用。缺省的PostNcDestroy什么也不干,但有些MFC窗口类会覆盖该函数并在新版本的PostNcDestroy中调用delete this来删除对象,从而具有了自动清除的功能。此类窗口对象通常是用new操作符创建在堆中的,但程序员不必操心用delete操作符去删除它们,因为一旦调用DestroyWindow删除窗口,对应的窗口对象也会紧接着被删除。
不具有自动清除功能的窗口类如下所示。这些窗口对象通常是以变量的形式创建的,无需自动清除功能。
所有标准的Windows控件类。
从CWnd类直接派生出来的子窗口对象(如用户定制的控件)。
切分窗口类CSplitterWnd。
缺省的控制条类(包括工具条、状态条和对话条)。
模态对话框类。
具有自动清除功能的窗口类如下所示,这些窗口对象通常是在堆中创建的。
主框架窗口类(直接或间接从CFrameWnd类派生)。
视图类(直接或间接从CView类派生)。
读者在设计自己的派生窗口类时,可根据窗口对象的创建方法来决定是否将窗口类设计成可以自动清除的。例如,对于一个非模态对话框来说,其对象是创建在堆中的,因此应该具有自动清除功能。
综上所述,对于MFC窗口类及其派生类来说,在程序中一般不必显式删除窗口对象。也就是说,既不必调用DestroyWindow来删除窗口对象封装的窗口,也不必显式地用delete操作符来删除窗口对象本身。只要保证非自动清除的窗口对象是以变量的形式创建的,自动清除的窗口对象是在堆中创建的,MFC的运行机制就可以保证窗口对象的彻底删除。
如果需要手工删除窗口对象,则应该先调用相应的函数(如CWnd::DestroyWindow)删除窗口,然后再删除窗口对象.对于以变量形式创建的窗口对象,窗口对象的删除是框架自动完成的.对于在堆中动态创建了的非自动清除的窗口对象,必须在窗口被删除后,显式地调用delete来删除对象(一般在拥有者或父窗口的析构函数中进行).对于具有自动清除功能的窗口对象,只需调用CWnd::DestroyWindow即可删除窗口和窗口对象。注意,对于在堆中创建的窗口对象,不要在窗口还未关闭的情况下就用delete操作符来删除窗口对象.
提示:在非模态对话框的OnCancel函数中可以不调用CWnd::DestroyWindow,取而代之的是调用CWnd::ShowWindow(SW_HIDE)来隐藏对话框.在下次打开对话框时就不必调用Create了,只需调用CWnd::ShowWindow(SW_SHOW)来显示对话框.这样做的好处在于对话框中的数据可以保存下来,供以后使用.由于拥有者窗口在被关闭时会调用DestroyWindow删除每一个所属窗口,故只要非模态对话框是自动清除的,程序员就不必担心对话框对象的删除问题. |
5.5 标签式对话框
在设计较为复杂的对话框时,常常会遇到这种情况:对某一事物的设置或选项需要用到大量的控件,以至于一个对话框放不下,而这些控件描述的是类似的属性,不能分开。用普通的对话框技术,这一问题很难解决。
MFC提供了对标签式对话框的支持,可以很好的解决上述问题。标签式对话框实际上是一个包含了多个子对话框的对话框,这些子对话框通常被称为页(Page)。每次只有一个页是可见的,在对话框的顶端有一行标签,用户通过单击这些标签可切换到不同的页。显然,标签式对话框可以容纳大量的控件。在象Word和Developer Studio这样复杂的软件中,用户会接触到较多的标签式对话框,一个典型的标签式对话框如图5.10所示。
图5.10 典型的标签式对话框
5.5.1 标签式对话框的创建
为了支持标签式对话框,MFC提供了CPropertySheet类和CPropertyPage类。前者代表对话框的框架,后者代表对话框中的某一页。CPropertyPage是CDialog类的派生类,而CPropertySheet是CWnd类的派生类。虽然CPropertySheet不是CDialog类的派生类,但使用CPropertySheet对象的方法与使用CDialog对象是类似的。标签式对话框是一种特殊的对话框,因此,和普通对话框相比,它的设计与实现既有许多相似之处,又有一些不同的特点。
创建一个标签式对话框一般包括以下几个步骤:
分别为各个页创建对话框模板,去掉缺省的OK和Cancel按钮。每页的模板最好具有相同的尺寸,如果尺寸不统一,则框架将根据最大的页来确定标签对话框的大小。在创建模板时,需要在模板属性对话框中指定下列属性:
指定标题(Caption)的内容。标题的内容将显示在该页对应的标签中。
选择TitleBar、Child、ThinBorder和Disable属性。
根据各个页的模板,用ClassWizard分别为每个页创建CPropertyPage类的派生类。这一过程与创建普通对话框类的过程类似,不同的是在创建新类对话框中应在Base class一栏中选择CPropertyPage而不是CDialog。
用ClassWizard为每页加入与控件对应的成员变量,这个过程与为普通对话框类加入成员变量类似。
程序员可直接使用CPropertySheet类,也可以从该类派生一个新类。除非要创建一个非模态对话框,或要在框架对话框中加入控件,否则没有必要派生一个新类。如果直接使用CPropertySheet类,则一个典型的标签式对话框的创建代码如清单5.12所示,该段代码也演示了标签式对话框与外界的数据交换。这些代码通常是放在显示对话框的命令处理函数中。可以看出,对话框框架的创建过程及对话框与外界的数据交换机制与普通对话框是一样的,不同之处是还需将页对象加入到CPropertySheet对象中。如果要创建的是模态对话框,应调用CPropertySheet::DoModal,如果想创建非模态对话框,则应该调用CPropertySheet::Create。
若从CPropertySheet类派生了一个新类,则应该将所有的页对象以成员变量的形式嵌入到派生类中,并在派生类的构造函数中调用CPropertySheet::AddPage函数来把各个页添加到对话框中。这样,在创建标签式对话框时就不用做添加页的工作了。
清单5.12 典型的标签式对话框创建代码
void CMyView::DoModalPropertySheet()
{
CPropertySheet propsheet;
CMyFirstPage pageFirst; // derived from CPropertyPage
CMySecondPage pageSecond; // derived from CPropertyPage
// Move member data from the view (or from the currently
// selected object in the view, for example).
pageFirst.m_nMember1 = m_nMember1;
pageFirst.m_nMember2 = m_nMember2;
pageSecond.m_strMember3 = m_strMember3;
pageSecond.m_strMember4 = m_strMember4;
propsheet.AddPage(&pageFirst);
propsheet.AddPage(&pageSecond);
if (propsheet.DoModal() == IDOK)
{
m_nMember1 = pageFirst.m_nMember1;
m_nMember2 = pageFirst.m_nMember2;
m_strMember3 = pageSecond.m_strMember3;
m_strMember4 = pageSecond.m_strMember4;
. . .
}
}
.5.2 标签式对话框的运行机制
标签式对话框的初始化包括框架对话框的初始化和页的初始化。页的初始化工作可在OnInitDialog函数中进行,而框架对话框的初始化应该在OnCreate函数中完成。
根据CPropertySheet::DoModal返回的是IDOK还是IDCANCEL,程序可判断出关闭对话框时按的是OK还是Cancel按钮,这与普通对话框是一样的。
如果标签式对话框是模态对话框,在其底部会有三个按钮,依次为OK、Cancel和Apply(应用)按钮,如果对话框是非模态的,则没有这些按钮。OK和Cancel按钮的意义与普通对话框没什么两样,Apply按钮则是标签对话框所特有的。普通的模态对话框只有在用户按下了OK按钮返回后,对话框的设置才能生效,而设计Apply按钮的意图是让用户能在不关闭对话框的情况下使对话框中的设置生效。由此可见,Apply的作用与前面例子中登录数据的“添加”按钮类似,用户不必退出对话框,就可以反复进行设置,这在某些应用场合下是很有用的。
为了对上述三个按钮作出响应,CPropertyPage类提供了OnOK、OnCancel和OnApply函数,用户可覆盖这三个函数以完成所需的工作。需要指出的是这三个函数并不是直接响应按钮的BN_CLICKED消息的,但在按钮按下后它们会被间接调用。这些函数的说明如下:
virtual void OnOK( );
在按下OK或Apply按钮后,该函数将被调用。缺省的OnOK函数几乎什么也不干,象数据交换和关闭对话框这样的工作是在别的地方完成的,这与普通对话框的OnOK函数是不同的。virtual void OnCancel( );
在按下Cancel按钮后,该函数将被调用。缺省的OnCancel函数也是几乎什么都不干。virtual BOOL OnApply( );
在按下OK或Apply按钮后,该函数将被调用。缺省的OnApply会调用OnOK函数。函数的返回值如果是TRUE,则对话框中的设置将生效,否则无效。
按理说,CPropertySheet类也应该提供上述函数,特别是OnApply。但奇怪的是,MFC并未考虑CPropertySheet类的按钮响应问题。读者不要指望能通过ClassWizard来自动创建按钮的BN_CLICKED消息处理函数,如果需要用到这类函数,那么只好手工创建了。
下列几个CPropertyPage类的成员函数也与标签对话框的运行机制相关。
void SetModified( BOOL bChanged = TRUE );
该函数用来设置修改标志。若参数bChanged为TRUE,则表明对话框中的设置已改动,否则说明设置未改动。该函数的一个主要用途是允许或禁止Apply按钮。在缺省情况下,Apply按钮是禁止的。只要一调用SetModified(TRUE),Apply按钮就被允许,而调用SetModified(FALSE)并不一定能使Apply按钮禁止,只有在所有被标为改动过的页都调用了SetModified(FALSE)后,Apply按钮才会被禁止。另外,该函数对OnApply的调用也有影响,当Apply按钮被按下后,只有那些被标为改动过的页的OnApply函数才会被调用。在调用该函数之前,程序需要判断页中的内容是否已被修改,可以通过处理诸如BN_CLICKED、EN_CHANG这样的控件通知消息来感知页的内容的改变。virtual BOOL OnSetActive( );
当页被激活或被创建时,都会调用该函数。该函数的缺省行为是若页还未创建,就创建之,若页已经创建,则将其激活,并调用UpdateData(FALSE)更新控件。用户可覆盖该函数完成一些刷新方面的工作。virtual BOOL OnKillActive( );
当原来可见的页被覆盖或被删除时,都会调用该函数。该函数的缺省行为是调用UpdateData(TRUE)更新数据。用户可覆盖该函数完成一些特殊数据的有效性检查工作。
需要说明的是,标签对话框中的所有页不一定都会被创建。实际上,那些从未打开过的页及其控件是不会被创建的。因此,在CPropertyPage类的派生类中,只有在确定了页已存在后,才能调用与对话框及控件相关的函数(如UpdateData)。如果收到控件通知消息,或OnSetActive函数被调用,则说明页已经存在。正是由于上述原因,使得标签式对话框的内部数据交换只能在OnSetActive和OnKillActive函数中进行。
5.5.3 标签式对话框的具体实例
通过上面的分析,读者对标签式对话框已经比较了解了。现在,让我们在前面做过的Register程序中加入一个标签式对话框来试验一下其功能。
在Register程序的登录数据对话框中有“个人情况”和“单位情况”两组控件,显然,我们可以创建一个标签式对话框并把两组控件分别放到两个页中。为了简单起见,我们仅要求输入姓名和单位名,简化后的标签式对话框如图5.11所示。
图5.11 简化后的标签式对话框
通过对标签式对话框的分析,读者已经知道CPropertySheet类未对Apply按钮的控件通知消息进行处理,这是一个不足之处。Register的新版本将向读者演示如何在CPropertySheet类的派生类中手工加入Apply按钮的BN_CLICKED消息处理函数。另外,新版本还演示了对话框与外部对象交流的一种较好办法,即通过发送用户定义消息来向外部对象传递信息。在登录数据对话框中,与外界交流的方法是在对话框内部直接访问派生的视图对象,这样做的优点是方便快捷,缺点则是对外界依赖较大,不利于移植。而用发送用户定义消息的方法则可以避免这个缺点。
具体工作请按下面几步进行:
在菜单资源中的Edit菜单的“登录数据...”项的后面插入一个名为“标签式对话框...”的菜单项,并指定其ID为ID_EDIT_PROPDLG。然后用ClassWizard,在CRegisterView类内为该菜单命令创建命令处理函数OnEditPropdlg,该函数将用来显示标签式对话框。
为标签式对话框的第一页创建对话框模板。去掉缺省的OK和Cancel按钮。注意应选择中文语种和宋体字体。在属性对话框中,指定对话框的ID为IDD_PERSONAL,标题为“个人情况”,在Styles页中,选中TitleBar项,并在Style栏中选择Child,在Border栏中选择ThinBorder。在More Styles页中,选中Disable。然后,在模板中加入控件,如图5.11和表5.6所示。
表5.6
控件类型 |
控件ID |
控件标题 |
静态正文 |
缺省 |
姓名: |
编辑框 |
IDC_NAME |
用ClassWizard为模板IDD_PERSONAL创建CPropertyPage类的派生类,类名为CPersonalPage。在该类中为控件IDC_NAME加入对应的成员变量,变量名为m_strName,类型为CString。为控件IDC_NAME加入EN_CHANGE消息处理函数OnChangeName,当编辑框的内容被改变时,控件会向对话框发出EN_CHANGE消息。在OnChangeName中,应该使Apply按钮允许。
仿照步2,为标签式对话框的第二页创建对话框模板。指定其ID为IDD_UNIT,标题为“单位情况”。在模板中加入的控件如图5.11和表5.7所示。
表5.7
控件类型 |
控件ID |
控件标题 |
静态正文 |
缺省 |
工作单位: |
编辑框 |
IDC_UNIT |
用ClassWizard为模板IDD_UNIT创建CPropertyPage类的派生类,类名为CUnitPage。在该类中为控件IDC_UNIT加入对应的成员变量,变量名为m_strUnit,类型为CString。为控件IDC_UNIT加入EN_CHANGE消息处理函数OnChangeUnit。
用ClassWizard创建一个CPropertySheet的派生类,类名为CRegisterSheet。
在CRegisterApp类的头文件的开头加入下面一行
#define WM_USER_OUTPUT (WM_USER+200)
WM_USER_OUTPUT不是标准的Windows消息,而是一个用户定义消息。在本例中,当标签式对话框的Apply按钮被按下后,程序会向编辑视图发送该消息,编辑视图对应的消息处理函数应该输出对话框的数据。用户定义消息的编码范围是WM_USER—0x7FFF。请读者按清单5.13、5.14、5.15修改程序,限于篇幅,这里仅列出了需要修改的部分源代码。
清单5.13 CPersonalPage类和CUnitPage类的部分代码
void CPersonalPage::OnChangeName()
{
// TODO: Add your control notification handler code here
SetModified(TRUE); //使Apply按钮允许
UpdateData(TRUE);
}
void CUnitPage::OnChangeUnit()
{
// TODO: Add your control notification handler code here
SetModified(TRUE); //使Apply按钮允许
UpdateData(TRUE);
}
当页中的编辑框的内容被改变时,页会收到EN_CHANGE消息,这将导致OnChangeName或OnChangeUnit被调用。对该消息的处理是使Apply按钮允许并调用UpdateData(TRUE)更新数据。
清单5.14 CRegisterSheet类的部分代码
//文件RegisterSheet.h
class CRegisterSheet : public CPropertySheet
{
. . . . . .
// Construction
public:
CRegisterSheet(UINT nIDCaption, CWnd* pParentWnd = NULL, UINT iSelectPage = 0);
CRegisterSheet(LPCTSTR pszCaption, CWnd* pParentWnd = NULL, UINT iSelectPage = 0);
public:
CPersonalPage m_PersonalPage;
CUnitPage m_UnitPage;
. . . . . .
protected:
//{{AFX_MSG(CRegisterSheet)
// NOTE - the ClassWizard will add and remove member functions here.
//}}AFX_MSG
afx_msg void OnApplyNow();
DECLARE_MESSAGE_MAP()
};
//文件RegisterSheet.cpp
#include "stdafx.h"
#include "Register.h"
#include "PersonalPage.h"
#include "UnitPage.h"
#include "RegisterSheet.h"
CRegisterSheet::CRegisterSheet(LPCTSTR pszCaption, CWnd* pParentWnd, UINT iSelectPage)
:CPropertySheet(pszCaption, pParentWnd, iSelectPage)
{
AddPage(&m_PersonalPage); //向标签对话框中添加页
AddPage(&m_UnitPage);
}
BEGIN_MESSAGE_MAP(CRegisterSheet, CPropertySheet)
//{{AFX_MSG_MAP(CRegisterSheet)
// NOTE - the ClassWizard will add and remove mapping macros here.
//}}AFX_MSG_MAP
ON_BN_CLICKED(ID_APPLY_NOW, OnApplyNow)
END_MESSAGE_MAP()
void CRegisterSheet::OnApplyNow()
{
CFrameWnd* pFrameWnd = (CFrameWnd*) AfxGetMainWnd();
//获取指向视图的指针
CView* pView = pFrameWnd->GetActiveFrame()->GetActiveView();
//发送用户定义消息,在视图中输出信息
pView->SendMessage(WM_USER_OUTPUT, (WPARAM)this);
m_PersonalPage.SetModified(FALSE);
m_UnitPage.SetModified(FALSE); //使Apply按钮禁止
}
在CRegisterSheet类内嵌入了CPersonalPage和CUnitPage对象,在该类的构造函数中调用CPropertySheet::AddPage将两个页添加到对话框中。
标签式对话框的OK、Cancel和Apply按钮的ID分别是IDOK、IDCANCEL和ID_APPLY_NOW。在按下Apply按钮后,CRegisterSheet对象应该作出响应,由于ClassWizard不能为CRegisterSheet类提供Apply按钮的BN_CLICKED消息处理函数,故必须手工声明和定义消息处理函数OnApplyNow,并在消息映射表中手工加入ID_APPLY_NOW的BN_CLICKED消息映射,该映射是通过ON_BN_CLICKED宏实现的。
函数OnApplyNow用CWnd::SendMessage向视图发送用户定义消息WM_USER_OUTPUT,并调用CPropertyPage::SetModified(FALSE)来禁止Apply按钮。在发送消息时,将this指针作为wParam参数一并发送,这是因为视图对象需要指向CRegisterSheet对象的指针来访问该对象。该函数演示了如何在程序的任意地方获得当前活动视图的方法:首先,调用AfxGetMainWnd()返回程序主窗口的CWnd类指针,然后将该指针强制转换成CFrameWnd类型,接着调用CFrameWnd::GetActiveFrame返回当前活动的框架窗口的一个CFrameWnd型指针,最后调用CFrameWnd::GetActiveView返回当前活动视图的一个Cview型指针。
在函数OnApplyNow中主要调用了下列函数:
CWnd* AfxGetMainWnd( );
该函数返回一个指向程序的主窗口CWnd指针。程序的主窗口可以是一个框架窗口,也可以是一个对话框。virtual CFrameWnd* GetActiveFrame( );
函数返回一个CFrameWnd型的指针。如果是MDI(多文档界面)程序,则该函数将返回当前活动的子框架窗口,如果是SDI(单文档界面)程序,该函数将返回主框架窗口本身。CView* GetActiveView( ) const;
返回一个指向当前活动视图的Cview型指针。LRESULT SendMessage( UINT message, WPARAM wParam = 0, LPARAM lParam = 0 );
用于向本窗口发送消息。SendMessage会直接调用发送消息的处理函数,直到发送消息被处理完后该函数才返回。参数message说明了要发送的消息,wParam和lParam则提供了消息的附加信息。
清单5.15 CRegisterView类的部分代码
//文件RegisterView.h
class CRegisterView : public CEditView
{
. . . . . .
// Generated message map functions
protected:
//{{AFX_MSG(CRegisterView)
afx_msg void OnEditRegister();
afx_msg void OnEditPropdlg();
//}}AFX_MSG
afx_msg LRESULT OnOutput(WPARAM wParam, LPARAM lParam);
DECLARE_MESSAGE_MAP()
};
//文件RegisterView.cpp
#include "stdafx.h"
#include "Register.h"
#include "RegisterDoc.h"
#include "RegisterView.h"
#include "RegisterDialog.h"
#include "PersonalPage.h"
#include "UnitPage.h"
#include "RegisterSheet.h"
BEGIN_MESSAGE_MAP(CRegisterView, CEditView)
. . . . . .
ON_MESSAGE(WM_USER_OUTPUT, OnOutput)
END_MESSAGE_MAP()
void CRegisterView::OnEditPropdlg()
{
// TODO: Add your command handler code here
CRegisterSheet RegisterSheet("登录");
RegisterSheet.m_PersonalPage.m_strName="张颖峰";
RegisterSheet.m_UnitPage.m_strUnit="南京邮电学院";
if(RegisterSheet.DoModal()==IDOK)
OnOutput((WPARAM)&RegisterSheet,0);
}
//用户定义消息WM_USER_OUTPUT的处理函数
LRESULT CRegisterView::OnOutput(WPARAM wParam, LPARAM lParam)
{
CRegisterSheet *pSheet=(CRegisterSheet*)wParam;
CString str;
GetWindowText(str);
str+="/r/n";
str+="姓名:";
str+=pSheet->m_PersonalPage.m_strName;
str+="/r/n";
str+="工作单位:";
str+=pSheet->m_UnitPage.m_strUnit;
str+="/r/n";
SetWindowText(str);
return 0;
}
OnEditPropdlg函数负责初始化和创建标签式对话框,这一过程与创建普通对话框差不多。如果用户是按OK按钮返回的,则调用OnOutput函数输出数据。
CRegisterView类的OnOutput函数负责处理标签对话框发来的用户定义消息WM_USER_OUTPUT。用户定义消息的处理函数只能用手工的方法加入。用户定义消息的消息映射是用ON_MESSAGE宏来完成的。
函数OnOutput的两个参数wParam和lParam分别对应消息的wParam和lParam值。该函数从wParam参数中获得指向CRegisterSheet对象的指针,然后将该对象中的数据输出到视图中。
编译并运行Register,试一试自己设计的标签式对话框。
5.6 公用对话框
在使用Windows的过程中,用户经常会遇到一些常用的有特定用途的对话框。例如,当选择File->Open,会弹出一个文件选择的对话框,用户可以在其中选择想要打开的文件。象文件选择这样的对话框,使用的非常普遍,因此Windows系统本身提供了对该对话框的支持,用户不必自己设计文件选择对话框。与文件选择对话框类似的还有颜色选择、字体选择、打印和打印设置以及正文搜索和替换对话框。这五种对话框均由Windows支持,被称为公用对话框。
MFC提供了一些公用对话框类,它们均是CDialog类的派生类,封装了公用对话框的功能。表5.6列出了MFC的公用对话框类。
表5.6 公用对话框类
通用对话框类 |
用途 |
CColorDialog |
选择颜色 |
CFileDialog |
选择文件名,用于打开和保存文件 |
CFindReplaceDialog |
正文查找和替换 |
CFontDialog |
选择字体 |
CPrintDialog |
打印和打印设置 |
通用对话框类使用方便,读者只需知道怎样创建对话框和访问对话框的数据,不必关心它们的内部细节。
5.6.1 CColorDialog类
CColorDialog类用于实现Color(颜色选择)公用对话框。Color对话框如图5.12所示,在Windows的画板程序中,如果用户在颜色面板的某种颜色上双击鼠标,就会显示一个Color对话框来让用户选择颜色。
图5.12 Color对话框
Color对话框的创建与一般的对话框没什么两样:首先是在堆栈上构建一个CColorDialog对象,然后调用CColorDialog::DoModal( )来启动对话框。CColorDialog的构造函数为
CColorDialog( COLORREF clrInit = 0, DWORD dwFlags = 0, CWnd* pParentWnd = NULL );
参数clrInit用来指定初始的颜色选择,dwFlags用来设置对话框,pParentWnd用于指定对话框的父窗口或拥有者窗口。
根据DoModal返回的是IDOK还是IDCANCEL可知道用户是否确认了对颜色的选择。DoModal返回后,调用CColorDialog::GetColor()可以返回一个COLORREF类型的结果来指示在对话框中选择的颜色。COLORREF是一个32位的值,用来说明一个RGB颜色。GetColor返回的COLORREF的格式是0x00bbggrr,即低位三个字节分别包含了蓝、绿、红三种颜色的强度。
读者将在后面的章节中看到颜色选择对话框的例子。
5.6.2 CFileDialog类
CFileDialog类用于实现文件选择对话框,以支持文件的打开和保存操作。用户要打开或保存文件,就会和文件选择对话框打交道,图5.13显示了一个标准的用于打开文件的文件选择对话框。用MFC AppWizard建立的应用程序中自动加入了文件选择对话框,在File菜单选Open或Save As命令会启动它们。
图5.13 文件选择对话框
文件选择对话框的创建过程与一般对话框的类似,首先是在堆栈上构建一个CFileDialog对象,然后调用CFileDialog::DoModal( )来启动对话框。文件对话框的构造函数为
CFileDialog( BOOL bOpenFileDialog, LPCTSTR lpszDefExt = NULL, LPCTSTR lpszFileName = NULL, DWORD dwFlags = OFN_HIDEREADONLY | OFN_OVERWRITEPROMPT, LPCTSTR lpszFilter = NULL, CWnd* pParentWnd = NULL );
如果参数bOpenFileDialog的值为TRUE,将创建Open(打开文件)对话框,否则就创建Save As(保存文件)对话框。参数lpszDefExt用来指定缺省的文件扩展名。lpszFileName用于规定初始文件名。dwFlags用于设置对话框的一些属性。lpszFilter指向一个过滤字符串,用户如果只想选择某种或某几种类型的文件,就需要指定过滤字符串。参数pParentWnd是指向父窗口或拥有者窗口的指针。
过滤字符串有特定的格式,它实际上是由多个子串组成,每个子串由两部分组成,第一部分是过滤器的字面说明,如“Text file (*.txt)”,第二部分是用于过滤的匹配字符串,如“*.txt”,子串的两部分用竖线字符“ | ”分隔开。各子串之间也要用“ | ”分隔,且整个串的最后两个字符必须是两个连续的竖线字符“ || ”。一个典型的过滤字符串如下面所示:
char szFilter[]=
“All files (*.*)|*.*|Text files(*.txt)|*.txt|Word documents(*.doc)|*.doc||”;
若CFileDialog::DoModal返回的是IDOK,那么可以用表5.7列出的CFileDialog类的成员函数来获取与所选文件有关的信息。
表5.7 CFileDialog类辅助成员函数
函数名 |
用途 |
GetPathName |
返回一个包含有全路径文件名的CString对象。 |
GetFileName |
返回一个包含有文件名(不含路径)的CString对象。 |
GetFileExt |
返回一个只含文件扩展名的CString对象。 |
GetFileTitle |
返回一个只含文件名(不含扩展名)的CString对象。 |
5.6.3 CFindReplaceDialog类
CFindReplaceDialog类用于实现Find(搜索)和Replace(替换)对话框,这两个对话框都是非模态对话框,用于在正文中搜索和替换指定的字符串。图5.14显示了一个Find对话框,图5.15显示了一个Replace对话框。
图5.14 Find对话框
图5.15 Replace对话框
由于Find和Replace对话框是非模式对话框,它们的创建方式与其它四类公用对话框不同。CFindReplaceDialog对象是用new操作符在堆中创建的,而不是象普通对话框那样以变量的形式创建。要启动Find/Replace对话框,应该调用CFindReplaceDialog::Create函数,而不是DoModal。Create函数的声明是
BOOL Create( BOOL bFindDialogOnly, LPCTSTR lpszFindWhat, LPCTSTR lpszReplaceWith = NULL, DWORD dwFlags = FR_DOWN, CWnd* pParentWnd = NULL );
当参数bFindDialogOnly的值为TRUE时,创建的是Find对话框,为FALSE时创建的是Replace对话框。参数lpszFindWhat指定了要搜索的字符串,lpszReplaceWith指定了用于替换的字符串。dwFlags用来设置对话框,其缺省值是FR_DOWN(向下搜索),该参数可以是几个FR_XXX常量的组合,用户可以通过该参数来决定诸如是否要显示Match case、Match Whole Word检查框等设置。参数pParentWnd指明了对话框的父窗口或拥有者窗口。
Find/Replace对话框与其它公用对话框的另一个不同之处在于它在工作过程中可以重复同一操作而对话框不被关闭,这就方便了频繁的搜索和替换。CFindReplaceDialog类只提供了一个界面,它并不会自动实现搜索和替换功能。CFindReplaceDialog使用了一种特殊的通知机制,当用户按下了操作的按钮后,它会向父窗口发送一个通知消息,父窗口应在该消息的消息处理函数中实现搜索和替换。
CFindReplaceDialog类提供了一组成员函数用来获得与用户操作有关的信息,如表5.8所示,这组函数一般应在通知消息处理函数中调用。
表5.8 CFindReplaceDialog类的辅助成员函数
函数名 |
用途 |
FindNext |
如果用户点击了Findnext按钮,该函数返回TRUE。 |
GetNotifier |
返回一个指向当前CFindReplaceDialog对话框的指针。 |
GetFindString |
返回一个包含要搜索字符串的CString对象。 |
GetReplaceString |
返回一个包含替换字符串的CString对象。 |
IsTerminating |
如果对话框终止了,则返回TRUE。 |
MatchCase |
如果选择了对话框中的Match case检查框,则返回TRUE。 |
MatchWholeWord |
如果选择了对话框中的Match Whole Word检查框,则返回TRUE。 |
ReplaceAll |
如果用户点击了Replace All按钮,该函数返回TRUE。 |
ReplaceCurrent |
如果用户点击了Replace按钮,该函数返回TRUE。 |
SearchDown |
返回TRUE表明搜索方向向下,返回FALSE则向上。 |
CEditView类自动实现了Find和Replace对话框的功能,但MFC AppWizard并未提供相应的菜单命令。读者可以在前面的Register工程的Edit菜单中加入&Find...和&Replace...两项,并令其ID分别为ID_EDIT_FIND和ID_EDIT_REPLACE,则Find/Replace对话框的功能就可以实现。
5.6.4 CFontDialog类
CFontDialog类支持Font(字体)对话框,用来让用户选择字体。图5.16显示了一个Font对话框。Font对话框的创建过程与Color对话框的类似,首先是在堆栈上构建一个CFontDialog对象,然后调用CFontDialog::DoModal来启动对话框。
图5.16 Font对话框
CFontDialog类的构造函数如下所示
CFontDialog( LPLOGFONT lplfInitial = NULL, DWORD dwFlags = CF_EFFECTS | CF_SCREENFONTS, CDC* pdcPrinter = NULL, CWnd* pParentWnd = NULL );
参数lplfInitial指向一个LOGFONG结构,用来初始化对话框中的字体设置。dwFlags用于设置对话框。pdcPrinter指向一个代表打印机的CDC对象,若设置该参数,则选择的字体就为打印机所用。pParentWnd用于指定对话框的父窗口或拥有者窗口。
若DoModal返回IDOK,那么可以调用CFontDialog的成员函数来获得所选字体的信息,这些函数在表5.9列出。
表5.9 CFontDialog类的辅助成员函数
函数名 |
用途 |
GetCurrentFont |
用来获得所选字体的属性。该函数有一个参数,该参数是指向LOGFONT结构的指针,函数将所选字体的各种属性写入这个LOGFONT结构中。 |
GetFaceName |
返回一个包含所选字体名字的CString对象。 |
GetStyleName |
返回一个包含所选字体风格名字的CString对象。 |
GetSize |
返回所选字体的尺寸(以10个象素为单位)。 |
GetColor |
返回一个含有所选字体的颜色的COLORREF型值。 |
GetWeight |
返回所选字体的权值。 |
IsStrikeOut |
若用户选择了空心效果则返回TRUE,否则返回FALSE。 |
IsUnderline |
若用户选择了下划线效果则返回TRUE,否则返回FALSE。 |
IsBold |
若用户选择了黑体风格则返回TRUE,否则返回FALSE。 |
IsItalic |
若用户选择了斜体风格则返回TRUE,否则返回FALSE。 |
.6.5 CPrintDialog类
CPrintDialog类支持Print(打印)和Print Setup(打印设置)对话框,通过这两个对话框用户可以进行与打印有关的操作。图5.17显示了一个Print对话框,图5.18显示了一个Print Setup对话框。
图5.17 Print对话框
图5.18 Print Setup对话框
Print和Print Setup对话框的创建过程与Color对话框类似。该类的构造函数是
CPrintDialog( BOOL bPrintSetupOnly, DWORD dwFlags = PD_ALLPAGES | PD_USEDEVMODECOPIES | PD_NOPAGENUMS | PD_HIDEPRINTTOFILE | PD_NOSELECTION, CWnd* pParentWnd = NULL );
参数bPrintSetupOnly的值若为TRUE,则创建的是Print对话框,否则,创建的是Print Setup对话框。dwFlags用来设置对话框,缺省设置是打印出全部页,禁止From和To编辑框(即不用确定要打印的页的范围),PD_USEDEVMODECOPIES使对话框判断打印设备是否支持多份拷贝和校对打印(Collate),若不支持,就禁止相应的编辑控件和Collate检查框。pParentWnd用来指定对话框的父窗口或拥有者窗口。
程序可以调用如表5.10所示的CPrintDialog的成员函数来获得打印参数。
表5.10 CPrintDialog的辅助成员函数
函数名 |
用途 |
GetCopies |
返回要求的拷贝数。 |
GetDefaults |
在不打开对话框的情况下返回缺省打印机的缺省设置,返回的设置放在m_pd数据成员中。 |
GetDeviceName |
返回一个包含有打印机设备名的CString对象。 |
GetDevMode |
返回一个指向DEVMODE结构的指针,用来查询打印机的设备初始化信息和设备环境信息。 |
GetDriverName |
返回一个包含有打印机驱动程序名的CString对象。 |
GetFromPage |
返回打印范围的起始页码。 |
GetToPage |
返回打印范围的结束页码。 |
GetPortName |
返回一个包含有打印机端口名的CString对象。 |
GetPrinterDC |
返回所选打印设备的一个 HDC 句柄。 |
PrintAll |
若要打印文档的所有页则返回TRUE。 |
PrintCollate |
若用户选择了Collate Copies检查框(需要校对打印拷贝)则返回TRUE。 |
PrintRange |
如果用户要打印文档的一部分页,则返回TRUE。 |
PrintSelection |
若用户想打印当前选择的部分文档,则返回TRUE。 |
用缺省配置的MFC AppWizard建立的程序支持Print和Print Setup对话框,用户可以在File菜单中启动它们。
5.6.6 公用对话框的使用实例
现在,让我们来测试一下公用对话框的使用。请读者用AppWizard创建一个单文档的MFC应用程序,名为CommonDlg。注意别忘了在AppWizard的第一步中选Single document。
CommonDlg程序要对所有的公用对话框进行了测试。为此,首先要提供用户命令接口。请读者在CommonDlg的菜单资源中插入一个名为&Common的新菜单,这个菜单插在Help菜单之前。然后,在Common菜单中,请按表5.11创建菜单项。
表5.11 Common菜单的菜单项
Caption |
ID |
&Color... |
ID_COMMON_COLOR |
&Open file... |
ID_COMMON_OPENFILE |
&Save file... |
ID_COMMON_SAVEFILE |
&Font... |
ID_COMMON_FONT |
&Print... |
ID_COMMON_PRINT |
P&rint setup... |
ID_COMMON_PRINTSETUP |
F&ind... |
ID_COMMON_FIND |
&Replace... |
ID_COMMON_REPLACE |
接下来的工作是编写测试程序的源代码。首先,利用ClassWizard为表5.11的菜单项创建消息处理函数,注意这些处理函数都是CCommonDlgView的成员。接着,请按清单5.10和5.11修改程序。限于篇幅,这里仅列出与测试相关的部分源代码。
清单5.10 头文件CommonDlgView.h
class CCommonDlgView : public CView
{
. . . . . .
#ifdef _DEBUG
virtual void AssertValid() const;
virtual void Dump(CDumpContext& dc) const;
#endif
protected:
void DispPrintInfo(CPrintDialog& dlg);
protected:
CFont m_Font; //正文的字体
COLORREF m_ForeColor; //正文的前景色
COLORREF m_BackColor; //正文的背景色
CFindReplaceDialog *m_pFindReplaceDlg;
BOOL m_bFindOnly;
// Generated message map functions
protected:
//Find和Replace对话框通知消息处理函数
afx_msg LRESULT OnFindReplaceCmd(WPARAM, LPARAM lParam);
//{{AFX_MSG(CCommonDlgView)
afx_msg void OnCommonColor();
afx_msg void OnCommonFont();
afx_msg void OnCommonOpenfile();
afx_msg void OnCommonSavefile();
afx_msg void OnCommonPrint();
afx_msg void OnCommonPrintsetup();
afx_msg void OnCommonFind();
afx_msg void OnCommonReplace();
//}}AFX_MSG
DECLARE_MESSAGE_MAP()
};
. . . . . .
清单5.11 文件CCommonDlgView.cpp
#include "stdafx.h"
#include "CommonDlg.h"
#include "CommonDlgDoc.h"
#include "CommonDlgView.h"
#ifdef _DEBUG
#define new DEBUG_NEW
#undef THIS_FILE
static char THIS_FILE[] = __FILE__;
#endif
IMPLEMENT_DYNCREATE(CCommonDlgView, CView)
//获取对本进程唯一的消息编号
static const UINT nMsgFindReplace = ::RegisterWindowMessage(FINDMSGSTRING);
BEGIN_MESSAGE_MAP(CCommonDlgView, CView)
//{{AFX_MSG_MAP(CCommonDlgView)
ON_COMMAND(ID_COMMON_COLOR, OnCommonColor)
ON_COMMAND(ID_COMMON_FONT, OnCommonFont)
ON_COMMAND(ID_COMMON_OPENFILE, OnCommonOpenfile)
ON_COMMAND(ID_COMMON_SAVEFILE, OnCommonSavefile)
ON_COMMAND(ID_COMMON_PRINT, OnCommonPrint)
ON_COMMAND(ID_COMMON_PRINTSETUP, OnCommonPrintsetup)
ON_COMMAND(ID_COMMON_FIND, OnCommonFind)
ON_COMMAND(ID_COMMON_REPLACE, OnCommonReplace)
//}}AFX_MSG_MAP
// Standard printing commands
ON_COMMAND(ID_FILE_PRINT, CView::OnFilePrint)
ON_COMMAND(ID_FILE_PRINT_DIRECT, CView::OnFilePrint)
ON_COMMAND(ID_FILE_PRINT_PREVIEW, CView::OnFilePrintPreview)
ON_REGISTERED_MESSAGE(nMsgFindReplace, OnFindReplaceCmd)
END_MESSAGE_MAP()
CCommonDlgView::CCommonDlgView()
{
// TODO: add construction code here
//缺省前景色为黑色,背景色为白色,字体为系统字体
m_ForeColor=0;
m_BackColor=0xFFFFFF;
m_Font.CreateStockObject(SYSTEM_FONT);
m_pFindReplaceDlg=NULL;
}
void CCommonDlgView::OnDraw(CDC* pDC)
{
CCommonDlgDoc* pDoc = GetDocument();
ASSERT_VALID(pDoc);
// TODO: add draw code for native data here
int x,y;
CFont *pOldFont;
TEXTMETRIC TM;
int textHeight;
//设置正文的字体
pOldFont=pDC->SelectObject(&m_Font);
//设置正文的前景色和背景色
pDC->SetTextColor(m_ForeColor);
pDC->SetBkColor(m_BackColor);
//计算每行正文的高度
pDC->GetTextMetrics(&TM);
textHeight=TM.tmHeight+TM.tmExternalLeading;
//输出正文
x=5;y=5;
pDC->TextOut(x,y,"ABCDEFG");
y+=textHeight;
pDC->TextOut(x,y,"abcdefg");
//恢复原来的字体
pDC->SelectObject(pOldFont);
}
void CCommonDlgView::OnCommonColor()
{
// TODO: Add your command handler code here
CColorDialog dlg;
if(dlg.DoModal()==IDOK)
{
m_BackColor=dlg.GetColor();
//重绘视图
Invalidate();
UpdateWindow();
}
}
void CCommonDlgView::OnCommonFont()
{
// TODO: Add your command handler code here
CFontDialog dlg;
if(dlg.DoModal()==IDOK)
{
LOGFONT LF;
//获取所选字体的信息
dlg.GetCurrentFont(&LF);
m_ForeColor=dlg.GetColor();
//建立新的字体
m_Font.DeleteObject();
m_Font.CreateFontIndirect(&LF);
Invalidate();
UpdateWindow();
}
}
void CCommonDlgView::OnCommonOpenfile()
{
// TODO: Add your command handler code here
//过滤字符串
char szFileFilter[]=
"Cpp files(*.cpp)|*.cpp|"
"Header files(*.h)|*.h|"
"All files(*.*)|*.*||";
CFileDialog dlg(TRUE, //Open对话框
"cpp", //缺省扩展名
"*.cpp",
OFN_HIDEREADONLY|OFN_FILEMUSTEXIST, //文件必须存在
szFileFilter,
this);
if(dlg.DoModal()==IDOK)
{
CString str="The full path name is:";
str+=dlg.GetPathName();
AfxMessageBox(str);
}
}
void CCommonDlgView::OnCommonSavefile()
{
// TODO: Add your command handler code here
char szFileFilter[]=
"Cpp files(*.cpp)|*.cpp|"
"Header files(*.h)|*.h|"
"All files(*.*)|*.*||";
CFileDialog dlg(FALSE, //Save对话框
"cpp",
"*.cpp",
OFN_HIDEREADONLY|OFN_OVERWRITEPROMPT,
szFileFilter,
this);
if(dlg.DoModal()==IDOK)
{
CString str="The file name is:";
str+=dlg.GetFileName();
AfxMessageBox(str);
}
}
void CCommonDlgView::OnCommonPrint()
{
// TODO: Add your command handler code here
CPrintDialog dlg(FALSE, PD_ALLPAGES); //Print对话框
//设置Print对话框的属性
dlg.m_pd.nCopies=2;
dlg.m_pd.nMinPage=1;
dlg.m_pd.nMaxPage=50;
dlg.m_pd.nFromPage=1;
dlg.m_pd.nToPage=50;
if(dlg.DoModal()==IDOK)
DispPrintInfo(dlg);
}
void CCommonDlgView::OnCommonPrintsetup()
{
// TODO: Add your command handler code here
CPrintDialog dlg(TRUE) //Print Setup对话框
if(dlg.DoModal()==IDOK)
DispPrintInfo(dlg);
}
void CCommonDlgView::DispPrintInfo(CPrintDialog& dlg)
{
CString str;
CString temp;
str+="Driver name:";
str+=dlg.GetDriverName();
str+="/nDevice name:";
str+=dlg.GetDeviceName();
str+="/nPort name:";
str+=dlg.GetPortName();
str+="/nNumber of copies:";
temp.Format("%d",dlg.GetCopies());
str+=temp;
str+="/nCollate:";
str+=dlg.PrintCollate()?"Yes":"No";
str+="/nPrint all:";
str+=dlg.PrintAll()?"Yes":"No";
str+="/nPrint range:";
str+=dlg.PrintRange()?"Yes":"No";
str+="/nSelection:";
str+=dlg.PrintSelection()?"Yes":"No";
str+="/nFrom page:";
temp.Format("%d",dlg.GetFromPage());
str+=temp;
str+="/nTo page:";
temp.Format("%d",dlg.GetToPage());
str+=temp;
AfxMessageBox(str);
}
void CCommonDlgView::OnCommonFind()
{
// TODO: Add your command handler code here
//判断是否已存在一个对话框
if(m_pFindReplaceDlg)
{
if(m_bFindOnly)
{
//若Find对话框已打开,则使之成为活动窗口
m_pFindReplaceDlg->SetActiveWindow();
return;
}
else
//关闭Replace对话框
m_pFindReplaceDlg->SendMessage(WM_CLOSE);
}
m_bFindOnly=TRUE;
//创建Find对话框
m_pFindReplaceDlg=new CFindReplaceDialog;
m_pFindReplaceDlg->Create(TRUE,NULL,NULL,FR_DOWN,this);
}
void CCommonDlgView::OnCommonReplace()
{
// TODO: Add your command handler code here
//判断是否已存在一个对话框
if(m_pFindReplaceDlg)
{
if(!m_bFindOnly)
{
//若Replace对话框已打开,则使之成为活动窗口
m_pFindReplaceDlg->SetActiveWindow();
return;
}
else
//关闭Find对话框
m_pFindReplaceDlg->SendMessage(WM_CLOSE);
}
m_bFindOnly=FALSE;
//创建Replace对话框
m_pFindReplaceDlg=new CFindReplaceDialog;
m_pFindReplaceDlg->Create(FALSE,NULL,NULL,FR_DOWN,this);
}
//Find和Replace对话框通知消息处理函数
LRESULT CCommonDlgView::OnFindReplaceCmd(WPARAM, LPARAM lParam)
{
//判断对话框是否被关闭
if(m_pFindReplaceDlg->IsTerminating())
m_pFindReplaceDlg=NULL;
return 0;
}
让我们先来看看对Color对话框的测试。在CCommonDlgView::OnCommonColor中创建了一个Color对话框,在此处该对话框的用途是为视图中显示的正文指定背景色。在CCommonDlgView的构造函数中将背景色m_BackColor的初值设置为白色(0x00000000)。若DoModal返回IDOK,则调用CColorDialog::GetColor获取用户选择的颜色并将之保存在m_BackColor成员中。然后,调用Invalidate和UpdateWindow函数以重绘视图。这两个函数的说明如下:
void Invalidate( BOOL bErase = TRUE );
该函数的作用是使整个窗口客户区无效。窗口的客户区无效意味着需要重绘,例如,如果一个被其它窗口遮住的窗口变成了前台窗口,那么原来被遮住的部分就是无效的,需要重绘。这时Windows会在应用程序的消息队列中放置WM_PAINT消息。MFC为窗口类提供了WM_PAINT的消息处理函数OnPaint,OnPaint负责重绘窗口。视图类有一些例外,在视图类的OnPaint函数中调用了OnDraw函数,实际的重绘工作由OnDraw来完成。参数bErase为TRUE时,重绘区域内的背景将被擦除,否则,背景将保持不变。void UpdateWindow( );
该函数的作用是使窗口立即重绘。调用Invalidate等函数后窗口不会立即重绘,这是由于WM_PAINT消息的优先级很低,它需要等消息队列中的其它消息发送完后才能被处理。调用UpdateWindow函数可使WM_PAINT被直接发送到目标窗口,从而导致窗口立即重绘。
在CCommonView::OnDraw函数中调用了CDC::SetBkColor来设置背景色。CDC类用于绘图,在后面的几章里将会对其作详细介绍。CDC::TextOut函数用于输出正文。两个函数的说明如下:
virtual COLORREF SetBkColor( COLORREF crColor );
用于设置背景色。参数crColor指定了背景色的RGB值。返回的是原来的背景色。BOOL TextOut( int x, int y, const CString& str );
在指定的位置输出正文。参数x和y指定了输出起点的横向和纵向坐标。str参数是输出的字符串。若该函数调用成功则返回TRUE。
对文件选择对话框的测试比较简单。在CCommonDlgView::OnCommonOpenfile和CCommonDlgView:OnCommonSavefile函数中,分别创建了一个Open对话框和一个Save对话框。在创建Open对话框时,在CFileDialog的构造函数中规定了OFN_FILEMUSTEXIST属性,这样当用户试图打开一个不存在的文件时,对话框会发出错误信息并让用户从新选择文件。在创建Save对话框时,在CFileDialog的构造函数中规定了OFN_OVERWRITEPROMPT属性,这样,当用户试图覆盖一个已存在的文件时,对话框会询问用户是否真的要覆盖该文件。
若用户确认了对文件的选择,那么在文件选择对话框关闭后,程序会将所选文件的文件名或全路径文件名输出到屏幕上。
Find和Replace对话框的创建工作分别由CCommonDlgView::OnCommonFind和CCommonDlgView::OnCommonReplace完成。
在OnCommonFind函数中,首先判断是否已经打开了一个Find/Replace对话框。这个判断是完全必要的,因为Find/Replace对话框是非模态对话框,打开一个对话框后,用户有可能通过菜单再次执行Find或Replace命令。成员m_pFindReplaceDlg是指向CFindReplaceDialog对象的指针,若该指针不为空,则说明对话框已打开。接着,根据成员m_bFindOnly来判断原先打开的是否是Find对话框,如果原先打开的是一个Find对话框,则此时不必创建新的对话框,只需激活已打开的Find对话框就行了;如果原先打开的是一个Replace对话框,则应该先关闭该对话框,然后再创建Find对话框。然后,给m_bFindOnly赋TRUE值,以表明现在打开的是一个Find对话框。最后,创建一个非模态的Find对话框,请注意其过程与创建模态对话框的不同之处:
对话框对象是用new操作符在堆上创建的,而不是以变量的形式创建。
对话框的启动是靠调用Create函数实现的,而不是DoModal函数。
调用CWnd::SetActiveWindow以激活窗口。调用CWnd::SendMessage(WM_CLOSE)来关闭窗口,这是因为WM_CLOSE消息会导致CWnd::DestroyWindow函数的调用。
OnCommonReplace函数的过程与OnCommonFind函数类似。在该函数中,对m_bFindOnly赋值FALSE以表明打开的是Replace对话框。
Find/Replace对话框通知消息的处理函数是CCommonDlgView::OnFindReplaceCmd,这个消息处理函数及消息映射均是手工加入的。请注意在CommonDlgView.cpp文件的开头部分定义了一个静态全局变量nMsgFindReplace
static const UINT nMsgFindReplace = :: RegisterWindowMessage( FINDMSGSTRING );
nMsgFindReplace变量用于存放消息码,这个消息是由函数RegisterWindowMessage提供的,该函数的声明为
UINT RegisterWindowMessage(LPCTSTR lpString);
参数lpString是一个消息字符串。调用RegisterWindowMessage函数会返回一个Windows注册消息,注册消息的编码在系统中是唯一的。当有多个应用程序需要处理同一个消息时,应调用该函数注册消息。如果消息是本应用程序专有的,则不必注册。如果两个应用程序使用相同的字符串注册消息,则会返回相同的消息,这样,通过该消息,两个应用程序可以进行通信。
注册消息的消息映射宏是ON_REGISTERED_MESSAGE,在CommonDlgView的消息映射中可以找到它。
在函数OnFindReplaceCmd中应该进行实际的搜索和替换工作,但在本例中该函数什么工作也不作。该函数只是判断一下对话框是否被关闭,若是,则给m_pFindReplaceDlg赋NULL值,以表明对话框已不存在了。
Font对话框的创建由函数CCommonView:: OnCommonFont完成。该函数收集了用户选择的字体的信息,并利用这些信息创建新的字体。成员m_ForeColor用来保存所选字体的颜色,成员m_Font是一个CFont对象,用来保存用户选择的字体。在CCommonView的构造函数中,m_ForeColor被初始化成黑色(0xFFFFFF),m_Font被初始化为系统字体。系统字体的获得是通过调用CGdiObject::CreateStockObject(SYSTEM_FONT)实现的,该函数用于获得系统库存的绘图对象,包括字体、画笔、刷子、调色板等。
在OnCommonFont函数中,主要调用了下列函数:
调用CFontDialog:: GetCurrentFont以获得用户选择字体的信息,该函数的声明为
void GetCurrentFont( LPLOGFONT lplf );
参数lplf是一个指向LOGFONT结构的指针,LOGFONT结构用来存放与字体有关的信息。调用CFontDialog::GetColor来获得所选字体的颜色(前景色)。
调用CGdiObject:: DeleteObject()来删除存放在CFont对象m_Font中的老字体。
调用CFont::CreateFontIndirect以创建一种字体,该函数的声明是
BOOL CreateFontIndirect(const LOGFONT* lpLogFont );
参数lpLogFont是一个指向LOGFONT结构的指针,函数根据该结构提供的信息来初始化Cfont对象。调用CWnd::Invalidate和CWnd::UpdateWindow重绘视图。
在CCommonView::OnDraw函数中,利用选择的字体和颜色输出两行正文。当视图需要重绘时,OnDraw就会被调用。在OnDraw函数中主要调用了下列函数:
在输出正文前,调用CDC::SelectObject指定输出正文的字体,输出完成后,调用CDC::SelectObject恢复被替换的字体。SelectObject有五个版本,用于为绘图指定画笔、刷子、字体、位图等绘图对象。在用该函数指定绘图对象时,应该把被替换的对象保存起来,在绘图完成后,需要再次调用该函数恢复被替换的绘图对象。如果不进行恢复,则可能会使设备对象CDC中含有非法的句柄。指定字体的SelectObject函数的声明是
virtual CFont* SelectObject( CFont* pFont );
参数pFont是指向CFont对象的指针。函数返回一个CFont对象的指针,指向被替代的字体。调用CDC::SetTextColor来指定正文显示的前景色,该函数的声明为
virtual COLORREF SetTextColor( COLORREF crColor );
参数crColor指定了RGB颜色。函数返回的是原来的正文颜色。调用CDC:: GetTextMetrics函数获得与绘图字体有关的各种信息,该函数的声明为
BOOL GetTextMetrics( LPTEXTMETRIC lpMetrics ) const;
参数lpMetrics是一个指向TEXTMETRIC结构的指针,该结构包含有字体的信息,其中tmHeight成员说明了字体的高度,tmExternalLeading成员说明了行与行之间的空白应该是多少。把这两个值相加就得到了每行正文的高度。调用CDC::TextOut在指定位置输出正文。
在CCommonDlgView::OnCommonPrint和CCommonDlgView::OnCommonPrintsetup()函数中,分别创建了一个Print对话框和Print Setup对话框。在创建Print对话框时,通过CPrintDialog对象的m_pd成员,对对话框进行了一些初始化,这包括对拷贝份数、打印范围等的设置。在两个对话框DoModal返回IDOK后,均调用CCommonDlgView::DispPrintInfo报告打印信息。DispPrintInfo函数的代码较简单,这里就不作解释了。