5.3 对话框类的设计 完成对话框模板的设计后,就需要设计一个对话框类以实现对话框的功能。设计对话框类主要包括下面几步:
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模板联系起来。
5.3.2为对话框类加入成员变量
对话框的主要功能是输出和输入数据,例子中的登录数据对话框的任务就是输入数据。对话框需要有一组成员变量来存储数据。在对话框中,控件用来表示或输入数据,因此,存储数据的成员变量应该与控件相对应。 与控件对应的成员变量即可以是一个数据,也可以是一个控件对象,这将由具体需要来确定。例如,可以为一个编辑框控件指定一个数据变量,这样就可以很方便地取得或设置编辑框控件所代表的数据,如果想对编辑框控件进行控制,则应该为编辑框指定一个CEdit对象,通过CEdit对象,程序员可以控制控件的行为。需要指出的是,不同类的控件对应的数据变量的类型往往是不一样的,而且一个控件对应的数据变量的类型也可能有多种。表5.3说明了控件的数据变量的类型。 表5.3
利用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
读者会注意到控件IDC_INCOME居然有两个变量,一个是CString型的,一个是CListBox型的,这是完全合法的,不会引起任何冲突。之所以要加入CListBox型的变量,是因为列表框的初始化要通过CListBox对象进行。
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的作用类似。
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对话框的运行机制 在程序中运行模态对话框有两个步骤:
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类的说明中加入下面一行 为实现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对象。
当用户用鼠标点击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函数,用来判断单选按钮或检查框是否被选择,该函数的声明为 CWnd::GetDlgItem函数,用来获得指向某一控件的指针,该函数的声明为 CWnd::EnableWindow函数,该函数使窗口允许或禁止,禁止的窗口呈灰色显示,不能接收键盘和鼠标的输入。该函数的声明是 CListBox::SetCurSel和CComboBox::SetCurSel函数功能类似,用来使列表中的某一项被选中,选中的项呈高亮度显示。函数的声明是 有时,需要将GetDlgItem返回的CWnd指针强制转换成控件对象的指针,以便调用控件对象专有的成员函数对控件进行控制。例如,在程序中GetDlgItem(IDC_KIND)返回的指针被强制转换成CComboBox类型,只有这样,才能调用CComboBox::SetCurSel成员函数。 为了对控件进行查询和控制,在程序中采用了两种访问控件的方法。一种方法是直接利用ClassWizard提供的控件对象,例如m_ctrlIncome列表框对象。另一种方法是利用CWnd类提供的一组管理对话框控件的成员函数,例如程序中用到的GetDlgItem和IsDlgButtonChecked。这两种方法是在对话框内访问控件的常用方法,读者都应该掌握。表5.5列出了管理对话框控件的Cwnd成员函数。
表5.5 用来管理对话框控件的CWnd成员函数
编译并运行Register看看,现在的登录数据对话框已经比较令人满意了。 |