上一页 下一页 返回

5.5 标签式对话框

  在设计较为复杂的对话框时,常常会遇到这种情况:对某一事物的设置或选项需要用到大量的控件,以至于一个对话框放不下,而这些控件描述的是类似的属性,不能分开。用普通的对话框技术,这一问题很难解决。 

  MFC提供了对标签式对话框的支持,可以很好的解决上述问题。标签式对话框实际上是一个包含了多个子对话框的对话框,这些子对话框通常被称为页(Page)。每次只有一个页是可见的,在对话框的顶端有一行标签,用户通过单击这些标签可切换到不同的页。显然,标签式对话框可以容纳大量的控件。在象Word和Developer Studio这样复杂的软件中,用户会接触到较多的标签式对话框,一个典型的标签式对话框如图5.10所示。 

 T5_10.tif (119141 bytes) 

 图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所示。 

 T5_11.tif (172776 bytes) 

 图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,试一试自己设计的标签式对话框。