基础语法篇_7——MFC对话框:逃跑按钮、属性表单、向导创建

35 篇文章 42 订阅

🔳🔳 绘制线条 、画刷绘图、绘制连续线条、绘制扇形效果的线条


🔳🔳 插入符【文本插入符|图形插入符】、窗口重绘、路径、字符输入【设置字体|字幕变色】


🔳🔳 菜单命令响应函数、菜单命令的路由、基本菜单操作、动态菜单操作、电话本实例


🔳🔳 对话框的创建与显示、动态创建按钮、控件的访问【控件调整|静态文本控件|编辑框控件】、对话框伸缩功能、输入焦点的传递、默认按钮的说明


🔳🔳 MFC对话框:逃跑按钮、属性表单、向导创建


🔳🔳 在对话框程序中让对话框捕获WM_KEYDOWN消息


🔳🔳修改应用程序窗口的外观【窗口光标|图标|背景】、模拟动画图标、工具栏编程、状态栏编程、进度栏编程、在状态栏上显示鼠标当前位置、启动画面


🔳🔳设置对话框、颜色对话框、字体对话框、示例对话框、改变对话框和控件的背景及文本颜色、位图显示

一、基于对话框的程序

新建一个基于对话框的项目,项目名称:DlgTest。

生成的项目结构为:
解决方案结构:

类视图下有三个类:

  • CAboutDlg
    派生于CDialog类,这个类与SDI应用程序中相应的类:CAboutDlg作用相同,用于显示一个关于对话框。
  • CDlgTestApp
    这是MFC应用程序中必不可少的一个类,派生于CWinApp类,它的对象代表了应用程序本身。
  • CDlgTestDlg
    派生于CDialog类,基于对话框的MFC应用程序的主界面。

基于对话框的应用程序中没有从CView类派生出来的视类,也没有从CFrameWnd类派生出来的框架类,以及从CDocument类派生的文档类,它只有从CDialog派生出来的一个对话框类:CTestDlg,这类应用程序的窗口就是一个对话框界面。
资源视图:

运行该程序生成的界面:

二、“逃跑”的按钮

实现功能:
  DlgTest程序的对话框主界面上增加一个按钮,当用鼠标单击这个按钮时,该按钮会自动移动到另一个位置,就像一个“逃跑”的按钮。

✨ 1)首先删除MFC AppWizard自动创建的对话框资源: IDD_DLGTEST_DIALOG上的所有控件。

✨✨2)然后添加一个按钮控件,利用其属性对话框,将其Caption修改为:你能抓住我吗?

打开IDD_DLGTEST_DIALOG对话框的属性对话框,可以看到在其字体选项卡上,有一个Font按钮:
✨✨✨3)为了实现这种“逃跑”按钮,可以通过捕获鼠标移动的消息,并在此消息响应函数中让这个按钮的位置发生移动来实现。

   介绍一种巧妙的实现方法:在IDD_DLGTEST_DIALOG对话框资源窗口中,复制刚才添加的那个按钮,并在其下方进行粘贴操作,这样IDD_DLGTEST_DIALOG对话框资源中就有了两个外观相同的按钮。在程序实现时,首先让其中的一个按钮隐藏,另一个按钮显示;当随后把鼠标移动到显示的按钮上时,将该按钮隐藏,把另一个显示出来。因为这两个按钮的外观是完全一样的,因此这样的效果给用户的感觉好像按钮是自动跑到新位置处的。

🔵🔵 3.1)在IDD_DLGTEST_DIALOG对话框资源窗口中,复制刚才添加的那个按钮。

为了实现上述所述功能,程序首先就要捕获鼠标移动消息,那么由谁来捕获这个消息比较合适呢?
  如果让对话框窗口(CDlgTestDlg类)来捕获,一旦鼠标在对话框窗口中秘动,程序就会让按钮上下移动,这当然不是想实现的功能。想要的功能是当鼠标移动到按钮上时,按钮才上下移动。鼠标移动的消息应该由按钮窗口来捕获。所以在MFC应用程序中,可以创建一个从CButton类派生的新类,然后将按钮控件上这种新类型的成员变量相关联,从而就把按钮控件与一个自定义的按钮窗口类关联起来

🟢🟢 3.2)为应用程序增加一个从CButton派生的新类,设置新类的名称(Name)为: CNewButton,基类为: CButton。

接下来把对话框中的两个按钮分别关联一个成员变量,关联的变量类型为CNewButton类型,即将变量的类型设置为上面添加的新类。两个Button按钮的ID分别为:

  • IDC_BUTTON1,设置变量名称为: m_btn1。
  • IDC_BUTTON2,设置变量名称为: m_btn2。


发现DlgTestDlg中有两处代码变化:

  • 第一处:头文件DlgTestDlg.h中生成代码:

    发现CNewButton处飘红,是由于为对话框的一个子控件关联了一个成员变量,而这个成员变量的类型是CNewButton类,这个类是刚刚创建的新类。如果在CDlgTestDlg类中想要识别这种类型的话,就必须在CDlgTestDlg类中包含这个新类的头文件

    点击飘红的CNewButton类,会给出解决方案。点击给出解决方案就可解决问题。

    在CNewButton源文件中添加头文件#include "pch.h",否则会报错:错误C1010:在查找预编译头时遇到意外的文件结尾。是否忘记了向源中添加“#include “pch.h””?

  • 第二处:源文件DlgTestDlg.cpp中生成代码:

    void CDlgTestDlg::DoDataExchange(CDataExchange* pDX)
    {
    	CDialogEx::DoDataExchange(pDX);
    
    	DDX_Control(pDX, IDC_BUTTON1, m_btn1);
    	DDX_Control(pDX, IDC_BUTTON2, m_btn2);
    }
    

🟣🟣3.3)CNewButton类捕获鼠标移动消息。

打开类视图的选项卡,在CNewButton上单击鼠标右键,点击属性,在消息列表中找WM_MOUSEMOVE。点击 OnMouseMove按钮。

CNewButton的源文件的中BEGIN_MESSAGE_MAPEND_MESSAGE_MAP宏之间即添加了消息响应函数,并添加了消息响应函数的实现:

#include "CNewButton.h"
BEGIN_MESSAGE_MAP(CNewButton, CButton)
	ON_WM_MOUSEMOVE()
END_MESSAGE_MAP()
void CNewButton::OnMouseMove(UINT nFlags, CPoint point)
{
	// TODO: 在此添加消息处理程序代码和/或调用默认值

	CButton::OnMouseMove(nFlags, point);
}

🟡🟡3.4)实现按钮的隐藏功能。因为当鼠标移动到该按钮上时,就会由这个按钮的鼠标移动消息的响应函数 OnMouseMove来响应,在此函数中以SW_HIDE参数去调用这个按钮的ShowWindow函数,即可将其隐藏。

这时为了让另一个按钮显示出来,必须要知道另一个按钮所关联的那个对象的内存地址,然后才能调用该对象的ShowWindow函数,将其显示出来
  为了在一个按钮对象中获取另一个按钮控件对象的地址,最简单的方式就是在CNewButton类中定义一个成员变量,让其指向另一个按钮对象的地址

💦💦💦 3.4.1)因此为CNewButton类再添加一个公开CNewButton* 类型的成员变量: m_pBtn

当用CNewButton类去实例化CDlgTestDlg类的成员变量 m_btn1m_btn2时,这两个对象内部就都有了一个 m_pBtn成员变量,让这两个对象内部的 m_pBtn变量分别保存对方的首地址,相当于这两个对象互相交换了自己的首地址。于是当m_btn1按钮隐藏时,就可以利用它的成员变量 m_pBtn去调用ShowWinolw函数,将m_btn2按钮显示出来;同样地,当m_btn2按钮隐藏时,可以利用它的成员变量 m_pBtn去调用ShowWindow函数,将m_btn1按钮显示出来。

💦💦💦 3.4.2)在CDlgTestDlg类中把m_btn1m_btn2这两个对象的首地址交换一下,这一工作可以放在OnInitDialog函数中实现。根据前面的知识,OnInitDialog函数就是 WM_INITDIALOG消息的响应函数,该消息是在对话框要显示之前发送的。在CDlgTestDlg类的OnInitDialog函数的最后,但要在return语句之前添加:

m_btn1.m_pBtn = &m_btn2;
m btn2.m_pBtn = &m_btnl;

💦💦💦 3.4.3)然后在CNewButton类的OnMouseMove函数中,先让对象自己隐藏起来,然后调用成员m_pBtn的ShowWindow函数将对方显示出来:

void CNewButton::OnMouseMove(UINT nFlags, CPoint point)
{
	// TODO: 在此添加消息处理程序代码和/或调用默认值
	//隐藏鼠标处的窗口
	ShowWindow(SW_HIDE);
	//显示m_pBtn所指向的窗口
	m_pBtn->ShowWindow(SW_SHOW);

	CButton::OnMouseMove(nFlags, point);
}

当鼠标移动到第一个按钮对象(m_btn1)上时,程序就会调用该对象的OnMouseMove函数。在这个函数中,首先调用ShowWindow函数将自身隐藏。因为第一个按钮对象的成员m_pBtn保存的是第二个按钮对象m_btn2的地址,所以接下来的m_pBtn->ShowWindow (SW_SHOW)的调用就将第二个按钮显示了出来。当随后鼠标移动到第二个按钮对象上时,实现原理相同,是对m_bnt2对象来说,它的m_pBtn成员变量保存的是m_btn1按钮的地址。

但是这个程序还有一个缺陷:初始显示时,两个按钮都是显示状态,这很容易让用户看出程序的实现方式。
📋📋 解决:在初始时应该隐藏一个按钮。利用按钮属性对话框,把第一个按钮的Visible属性去掉。这时只有一个按钮处于显示状态,然后把鼠标移到这个按钮上,这个按钮就隐藏了,并显示出另一个按钮。再把鼠标移到这个显示的按钮上时,它又消失了,另一个又显示出来。

✨✨✨✨ 4)利用SetWindowPos函数来设置按钮在屏幕上移动的新位置

三、属性表单和向导的创建

点击VS的开发窗口中菜单项中的工具菜单命令,在选择此菜单项下的子菜单选项。打开的对话框就是一个属性表单,它的每一个选项卡就是一个属性页。一个属性表单由一个或多个属性页组成。它有效解决了大量信息无法在一个对话框上显示的问题,并提供了对信息的分类和组织管理功能。在程序设计中,可以将相关的选项放到一个属性页中。

现在重新建一个叫Prop的应用程序。项目样式:MFC standard、应用程序类型:单个文档。
生成的界面:

3.1 创建属性页

为了创建属性表单,首先需要创建属性页,MFC中属性页对应的类是CPropertyPage,该类生成的对象代表了属性表单中一个单独的属性页。该类的继承层次结构:

  可以看出,CPropertyPage类是从CDialog派生而来的。因此,一个属性页窗口其实就是一个对话框窗口。

✨ 1)创建一个对话框窗口,首先需要创建对话框资源。点击 资源视图 –>右键Dialog–> 添加资源–> 资源视图 ,在弹出的资源类型对话框中点击Dialog菜单项左侧的 +,即可看到三种属性页资源:

  • IDD_PROPPAGE_LARGE、
  • IDD_PROPPAGE_MEDIUM、
  • IDD_PROPPAGE_SMALL。


创建3个IDD_PROPPAGE_LARGE类型的属性页资源。

修改3个属性页资源的ID与标题:

  • 第一个:IDD_PROP1+Page1
  • 第二个:IDD_PROP2+Page2
  • 第三个:IDD_PROP3+Page3

属性页资源和通常插入的对话框资源之间的区别:
1. 对比外观选项卡属性:

可以看出,二者区别:

2. 对比其他属性:

📢📢📢 知道了这两种资源之间的区别后,可以在程序中先增加一个普通对话框资源,然后修改其属性,使其符合属性页资源的要求,然后把它当作属性页资源来使用。

✨✨ 2)首先删除Prop程序中各个属性页资源上已有的静态文本控件,然后在每一个属性页中增加一些控件。

  1. 在第一个属性页:
    1) 放置一个组框(Group Box)。组框可以用来起一个分组的作用,可以把相关的一些选项放置在一个组框中
      将新添加的这个组框的标题修改为:请选择你的职业
      1.1)然后在这个组框内放置三个单选按钮( Radio Button)。更改属性名依次为:程序员、老师、老板。

    2)再放置一个列表框控件(List Box),这种类型的控件提供了信息的一种简单的组织方式,可以排列一些字符串提供给用户进行选择
      2.1)在该列表框上放置一个静态文本控件(Static Text),这种控件主要起标示作用。
      将其文本属性修改为:请选择你的工作地点

    最后应该调整一下各个控件的Caption,以及相对位置,使其美观些。

  2. 在第二个属性页:
    1)首先放置一个组框(Group Box)。
      将其标题修改为: 请选择你的兴趣爱好
    2)组框内添加四个复选框(Check Box)。
      把它们的标题分别修改为:“足球”、“篮球”、“排球”、“游泳”

  3. 在第三个属性页:
    1)首先增加一个组合框(Combo Box)。
      增加组合框时应注意:拖放时要将它的范围拉得大些,否则在程序运行时单击它右边的下拉箭头时,显示的下拉空间很小。无法将其下拉框中的内容显示出来。
      Type选择Drop List类型。

    调整该组合框下拉列表部分的范围,方法是在对话框资源处于编辑状态时,把鼠标移动到该组合框控件右边向下的箭头上,当鼠标变成双向箭头形状时,按下鼠标左键,把组合框的下拉列表范围拖动到合适大小。
      组合框提供了编辑框加列表框的功能。VC++提供了三种类型的组合框,打开组合框控件的属性对话框,并单击Type选项页:
    🔳 简易式(Simple)
    这种类型的组合框包含一个编辑框和一个总是显示的列表
    🔳下拉式(Dropdown)
    类似于简易式组合框,二者的区别在于下拉式组合框仅当单击下拉箭头后,列表框才会弹出。
    🔳下拉列表式(Drop List)
    下拉列表式组合框也有一个下拉的列表框,但它的编辑框是只读的,不能输入字符。也就是说,这种类型的组合框只能从其下拉列表中选择内容。

    2)在第三个属性页上,在添加的组合框控件上方摆放一个静态文本,其标题设置为: 请选择你的薪资水平

✨✨✨ 3)为属性页对话框资源添加3个属性页类。
  设定新类的名称分别为CProp1、CProp2、CProp3,并把它的基类选择为: CPropertyPage。
    
  根据下面的方法添加三个新类:解决用类向导添加MFC类,基类列表没有CPropertyPage类。

在实际编程过程中,有时利用上述方法添加新类后,可能会出现这样的现象:系统会提示无法打开新类的源文件和头文件。这是VC++自身的问题。实际上,这时程序已经完成了新类的添加,只不过这个类的信息没有记录在类向导中。在类向导对话框的类名下拉列表中找不到这个新添加的类名,但这个类确实是一个完整的类,它有源文件和头文件。为了解决这个问题,即如何让类向导找到新添加的类,可以按以下步骤:
① 保存工程。
② 利用文件中的关闭解决方案关闭当前工作区。
③ 在Windows资源浏览器中找到该工程所在的目录,并找到.clw文件,该文件存储的就是类向导的一些相关信息。
④ 回到VC++开发环境,打开刚刚关闭的工程,打开类向导。会弹出一个话话框,该对话框提示类向导的数据库(即.clw文件) 不存在,询问用户是否愿意从工程的源文件中创建这个数据库。
⑤ 单击【是】按钮,就会弹出一个对话框,通常不需要对此对话框进行任何修改,直接单击【OK】按钮即可,完成.clw文件的创建。这时,就会发现在类向导对话框的类名下拉列表中就可以看到先前添加的新类了。

3.2 创建属性表单

  为了创建一个属性表单,首先需要创建一个CPropertySheet对象,接下来,在此对象中为每一个属性页创建一个对象(CPropertyPage类型),并调用AddPage函数添加每一个属性页,然后调用DoModal函数显示一个模态属性表单,或者调用Create函数创建一个非模态属性表单
  因此,可以通过以下几个步骤实现属性表单创建的功能:

✨1)为Prop程序创建一个属性表单对象。
  通过类向导添加MFC类,新类命名为:CPropSheet,并选择其基类为:CPropertySheet

✨✨2)在属性表单对象CPropSheet中添加属性页。
需要调用CPropertySheet类的成员函数:AddPage。其申明如下:

void AddPage(CPropertyPage *pPage);

可以看出此函数有一个CPropertyPage类型指针的参数,它指向的就是需要添加到属性表单中的属性页对象。也就是说,通过此函数可以将属性页对象添加到属性表单中。

首先在属性表单对象(CPropSheet)的头文件中为先前创建的三个属性页分别定义一个成员对象:

CProp1 m_prop1;
CProp2 m_prop2;
CProp3 m_prop3;

通常都是在属性表单对象的构造函数中添加属性页对象。但是对CPropSheet对象来说,此时它还不知道CProp1、CProp2和CProp3这三种类型的定义,所以还必须在CPropSheet类的头文件中分别把这三个属性页类的头文件包含进来:

#include "CProp1.h"
#include "CProp2.h"
#include "CProp3.h"

接下来就可以在CPropSheet类的构造函数中添加这三个属性页对象,但是发现CPropSheet有两个构造函数:

CPropSheet::CPropSheet(UINT nIDCaption, CWnd* pParentWnd, UINT iSelectPage)
	:CPropertySheet(nIDCaption, pParentWnd, iSelectPage)
{

}

CPropSheet::CPropSheet(LPCTSTR pszCaption, CWnd* pParentWnd, UINT iSelectPage)
	:CPropertySheet(pszCaption, pParentWnd, iSelectPage)
{

}

其中一个函数是用ID号(nlDCaption),另一个函数是用标题字符串(pszCaption)来构造属性表单对象。对应的基类: CPropertySheet的两个构造函数的声明原型:

CPropertysheet( UINT nIDCaption, cwnd *pParentWnd = NULL, UINT iSelectPage =0 );
CPropertySheet( LPCTSTR pszCapion, CWnd *pParentWnd NULL, UINTiselectPage = o);

这两个构造函数的后两个参数都是相同的。

  • 第二个参数pParentWnd,即父窗口指针都有默认值:NULL,此时的属性表单的父窗口就是应用程序的主窗口。对于SDI应用程序来说,就是应用程序的主框架窗口。
  • 第三个参数iSelectPage指定的是属性表单初始选择的属性页,可以通过这个参数指定属性表单初始显示时显示的属性页,默认是第一个页面。

因为属性表单类有两个构造函数,在构造属性表单对象时,可以任选其中一个构造函数。在这两个构造函数都调用AddPage函数添加属性页对象:

CPropSheet::CPropSheet(UINT nIDCaption, CWnd* pParentWnd, UINT iSelectPage)
	:CPropertySheet(nIDCaption, pParentWnd, iSelectPage)
{
	AddPage(&m_prop1);
	AddPage(&m_prop2);
	AddPage(&m_prop3);
}

CPropSheet::CPropSheet(LPCTSTR pszCaption, CWnd* pParentWnd, UINT iSelectPage)
	:CPropertySheet(pszCaption, pParentWnd, iSelectPage)
{
	AddPage(&m_prop1);
	AddPage(&m_prop2);
	AddPage(&m_prop3);
}

✨✨✨3)显示属性表单。
CPropertySheet类的继承关系结构图:

CPropertySheet类从CWnd类派生而来。而不是派生于CDialog类。但是CPropertySheet对象和CDialog对象的操纵方式是类似的。属性表单对象的创建也需要两个步骤:第一步调用构造函数定义一个属性表单对象,然后调用DoModal成员函数创建一个模态属性表单或者调用Create成员函数创建一个非模态属性表单

🟡🟡 1)在主菜单上添加一个菜单项,当用户单击这个菜单项后,程序显示CPropertyPage属性表单对象。
  在帮助菜单项后添加一个属性表单菜单项。其属性对话框中设置PopUp选项为False、Caption设置:属性表单、ID设置为IDM_PROPERTYSHEET。

🔵🔵 2)为此新菜单项添加命令响应函数。
  让CPropView类捕获此菜单命令,并接受系统自动赋予的响应函数名称OnPropertysheet。

🟣🟣 3)在此函数中创建属性表单。
1、先在CPropView类的源文件中包含CPropSheet类的头文件。

#include "pch.h"
#include "framework.h"
// SHARED_HANDLERS 可以在实现预览、缩略图和搜索筛选器句柄的
// ATL 项目中进行定义,并允许与该项目共享文档代码。
#ifndef SHARED_HANDLERS
#include "Prop.h"
#endif

#include "PropDoc.h"
#include "PropView.h"
#include "CPropSheet.h"

2、添加创建属性表单的代码:

void CPropView::OnPropertysheet()
{
	// TODO: 在此添加命令处理程序代码
	CPropSheet propSheet(_T("属性表单"));
	propSheet.DoModal();
}

运行代码:

3.3 向导的创建

创建一个向导类型的对话框,应该遵循创建一个标准属性表单的步骤来实现。但在调用属性表单对象的DoModal函数之前,应该先调用SetWizardMode这一函数。因此,在Prop工程的CPropView类的OnPropertysheet函数中修改代码:

void CPropView::OnPropertysheet()
{
	// TODO: 在此添加命令处理程序代码
	CPropSheet propSheet(_T("属性表单"));
	propSheet.SetWizardMode();
	propSheet.DoModal();
}

运行Prop程序,单击属性表单菜单命令,发现该对话框已经变成了一种向导的模式,它底部的按钮变成了:上一步下一步

🔳🔳 问题:但是,上述这个向导对话框仍存在一些问题:在第一个页面上,不应该有上一步这个按钮;在最后一个页面上,不应该是下一步按钮,而应该是完成按钮。在前面定义属性页资源时,并没有增加这些按钮,可见这些按钮是属于属性表单的,那么就需要调用属性表单的相关函数来修改它的按钮。

CPropertySheet类提供了一个 SetWizardButtons成员函数,可以用来设置向导对话框上的按钮。该函数声明:

void SetWizardButtons(DWORD dwFlags);

dwFlags参数可以是下表中所列各值的组合:

一般来说,应该在属性页的OnSetActive函数中调用SetWizardButtons这个函数。当属性页被选中,从而成为一个活动的页面时,应用程序框架就会调用OnSetActive这个函数。OnSetActive函数是一个虚函数,因此,在属性页子类中重写这个函数,然后根据需要设置该属性页上的按钮。

✨1)首先在每个属性页资源关联的类CProp1、CProp2、CProp3中重写OnSetActive函数。

CProp2和CProp3操作同CProp1。

✨✨2)在OnSetActive函数中调用属性表单对象的SetWizardButtons函数,设置每个属性页上的按钮。

  由于属性页是被添加到属性表单中的,所以属性表单是属性页的父窗口,可通过GetParent函数获取属性页父窗口的指针,即属性表单的指针。但该函数返回的是CWnd类型的指针,需要进行强制转换,将CWnd类型的指针转化成CPropertySheet类型的指针。然后利用此指针,调用SetWizardButtons函数。

  • 第一个属性页应该只有一个下一页按钮。
    因此SetWizardButtons函数的参数应该为PSWIZB_NEXT,所以CProp1类的OnSetActive函数的具体实现:
    BOOL CProp1::OnSetActive()
    {
    	// TODO: 在此添加专用代码和/或调用基类
    	CPropertySheet* pCPS = (CPropertySheet*)GetParent();
    	pCPS->SetWizardButtons(PSWIZB_NEXT);
    
    	return CPropertyPage::OnSetActive();
    }
    
  • 第二个属性页应该有一个上一页按钮和下一页按钮。
    BOOL CProp2::OnSetActive()
    {
    	// TODO: 在此添加专用代码和/或调用基类
    	((CPropertySheet*)GetParent())->SetWizardButtons(PSWIZB_BACK|PSWIZB_NEXT);
    
    	return CPropertyPage::OnSetActive();
    }
    
  • 第三个属性页应该有一个上一页按钮和完成按钮。
    BOOL CProp3::OnSetActive()
    {
    	// TODO: 在此添加专用代码和/或调用基类
    	((CPropertySheet*)GetParent())->SetWizardButtons(PSWIZB_BACK|PSWIZB_FINISH);
    
    	return CPropertyPage::OnSetActive();
    }
    

最终:

对于向导来说,通常是希望用户在每个属性页中进行一些选择。接下来,我们就对每个页面进行一个判断,检查用户是否做出选择,如果没有,就禁止程序进入下一个页面。也就是说,用户必须进行了一项选择之后,才能进入下一个页面。

3.3.1 处理第一个页面

✨ 1)首先处理第一个页面,为这个页面上的单选按钮关联一个成员变量。
  在第一个单选按钮(其ID为IDC_RADIO1)上单击鼠标右键,从弹出的快捷菜单中选择类向导,打开成员变量选项,发现成员变量的列表中并无IDC_RADIO1、IDC_RADIO2、IDC_RADIO3这三个ID。

📋📋 原因:因为对一组单选按钮来说,需要设置该组中第一个单选按钮的Group属性。
🟢🟢 解决:打开单选按钮的属性对话框,设置Group选项为True。

再次打开类向导对话框,成员变量列表中出现了单选按钮的ID号。为此ID添加一个值类型的成员变量:m_occupation,数据类型选择:int。

当为第一个单选按钮设置了Group选项后,随后的两个单选按钮就和这个按钮属于同一组了,直到遇到下一个(按照Tab顺序)具有Group属性的控件为止。

◼◻◼ 通过判断成员变量的值判断当前选中的是哪个单选按钮控件。
  在Prop程序运行时,当选中第一个单选按钮后,它所关联的成员变量m_occupation的值就是0;当选中第二个单选按钮后,m_occupation变量的值就是1;当选中第三个单选按钮后,m_occupation变量的值就是2。于是在程序中,通过判断这个成员变量的值就可以知道当前选中的是哪个单选按钮控件。

程序中变量在构造函数中自动被初始化为0,可以更改为-1,表明初始显示时,三个单选按钮一个也没有选中。因此在程序中就可以对这个变量进行判断,如果其值为-1,就说明用户没有选择单选按钮选项。

另外,在CProp1的DoDataExchange函数中,可以看到添加了一条DDX_Radio函数的调用,用来在单选按钮控件与成员变量之间交换数据

void CProp1::DoDataExchange(CDataExchange* pDX)
{
	CPropertyPage::DoDataExchange(pDX);
	DDX_Radio(pDX, IDC_RADIO1, m_occupation);
}

✨✨ 2)当用户单击第一个属性页上的 下一步按钮后,应该判断用户是否选择了某个职业,只有当用户选择了某个职业时,程序才能进入下一个属性页。
📋📋 依据:当用户单击属性页上的下一步按钮后,程序将调用OnWizardNext这个虚函数。如果这个函数返回0,那么程序自动进入当前向导的下一个属性页;如果返回-1,将禁止属性页发生变更。
  为CProp1类添加OnWizardNext这个虚函数的处理,来完成对该属性页上下一步按钮的命令响应。该虚函数的添加方法与前面OnsetActive虚函数的添加方法相同。


LRESULT CProp1::OnWizardNext()
{
	// TODO: 在此添加专用代码和/或调用基类

	return CPropertyPage::OnWizardNext();
}

添加之后,可以在这个虚函数中判断m_occupation变量的值,如果是-1,说明用户没有选择任何一个职业,则会弹出一个对话框,提示用户应选择一个职业,然后让这个虚函数返回-1,禁止进入下一个属性页。

LRESULT CProp1::OnWizardNext()
{
	// TODO: 在此添加专用代码和/或调用基类
	//未选中
	if (m_occupation == -1) {
		MessageBox(_T("请选择您的职业!"));
		return -1;
	}
	
	return CPropertyPage::OnWizardNext();
}


📋📋 出现此问题原因:控件与成员变量的数据交换是通过DoDataExchange函数来完成的,而程序中并不直接调用这个函数,而是通过调用UpdateData函数来调用它。对UpdateData来说,当它的参数为TRUE时,是从控件得到成员变量的值;当参数值为FALSE时,是用成员变量的值初始化控件

🟢🟢 解决:在CProp1类的OnWizardNext函数中要从控件得到相关联的变量的值,就应该以TRUE为参数来调用UpdateData函数,由于这个参数的默认值就是TRUE,因此可以以不带参数的形式直接调用UpdateData函数:

LRESULT CProp1::OnWizardNext()
{
	// TODO: 在此添加专用代码和/或调用基类
	//未选中
	UpdateData();
	if (m_occupation == -1) {
		MessageBox(_T("请选择您的职业!"));
		return -1;
	}
	return CPropertyPage::OnWizardNext();
}


✨✨✨ 3)为第一个属性页添加对工作地点的选择进行判断的代码。
  首先需要在工作地点列表框中增加一些工作地点。应该在响应这个属性页的WM_INITDIALOG消息的函数中完成这一任务,也就是在这个属性页显示之前向列表框中增加一些工作地点。因此首先为CPropl类添加WM_INITDIALOG消息的响应函数(OnInitDialog)。对话框“消息”中找不到WM_INITDIALOG

BOOL CProp1::OnInitDialog()
{
	CPropertyPage::OnInitDialog();
	// TODO:  在此添加额外的初始化

	return TRUE;  // return TRUE unless you set the focus to a control
				  // 异常: OCX 属性页应返回 FALSE
}

前面已经介绍过,在MFC编程中,对控件的操作都是通过相关的MFC类来完成的。对于列表框,也有一个与之对应的MFC类:CListBox。该类提供了一个成员函数AddString,用于向列表框添加字符串。因此在OnInitDialog函数中,首先需要获得这个列表框控件对象,此列表框的ID:IDC_LIST2。然后调用该对象的AddString函数完成工作地点的添加。

BOOL CProp1::OnInitDialog()
{
	CPropertyPage::OnInitDialog();

	// TODO:  在此添加额外的初始化
	//获取列表框控件对象
	CListBox* pCLB = (CListBox*)GetDlgItem(IDC_LIST2);
	pCLB->AddString(TEXT("北京"));
	pCLB->AddString(TEXT("信阳"));
	pCLB->AddString(TEXT("上海"));
	((CListBox*)GetDlgItem(IDC_LIST2))->AddString(TEXT("天津"));
	((CListBox*)GetDlgItem(IDC_LIST2))->AddString(TEXT("郑州"));
	((CListBox*)GetDlgItem(IDC_LIST2))->AddString(TEXT("南京"));


	return TRUE;  // return TRUE unless you set the focus to a control
				  // 异常: OCX 属性页应返回 FALSE
}

✨✨✨✨ 4)对工作列表框控件进行判断,让用户必须选择一个工作地点;否则,不能进入下一个属性页面。
  同前面的单选按钮一样,首先需要给这个列表框控件关联一个成员变量,方法同上。添加成员变量对话框中,设置这个成员变量的名称为: m_workAddr、选择值类型、变量类型选择CString。同前面的m-occupation变量一样, CProp1类在其构造函数中对m_workAddr变量也需要进行初始化。

其初始化如下:

CProp1::CProp1()
	: CPropertyPage(IDD_PROP1)
	, m_occupation(-1)
	, m_workAddr(_T(""))
{

}

并在DoDataExchange函数中添加:

void CProp1::DoDataExchange(CDataExchange* pDX)
{
	CPropertyPage::DoDataExchange(pDX);
	DDX_Radio(pDX, IDC_RADIO1, m_occupation);
	DDX_LBString(pDX, IDC_LIST2, m_workAddr);
}

可以在CProp1类的OnWizardNext函数中对列表框控件相关联的成员变量进行判断,检查用户是否选择了一个工作地点。如果m_workAddr变量为空,那么说明用户没有选择工作地点,OnWizardNext函数就返回一个-1值,禁止进入下一个属性页。

LRESULT CProp1::OnWizardNext()
{
	// TODO: 在此添加专用代码和/或调用基类
	//未选中
	UpdateData();
	if (m_occupation == -1) {
		MessageBox(_T("请选择您的职业!"));
		return -1;
	}
	if (m_workAddr == "") {
		MessageBox(_T("请选择你的工作地点!"));
		return -1;
	}
	return CPropertyPage::OnWizardNext();
}

3.3.2 处理第二个页面

✨1)为四个复选框分别关联一个值类型的成员变量。



对于复选框控件来说,当选中时,它所关联的成员变量的值应该为TRUE,否则为FALSE。在Prop2类的构造函数中,可以看到它将新添加的四个成员变量都初始化为FALSE。

CProp2::CProp2()
	: CPropertyPage(IDD_PROP2)
	, m_football(FALSE)
	, m_basketball(FALSE)
	, m_volleyball(FALSE)
	, m_swim(FALSE)
{

}

数据交换情况:

void CProp2::DoDataExchange(CDataExchange* pDX)
{
	CPropertyPage::DoDataExchange(pDX);
	DDX_Check(pDX, IDC_CHECK1, m_football);
	DDX_Check(pDX, IDC_CHECK2, m_basketball);
	DDX_Check(pDX, IDC_CHECK3, m_volleyball);
	DDX_Check(pDX, IDC_CHECK4, m_swim);
}

✨✨2)如果用户没有选择任何一个兴趣爱好,就不让程序进入下一个属性页面。因此同前面的第一个属性页一样,**首先为CProp2类添加OnWizardNext虚函数的重写。

LRESULT CProp2::OnWizardNext()
{
	// TODO: 在此添加专用代码和/或调用基类


	return CPropertyPage::OnWizardNext();
}

然后在此函数中,对用户是否做出选择进行判断。实际上,对这四个成员变量,如果有任有一个变量为TRUE,就可以进入下一个属性页面;否则显示一个对话框,提示用户必须先选择一个兴趣爱好,然后该虚函数返回-1,禁止程序进入下一个属性页。

需要注意一点,根据前面对第一个属性页面的处理,有了这样的经验,就是在对与控件相关联的变量进行判断之前,需要调用UpdateData函数,以实现控件与成员变量的数据交换。

LRESULT CProp2::OnWizardNext()
{
	// TODO: 在此添加专用代码和/或调用基类
	UpdateData();
	if (m_basketball || m_football || m_volleyball || m_swim) {
		return CPropertyPage::OnWizardNext();
	}
	else
	{
		//未选中任何复选框
		MessageBox(TEXT("请选择你的兴趣爱好!"));
		return -1;
	}
}


3.3.3 处理第三个页面

第三个属性页中摆放的是一个组合框控件,这时,要向这个组合框中添加一些关于薪资的选项,以便用户进行选择。

组合框控件由一个编辑框和一个列表框组成,其相对应的MFC类是CComboBox,该类也有一个成员函数:AddString,用来向组合框控件的列表框中添加字符串选项

✨1)首先为CProp3类添加WM_INITDIALOG消息的响应函数(即重写OnInitDialog函数)。

✨✨ 2)在此函数中对这个属性页对话框进行初始化,即在此函数中调用组合框对象的AddString函数,向组合框控件的列表框中添加一些薪资选项:


BOOL CProp3::OnInitDialog()
{
	CPropertyPage::OnInitDialog();

	// TODO:  在此添加额外的初始化
	CComboBox* pCCB = (CComboBox*)GetDlgItem(IDC_COMBO2);
	pCCB->AddString(_T("10000元以下"));
	pCCB->AddString(_T("10000~20000元"));
	pCCB->AddString(_T("20000~30000元"));

	((CComboBox*)GetDlgItem(IDC_COMBO2))->AddString(_T("30000~50000元"));
	((CComboBox*)GetDlgItem(IDC_COMBO2))->AddString(_T("50000元以上"));




	return TRUE;  // return TRUE unless you set the focus to a control
				  // 异常: OCX 属性页应返回 FALSE
}


发现这四个选项的显示顺序与代码中添加的顺序不一样。这主要是因为组合框默认情况下具有排序的功能,若希望组合框的列表框中的字符串按照代码中添加的顺序显示的话,可以打开这个组合框控件的属性对话框,设置Sort选项为False。


✨✨✨ 3)在第三个属性页对话框初始显示时,这个组合框在其编辑框中有一个初始选择的项。
  通过组合框的一个成员函数: SetCursel来完成,该函数的功能是选择组合框的列表框中的一个字符串,并将其显示在该组合框的编辑框中。SetCurSel函数的声明:

int SetCurSel(int nSelect);
  • nSelect是一个基于0的索引,指定选择项的索引位置。如果其值为-1,那么将移除该组合框的当前选择,并清空该组合框的编辑框中的内容。

因此在CProp3类的OnInitDialog函数中实现组合框初始显示时选中第一个选项。


BOOL CProp3::OnInitDialog()
{
	CPropertyPage::OnInitDialog();

	// TODO:  在此添加额外的初始化
	CComboBox* pCCB = (CComboBox*)GetDlgItem(IDC_COMBO2);
	pCCB->AddString(_T("10000元以下"));
	pCCB->AddString(_T("10000~20000元"));
	pCCB->AddString(_T("20000~30000元"));

	((CComboBox*)GetDlgItem(IDC_COMBO2))->AddString(_T("30000~50000元"));
	((CComboBox*)GetDlgItem(IDC_COMBO2))->AddString(_T("50000元以上"));


	((CComboBox*)GetDlgItem(IDC_COMBO2))->SetCurSel(0);

	return TRUE;  // return TRUE unless you set the focus to a control
				  // 异常: OCX 属性页应返回 FALSE
}

3.3.4 接收用户在向导中所作的选择

实现的功能:
   Prop程序要将向导中用户的选择输出到视类的窗口中。

为了在视类中得到用户在这三个页面中所进行的选择:
1)首先为第三个页面添加一个CString类型的成员变量: m_strSalary,用来接收用户的选择。

2)给CProp3类添加OnWizardFinish虚函数。
  程序应该在用户单击向导的完成按钮时,将用户所作的薪资水平选择保存到这个变量中,所以应该给CProp3类添加一个虚函数:OnWizardFinish,以处理完成按钮的单击消息。

3)获取用户选择的薪资选项。
  ◼◻◼ 为了获取用户选择的薪资选项,首先需要得到该选项的索引值,利用CComboBox类的GetCurSel成员函数实现。该函数的返回值是一个基于0的索引,表明组合框的列表框中当前选中项的位置。
  ◻◼◻ 获得用户选择的薪资选项索引之后,再利用CComboBox类的另一个成员函数:GetLBText从组合框的列表框中指定位置处得到一个字符串,该函数有两种声明原型,其中一种:

void GetLBText(int nIndex,cstring& rstring ) const;
  • 第一个参数指定列表框中将被复制的字符串的索引位置。本例就可以将它设置为GetCurSel函数的返回值,即得到当前选中项的字符串。
  • 第二个参数就是指定用来接收复制字符串的缓存。
BOOL CProp3::OnWizardFinish()
{
	// TODO: 在此添加专用代码和/或调用基类
	int pos=((CComboBox*)GetDlgItem(IDC_COMBO2))->GetCurSel();
	((CComboBox*)GetDlgItem(IDC_COMBO2))->GetLBText(pos, m_strSalary);
	MessageBox(m_strSalary);
	return CPropertyPage::OnWizardFinish();
}


4)为了接收用户在向导中做出的选择,在视类中需要定义一些变量来保存它们,下面列出了为视类添加的成员变量,并且将它们的访问权限都设置为私有的。

◻◼◻ 在视类的构造函数中初始化这些添加的变量。

CPropView::CPropView() noexcept
{
	// TODO: 在此处添加构造代码
	m_iOccupation = -1;
	m_strWorkAddr = "";
	memset(m_bLike, 0, sizeof(m_bLike));
	m_strSalary = "";
}

使用C语言的memset函数对m_bLike数组进行快速初始化,该函数的声明:

void* memset(void *dest, int c, size_t count );

该函数的功能是把dest参数指定的内存中前count个字节设置为字符: c。

  • dest
    指向将被赋值的目标内存。
  • c
    设置的字符值。
  • count
    设置的字节数。

在C/C++语言中,非0值即为真(TRUE), 0值即为假(FALSE),并且对数组来说,数组名就是它的首地址,数组大小可以利用sizeof函数来获取。

因此可以用0值设置数组mbLike指向的内存缓存,从而将它的元素都设置为FALSE。

5)接下来就要在视类窗口把用户在向导中的选择输出到窗口中。
  但有一点需要注意:只有用户单击完成按钮关闭向导后,才输入用户的选择;如果用户单击的是取消按钮,即放弃当前所作的选择,程序就不应该输出用户的选择。一般情况下,CPropertySheet类的DoModal函数的返回值是IDOK或IDCANCEL,但是如果属性表单已经被创建为向导了,那么该函数的返回值将是ID_WIZFINISH或IDCANCEL。因此在程序中应该对属性表单对象的DoModal函数的返回值进行判断,如果返回的是完成按钮的ID: ID_WIZFINISH,那么才进行输出处理。

  这里有一点需要注意,当DoModal函数返回后,属性表单窗口就被销毁了,但propSheet这个属性表单对象的生命周期并没有结束。因此,仍然可以利用这个对象去访问它的内部成员。这里又一次提到窗口和对象的关系,它们并不是同一个事物

改错:前面CProp1、CProp2、CProp3中添加的成员变量类型设置为public类型的。因为后续取值需要访问这些变量的值。CPropSheet中的成员变量也设置为public类型。

void CPropView::OnPropertysheet()
{
	// TODO: 在此添加命令处理程序代码
	CPropSheet propSheet(_T("属性表单"));
	//设置成向导模式
	propSheet.SetWizardMode();
	//判断DoModal函数的返回值是否为完成
	if (ID_WIZFINISH==propSheet.DoModal())
	{
	    //等号右侧成员变量必须是公开的,否则此处无法访问
		m_iOccupation = propSheet.m_prop1.m_occupation;
		m_strWorkAddr = propSheet.m_prop1.m_workAddr;
		m_bLike[0] = propSheet.m_prop2.m_football;
		m_bLike[1] = propSheet.m_prop2.m_basketball;
		m_bLike[2] = propSheet.m_prop2.m_volleyball;
		m_bLike[3] = propSheet.m_prop2.m_swim;
		m_strSalary = propSheet.m_prop3.m_strSalary;
		Invalidate();
	}	
}

调用Invalidate函数,让视类窗口无效,从而引起重绘操作。然后,就可以在视类的OnDraw函数中完成这些消息的输出:

void CPropView::OnDraw(CDC* pDC)
{
	CPropDoc* pDoc = GetDocument();
	ASSERT_VALID(pDoc);
	if (!pDoc)
		return;

	// TODO: 在此处为本机数据添加绘制代码
	//设置画笔的格式
	CFont font;
	font.CreatePointFont(300, _T("华文行楷"));
	//保存旧的画笔
	CFont* pOldFont;
	pOldFont = pDC->SelectObject(&font);

	//临时变量
	CString strTemp;
	
	//职业:
	strTemp = _T("职业:");
	switch (m_iOccupation)
	{
	case 0:
		strTemp += "程序员";
		break;
	case 1:
		strTemp += "老师";
		break;
	case 2:
		strTemp += "老板";
		break;
	default:
		break;
	}
	//(0,0)处显示职业
	pDC->TextOutW(0, 0, strTemp);


	//定义文本信息结构体变量
	TEXTMETRIC tm;
	//得到设备描述表中当前字体的度量信息:字体高度
	pDC->GetTextMetrics(&tm);


	//工作地点
	strTemp = _T("工作地点:");
	strTemp += m_strWorkAddr;
	//在(0,字体高度)处显示工作地点
	pDC->TextOutW(0, tm.tmHeight, strTemp);


	//兴趣爱好
	//选中,则在字符串末尾追加兴趣
	strTemp = _T("兴趣爱好:");
	if (m_bLike[0]) {
		strTemp += "足球 ";
	}
	if (m_bLike[1]) {
		strTemp += "篮球 ";
	}
	if (m_bLike[2]) {
		strTemp += "排球 ";
	}
	if (m_bLike[3]) {
		strTemp += "游泳 ";
	}
	//在(0,2倍字体高度)处显示兴趣爱好
	pDC->TextOutW(0, tm.tmHeight * 2, strTemp);

	//薪资
	strTemp= _T("薪资:");
	strTemp += m_strSalary;
	//在(0,3倍字体高度)处开始显示薪资
	pDC->TextOutW(0, tm.tmHeight * 3, strTemp);

	//恢复原来画笔
	pDC->SelectObject(pOldFont);


}

运行程序,输出如下内容:
在这里插入图片描述
点击属性列表,并进行选择:

最终窗口显示如下:

这是由于调用Invalidate后,引起了窗口重绘,重新调用了视类的OnCreate函数,此时获取到各个属性页的内容,并显示出来。

  • 2
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值