第15单元 控件本单元教学目标
介绍各种常用控件的使用方法及基于对话框的应用程序。
学习要求
掌握常用控件的使用方法以及基于对话框的应用程序开发。
授课内容
对话框与控件是密不可分的。Windows提供了一批基本控件,如静态控件(Static Text)、编辑控件(Edit Box)、组框、等,可解决大部分用户输入界面设计的需求。另外,使用Visual C++编程还可使用一批通用控件,包括动画控件(Animate)、标题控件(Header)、复合文本编辑控件(Rich Edit)、标签控件(Tab)和树状列表控件(Tree List)等,可大大提高应用程序界面的表现力。
15.1 常用控件控件(Control)是Windows提供的独立小部件,在对话框与用户的交互过程担任主要角色,如显示文本、图片和图标、命令按钮、编辑文字或数据和滚动条等。
控件的外观和功能是由其属性(Property)决定的。在编辑对话框模板资源时,对准某个控件按下鼠标右键可调出其属性设置对话框。不同的控件属性也不完全相同。属性对话框中有若干选项卡,如General选项卡、Styles选项卡和Extend Styles选项卡等,控件的属性就分布在各选项卡上(参看14.7:“对话框模板资源的编辑”)。
控件看似简单,但实际上也是一个窗口,对应一个CWnd派生类的对象。例如,编辑控件对应Cedit类的对象,静态文本控件对应CStatic类的对象。每个控件均有自己的标识符,在程序中可使用对话框类的成员函数GetDlgItem()取得指向具体控件对象的指针,然后对其进行编程。本节介绍几个常用的控件的使用方法。
1.静态文本(Static Text)控件:用于显示字符串,不接受输入信息。多用于显示其他控件的标题。使用静态文本控件一般均可使用缺省属性。
2.图片(Picture)控件,用于显示位图、图标、方框等,不接受输入信息。在图片控件的属性中,最重要的是其Type(在控件属性对话框的General选项卡中设置),可选类型有Frame(矩形框)、Rectangle(矩形块)、Icon(图标)和Bitmap(位图)等。如果类型选择Frame和Rectangle,可通过Color选项选择其颜色;如果选择Icon和Bitmap,可通过Image选项选择相应的资源。
3.组框(Group Box)控件:显示一个文本字符串和一个方框,通常用于组合一组相关控件。
以上三个控件均对应CStatic类型的对象。应该说明的是,如果无需对静态控件编程,则也不要求其标识符唯一,通常可选用对话框模板编辑器自动提供的缺省标识符(IDC_STATIC)。
4.编辑(Edit Box)控件。编辑控件是最常用的控件,可用于单行或多行文本编辑,其功能十分强大,相当于一个小型文本编辑器。编辑控件亦可用来输入数值数据和日期、时间数据。主要属性有Align Text(文本对齐方式)、Multiline(多行编辑)、AutoHScroll(输入到窗口右边界后自动横滚)等(均在控件属性对话框的Styles选项卡中设置)。编辑控件对应CEdit类的对象。
5.按钮(Button)控件,用于响应用户的鼠标按键等操作,触发相应的事件。编程时按钮的处理与菜单选项类似,可为其添加命令响应函数(通常借助ClassWizard完成)。
6.检查框(Check Box)控件,用作选择标记,有选中、不选中和不确定等状态。
7.单选按钮(Radio Button)控件,用来作多项选择。单选按钮总是成组使用的。在一组单选按钮中,第一个按钮最为重要,其ID可用于在对话框类中建立对应的数据成员(一定要设置其Group属性为选中)。按钮、检查框和单选按钮三种控件均对应CButton类的对象。
8.列表框(List Box)控件,显示一个文字列表,用户可从表中选择一项或多项。主要属性为Selection(位于Styles选项卡中)。可选择“Single”(单选)、“Multiple”(多选)等。属性Sort表示是否将列表框的内容排序。列表框中的文字列表需在编程时确定,通常是在对话框类的InitDialog()成员函数中给出。列表框控件对应CListBox类对象。
9.组合框(Combo Box)是编辑控件和列表框的组合,可分为简易式(Simple)、下拉式(Dropdown)和下拉列表式(Drop List)。组合框中列表的内容可在设置时用Data选项卡输入。注意输入各列表项时要使用Ctrl+Enter开始新的一项。组合框控件对应CCombo类的对象。
为了在程序中对控件进行查询和控制,可以利用CWnd类提供的一组管理对话框控件的成员函数。这类函数很多,以下仅举几例:
对话框控件管理函数 说明
GetCheckedRadioButton() 返回指定单选按钮组中被选择的单选按钮的ID;
GetDlgItem() 返回一个指向一给定控件的指针;
GetDlgItemText() 获得在一个控件内显示的正文;
SetDlgItemText() 设置一个控件显示的正文。
[例15-1] 为某公司设计一个人事管理系统,其基本功能为输入、编辑、查看和保存公司的人事档案。职工人事档案包括姓名、性别、出生日期、婚姻状况、所在部门、职务和工资。
说 明:为了管理和保存档案,选用SDI(单文档/视图界面)程序结构。首先用AppWizard生成一个名为EMP的SDI程序框架,在第4步中时按下“Advanced…”按钮调出Advanced Options对话框,在Document Template Strings选项卡中将File extension(文件名后缀)项设置为“dat”,在Window Styles选项卡中设置Maximized检查框为“选中”状态。其他选项均可用缺省设置。
使用Developer Studio菜单的Insert/Resource…选项调出Insert Source对话框,为项目添加一个对话框模板资源,并将其标识符改为IDD_EMPLOYEE。适当调整其大小,并在其上添加如下控件,使其看上去与图15-1类似。
静态文本控件:姓名、出生日期、部门、职务、工资;
图片控件:类型为Icon,使用项目自带的IDR_MAINFRAME图标,也可自己为项目添加一个图标资源并编辑成自己喜欢的样式;
编辑控件三个,其ID分别改为IDC_NAME、IDC_BIRTHDATE和IDC_SALARY;
单选按钮两个,其ID和Caption分别改为IDC_MALE,“男”和IDC_FEMALE,“女”。为IDC_MALE设置属性Group;
检查框一个,其ID和Caption分别改为IDC_MARRIED和“婚否”;
列表框一个,其ID改为IDC_DEPT,保证Styles选项卡中的Selection项为Single,并将Sort检查框设置为非选中状态;
组合框一个,其ID改为IDC_POSITION,并在Style选项卡中将Type设置为Drop List,并将Sort检查框设置为非选中状态。然后在Data选项卡中输入各种职务的名称:
总经理
副总经理
部门经理
项目经理
业务经理注意使用Ctrl+Enter键回车换行。
组框一个,将其Caption改为“个人资料”,调整其大小和位置,使之可以框住姓名、性别、出生日期和婚姻状况等控件。
然后为各控件设置Tab Order。所谓Tab Order,是指在对话框中用Tab键选择控件的顺序。使用Tab键选择控件在鼠标发生故障,或大量键盘输入时特别有用。选择Developer Studio菜单的Layout/Tab Order选项,然后按最方便的顺序用鼠标逐一点击对话框模板上的各控件即可。
设计好对话框模板后,利用ClassWizard自动建立对话框类。用Ctrl+W键可直接调出ClassWizard,也可以通过Developer Studio菜单的View/ClassWizard…选项调出。进入ClassWizard后,它会发现已建立的对话框模板资源,并弹出一个对话框询问是否要为该对话框模板建立类。按下“OK”按钮,会弹出New Class对话框,填写类名(CEmpDlg)后按“OK”按钮即可为对话框建立一个类。
利用ClassWizard为对话框类添加与各控件对应的数据成员。选择Member Variables选项卡,确保Class Name项为新的对话框类,然后在选项卡下方的窗口中选择各控件的ID并按下“Add Variable…”按钮为其添加对应成员变量:
Control IDs Variable Type Member variable name
IDC_BIRTHDATE COleDateTime m_tBirthdate
IDC_DEPT CString m_strDept
IDC_MALE int m_nSex
IDC_MARRIED BOOL m_bMarried
IDC_NAME CString m_strName
IDC_POSITION CString m_strPosition
IDC_SALARY float m_fSalary
注意在添加成员变量对话框中的Category项均选择Value(值)并在Variable type组合框中选择相应的数据类型。注意编辑控件IDC_BIRTHDATE对应了一个COleDateTime类的数据成员。COleDateTime类是用于表示一个时间时间,包括日期和时刻。ColeDateTime类的成员函数很多,主要有:
COleDateTime ( int nYear,int nMonth,int nDay,int nHour,int nMin,int nSec );
// 根据给定值构造时间对象
static COleDateTime GetCurrentTime ( ); // 用当前系统时间填写对象
int GetYear ( ) const; // 取对象中的年值
int GetMonth ( ) const; // 取对象中的月值
int GetDay ( ) const; // 取对象中的日值
int GetHour ( ) const; // 取对象中的小时值
int GetMinute ( ) const; // 取对象中的分钟值
int GetSecond ( ) const; // 取对象中的秒值
int GetDayOfWeek ( ) const; // 取对象中的星球值
int GetDayOfYear ( ) const; // 取日相对上年末的天数
int SetDateTime ( int nYear,int nMonth,int nDay,int nHour,int nMin,int nSec );
// 用给定值填写对象
int SetDate ( int nYear,int nMonth,int nDay ); // 用给定日期填写对象
int SetTime ( int nHour,int nMin,int nSec ); // 用给定时间填写对象
CString Format ( LPCTSTR lpszFormat ) const; // 将对象值按指定格式写入字符串其中大部分的含义很明显,也可通过查询MSDN明确其用法。下面介绍最后一个函数,即将COleDateTime对象的值写入字符串函数的用法。该函数的参数lpszFormat是输出格式字符串,其中除了一般的字符信息外,还可加入形如“%A”的格式说明。常用的格式说明有:
格式说明 说明
%a 星期几的缩写
%A 星期几的全称
%b 月份名的缩写
%B 月份名的全称
%c 日期和时间的习惯表示
%d 日期 (01 – 31)
%H 24时制的小时 (00 – 23)
%I 12时制的小时 (01 – 12)
%j 当前日期与上年年底之间的天数 (001 – 366)
%m 数字表示的月份 (01 – 12)
%M 分钟 (00 – 59)
%p 12时制的时间(带A.M./P.M,表示)
%S 秒 (00 – 59)
%U 一年中的第几个星期 (00 – 53)
%w 星期几 (0 – 6; 星期日为0)
%W 一年中的第几个星期,以星期一作为星期的开始 (00 – 53)
%x 日期
%X 时间
%y 年份 (00 – 99)
%Y 年份(全4位)
%z,%Z 时区名或其简称
%% 百分号例如:
ColeTateTime tm (2000,3,16,10,30,0);
Cstring s;
s = tm.Format (“%Y.%m.%d (%A),%H,%M,%S”);
则字符串s中的内容为“2000.03.16 (Thursday),10,30,00”。
使用COleDateTime类要在头文件中添加文件包含命令:
#include <afxdisp.h>
如果添加的是对应编辑控件的成员变量,可在选项卡左下方输入数据检验信息。对于字符串变量,可输入最大字符个数;对于日期和数值变量,可分别输入上限值和下限值。
以上工作完成后,可检查一下由ClassWizard生成的CEmpDlg类的代码。CEmpDlg类的定义为:
class CEmpDlg,public CDialog
{
public:
CEmpDlg(CWnd* pParent = NULL); // standard constructor
// Dialog Data
//{{AFX_DATA(CEmpDlg)
enum { IDD = IDD_EMPLOYEE };
ColeDateTime m_tBirthdate;
CString m_strDept;
BOOL m_bMarried;
float m_fSalary;
Cstring m_strName;
int m_nSex;
CString m_strPosition;
//}}AFX_DATA
// Overrides
// ClassWizard generated virtual function overrides
//{{AFX_VIRTUAL(CEmpDlg)
protected:
virtual void DoDataExchange(CDataExchange* pDX); // DDX/DDV support
//}}AFX_VIRTUAL
// Implementation
protected:
// Generated message map functions
//{{AFX_MSG(CEmpDlg)
virtual BOOL OnInitDialog();
//}}AFX_MSG
DECLARE_MESSAGE_MAP()
};
对话框类的构造函数和DoDataExchange()成员函数也是由ClassWizard自动维护的:
CEmpDlg::CEmpDlg(CWnd* pParent /*=NULL*/)
,CDialog(CEmpDlg::IDD,pParent)
{
//{{AFX_DATA_INIT(CEmpDlg)
m_tBirthdate = COleDateTime::GetCurrentTime();
m_strDept = _T("");
m_bMarried = FALSE;
m_fSalary = 0.0f;
m_strName = _T("");
m_nSex = -1;
m_strPosition = _T("");
//}}AFX_DATA_INIT
}
void CEmpDlg::DoDataExchange(CDataExchange* pDX)
{
CDialog::DoDataExchange(pDX);
//{{AFX_DATA_MAP(CEmpDlg)
DDX_Text(pDX,IDC_BIRTHDATE,m_tBirthdate);
DDX_LBString(pDX,IDC_DEPT,m_strDept);
DDX_Check(pDX,IDC_MARRIED,m_bMarried);
DDX_Text(pDX,IDC_SALARY,m_fSalary);
DDV_MinMaxFloat(pDX,m_fSalary,0.f,10000.f);
DDX_Text(pDX,IDC_NAME,m_strName);
DDV_MaxChars(pDX,m_strName,20);
DDX_Radio(pDX,IDC_MALE,m_nSex);
DDX_CBString(pDX,IDC_POSITION,m_strPosition);
DDV_MaxChars(pDX,m_strPosition,20);
//}}AFX_DATA_MAP
}
其中构造函数中有CEmpDlg类各数据成员的初始化代码,DoDataExchange()函数中为这些数据成员与控件之间的数据交换和数据检验代码。
接着,还应编辑项目的菜单资源,在框架窗口的主菜单(IDR_MAINFRAME)中添加两个菜单选项,并为下拉菜单“编辑”添加三个菜单选项:
ID Caption Prompt
ID_NEXT 下一记录 下一职工档案\n下一记录
ID_PREV 上一记录 上一职工档案\n上一记录”
ID_APPEND 输入(&I)\tCtrl+I 输入新职工档案\n输入
ID_EDIT 编辑(&E)\tCtrl+E 编辑职工档案\n编辑
ID_DELETE 删除(&D)\tCtrl+D 删除职工档案\n删除然后利用ClassWizard为视图类添加与这些菜单选项对应的成员函数。进入在ClassWizard的Message Maps选项卡,选择Class Name项为CMyView,在Object IDs列表框中分别选择新添加的菜单选项的ID,在Messages列表框中选择COMMAND,按下Add Function按钮添加成员函数。ClassWizard会为这些菜单选项添加相应的消息响应函数(目前尚没有具体内容,需要程序员自行加入有关的处理代码),并将其声明加入CMyView类定义,在消息映射宏中加入相应的消息映射。
完成以上工作后,即可修改程序框架,添加必要的代码。
程 序:由于列表框尚未初始化,所以为CEmpDlg类重载OnInitDialog()成员函数(可使用ClassWizard完成),并添加相应代码:
BOOL CEmpDlg::OnInitDialog()
{
CListBox *pLB = (CListBox *)GetDlgItem(IDC_DEPT);
pLB->InsertString(-1,"办公室");
pLB->InsertString(-1,"开发部");
pLB->InsertString(-1,"生产部");
pLB->InsertString(-1,"销售部");
pLB->InsertString(-1,"人事部");
return CDialog::OnInitDialog();
}
其中GetDlgItem()为对话框类的成员函数,用于取对话框控件的指针。
为项目添加有关自定义的职工类CEmployee。选择Developer Studio菜单的Insert/New Class…选项,调出New Class对话框。在Class Type组合框中选择Generic(普通类),填写类名CEmployee,在对话框下方的Base class (es)框中输入基类CObject。
在Workspace窗口的Class View中选择生成的CEmployee类的定义,添加代码:
class CEmployee,public CObject
{
DECLARE_SERIAL(CEmployee)
public:
CString m_strName; // 姓名
int m_nSex; // 性别
COleDateTime m_tBirthdate; // 出生日期
BOOL m_bMarried; // 婚否
CString m_strDept; // 工作部门
CString m_strPosition; // 职务
float m_fSalary; // 工资
CEmployee(){}
CEmployee& operator = (CEmployee& e);
virtual ~CEmployee();
virtual void Serialize(CArchive &ar);
};
CEmployee类的对象即为一个职工的档案,我们用序列化实现文档的存取,所以要为CEmployee类编写序列化代码。这包括DECLARE_SERIAL()宏和IMPLEMENT_SERIAL()宏(在CEmployee类的源代码文件中),一个没有参数的构造函数,重载的赋值运算符和Serialize()成员函数。在CEmployee类的源代码文件中添加以下代码:
IMPLEMENT_SERIAL(CEmployee,CObject,1)
// 重载的赋值运算符
CEmployee& CEmployee::operator = (CEmployee& e)
{
m_strName = e.m_strName;
m_nSex = e.m_nSex;
m_tBirthdate = e.m_tBirthdate;
m_bMarried = e.m_bMarried;
m_strDept = e.m_strDept;
m_strPosition = e.m_strPosition;
m_fSalary = e.m_fSalary;
return *this;
}
// 序列化函数
void CEmployee::Serialize(CArchive& ar)
{
CObject::Serialize(ar);
if(ar.IsStoring())
{
ar << m_strName;
ar << m_nSex;
ar << m_tBirthdate;
ar << m_bMarried;
ar << m_strDept;
ar << m_strPosition;
ar << m_fSalary;
}
else
{
ar >> m_strName;
ar >> m_nSex;
ar >> m_tBirthdate;
ar >> m_bMarried;
ar >> m_strDept;
ar >> m_strPosition;
ar >> m_fSalary;
}
}
然后修改文档类CMyDocument类定义,添加一个CEmployee类的数组:
#include "employee.h"
#define MAX_EMPLOYEE 1000
class CMy1501Doc,public CDocument
{
DECLARE_DYNCREATE(CMy1501Doc)
public:
CEmployee m_empList[MAX_EMPLOYEE];
int m_nCount;
public:
virtual BOOL OnNewDocument();
virtual void Serialize(CArchive& ar);
virtual void DeleteContents();
DECLARE_MESSAGE_MAP()
};
为了节省篇幅,这段程序经过删节,与原来由AppWizard生成的程序有所不同。其中黑体部分为要添加的代码。注意重载成员函数DeleteContents()可以手工进行,也可以通过ClassWizard进行。Serialize()和DeleteContents()两个成员函数的代码如下:
void CMy1501Doc::Serialize(CArchive& ar)
{
if(ar.IsStoring())
ar << m_nCount;
else
ar >> m_nCount;
for(int i=0; i<m_nCount; i++)
m_empList[i].Serialize(ar);
}
void CMy1501Doc::DeleteContents()
{
m_nCount = 0; // 在打开文件和建立新文件时将数组大小置0
CDocument::DeleteContents();
}
即在文档类的Serialize()函数中,数据的序列化工作是通过调用Cemployee类的Serialize()函数实现的。
实际上,要为本程序添加的大部分代码均在视图类中。首先在视图类CmyView类的定义中添加一个用于记录当前操作的是哪个记录的数据成员:
int m_nCurrEmp;
并为视图类重载OnInitialUpdate()成员函数,在其中初始化该变量:
void CMy1501View::OnInitialUpdate()
{
CView::OnInitialUpdate();
m_nCurrEmp = 0;
Invalidate();
}
视图类的OnDraw()成员函数用于显示正在操作的职工档案:
void CMy1501View::OnDraw(CDC* pDC)
{
CMy1501Doc* pDoc = GetDocument();
ASSERT_VALID(pDoc);
// 显示职工人数和当前职工编号
CString s;
s.Format("职工人数,%d",pDoc->m_nCount);
pDC->SetTextColor(RGB(255,0,0));
pDC->TextOut( 40,40,s);
s.Format("职工编号,%d",m_nCurrEmp+1);
pDC->TextOut(340,40,s);
pDC->MoveTo( 40,70);
pDC->LineTo(600,70);
// 如果档案非空,显示当前记录
if(pDoc->m_nCount > 0)
{
// 显示栏目名称
pDC->SetTextColor(RGB(0,0,0));
pDC->TextOut(140,90,"姓 名:");
pDC->TextOut(140,130,"性 别:");
pDC->TextOut(140,170,"出生日期,");
pDC->TextOut(140,210,"婚姻状态:");
pDC->TextOut(140,250,"部 门:");
pDC->TextOut(140,290,"职 务:");
pDC->TextOut(140,330,"工 资:");
// 显示栏目内容
pDC->SetTextColor(RGB(0,0,255));
pDC->TextOut(300,90,pDoc->m_empList[m_nCurrEmp].m_strName);
if(pDoc->m_empList[m_nCurrEmp].m_nSex==0)
pDC->TextOut(300,130,"男");
else
pDC->TextOut(300,130,"女");
s = pDoc->m_empList[m_nCurrEmp].m_tBirthdate.Format("%Y.%m.%d");
pDC->TextOut(300,170,s);
if(pDoc->m_empList[m_nCurrEmp].m_bMarried)
pDC->TextOut(300,210,"已婚");
else
pDC->TextOut(300,210,"未婚");
pDC->TextOut(300,250,pDoc->m_empList[m_nCurrEmp].m_strDept);
pDC->TextOut(300,290,pDoc->m_empList[m_nCurrEmp].m_strPosition);
s.Format("%8.2f",pDoc->m_empList[m_nCurrEmp].m_fSalary);
pDC->TextOut(300,330,s);
}
}
在编辑资源时,我们框架窗口添加了5个菜单选项,并将对应的消息响应函数映射到了视图类中。这些消息响应函数的代码如下:
void CMy1501View::OnAppend()
{
CMy1501Doc* pDoc = GetDocument();
ASSERT_VALID(pDoc);
CEmpDlg dlg;
if(dlg.DoModal() == IDOK)
{
pDoc->m_nCount++;
m_nCurrEmp = pDoc->m_nCount-1;
pDoc->m_empList[m_nCurrEmp].m_strName = dlg.m_strName;
pDoc->m_empList[m_nCurrEmp].m_nSex = dlg.m_nSex;
pDoc->m_empList[m_nCurrEmp].m_tBirthdate = dlg.m_tBirthdate;
pDoc->m_empList[m_nCurrEmp].m_bMarried = dlg.m_bMarried;
pDoc->m_empList[m_nCurrEmp].m_strDept = dlg.m_strDept;
pDoc->m_empList[m_nCurrEmp].m_strPosition = dlg.m_strPosition;
pDoc->m_empList[m_nCurrEmp].m_fSalary = dlg.m_fSalary;
pDoc->SetModifiedFlag();
Invalidate();
}
}
// 删除当前记录
void CMy1501View::OnDelete()
{
CMy1501Doc* pDoc = GetDocument();
ASSERT_VALID(pDoc);
if(pDoc->m_nCount)
{
for(int i=m_nCurrEmp; i<pDoc->m_nCount-1; i++)
pDoc->m_empList[i] = pDoc->m_empList[i+1];
pDoc->m_nCount--;
if(m_nCurrEmp > pDoc->m_nCount-1)
m_nCurrEmp = pDoc->m_nCount-1;
pDoc->SetModifiedFlag();
Invalidate();
}
}
// 编辑当前记录
void CMy1501View::OnEdit()
{
CMy1501Doc* pDoc = GetDocument();
ASSERT_VALID(pDoc);
if(pDoc->m_nCount)
{
CEmpDlg dlg;
dlg.m_strName = pDoc->m_empList[m_nCurrEmp].m_strName;
dlg.m_nSex = pDoc->m_empList[m_nCurrEmp].m_nSex;
dlg.m_tBirthdate = pDoc->m_empList[m_nCurrEmp].m_tBirthdate;
dlg.m_bMarried = pDoc->m_empList[m_nCurrEmp].m_bMarried;
dlg.m_strDept = pDoc->m_empList[m_nCurrEmp].m_strDept;
dlg.m_strPosition = pDoc->m_empList[m_nCurrEmp].m_strPosition;
dlg.m_fSalary = pDoc->m_empList[m_nCurrEmp].m_fSalary;
if(dlg.DoModal() == IDOK)
{
pDoc->m_empList[m_nCurrEmp].m_strName = dlg.m_strName;
pDoc->m_empList[m_nCurrEmp].m_nSex = dlg.m_nSex;
pDoc->m_empList[m_nCurrEmp].m_tBirthdate = dlg.m_tBirthdate;
pDoc->m_empList[m_nCurrEmp].m_bMarried = dlg.m_bMarried;
pDoc->m_empList[m_nCurrEmp].m_strDept = dlg.m_strDept;
pDoc->m_empList[m_nCurrEmp].m_strPosition = dlg.m_strPosition;
pDoc->m_empList[m_nCurrEmp].m_fSalary = dlg.m_fSalary;
pDoc->SetModifiedFlag();
Invalidate();
}
}
}
// 查看后一记录
void CMy1501View::OnNext()
{
CMy1501Doc* pDoc = GetDocument();
ASSERT_VALID(pDoc);
if(pDoc->m_nCount > 1)
{
if(m_nCurrEmp == pDoc->m_nCount-1)
m_nCurrEmp = 0;
else
m_nCurrEmp++;
}
Invalidate();
}
// 查看前一记录
void CMy1501View::OnPrev()
{
CMy1501Doc* pDoc = GetDocument();
ASSERT_VALID(pDoc);
if(pDoc->m_nCount > 1)
{
if(m_nCurrEmp == 0)
m_nCurrEmp = pDoc->m_nCount-1;
else
m_nCurrEmp--;
}
Invalidate();
}
输入输出:用户可选择框架窗口的菜单“编辑”中的选项“输入”和“编辑”调出档案编辑对话框输入一个职工档案或编辑当前职工档案。用“删除”选项可删除当前职工档案。当前职工档案显示在框架窗口的客户区,可用菜单选项“上一记录”和“下一记录”改变当前职工档案。可通过“文件”菜单对职工档案文件进行存取。
分 析:部门列表框控件的列表内容应在程序中给出。在CEmpDlg类的成员函数OnInitDialog()中声明一个指向CListBox类的指针,然后用对话框类的成员函数GetDlgItem()将其与对话框模板中的列表框控件IDC_DEPT联系起来,并使用CListBox类的成员函数InsertString()为列表框添加列表内容。
在视图类中有一个数据成员m_nCurrEmp,用于指示当前职工档案。实际上,“上一记录”和“下一记录”等命令消息响应函数就是通过对这个变量的操作来实现的。
插入的新记录放在数组的最后面。
插入新记录和编辑当前记录均通过一个CEmpDlg类的对象实现。在编辑当前记录前,要将当前职工档案的有关内容复制到对话框对象的有关数据成员中。
删除当前记录是通过将当前记录后面的所有记录均向前移一个实现的。
14.2 基于对话框的应用程序
对话框不仅可以作为应用程序中的一个部件,而且还可以建立以对话框为主界面窗口的应用程序,称为基于对话框的应用程序。基于对话框的应用程序结构适合于比较简单的应用,例如Windows提供的Character Map(字符映射表),Calculator(计算器)和Phone Dialer(电话拨号程序)等实用程序。
基于对话框的应用程序的结构与基于框架窗口的应用程序结构类似,只是使用CDialog派生类对象代替了CFrameWnd派生类对象作为主窗口。由于CDialog也是CWnd派生类,所以也继承了CWnd类的所有成员函数。不同的是,窗口的大小可以在运行时动态调整,客户区为白色,有边框;而对话框的大小固定,无明显的客户区边框(除标题栏外的所有灰色部分均为客户区)。在基于对话框的应用程序中,除了可以使用各种控件外,还可响应鼠标或键盘消息,使用OnPaint()函数在对话框中进行绘图或输出文字信息。
基于对话框的应用程序框架可使用AppWizard建立。
[例15-2] 用基于对话框的应用程序结构实现例14-5的彩色吹泡泡程序。由于对话框本身结构简单,没有明显的客户区,颜色也不醒目,所以我们在对话框上自行建立一个矩形区域作为吹泡泡的客户区,并通过一个“颜色设置”按钮来设置泡泡的颜色。
说 明:用AppWizard建立一个基于对话框的应用程序框架(参看15.4:“用AppWizard生成基于对话框的应用程序”),所有设置均使用缺省值。
使用对话框模板编辑器编辑作为主界面窗口的对话框模板,将其上的静态文本控件和“Cancel”按钮删除,将“OK”按钮的Caption设置为“完成”,并将对话框大小调整为400×300左右。
为对话框模板添加一个Picture控件,将其Type设置为Frame,Color设置为Black,并设置Sunken属性(在Styles选项卡中)。调整其位置为(7,7),大小为287×287。这个框中即为自定义的吹泡泡客户区,所有的吹泡泡活动均在该区域中进行。
为对话框模板添加一个按钮,将其ID改为IDC_COLOR,Caption改为“颜色设置”。
使用ClassWizard为对话框类添加一个鼠标左键消息响应函数OnLButtonDown()和一个按钮命令消息响应函数OnColor()。
程 序:在对话框类的头文件前面添加一行:
#define MAX_BUBBLE 250
并在对话框类定义中添加存放泡泡的几何参数和颜色的数组数据成员:
CRect m_rectBubble[MAX_BUBBLE];
COLORREF m_colorBubble[MAX_BUBBLE];
int m_nBubbleCount;
以及一个存放自定义客户区矩形的数据成员和一个存放当前泡泡颜色设置的数据成员:
CRect m_rectClient;
COLORREF m_colorCurrent;
修改对话框类的OnInitDialog()成员函数,添加计算自定义客户区位置和大小的代码,并将泡泡的数目初始化为0:
BOOL CMyDlg::OnInitDialog()
{
CDialog::OnInitDialog();
CStatic *pST = (CStatic *)GetDlgItem(IDC_CLIENT);
pST->GetWindowRect(&m_rectClient);
ScreenToClient(&m_rectClient);
m_nBubbleCount = 0;
return TRUE;
}
修改OnPaint()成员函数,添加画出泡泡的有关代码:
void CMyDlg::OnPaint()
{
CPaintDC dc(this);
CRgn rgn;
rgn.CreateRectRgnIndirect(&m_rectClient); // 生成一个区域对象
dc.SelectClipRgn(&rgn); // 选择区域
dc.Rectangle(m_rectClient); // 将客户区背景设置
CBrush brushNew,*pbrushOld; // 白色
for(int i=0; i<m_nBubbleCount; i++)
{
brushNew.CreateSolidBrush(m_colorBubble[i]);
pbrushOld = dc.SelectObject(&brushNew);
dc.Ellipse(m_rectBubble[i]);
dc.SelectObject(pbrushOld);
brushNew.DeleteObject();
}
}
修改由ClassWizard生成的鼠标左键消息响应函数OnLButtonDown(),添加吹泡泡的有关代码:
void CMyDlg::OnLButtonDown(UINT nFlags,CPoint point)
{
if(m_nBubbleCount < MAX_BUBBLE)
{
int r = rand()%50+10;
CRect rect(point.x-r,point.y-r,point.x+r,point.y+r);
m_rectBubble[m_nBubbleCount] = rect;
m_colorBubble[m_nBubbleCount] = m_colorCurrent;
m_nBubbleCount++;
InvalidateRect(rect,FALSE);
}
}
最后修改由ClassWizard生成的按钮消息响应函数OnColor(),添加调用颜色设置公用对话框的代码:
void CMyDlg::OnColor()
{
m_colorCurrent = RGB(200,200,200);
CColorDialog dlg(m_colorCurrent);
if(dlg.DoModal() == IDOK)
m_colorCurrent = dlg.GetColor();
}
输入输出:鼠标左键点击对话框中的客户区可生成有关泡泡,按下按钮“颜色设置”可调用颜色设置公用对话框设置泡泡的颜色(图15-2)。
分 析:在OnInitDialog()函数中,对应了一个指向CStatic对象的指针pST并使之与对话框模板上的图片控件IDC_CLIENT联系起来。通过该指针调用成员函数GetWindowRect()(实际上是从CWnd类继承来的)取得该控件的矩形框。由于该矩形采用屏幕坐标,所以还要使用ScreenToClient()成员函数将其转换为对对话框的相对坐标。
在OnPaint()函数中,首先根据图片控件的参数建立一个CRgn类的对象,然后使用CDC类的成员函数SelectClipRgn()使裁剪区域生效。由于图片控件的内部仍为对话框的底色(灰色),因此在其相应位置上画了一个缺省设置矩形,使客户区变为白色。
自学内容
15.3 动画控件
动画控件用来在一块矩形区域中播放简单的AVI动画文件。动画控件可在编辑对话框模板时加入对话框,其主要属性有:
ID 说明 设置方法
ACS_CENTER 动画在控件中央播放 Styles/Center
ACS_TRANSPARENT 透明背景 Styles/Transparent
ACS_AUTOPLAY 打开后自动播放 Styles/Auto play
动画控件对应CAnimateCtrl类对象。CAnimateCtrl类的主要成员函数有:
1.打开AVI文件成员函数,原型为:
BOOL CAnimateCtrl::Open (LPCTSTR lpszFileName);
BOOL CAnimateCtrl::Open (UINT nID);
其中参数lpszFileName为AVI文件名,nID为动画资源的ID。
2.播放动画成员函数,原型为:
BOOL CAnimateCtrl::Play (UINT nFrom,UINT nTo,UINT nRep);
其中参数nFrom为起始位置,nTo为终止位置,若取0xFFFF为播放到结束为止。nRep为播放次数,若取0XFFFF则为循环播放。
3.停止播放动画函数,原型为:
BOOL CAnimateCtrl::Stop();
4.查找指定画面函数,原型为:
BOOL CAnimateCtrl::Seek(UINT nTo);
其中参数nTo为指定位置。
5.关闭动画函数,原型为:
BOOL CAnimateCtrl::Close();
[例15-3] 动画播放器程序,可用文件查找公用对话框打开AVI文件并播放。
说 明:用AppWizard生成一个基于对话框的应用程序框架并编辑对话框模板资源。首先删除原来所有的控件,然后为其添加一个动画控件,设置其ID为IDC_AVI和属性Styles/Center,然后适当调整其大小和位置。
为对话框模板添加3个按钮,其属性如下:
ID Caption 消息响应函数
IDC_PLAY Play OnPlay()
IDC_OPEN Open OnOpen()
IDC_STOP Stop OnStop()
图15-3 动画播放器
用ClassWizard为动画控件添加一个CAnimateCtrl类的数据成员m_ctrAvi,并为3个按钮添加对应的消息响应函数。编辑好的对话框模板如图15-3所示。
程 序:首先在对话框类定义中添加一个存放AVI文件名的变量:
CString m_strAviname;
并在OnInitDialog()函数中添加初始化代码:
m_strAviname = _T(“”);
修改对应于3个按钮的消息响应函数:
void CMy1503Dlg::OnOpen()
{
CFileDialog dlg(TRUE,".AVI","*.AVI",
OFN_FILEMUSTEXIST|OFN_LONGNAMES|OFN_PATHMUSTEXIST,
"*.AVI",this);
if(dlg.DoModal() == IDOK)
{
m_ctrAvi.Close();
m_strAviname = dlg.GetPathName();
m_ctrAvi.Open(LPCTSTR(m_strAviname));
}
}
void CMy1503Dlg::OnPlay()
{
m_ctrAvi.Play(0,0xffff,0xffff);
}
void CMy1503Dlg::OnStop()
{
m_ctrAvi.Stop();
}
输入输出:按下Open按钮可调出一个文件搜索公用对话框,找到一个AVI文件后可进行播放。按下Play按钮后可重复播放动画,按下Stop按钮后可停止播放。
调试技术
15.4 用AppWizard生成基于对话框的应用程序用AppWizard创建基于对对话框的应用程序比较简单:使用Developer Studio的菜单选项File/New…,在New对话框中的Projects选项卡中选用MFC AppWizard(exe)并填写Project Name(项目名),然后按OK按钮。在AppWizard第1步对话框中,选择生成Dialog Based(基于对话框)的应用程序即可,以下步骤与生成基于文档/视图结构的应用程序类似,只是更简单些。生成基于对话框的应用程序只需4步,第2步用于设置应用程序的属性,包括是否使用版权对话框、是否需要生成上下文有关的帮助、是否使用3D风格的控件,以及是否需要对ActiveX控件的支持等问题。通常这些属性均可使用缺省设置。第3步用于设置程序风格,第4步用于检查和修改拟为应用程序生成的类的有关参数,这两步与生成基于文档/视图结构的应用程序框架中的对应步骤相同。
在AppWizard生成源代码基础上开发程序非常容易,例如OnInitDialog()函数,只需在该函数的“TODO”行后添加自己的初始化代码即可。程序员所要做的主要是编辑对话框模板资源,利用ClassWizard为对话框类添加相应的数据成员和消息响应函数,以及为这些函数添加必要的程序代码。
程序设计举例
[例15-4] 编写一个计算器程序。该计算器使用编辑控件直接输入数据,并有“加”、“减”、“乘”、“除”、“平方根”和“倒数”计算功能,如图15-4。
说 明:用AppWizard生成一个基于对话框的应用程序框架。编辑对话框模板资源,删除静态文本和“Cancel”按钮,添加一个编辑控件,将其ID改为IDC_INPUT,设置属性Align text为right(在Styles选项卡中)。添加8个按钮,将其ID和Caption分别改为:
图15-4 计算器
ID Caption
IDC_ADD +
IDC_SUB (
IDC_MUL *
IDC_DIV /
IDC_CLEAR C
IDC_SQRT SQRT
IDC_CALC =
IDC_RECIPROCAL 1/X
并修改对话框模板和以上各控件的尺寸、位置,使之看起来象图15-3那样。
使用ClassWizard为编辑控件IDC_INPUT在对话框类中添加一对应数据成员:
double m_fInput;
并为该控件的消息EN_SETFOCUS添加一映射函数OnSetfocusInput()。每当该控件获得焦点时(如用户用鼠标点击该控件)发出这个消息。
用ClassWizard为新添的8个按钮添加鼠标点击命令函数:OnAdd(),OnSub(),OnMul(),OnDiv(),OnClear(),OnCalc(),OnReciprocal()和OnSqrt()。
程 序:在对话框类中添加以下数据成员:
int m_nOP; // 运算符
double m_fResult; // 运算中间结果并在OnInitDialog()函数中添加相应的初始化代码:
m_fResult = 0.0;
m_nOP = 0;
为对话框类添加一个Calc()成员函数:
void CMy1504Dlg::Calc() // 计算
{
UpdateData(TRUE);
switch(m_nOP)
{
case 0: // 第1运算对象
m_fResult = m_fInput;
break;
case 1: // +
m_fResult += m_fInput;
break;
case 2: // -
m_fResult -= m_fInput;
break;
case 3: // *
m_fResult *= m_fInput;
break;
case 4: // /
m_fResult -= m_fInput;
break;
case 5: // 1/X
m_fResult = 1/m_fInput;
break;
case 6: // sqrt(X)
m_fResult = sqrt(m_fInput);
break;
}
m_fInput = m_fResult;
UpdateData(FALSE);
}
最后为所有按钮的消息响应函数添加代码:
void CMy1504Dlg::OnAdd() // 加法
{
Calc();
m_nOP = 1;
}
void CMy1504Dlg::OnSub() // 减法
{
Calc();
m_nOP = 2;
}
void CMy1504Dlg::OnMul() // 乘法
{
Calc();
m_nOP = 3;
}
void CMy1504Dlg::OnDiv() // 除法
{
Calc();
m_nOP = 4;
}
void CMy1504Dlg::OnReciprocal() // 倒数
{
m_nOP = 5;
Calc();
m_nOP = 0;
}
void CMy1504Dlg::OnSqrt() // 平方根
{
m_nOP = 6;
Calc();
m_nOP = 0;
}
void CMy1504Dlg::OnSetfocusInput() // 为输入数据做准备
{
m_fInput = 0.0;
UpdateData(FALSE);
}
void CMy1504Dlg::OnClear() // 清除
{
m_fResult = 0.0;
m_fInput = 0.0;
m_nOP = 0;
UpdateData(FALSE);
}
void CMy1504Dlg::OnCalc() // 显示计算结果
{
Calc();
m_nOP = 0;
}
输入输出:在编辑框中可直接输入数据,按下“+”、“(”、“*”、“/”、“SQRT”和“1/X”等按钮可执行相应运算,按下“=”按钮得到运算结果,按下“C”按钮清除。
分 析:对话框类的成员函数UpdateData()用于更新数据。其原型为:
BOOL UpdateData( BOOL bSaveAndValidate = TRUE );
其中参数bSaveAndValidate为TRUE时用控件中的值更新对话框类的相应数据成员;为FALSE时用数据成员的值更新对话框模板上的相应控件。
单元上机练习题目修改例15-1的人事管理系统,增添根据人名查询的功能。
仿照例15-1设计一个图书卡片管理系统。图书卡片应包含下列内容:书名,分类号,作者,出版社,出版日期,单价,关键词,摘要。其中摘要最好使用多行编辑控件。
将例11-7的拼图游戏程序改编为基于对话框的程序。