登录  
 加关注
   显示下一条  |  关闭
温馨提示!由于新浪微博认证机制调整,您的新浪微博帐号绑定已过期,请重新绑定!立即重新绑定新浪微博》  |  关闭

sunwenhua168的博客

海纳百川,有容乃大

 
 
 

日志

 
 

MFC编程——对话框  

2010-10-30 16:00:33|  分类: 默认分类 |  标签: |举报 |字号 订阅

  下载LOFTER 我的照片书  |

正文
MFC编程——对话框
二、工具条的编程技术
  1、命令处理
  要使菜单和工具条执行命令,光为它们指定命令ID是不行的,必须为每个命令ID定义命令处理函数。如果不为命令定义命令处理函数或下面将要提到的命令更新处理函数,则框架将自动使该命令对应的菜单项和按钮禁止(灰化)。
  利用ClassWizard在CmainFrame中,加入OnRecordStart和OnRecordStop消息处理函数。
  现在要在这两个命令处理函数中插入相应的源代码以实现其功能。我们只是让这两个函数发出一个声音,象征性地表示功能的执行,具体代码如清单4.2所示。
  清单4.2 OnRecordStart和OnRecordStop函数
  void CMainFrame::OnRecordStart()
  {
  // TODO: Add your command handler code here
  MessageBeep((UINT)(-1));
  }
  void CMainFrame::OnRecordStop()
  {
  // TODO: Add your command handler code here
      MessageBeep((UINT)(-1));
  }
  2、命令更新
  虽然Start和Stop命令可以执行了,但是还有一个不足之处。在没有开始录音之前,Stop命令应该是禁止的,也即对应的菜单项和按钮应是禁止的,这是因为此时没有必要执行该命令。录音开始后,Stop命令应该允许,而Start命令则应变为禁止。我们可以利用MFC的命令更新机制实现此逻辑功能。
  在菜单下拉之前,或在工具条按钮处在空闲循环期间,MFC会发一个更新命令,这将导致命令更新处理函数的调用。命令更新处理函数可以根据情况,使用户接口对象(主要指菜单项和工具条按钮)允许或禁止。定义命令更新处理函数的方法如下:
  按Ctrl+W键进入ClassWizard。
  在Class name栏中选择CMainFrame,在Object IDs栏中选择ID_RECORD_START,在Messages栏中双击UPDATE_COMMAND_UI项,则ClassWizard会弹出一个对话框询问命令更新处理函数的名字,使用其提供的函数名即可。按OK按钮后,函数OnUpdateRecordStart就被加入到了Member
functions栏中。
  仿照步2,为ID_RECORD_STOP定义一个命令更新处理函数。
  按OK按钮关闭ClassWizard对话框。这时读者会发现CMainFrame类多了两个成员函数,OnUpdateRecordStart和OnUpdateRecordStop。
 命令更新处理函数有一个参数是CCmdUI类的指针,通过调用CCmdUI类的成员函数Enable(TRUE)或Enable(FALSE)可以使用户接口对象允许或禁止。需要给CMainFrame加一个布尔型成员变量以表明是否正在录音,这样命令更新处理函数可根据这个变量来决定用户接口对象的状态。请读者在CMainFrame类内加入下面一行代码:
  BOOL m_bWorking;
  接下来请读者按清单4.3进行修改。
  清单4.3 命令更新处理
  CMainFrame::CMainFrame()
  {
  // TODO: add member initialization code here
  m_bWorking=FALSE;
  }
  void CMainFrame::OnRecordStart()
  {
  // TODO: Add your command handler code here
  MessageBeep((UINT)(-1));
  m_bWorking=TRUE;
  }
  void CMainFrame::OnRecordStop()
  {
  // TODO: Add your command handler code here
  MessageBeep((UINT)(-1));
  m_bWorking=FALSE;
  }
  void CMainFrame::OnUpdateRecordStart(CCmdUI* pCmdUI)
  {
  // TODO: Add your command update UI handler code here
  pCmdUI->Enable(!m_bWorking);
  }
  void CMainFrame::OnUpdateRecordStop(CCmdUI* pCmdUI)
  {
  // TODO: Add your command update UI handler code here
  pCmdUI->Enable(m_bWorking) ;
  }
  m_bWorking的初值应是FALSE,对它的初始化工作在CMainFrame的构造函数中完成。m_bWorking的值在处理Start和Stop命令时会被更新以反映当前的状态。两个命令更新处理函数都调用了CCmdUI::Enable,该函数根据m_bWorking的值来更新命令接口对象。
  编译并运行Record,现在Start和Stop命令的逻辑功能已经实现了。
  3、按钮风格
  在Record程序中,用户可以选择两种采样频率来录音。用户接口对象应该能反映出当前的采样频率。普通的工具条按钮在按下后会立刻弹起来,我们希望Record程序的频率选择按钮具有单选按钮的风格,即当用户选择了一个采样频率时,该采样频率对应的按钮一直处于按下的状态,而另一个频率选择按钮应处于弹起状态。
  我们可以利用CCmdUI::SetCheck函数来实现这一功能,在命令更新函数中调用CCmdUI::SetCheck(TRUE)或CCmdUI::SetCheck(FALSE)可将用户接口对象设定为选中或不选中状态,当一个用户接口对象被选中时,相应的工具按钮会处于按下的状态,并且相应的菜单项的前面会加上一个选中标记。这里需要给CMainFrame类加一个布尔型成员变量以表明当前的采样频率。请读者在CMainFrame类内加入下面一行代码:BOOL
m_bHighQuality;
  接下来请读者按清单4.4进行修改。
  清单4.4
  CMainFrame::CMainFrame()
  {
  m_bWorking=FALSE;
  m_bHighQuality=TRUE;
  }
  void CMainFrame::OnHighQuality()
  {
  m_bHighQuality=TRUE;
  }
  void CMainFrame::OnLowQuality()
  {
  m_bHighQuality=FALSE;
  }
  void CMainFrame::OnUpdateHighQuality(CCmdUI* pCmdUI)
  {
  pCmdUI->SetCheck(m_bHighQuality);
  }
  void CMainFrame::OnUpdateLowQuality(CCmdUI* pCmdUI)
  {
  pCmdUI->SetCheck(!m_bHighQuality);
  }
  m_bHighQuality的初值是TRUE,即缺省时是高频采样,对它的初始化工作在CMainFrame的构造函数中完成。m_bHighQuality的值在处理High
quality和Low quality命令时会被更新以反映当前的状态。两个命令更新处理函数都调用了CCmdUI::SetCheck,该函数根据m_bHighQuality的值来更新命令接口对象,从而使工具条按钮具有了单选按钮的风格。
  4、工具条的隐藏/显示
  读者可能已经试过了Record程序的View菜单的功能。通过该菜单用户可以隐藏/显示工具条和状态栏,这个功能是由AppWizard自动实现的。由于第二个工具条是手工建立的,因此它不会自动具备隐藏/显示功能。但我们可以通过编程来实现第二个工具条的隐藏/显示:
  打开IDR_MAINFRAME菜单资源
  在View菜单中加入一个名为Toolbar1的菜单项,指定其ID为ID_VIEW_TOOLBAR1,并在Prompt栏中输入Show or hide the toolbar1\nToggle ToolBar1。
  按Ctrl+W键进入ClassWizard。在Class name栏中选择CMainFrame,在Object IDs栏中选择ID_VIEW_TOOLBAR1,并为该命令ID定义命令处理函数OnViewToolbar1和命令更新处理函数OnUpdateViewToolbar1。
  按清单4.5修改程序。
  清单4.5 显示/隐藏工具条
  void CMainFrame::OnViewToolbar1()
  {
  m_wndToolBar1.ShowWindow(m_wndToolBar1.IsWindowVisible()?SW_HIDE:SW_SHOW);
  RecalcLayout();
  }
  void CMainFrame::OnUpdateViewToolbar1(CCmdUI* pCmdUI)
  {
  pCmdUI->SetCheck(m_wndToolBar1.IsWindowVisible());
  }
  调用CWnd::ShowWindow(SW_SHOW)或CWnd::ShowWindow(SW_HIDE)可以显示或隐藏窗口。由于工具条也是窗口,CToolBar是CWnd类的继承类,故该函数也是CToolBar的成员。在命令处理函数OnViewToolbar1中,我们调用CToolBar::ShowWindow来显示/隐藏工具条,在调用时会利用CWnd::IsWindowVisible函数作出判断,如果工具条是可见的,就传给ShowWindow函数SW_HIDE参数以隐藏工具条,否则,就传SW_SHOW参数显示工具条。接着要调用CMainFrame::RecalcLayout以重新调整主框架窗口的布局。
  命令更新处理函数OnUpdateViewToolbar1会根据工具条是否可见使View->Toolbar1菜单项选中或不选中。
  三、状态栏的设计与实现
  状态栏实际上是个窗口,一般分为几个窗格,每个窗格显示不同的信息。AppWizard会为应用程序自动创建一个状态栏,该状态栏包括几个窗格,分别用来显示状态栏提示和CAPS
LOCK、NUM LOCK 、SCROLL LOCK键的状态。在MFC中,状态栏的功能由CStatusBar类实现。创建一个状态栏需要以下几个步骤:
  构建一个CStatusBar对象。
  调用CStatusBar::Create创建状态栏窗口。
  调用CStatusBar::SetIndicators函数分配窗格,并将状态栏的每一个窗格与一个字符串ID相联系。
  相应的代码读者可以在Record工程的CMainFrame::OnCreate成员函数中找到。
  清单4.6 创建状态栏
  …
  if (!m_wndStatusBar.Create(this) ||
  !m_wndStatusBar.SetIndicators(indicators, sizeof(indicators)/sizeof(UINT)))
  {
  TRACE0("Failed to create status bar\n");
  return -1; // fail to create
  }
  …
  SetIndicators函数的第一个参数indicators是一个ID数组,在CMainFrame类所在的CPP文件的开头部分可以找到该数组,如清单4.7所示。
  清单4.7 ID数组
  static UINT indicators[] =
  {
  ID_SEPARATOR, // status line indicator
  ID_INDICATOR_CAPS,
  ID_INDICATOR_NUM,
  ID_INDICATOR_SCRL,
  };
  indicator数组提供了状态栏窗格的分配信息,它的第一项一般为ID_SEPARATOR,该ID对应的窗格用来显示命令提示信息,后三项都是字符串ID,读者可以在String Table字符串资源中找到这三个字符串分别是CAP、NUM和SCRL。它们对应的三个窗格用来显示键盘的状态。
  现在让我们来给状态栏再加一个时间窗格,它将用来显示系统时间。显示的格式是hh:mm:ss,即时:分:秒。
  首先在indicators数组的ID_SEPARATOR项之后插入一个名为ID_INDICATOR_CLOCK的ID。然后找到并双击名为String Table的字符串资源,打开字符串资源编辑窗口。接着在编辑窗口内按Insert键以插入一个新的字符串,请指定字符串的ID为ID_INDICATOR_CLOCK,内容为00:00:00。状态栏将根据字符串的长度来确定相应窗格的缺省宽度,所以指定为00:00:00就为时间的显示预留了空间。
  提示:上述方法不能动态改变窗格宽度,并且有时是不精确的,当系统字体改变时,这种做法可能会导致一些误差。考虑到该方法简单直观,且一般情况下问题不大,故本文用它来举例。
  时间窗格显示的时间必须每隔一秒钟更新一次。更新时间窗格的正文可调用CStatusBar::SetPaneText函数,要定时更新,则应利用WM_TIMER消息。在Windows中用户可以安装一个或多个计时器,计时器每隔一定的时间间隔就会自动发出一个WM_TIMER消息,而这个时间间隔可由用户指定。MFC的Window类提供了WM_TIMER消息处理函数OnTimer,我们应在该函数内进行更新时间窗格的工作。
  请读者利用ClassWizard给CMainFrame类加入WM_TIMER的消息处理函数OnTimer和WM_CLOSE消息的处理函数OnClose。CMainFrame::OnClose函数是在关闭主框架窗口是被调用的,程序可以在该函数中做一些清除工作。接下来请按清单4.8修改程序。
  清单4.8 CMainFrame类的部分代码
  int CMainFrame::OnCreate(LPCREATESTRUCT lpCreateStruct)
  {
  …
  SetTimer(1,1000,NULL);
  return 0;
  }
  void CMainFrame::OnTimer(UINT nIDEvent)
  {
    CTime time;
    CFrameWnd::OnTimer(nIDEvent);
    time=CTime::GetCurrentTime();
    CString s=time.Format("%H:%M:%S");
    m_wndStatusBar.SetPaneText(m_wndStatusBar.CommandToIndex(ID_INDICATOR_CLOCK),s);
    CFrameWnd::OnTimer(nIDEvent);
  }
  void CMainFrame::OnClose()
  {
  KillTimer(1);
  CFrameWnd::OnClose();
  }
  在CMainFrame::OnCreate函数内调用了CWnd::SetTimer以安装一个计时器,SetTimer的第一个参数指定计时器ID为1,第二个参数则规定了计时器的时间间隔为1000毫秒即1秒。这样,每隔1秒OnTimer函数就会被调用一次。
  在OnTimer函数中,首先构建了一个CTime对象,接着调用CTime的静态成员函数GetCurrentTime以获得当前的系统时间,然后利用CTime::Format函数返回一个按时:分:秒的格式表示的字符串,最后调用CStatusBar::SetPaneText来更新时间窗格显示的正文。SetPaneText的第一个参数是窗格的索引,对于某一个窗格ID,可调用CStatusBar::CommandToIndex来获得索引。
  在撤销主框架窗口时应关闭计时器,因此在CMainFrame::OnClose函数内调用了KillTimer函数。
  现在让我们来看一下CMainFrame的消息映射,在CMainFrame类所在CPP文件的开始部分可以找到该类的消息映射,如清单4.9所示。
  清单4.9
  BEGIN_MESSAGE_MAP(CMainFrame, CFrameWnd)
  //{{AFX_MSG_MAP(CMainFrame)
  ON_WM_CREATE()
  ON_COMMAND(ID_RECORD_STOP, OnRecordStop)
  ON_COMMAND(ID_RECORD_START, OnRecordStart)
  ON_UPDATE_COMMAND_UI(ID_RECORD_START, OnUpdateRecordStart)
  ON_UPDATE_COMMAND_UI(ID_RECORD_STOP, OnUpdateRecordStop)
  ON_COMMAND(ID_HIGH_QUALITY, OnHighQuality)
  ON_COMMAND(ID_LOW_QUALITY, OnLowQuality)
  ON_UPDATE_COMMAND_UI(ID_HIGH_QUALITY, OnUpdateHighQuality)
  ON_UPDATE_COMMAND_UI(ID_LOW_QUALITY, OnUpdateLowQuality)
  ON_COMMAND(ID_VIEW_TOOLBAR1, OnViewToolbar1)
  ON_UPDATE_COMMAND_UI(ID_VIEW_TOOLBAR1, OnUpdateViewToolbar1)
  ON_WM_TIMER()
  ON_WM_CLOSE()
  //}}AFX_MSG_MAP
  END_MESSAGE_MAP()
  读者可以看到,在消息映射表中,ClassWizard为消息处理函数和命令处理函数自动加入了消息映射。自动加入的部分呈灰色显示,位于注释行//{{AFX_MSG_MAP和//}}AFX_MSG_MAP之间。命令处理函数由ON_COMMAND宏来映射,命令更新处理函数由ON_UPDATE_COMMAND_UI,而WM_消息的处理函数由ON_WM_消息宏来映射。
  提示:今后只要看到//{{AFX_...的注释对,则说明它们之间的部分是ClassWizard自动加入的,这部分呈灰色显示。请不要随便修改它们,更不能把手工加入的部分放在//{{AFX_...注释对内,否则有可能导致ClassWizard出错。
  四、小 结
  本章主要向读者介绍了工具条和状态栏的一些实用技术。要点如下:
  在MFC中,创建一个窗口一般分两步:
  1.构建一个窗口对象。构建的方法是定义一个对象或用new操作符动态创建之。
  2.调用窗口类的Create成员函数。该函数把实际的窗口作出来,并将其HWND保存在窗口的公共数据成员m_hWnd中。
  创建工具条和状态栏的工作是在CMainFrame::OnCreate函数中完成的,OnCreate函数是在创建窗口时被调用的,这时窗口的创建已部分完成,窗口对象的HWND句柄也已有效,但窗口还是不可见的。因此一般在OnCreate函数中作一些诸如创建子窗口的初始化工作。
  afx_msg前缀保证了正确版本的消息处理函数被调用。
  工具条有两个要素:工具条资源和工具条类CToolBar。若用户只需要一个工具条,可利用AppWizard自动生成,然后再修改之。若需要多个工具条,则必须手工创建。
  如果不为命令定义命令处理函数或命令更新处理函数,则框架将自动使该命令对应的用户接口对象(主要指菜单项和按钮)禁止。利用ClassWizard可以十分方便的加入命令处理函数和命令更新处理函数。
  在菜单下拉之前,或在工具条按钮处在空闲循环期间,MFC会发一个更新命令,这将导致命令更新处理函数的调用。命令更新处理函数利用CCmdUI类来更新用户接口对象。调用CCmdUI::Enable可使用户接口对象允许或禁止,调用CCmdUI::SetCheck可使用户接口对象选中或不选中。调用CWnd::ShowWindow可以隐藏/显示一个窗口。
  要在状态栏中插入新的窗格,需要在indicator数组中插入新的字符串ID。而状态栏将根据这个字符串的长度来确定新窗格的缺省宽度。
  调用CStatusBar:: SetPaneText可更新状态栏窗格显示的正文。
  对话框
  对话框是一种用户界面,它的主要功能是输出信息和接收用户的输入。对话框与控件是密不可分的,在每个对话框内一般都有一些控件,对话框依靠这些控件与用户进行交互.一个典型的对话框例子是选择了File-Open命令后弹出的文件对话框.
  一、对话框和控件的基本概念
  1、对话框的基本概念
  对话框(Dialog)实际上是一个窗口.在MFC中,对话框的功能被封装在了CDialog类中,CDialog类是CWnd类的派生类.
  对话框分为模态对话框和非模态对话框两种。模态对话框垄断了用户的输入,当一个模态对话框打开时,用户只能与该对话框进行交互,而其它用户界面对象收不到输入信息。非模态对话框的典型例子是Windows95提供的写字板程序中的搜索对话框,搜索对话框不垄断用户的输入,打开搜索对话框后,仍可与其它用户界面对象进行交互,用户可以一边搜索,一边修改文章,这样就大大方便了使用。
  从MFC编程的角度来看,一个对话框由两部分组成:
  对话框模板资源:对话框模板用于指定对话框的控件及其分布,Windows根据对话框模板来创建并显示对话框.
  对话框类:对话框类用来实现对话框的功能,由于对话框行使的功能各不相同,因此一般需要从CDialog类派生一个新类,以完成特定的功能.
  相应地,对话框的设计包括对话框模板的设计和对话框类的设计两个主要方面.
  与对话框有关的消息主要包括WM_INITDIALOG消息和控件通知消息。在对话框创建时,会收到WM_INITDIALOG消息,对话框对该消息的处理函数是OnInitDialog。
  OnInitDialog的主要用处是初始化对话框。对话框的控件会向对话框发送控件通知消息,以表明控件的状态发生了变化。
  2、控件的基本概念
  图5.1对话框中的控件
  控件(Control)是独立的小部件,在对话框与用户的交互过程中,控件担任着主要角色.控件的种类较多,图5.1显示了对话框中的一些基本的控件.MFC的控件类封装了控件的功能,下表介绍了一些常用的控件及其对应的控件类。
  控件功能对应控件类
  静态正文(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类的派生类。控件通常是作为对话框的子窗口而创建的,控件也可以出现在视窗口,工具条和状态条中。
  二、对话框模板的设计
  利用Developer Studio提供的可视化设计工具,用户可以方便地设计对话框模板.
  请读者按前面章节介绍的方法利用AppWizard建立一个名为Register的MFC应用程序,并在进入MFC AppWizard对话框后按下面几步操作:
  1.在第1步中选中Single document以建立一个单文档应用程序.
  2.在第4步中使Docking toolbar项不选中,这样AppWizard就不会创建工具条.
  3.在第6步中先选择CRegisterView,然后在Base class栏中选择CEditView,这样CRegisterView将是CEditView的继承类,从而使视图具有了编辑功能.
  编译并运行Register,读者会发现Register居然是个编辑器,它可以打开、编辑和保存文本文件。当然,Register的目的不仅仅是个编辑器。假设要对某一地区的就业情况进行调查,我们希望Register程序能够登录就业情况数据并将数据存储起来。
  要登录数据,用对话框是再合适不过了。一个典型的就业情况登录对话框如图5.1所示,本节的任务就是设计如图5.1的中文对话框模板。
  切换至资源视图,选择Insert-Resource命令,并在Insert Resource对话框中双击Dialog项。完成后在资源视图中会出现一个名为IDD_DIALOG1的新的对话框模板资源。双击IDD_DIALOG1,则会打开该对话框模板的编辑窗口。缺省的对话框模板有OK和Cancel两个按钮,在窗口的旁边有一个控件面板,在控件面板上用鼠标选择一个控件,然后在对话框中点击,则相应的控件就被放置到了对话框模板中。
  选中控件或对话框后按回车键,则会弹出一个属性对话框,属性对话框用来设置控件或对话框的各种属性。属性对话框是标签式对话框,第一页是常规属性(General)。
  在控件属性对话框的常规属性中,有一些控件共同的属性:
  ID属性。用于指定控件的标识符,Windows依靠ID来区分不同的控件。
  Caption(标题)属性。静态正文、组框、按钮、检查框、单选按钮等控件可以显示标题,用来对控件进行文字说明。控件标题中的字符&使紧跟其后的字符有下划线,按Alt+下划线将启动该控件。若控件是一个单选按钮,则Alt+下划线字符将选择该按钮;若是检查框,则相当于对该检查框按空格键;若是按钮,则将激活按钮命令;若控件是一个静态正文,则将激活按tab顺序紧随其后的下一个控件。
  Visible属性。用来指定控件是否是可见的。
  Disable属性。使控件允许或禁止,一个禁止的控件呈灰色显示,不能接收任何输入。
  Tabstop属性。用户可以按Tab键移动到具有Tabstop属性的控件上。Tab移动的顺序可以由用户指定。按Ctrl+D则Tab顺序会显示出来,用户可以用鼠标来重新指定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) 缺省

  请注意:
  组合框有简易式(Simple)、下拉式(Dropdown)和下拉列表式(Drop List)三种。简易式组合框包含一个编辑框和一个总是显示的列表框。下拉式组合框同简易式组合框的区别在于仅当单击下滚箭头时才出现列表框。下拉列表式组合框也有一个下拉的列表框,但它的编辑框是只读的,不能输入字符。
  组合框的列表项可以在设计模板时初始化,而列表框的初始化只能在程序中进行。请读者在组合框IDC_KIND的属性对话框的Data页中输入以下几个列表项,以作为单位性质的选项。输入时要注意,换行时不要按回车键,而应按Ctrl+回车键。国有企事业、集体企业、私有企业、中外合资、外商独资
  组合框控件的一个与众不同之处是它有两个尺寸,一个是下拉前的尺寸,一个是下拉后的尺寸。当用鼠标点击组合框上的箭头后,可设定下拉后的尺寸。
  控件最好都放在对话框模板的蓝色虚框内,控件之间的距离不要太近,否则有可能造成不正确的显示。
  安置好控件之后,下一步的任务是指定Tab顺序。按Ctrl+D键后,会显示当前的Tab顺序,通过用鼠标点击控件可以设定新的Tab顺序,如果想放弃本次修改,在对话框的空白处点击一下即可。
  最后,需要测试一下对话框。按Ctrl+T,则会弹出一个当前模板的测试对话框,这个对话框的外观和基本行为与程序中将要弹出的对话框一样。这样,读者不用编译运行程序,通过测试对话框就可以评估对话框是否合乎要求。如果发现了错误或不满意的地方,可按ESC键退出测试对话框并重新修改对话框模板。
  至此,对话框模板的设计就完成了。
  三、对话框类的设计
  完成对话框模板的设计后,就需要设计一个对话框类以实现对话框的功能。设计对话框类主要包括下面几步:
  创建对话框类。该类应从CDialog类派生。
  为对话框类加入与控件相对应的成员变量。
  为对话框进行初始化工作。
  增加对控件通知消息的处理
  1、对话框类的创建
  利用ClassWizard,程序员可以十分方便的创建MFC窗口类的派生类,对话框类也不例外。请读者按以下几步操作:
  打开IDD_REGISTER对话框模板,然后按Ctrl+W进入ClassWizard。
  进入ClassWizard后,ClassWizard发现IDD_REGISTER是一个新的对话框模板,于是它会询问是否要为IDD_REGISTER创建一个对话框类。按OK键确认。
  Create New Class对话框中,在Name栏中输入CRegisterDialog,在Base class栏中选择CDialog,在Dialog ID栏中选择IDD_REGISTER。按Create按钮后,对话框类CRegisterDialog即被创建。
  ClassWizard自动使类CRegesterDialog与IDD_REGISTER模板联系起来。
  提示:只要想创建的类是某一MFC窗口类的派生类,一般都可以利用ClassWizard来自动完成创建。创建的一般方法是:打开ClassWizard,选择Add Class->New,然后在Create New Class对话框中输入新类的类名,选择其MFC基类,如果是对话框类,则还要选择对话框的ID。
  2、为对话框类加入成员变量
  对话框的主要功能是输出和输入数据。对话框需要有一组成员变量来存储数据。在对话框中,控件用来表示或输入数据,因此,存储数据的成员变量应该与控件相对应。
  与控件对应的成员变量即可以是一个数据,也可以是一个控件对象,这将由具体需要来确定。例如,可以为一个编辑框控件指定一个数据变量,这样就可以很方便地取得或设置编辑框控件所代表的数据,如果想对编辑框控件进行控制,则应该为编辑框指定一个CEdit对象,通过CEdit对象,程序员可以控制控件的行为。需要指出的是,不同类的控件对应的数据变量的类型往往是不一样的,而且一个控件对应的数据变量的类型也可能有多种。表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。
  双击列表中的ID_AGE会弹出Add Member Variable对话框。在Member variable name栏中输入m_nAge,在Category栏中选择Value,在Variable type栏中选择UINT。按OK按钮后,数据变量m_nAge就会被加入到变量列表中。
  仿照第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中可分别为一个控件指定一个数据变量和一个控件对象,这样做的好处是即能方便地获得数据,又能方便地控制控件。
  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自动完成的。
  在对话框创建时,会收到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();
  m_ctrlIncome.AddString("500元以下");
  m_ctrlIncome.AddString("500-1000元");
  m_ctrlIncome.AddString("1000-2000元");
  m_ctrlIncome.AddString("2000元以上");
  return TRUE;
  }
  CRegisterDialog::OnInitDialog()的主要任务是对工资收入列表框的列表项进行初始化。调用CListBox::AddString可将指定的字符串加入到列表框中。由于该列表是不自动排序的,因此AddString将表项加在列表框的末尾。
  4、对话框的数据交换机制
  对话框的数据成员变量存储了与控件相对应的数据。数据变量需要和控件交换数据,以完成输入或输出功能。例如,一个编辑框即可以用来输入,也可以用来输出:用作输入时,用户在其中输入了字符后,对应的数据成员应该更新;用作输出时,应及时刷新编辑框的内容以反映相应数据成员的变化。对话框需要一种机制来实现这种数据交换功能,这对对话框来说是至关重要的。
  MFC提供了类CDataExchange来实现对话框类与控件之间的数据交换(DDX),该类还提供了数据有效机制(DDV)。数据交换和数据有效机制适用于编辑框、检查框、单选按钮、列表框和组合框。
  数据交换的工作由CDialog::DoDataExchange来完成。读者可以找到CRegisterDialog::DoDataExchange函数,如清单5.3所示。0
  清单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、对话框的运行机制
  在程序中运行模态对话框有两个步骤:
  在堆栈上以变量的形式构建一个对话框对象。
  调用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(最好在该项之前加一条分隔线,以便和前面的菜单项分开)。
  接着,用ClassWizard为该菜单命令创建命令处理函数CRegisterView::OnEditRegister。注意,OnEditRegister是类CRegisterView的成员函数,这是因为CRegisterView要负责打开和关闭登录数据对话框,并将从对话框中输入的数据在视图中输出。
  然后,请读者在RegisterView.cpp文件的开头加入一行  #include "RegisterDialog.h"
  最后,按清单5.4修改程序。
  清单5.4 OnEditRegister函数
  void CRegisterView::OnEditRegister()
  {
  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已经是一个简易的数据库应用程序了,它可以将与就业情况有关的数据输出到一个编辑视图中。用户可以编辑视图中的正文,并将结果保存在文本文件中。
  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看看,现在的登录数据对话框已经比较令人满意了。
  四、非模态对话框
  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 标签式对话框
  5.6 公用对话框
  5.7 小结
正文完
  评论这张
 
阅读(7240)| 评论(0)

历史上的今天

评论

<#--最新日志,群博日志--> <#--推荐日志--> <#--引用记录--> <#--博主推荐--> <#--随机阅读--> <#--首页推荐--> <#--历史上的今天--> <#--被推荐日志--> <#--上一篇,下一篇--> <#-- 热度 --> <#-- 网易新闻广告 --> <#--右边模块结构--> <#--评论模块结构--> <#--引用模块结构--> <#--博主发起的投票-->
 
 
 
 
 
 
 
 
 
 
 
 
 
 

页脚

网易公司版权所有 ©1997-2018