第10章 MFC与对话框
上一章我们为 Scribble 新增了一个【Pen】选单,其中第二个命令项【Pen Width...】准备用来提供一个对话框,让使用者设定笔的宽度。每一线条都可以拥有自己的笔宽。原预设粗笔是5个图素宽,细笔是2个图素宽。
为了这样的目的,在对话框中放个 Spin 控制组件是极佳的选择。Spin 就是那种有着上下小三角形箭头、可搭配一个文字显示器的控制组件,有点像转轮,用来选择数字最合适:
但是,Scribble Step3 只是想示范如何在MFC程序中经由选单命令项唤起一个对话框,并示范所谓的数据交换与数据检验(DDX/DDV)。所以,笔宽对话框中只选用两个小小的 Edit 控制组件而已。
本章还可以学习到如何利用对话框编辑器设计对话框的面板,并利用 ClassWizard 制作一个对话框类,定义消息处理函数,把它们与对话框「绑」在一块儿。
图10-1 【 Pen Widths】对话框
对话框编辑器
把对话框函数抛在一旁,把所有程序烦恼抛在一旁,我们先享受一下Visual C++ 整合环境中的对话框编辑器带来的对话框面板(Dialog Template)设计快感。
设计对话框面板,有两个重要的步骤,第一是从工具箱中选择控制组件(control,功能各异的小小零组件)加到对话框中,第二是填写此一控制组件的标题、ID、以及其它性质。
以下就是利用对话框编辑器设计【Pen Widths】对话框的过程。
+在Visual C++整合环境中选按【Insert/Resource】命令项,并在随后而来的【Insert Resource】对话框中,选择【resource types】为Dialog。
或是直接在Visual C++整合环境中按下工具列的【New Dialog】按钮。
Scribble.rc 文件会被打开,对话框编辑器出现,自动给我们一个空白对话框,内含两个按钮,分别是【OK】和【Cancel】。控制组件工具箱出现在画面右侧,内含许多控制组件。
为了设定控制组件的属性,必须用到【Dialog Properties】对话框。如果它最初没有出现,只要以右键选按对话框的任何地方,就会跑出一份选单,再选择其中的Properties,即会出现此对话框。按下对话框左上方的push-pin 钮(大头针)可以常保它浮现为最上层窗口。现在把对话框ID改为IDD_PEN_WIDTHS,把标题改为"Pen Widths"。
为对话框加入两个Edit控制组件,两个Static 控制组件,以及一个按钮。
右键选按新增的按钮,在Property page中把其标题改为"Default",并把ID改为IDC_DEFAULT_PEN_WIDTHS。
右键选按第一个Edit控制元件,在Property page中把ID改为IDC_THIN_PEN_WIDTH。以同样的方式把第二个 Edit 控制组件的 ID 改为 IDC_THICK_PEN_WIDTH。
右键选按第一个Static控制组件,Property page 中出现其属性,现在把文字内容改为"Thin Pen Width: "。以同样的方式把第二个Static 控制组件的文字内容改为"Thick Pen Width:"。不必在意Static控制组件的ID值,因为我们根本不可能在程序中用到Static控制组件的ID。
调整每一个控制组件的大小位置,使之美观整齐。
调整tab order。所谓tab order是使用者在操作对话框时,按下Tab键后,键盘输入焦点在各个控制组件上的巡回次序。调整方式是选按Visual C++ 整合环境中的【Layout/Tab Order】命令项,出现带有标号的对话框如下,再依你所想要的次序以鼠标点选一遍即可。
测试对话框。选按Visual C++ 整合环境中的【Layout/Test】命令项,出现运作状态下的对话框。你可以在这种状态下测试tab order和预设按钮( default button)。若欲退出,请选按【OK】或【Cancel】或按下ESC键。
注意:所谓default button,是指与<Enter>
键相通的那个按钮。
所有调整都完成之后,存盘。于是SCRIBBLE.RC 增加了下列内容(一个对话框面板):
IDD_PEN_WIDTHS DIALOG DISCARDABLE 0, 0, 203, 65
STYLE DS_MODALFRAME | WS_POPUP | WS_VISIBLE | WS_CAPTION | WS_SYSMENU
CAPTION "Pen Widths"
FONT 8, "MS Sans Serif"
BEGIN
DEFPUSHBUTTON "OK",IDOK,148,7,50,14
PUSHBUTTON "Cancel",IDCANCEL,148,24,50,14
PUSHBUTTON "Default",IDC_DEFAULT_PEN_WIDTHS,148,41,50,14
LTEXT "Thin Pen Width:",IDC_STATIC,10,12,70,10
LTEXT "Thick Pen Width:",IDC_STATIC,10,33,70,10
EDITTEXT IDC_THIN_PEN_WIDTH,86,12,40,13,ES_AUTOHSCROLL
EDITTEXT IDC_THICK_PEN_WIDTH,86,33,40,13,ES_AUTOHSCROLL
END
利用ClassWizard连接对话框与其专属类
一旦完成了对话框的外貌设计,再来就是设计其行为。我们有两件事要做:
1. 从 MFC 的 CDialog 中派生出一个类,用来负责对话框行为。
2. 利用 ClassWizard 把这个类和先前你产生的对话框资源连接起来。通常这意味着你必须声明某些函数,用以处理你感兴趣的对话框消息,并将对话框中的控制组件对应到类的成员变量上,这也就是所谓的Dialog Data eXchange(DDX)。如果你对这些变量内容有任何「确认规则」的话,ClassWizard 也允许你设定之,这就是所谓的 Dialog Data Validation(DDV)。
注意:所谓「确认规则」是指对某些特殊用途的变量进行内容查验工作。例如月份一定只可能在1~12 之间,日期一定只可能在1~31 之间,人名一定不会有数字夹杂其中,金钱数额不能夹带文字,新竹的电话号代码必须是03开头后面再加7位数...等等等。
所有动作当然都可以手工完成,然而ClassWizard 的表现非常好,让我们快速又轻松地完成这些事样。它可以为你的对话框产生一个 .H 檔,一个 .CPP 文件,内有你的对话框类、函数骨干、一个Message Map、以及一个Data Map。哎呀,我们又看到了新东西,稍后我会解释所谓的 Data Map。
回忆Scribble诞生之初,程序中有一个About对话框,寄生于 SCRIBBLE.CPP 中。AppWizard 并没有询问我们有关这个对话框的任何意见,就自作主张地放了这些代码:
CAboutDlg虽然派生自CDialog,但太简陋,不符合我们新增的这个【Pen Width】对话框所需,所以我们首先必须另为【Pen Width】对话框产生一个类,以负责其行径。步骤如下:
接续刚才完成对话框面板的动作,选按整合环境的【View/ClassWizard】命令项(或是直接在对话框面板上快按两下),进入 ClassWizard。这时候【Adding a Class】对话框会出现,并以刚才的 IDD_PEN_WIDTHS 为新资源,这是因为ClassWizard 知道你已在对话框编辑器中设计了一个对话框面板,却还未设计其对应类(整合环境就是这么便利)。好,按下【OK】。
在【Create New Class】对话框中设计新类。键入 "CPenWidthsDlg" 做为类名称。请注意类的基础类型为CDialog,因为ClassWizard 知道目前是由对话框编辑器过来:
ClassWizard 把类名称再加上 .cpp 和 .h,作为预设文件名。毫无问题,因为Windows 95 和Windows NT 都支持长文件名。如果你不喜欢,按下上图右侧的【Change】钮去改它。本例改用PENDLG.CPP和PENDLG.H 两个文件名。
按下上图的【OK】钮,于是类产生,回到ClassWizard 画面。
这样,我们就进账了两个新文件:
PENDLG.H
PENDLG.CPP
稍早我曾提过,ClassWizard 会为我们做出一个 Data Map。此一Data Map将放在DoDataExchange 函数中。目前Data Map还没有什么内容,CPenWidthsDlg 的 Message Map也是空的,因为我们还未透过ClassWizard 加料呢。
请注意,CPenWidthsDlg 构造函数会先引发基类 CDialog 的构造函数,后者会产生一个modal对话框。CDialog 构造函数的两个参数分别是对话框ID以及父窗口指针:
#0018 CPenWidthsDlg::CPenWidthsDlg(CWnd* pParent /*=NULL*/)
#0019 : CDialog(CPenWidthsDlg::IDD, pParent)
#0020 {
#0021 //{{AFX_DATA_INIT(CPenWidthsDlg)
#0022 // NOTE: the ClassWizard will add member initialization here
#0023 //}}AFX_DATA_INIT
#0024 }
ClassWizard帮我们把CPenWidthsDlg::IDD塞给第一个参数,这个值 定 义 于PENDLG.H 的 AFX_DATA 区中,其值为 IDD_PEN_WIDTHS:
#0013 // Dialog Data
#0014 //{{AFX_DATA(CPenWidthsDlg)
#0015 enum { IDD = IDD_PEN_WIDTHS };
#0016 // NOTE: the ClassWizard will add data members here
#0017 //}}AFX_DATA
也就是【Pen Widths】对话框资源的 ID:
// in SCRIBBLE.RC
IDD_PEN_WIDTHS DIALOG DISCARDABLE 0, 0, 203, 65
STYLE DS_MODALFRAME | WS_POPUP | WS_VISIBLE | WS_CAPTION | WS_SYSMENU
CAPTION "Pen Widths"
FONT 8, "MS Sans Serif"
BEGIN
DEFPUSHBUTTON "OK",IDOK,148,7,50,14
PUSHBUTTON "Cancel",IDCANCEL,148,24,50,14
PUSHBUTTON "Default",IDC_DEFAULT_PEN_WIDTHS,148,41,50,14
LTEXT "Thin Pen Width:",IDC_STATIC,10,12,70,10
LTEXT "Thick Pen Width:",IDC_STATIC,10,33,70,10
EDITTEXT IDC_THIN_PEN_WIDTH,86,12,40,13,ES_AUTOHSCROLL
EDITTEXT IDC_THICK_PEN_WIDTH,86,33,40,13,ES_AUTOHSCROLL
END
对话框类CPenWidthsDlg 因此才有办法取得「RC 檔中的对话框资源」。
对话框的消息处理函数
CDialog本就定义有两个按钮【OK】和【Cancel】,【Pen Widths】对话框又新增一个【Default】钮,当使用者按下此钮时,粗笔与细笔都必须回复为预设宽度(分别是5个图素和2个图素)。那么,我们显然有两件工作要完成:
1. 在CPenWidthsDlg 中增加两个变量,分别代表粗笔与细笔的宽度。
2. 在CPenWidthsDlg 中增加一个函数,负责【Default】钮被按下后的动作。
以下是ClassWizard 的操作步骤(增加一个函数):
进入ClassWizard,选择【Message Maps】附页,再选择【Class name】清单中的CPenWidthsDlg。
左侧的【 Object IDs】清单列出对话框中各个控制组件的 ID。请选择其中的IDC_DEFAULT_PEN_WIDTHS(代表【Default】钮)。
在右侧的【Messages】中选择 BN_CLICKED。这和我们在前两章的经验不同,如今我们处理的是控制组件,它所产生的消息是特别的一类,称为 Notification消息,这种消息是控制组件用来通知其父窗口(通常是个对话框)某些状况发生了,例如BN_CLICKED 表示按钮被按下。至于不同的 Notification 所代表的意义,画面最下方的"Description" 会显示出来。
按下【Add Function】钮,接受预设的 OnDefaultPenWidths 函数(也可以改名):
现在,【Member Functions】清单中出现了新函数,以及它所对映之控制组件与Notification 消息。
按下【Edit Code】钮,光标落在OnDefaultPenWidths 函数身上,我们看到以下内容:
上述动作对原始代码造成的影响是:
// in PENDLG.H
class CPenWidthsDlg : public CDialog
{
protected:
afx_msg void OnDefaultPenWidths();
...
};
// in PENDLG.CPP
BEGIN_MESSAGE_MAP(CPenWidthsDlg, CDialog)
ON_BN_CLICKED(IDC_DEFAULT_PEN_WIDTHS, OnDefaultPenWidths)
END_MESSAGE_MAP()
void CPenWidthsDlg::OnDefaultPenWidths()
{
// TODO : Add your control notification handler here
}
MFC 中各式各样的MAP
如果你以为MFC中只有Message Map和Data Map,那你就错了。另外还有一个Dispatch Map,使用于OLE Automation,下面是其形式:
DECLARE_DISPATCH_MAP() // .H 文件中的宏,声明 Dispatch Map。
BEGIN_DISPATCH_MAP(CClikDoc, CDocument) // .CPP 檔中的 Dispatch Map
//{{AFX_DISPATCH_MAP(CClikDoc)
DISP_PROPERTY(CClikDoc, "text", m_str, VT_BSTR)
DISP_PROPERTY_EX(CClikDoc, "x", GetX, SetX, VT_I2)
DISP_PROPERTY_EX(CClikDoc, "y", GetY, SetY, VT_I2)
//}}AFX_DISPATCH_MAP
END_DISPATCH_MAP()
此外还有Event Map,使用于OLE Custom Control(也就是OCX),下面是其形式:
DECLARE_EVENT_MAP() // .H 文件中的宏,声明 Event Map。
BEGIN_EVENT_MAP(CSmileCtrl, COleControl) // .CPP 檔中的 Event Map
//{{AFX_EVENT_MAP(CSmileCtrl)
EVENT_CUSTOM("Inside", FireInside, VTS_I2 VTS_I2)
EVENT_STOCK_CLICK()
//}}AFX_EVENT_MAP
END_EVENT_MAP()
至于Message Map,我想你一定已经很熟悉了:
DECLARE_MESSAGE_MAP()// .H 文件中的宏,声明Message Map。
BEGIN_MESSAGE_MAP(CScribDoc, CDocument) // .CPP 檔中的 Message Map
//{{AFX_MSG_MAP(CScribDoc)
ON_COMMAND(ID_EDIT_CLEAR_ALL, OnEditClearAll)
ON_COMMAND(ID_PEN_THICK_OR_THIN, OnPenThickOrThin)
ON_COMMAND(ID_PEN_WIDTHS, OnPenWidths)
//}}AFX_MSG_MAP
END_MESSAGE_MAP()
MFC所谓的Map,其实就是一种类似表格的东西,它的背后是什么?可能是一个巨大的数据结构(例如Message Map)。最和其它Map形式不同的,就属 Data Map了,它的形式是:
//{{AFX_DATA_MAP(CPenWidthsDlg) // .CPP 檔中的 Data Map
DDX_Text(pDX, IDC_THIN_PEN_WIDTH, m_nThinWidth);
DDV_MinMaxInt(pDX, m_nThinWidth, 1, 20);
DDX_Text(pDX, IDC_THICK_PEN_WIDTH, m_nThickWidth);
DDV_MinMaxInt(pDX, m_nThickWidth, 1, 20);
//}}AFX_DATA_MAP
针对同一个数据目标(成员变量),Data Map 之中每组有两笔记录,一笔负责DDX,一笔负责DDV。
对话框数据交换与查核(DDX & DDV)
在解释 DDX/DDV 的来龙去脉之前,我想先描述一下 SDK 程序处理对话框数据的作法。如果你设计一个对话框如下图:
当【OK】钮被按下,程序应该一一取得按钮状态以及Edit内容:
char _OpenName[128];
GetDlgItemText(hwndDlg, IDC_EDIT, _OpenName, 128);
If (IsDlgButtonChecked(hDlg,IDC_1))
...;
If (IsDlgButtonChecked(hDlg,IDC_2))
...;
If (IsDlgButtonChecked(hDlg,IDC_3))
...;
If (IsDlgButtonChecked(hDlg,IDC_4))
...;
// hDlg 代表对话框的窗口 handle
虽然Windows 95和Windows NT有所谓的通用型对话框(Common Dialog,第6章末尾曾介绍过),某些个标准对话框的设计因而非常简单,但非标准的对话框还是得像上面那样自己动手。
MFC的方式就简单多了。它提供的DDX(X 表示 eXchange),允许程序员事先设定控制组件与变量的对应关系。我们不但可以令控制组件的内容一有改变就自动传送到变量去,也可以藉MFC提供的DDV(V 表示Validation)设定字段的合理范围。如果使用者在字段上键入超出合理范围的数字,就会在按下【OK】后出现类似以下的画面:
数据的查核(Data Validation)其实是一件琐碎又耗人力的事情,各式各样的数据都应该要检查其合理范围,程序才算面面俱到。例如日期字段绝不能允许12 以上的月份以及31 以上的日子(如果程序还能自动检查2月份只有 28 天而遇闰年有 29 天那就更棒了);金额字段里绝不能允许文字出现,电话号代码字段一定只有9位(至少台湾目前是如此)。为了解决这些琐碎又累人的工作,市售有一些链接库,专门做数据查核工作。
然而不要对 MFC 的 DDV 能力期望过高,稍后你就会看到,它只能满足最低层次的要求而已。就 DDV 而言,Borland 的OWL表现较佳。
现在我打算以两个成员变量映射到对话框上的两个Edit字段。我希望当使用者按下【OK】钮,第一个Edit字段的内容自动储存到m_nThinWidth变量中,第二个Edit栏位的内容自动储存到m_nThickWidth 变量中:
下面是 ClassWizard 的操作步骤(为对话框类增加两个成员变量,并设定 DDX / DDV):
进入ClassWizard,选择【Member Variables】附页,再选择 CPenWidthsDlg。对话框中央部分有一大块局部用来显示控制组件与变量间的对映关系(见下一页图)。
选择IDC_THIN_PEN_WIDTH,按下【Add Variable...】钮,出现对话框如下。
键入变量名称为m_nThinWidth。
选择变量型别为int 。
按下【 OK 】键 , 于是ClassWizard为CPenWidthsDlg 增加了一个变量m_nThinWidth。
在ClassWizard 对话框最下方(见下一页图)填入变量的数值范围,以为 DDV之用。
重复前述步骤,为IDC_THICK_PEN_WIDTH 也设定一个对应变量,范围也是1~20。
上述动作影响我们的程序代码如下:
class CPenWidthsDlg : public CDialog
{
// Dialog Data
//{{AFX_DATA(CPenWidthsDlg)
enum { IDD = IDD_PEN_WIDTHS };
int m_nThinWidth;
int m_nThickWidth;
//}}AFX_DATA
...
CPenWidthsDlg::CPenWidthsDlg(CWnd* pParent /*=NULL*/)
: CDialog(CPenWidthsDlg::IDD, pParent)
{
m_nThickWidth = 0;
m_nThinWidth = 0;
...
}
void CPenWidthsDlg::DoDataExchange(CDataExchange* pDX)
{
CDialog::DoDataExchange(pDX);
//{{AFX_DATA_MAP(CPenWidthsDlg)
DDX_Text(pDX, IDC_THIN_PEN_WIDTH, m_nThinWidth);
DDV_MinMaxInt(pDX, m_nThinWidth, 1, 20);
DDX_Text(pDX, IDC_THICK_PEN_WIDTH, m_nThickWidth);
DDV_MinMaxInt(pDX, m_nThickWidth, 1, 20);
//}}AFX_DATA_MAP
}
只要数据「有必要」在成员变量与控制组件之间搬移,Framework 就会自动调用DoDataExchange。我所说的「有必要」是指,对话框初次显示在屏幕上,或是使用者按下【OK】离开对话框等等。CPenWidthsDlg::DoDataExchange 由一组一组的DDX/DDV函数完成之。先做DDX,然后做DDV,这是游戏规则。如果你纯粹借助ClassWizard,就不必在意此事,如果你要自己动手完成,就得遵循规则。
该是完成上一节的OnDefaultPenWidths 的时候了。当【Default】钮被按下,Framework会调用OnDefaultPenWidths,我们应该在此设定粗笔细笔两种宽度的默认值:
void CPenWidthsDlg::OnDefaultPenWidths()
{
m_nThinWidth = 2;
m_nThickWidth = 5;
UpdateData(FALSE); // causes DoDataExchange()
// bSave=FALSE means don't save from screen,
// rather, write to screen
}
MFC 中各式各样的DDx_函数
如果你以为MFC对于对话框的照顾,只有DDX和DDV,那你就又错了,另外还有一个DDP,使用于OLE Custom Control(也就是OCX)的Property page 中,下面是它的形式:
//{{AFX_DATA_MAP(CSmilePropPage)
DDP_Text(pDX, IDC_CAPTION, m_caption, _T("Caption") );
DDX_Text(pDX, IDC_CAPTION, m_caption);
DDP_Check(pDX, IDC_SAD, m_sad, _T("sad") );
DDX_Check(pDX, IDC_SAD, m_sad);
//}}AFX_DATA_MAP
什么是Property page?这是最新流行(Microsoft 强力推销?)的界面。这种界面用来解决过于拥挤的对话框。ClassWizard 就有四个 Property page,我们又称为tag(附页)。拥有 property page 的对话框称为 property sheet,也就是 tagged dialog(带有附页的对话框)。
如何唤起对话框
【Pen Widths】对话框是一个所谓的 Modal 对话框,意思是除非它关闭(结束),否则它会紧抓住这个程序的控制权,但不影响其它程序。相对于Modal对话框,有一种Modeless 对话框就不会影响程序其它动作的进行;通常你在文字处理软件中看到的文字搜寻对话框就是Modeless 对话框。
过去, MFC有两个类,分别负责Modal对话框和Modeless对话框,它们是CModalDialog和CDialog。如今已经合并为一,就是CDialog。不过为了回溯相容,MFC 有这么一个定义:
#define CModalDialog Cdialog
要做出Modal对话框,只要调用CDialog::DoMoal 即可。
我们希望Step3 的命令项【Pen/Pen Widths】被按下时,【Pen Widths】对话框能够执行起来。要唤起此一对话框,得做到两件事情:
1. 产生一个 CPenWidthsDlg 对象,负责管理对话框。
2. 显示对话框窗口。这很简单,调用DoMoal 即可办到。
为了把命令消息连接上CPenWidthsDlg,我们再次使用ClassWizard,这一次要为CScribbleDoc 加上一个命令处理例程。为什么选择在 CScribbleDoc 而不是其它类中处理此一命令呢?因为不论是粗笔或细笔,乃至于目前正使用的笔,其宽度都被记录在CScribbleDoc 中成为它的一个成员变量:
// in SCRIBDOC.H
class CScribbleDoc : public CDocument
{
protected:
UINT m_nPenWidth; //current user-selected pen width
UINT m_nThinWidth;
UINT m_nThickWidth;
...
}
所以由CScribDoc负责唤起对话框,接受笔宽设定,是很合情合理的事。
如果命令消息处理例程名为OnPenWidths,我们希望在这个函数中先唤起对话框,由对话框取得粗笔和细笔的宽度,然后再把这两个值设定给 CScribbleDoc 中的两个对应变量。下面是设计步骤。
执行ClassWizard,选择【Message Map】附页,并选择CScribbleDoc。
在【Object IDs】清单中选择ID_PEN_WIDTHS。
在【Messages】清单中选择COMMAND。
按下【Add Function】钮并接受ClassWizard 给予的函数名称 OnPenWidths。
按下【Edit Code】钮,游标落在 OnPenWidths 函数内,键入以下内容:
// SCRIBDOC.CPP #include "pendlg.h" ... void CScribbleDoc::OnPenWidths() { CPenWidthsDlg dlg; // Initialize dialog data dlg.m_nThinWidth = m_nThinWidth; dlg.m_nThickWidth = m_nThickWidth; // Invoke the dialog box if (dlg.DoModal() == IDOK) { // retrieve the dialog data m_nThinWidth = dlg.m_nThinWidth; m_nThickWidth = dlg.m_nThickWidth; //Update the pen that is used by views when drawing new strokes, //to reflect the new pen width definitions for "thick" and "thin". ReplacePen(); } }
现在,Scribble Step3 全部完成,制作并测试之。
本章回顾
上一章我们为Scribble加上三个新的选单命令项。其中一个命令项【Pen/Pen Widths...】将引发对话框,这个目标在本章实现。
制作对话框,我们需要为此对话框设计面板(Dialog Template),这可藉 Visual C++整合环境之对话框编辑器之助完成。我们还需要一个派生自 CDialog 的类(本例为CPenWidthsDlg)。ClassWizard 可以帮助我们新增类,并增加该类的成员变量,以及设定对话框之 DDX/DDV 。以上都是透过 ClassWizard 以鼠标点点选选而完成,过程中不需要写任何一进程序代码。
所谓DDX是让我们把对话框类中的成员变量与对话框中的控制组件产生关联,于是当对话框结束时,控制组件的内容会自动传输到这些成员变量上。
所谓DDV是允许我们设定对话框控制组件的内容类型以及数据(数值) 范围。
对话框的写作,在MFC程序设计中轻松无比。你可以尝试练习一个比较复杂的对话框。