VC++深入详解(5):MFC对话框(一)

对话框可以分为两大类:模态对话框和非模态对话框。模态对话框是指,当其显示时,程序会暂停执行,直到关闭这个对话框之后,才能继续执行程序中的其他任务。非模态对话框是指,当对话框显示时,允许转而执行程序中的其他任务,而可以不理会这个对话框。
对话框也是一种资源,可以在资源视图中新建一个对话框来实现。新建的对话框已经包含了2个按钮,确定和取消。
在MFC中,对资源的操作都是通过一个与资源相关的类来完成的。与话框通相关的是CDialog类。当我们使用类向导时,编译器自动检测出我们添加了一个资源,提示我们是否需要为他创建一个类,然后编译器会自动将我们的资源,以及基类选择好,我们只需要填写类名就可以了,我们起名为CTestDlg。编译器会自动的帮我们填写对应的源文件和头文件名,如果我们的类名很长,那么文件名也很长,可以改变。
此时,编译器为这个类编写了两个函数:1.构造函数2.DoDataExchange,用来完成数据的交换和校验。
下面我们希望在程序中显示这个对话框,最简单的做法:增加一个菜单项,点击菜单显示对话框。

我们我们在菜单中添加一个菜单项,然后在VIEW类中为其添加消息响应函数:建立一个CTestDlg对象,调用DoModal函数来实现模态对话框:

void CCH_7_DialogView::OnDialog() 
{
	// TODO: Add your command handler code here
	CTestDlg dlg;
	dlg.DoModal();
}
注意,我们还得自己包含#include "TestDlg.h",因为这句代码类向导并没有帮我们添加。
非模态对话框使用的是Create 创建的,注意,创建完成后必须调用ShowWindow函数来显示对话框。
	CTestDlg dlg;
	dlg.Create(IDD_DIALOG1,this);
	dlg.ShowWindow(SW_SHOW


可是程序编译后依然没有显示,这是为什么呢?因为这里的dlg是一个局部变量,程序执行完以后,就析构了。那为什么前面的模态对话框就可以呢?因为模态对话框创建之后,程序就停在了这里,所以他的生命周期并没有结束。那么怎么解决呢?
1.把这个对话框变量定义为局部变量。
2.定义一个指针,指向堆上的数据:
	CTestDlg *pDlg = new CTestDlg;
	pDlg->Create(IDD_DIALOG1,this);
	pDlg->ShowWindow(SW_SHOW);
虽然这样做可以实现功能,但是并不好,因为pDlg会在程序结束时被释放,所以我们以后就拿不到这个指针了,自然也就无法释放它,会造成内存泄露。这个问题可以通过将指针定义为类的成员变量来解决。
还有一点区别在于:对于模态对话框,点击“确定”后,对话框是被“销毁了”;而对于非模态对话框,则是“隐藏了”,你需要重写虚函数OnOK,并在其中调用DestroyWindow ,基类的OnOK只是调用了EndDialog来隐藏对话框。


下面实现一个功能:单击对话框的按钮时,在对话框中动态的创建一个新的按钮。
这里为了方便,使用模态对话框。我们回到资源视图,打开原来的对话框,从工具箱中托一个按钮到对话框上,修改的它的ID为IDC_BTN_ADD,名字为Add。然后右键点击它,选择类向导,然后为其添加消息响应函数OnBtnAdd:
void CTestDlg::OnBtnAdd() 
{
	// TODO: Add your control notification handler code here
	m_btn.Create("TEST",BS_DEFPUSHBUTTON | WS_VISIBLE|WS_CHILD,
		CRect(0,0,100,100),this,123);
}
我们发现,程序运行时,当我们点击Add按钮,的确会显示一个新的按钮,但是我们再次点击,程序就会错误,这是因为,当点击时,会调用OnBtnAdd,这个函数调用Create函数创建了一个按钮;但是如果你又点击一次,那么还会调用Create函数将按钮与m_btn关联,可是m_btn已经和第一次创建的按钮相关联了,所以会出错。为此,我们可以增加一个成员变量m_bIsCreate来记录窗口是否已经创建,如果创建,那么就会销毁窗口,而在下一次点击时,又能创建了:
void CTestDlg::OnBtnAdd() 
{
	// TODO: Add your control notification handler code here
	if(m_bIsCreate == FALSE)
	{
		m_btn.Create("TEST",BS_DEFPUSHBUTTON | WS_VISIBLE|WS_CHILD,
			CRect(0,0,100,100),this,123);
		m_bIsCreate = TRUE;
	}
	else
	{
		m_btn.DestroyWindow();
		m_bIsCreate = FALSE;
	}

}
其实这里还有一个简单的办法,并不使用成员变量,而是用静态局部变量也能解决这个问题。
实际上,有一种更为直接的方法解决这个问题:CWnd有一个成员变量m_hWnd指向窗口句柄,我们可以利用这个句柄是否为空来判断:
	if(!m_btn.m_hWnd)
	{
		m_btn.Create("TEST",BS_DEFPUSHBUTTON | WS_VISIBLE|WS_CHILD,
			CRect(0,0,100,100),this,123);
	}
	else
	{
		m_btn.DestroyWindow();
	}

下面我们我们看看控件的访问:
我们给对话框上添加3个静态文本框没把他们改名为Number1,Number2,Number3;再添加3个编辑框。我们可以通过Layout菜单提供的功能来将它们对其。
首先我们实现一个简单的功能:当点击Number1以后,将它的显示变为“数字1”:
大的思路肯定是对这个控件响应BN_CLICKED消息。可是我们发现,这个控件的ID是IDC_STATIC,但是却在类向导中根本找不见这个ID,原因是因为这个所有的静态文本框都是这个ID。实际上,这个控件一般是用来当做标签用的,并不是用来响应消息的。但是如果我们非要让它响应消息,也是可以的,但是需要改变它的标签,我们改为IDC_NUMBER1,然后为其添加消息响应函数:
首先,按钮也是一个窗口,我们先要获得这个静态文本框,然后再获得它上面的文本。然后把文本重新设置一下:

void CTestDlg::OnNumber1() 
{
	// TODO: Add your control notification handler code here
	CString str;
	if(GetDlgItem(IDC_NUMBER1)->GetWindowText(str),str == "Number1")
	{
		GetDlgItem(IDC_NUMBER1)->SetWindowText("数值1");
	}
	else
	{
		GetDlgItem(IDC_NUMBER1)->SetWindowText("Number1");
	}
}
但是程序运行以后并没有反应,原因是因为静态文本控件在默认状态下,是不接收通告消息的:我们得在这个控件的属性->样式中,把通知选上,就行了。可见,为了让静态文本框响应消息,需要两个特殊的步骤:1.改变它的ID。2.在它的样式中,选择通知消息。


下面实现一个稍微复杂一点的功能:在两个编辑框中输入数字,然后点击Add按钮,在第三个编辑框中显示结果。
实现的方法有很多种,我们一种一种介绍:
第1种:

void CTestDlg::OnBtnAdd() 
{
	// TODO: Add your control notification handler code here
	int num1,num2,num3;
	char ch1[10],ch2[10],ch3[10];
	GetDlgItem(IDC_EDIT1)->GetWindowText(ch1,10);
	GetDlgItem(IDC_EDIT2)->GetWindowText(ch2,10);
	num1 = atoi(ch1);
	num2 = atoi(ch2);
	num3 = num1 + num2;
	itoa(num3,ch3,10);
	GetDlgItem(IDC_EDIT3)->SetWindowText(ch3);
}
这种方法思路清晰:定义的3个字符串和3个int。将编辑框中的字符串获取出来以后转化为int,然后做加法,做完以后转化回去,然后显示在编辑框上。


第二种:

	int num1,num2,num3;
	char ch1[10],ch2[10],ch3[10];
	GetDlgItemText(IDC_EDIT1,ch1,10);
	GetDlgItemText(IDC_EDIT2,ch2,10);
	num1 = atoi(ch1);
	num2 = atoi(ch2);
	num3 = num1 + num2;
	itoa(num3,ch3,10);
	SetDlgItemText(IDC_EDIT3,ch3);

其实跟第一种差不多,但是通过GetDlgItemText和SetDlgItemText完成了第一种中需要两步才能完成工作。


第三种:
使用GetDlgItemInt 函数,这个函数直接通过控件ID获取空间的文本,并把它转化为int类型,然后使用SetDlgItemInt函数:

	int num1,num2,num3;
	num1 = GetDlgItemInt(IDC_EDIT1);
	num2 = GetDlgItemInt(IDC_EDIT2);
	num3 = num1 + num2;
	SetDlgItemInt(IDC_EDIT3,num3);
第四种:
将编辑框分对话框类的三个成员变量相关,然后通过成员变量来检索和设置编辑框的文本。
首先,先要为这三个编辑框关联3个成员变量:在类视图下的建立向导类中,选择CTestDlg类,依次选择
IDC_EDIT1、IDC_EDIT2、IDC_EDIT3,为它们添加int型的变量m_num1、m_num2、m_num3。我们发现classWizzard为我们添加3处代码:
在头文件中,注释宏之间:
// Dialog Data
	//{{AFX_DATA(CTestDlg)
	enum { IDD = IDD_DIALOG1 };
	int		m_num1;
	int		m_num2;
	int		m_num3;
	//}}AFX_DATA
源文件的构造函数中:

CTestDlg::CTestDlg(CWnd* pParent /*=NULL*/)
	: CDialog(CTestDlg::IDD, pParent)
{
	//{{AFX_DATA_INIT(CTestDlg)
	m_num1 = 0;
	m_num2 = 0;
	m_num3 = 0;
	//}}AFX_DATA_INIT
}
在源文件的DoDataExchange中:

void CTestDlg::DoDataExchange(CDataExchange* pDX)
{
	CDialog::DoDataExchange(pDX);
	//{{AFX_DATA_MAP(CTestDlg)
	DDX_Text(pDX, IDC_EDIT1, m_num1);
	DDX_Text(pDX, IDC_EDIT2, m_num2);
	DDX_Text(pDX, IDC_EDIT3, m_num3);
	//}}AFX_DATA_MAP
}

其中DDX_Text函数完成数据转化的功能。
我们发现,程序运行时,编辑框里的值被自动的设为0了。这个映射关系时通过DoDataExchange函数来实现的。如果这么方便,我们在OnBtnAdd函数中添加一句代码就够了:

void CTestDlg::OnBtnAdd() 
{
	m_num3 = m_num1 + m_num2;
}

可是编译后发现运行时不能显示正确的结果,这是为什么呢?通过单步调试,我们可以发现:不论我们的输入是多少,执行到m_num3 = m_num1 + m_num2时,m_num1 和 m_num2的值总是为0!看来我们有必要仔细看看DoDataExchange函数,通过MSDN,我们可以看出,这个函数不是直接调用的,而是通过UpdateData 函数调用的。而UpdateData 函数有一个参数:当他为true时(默认值),是从对话框获取数据,当他为false时,向对话框写数据。于是我们只要改为下面的代码就行了:

	UpdateData();
	m_num3 = m_num1 + m_num2;
	UpdateData(FALSE);

假设我们向带画框输入一个字符,那么点击Add的时候,它会提示你请输入一个整数;甚至,在类向导中,你可以设置整数的范围。如果越界也会提示错误。


第五种方式:
将编辑框与控件变量相关联。我们这里选择的是CEdit类型。有了这些成员变量,我们就不需要第一种方法中的GetDlgItem(IDC_EDIT1)步骤了,直接用这些变量来调用GetWindowText或者SetWindowText就行了。
	int num1,num2,num3;
	char ch1[10],ch2[10],ch3[10];
	m_edit1.GetWindowText(ch1,10);
	m_edit2.GetWindowText(ch2,10);

	num1 = atoi(ch1);
	num2 = atoi(ch2);
	num3 = num1 + num2;
	itoa(num3,ch3,10);
	
	m_edit3.SetWindowText(ch3);
第六种方式:
通过消息。通过向编辑框发送指定的消息(获取文本WM_GETTEXT、设置文本WM_SETTEXT),来实现:
	int num1,num2,num3;
	char ch1[10],ch2[10],ch3[10];
	::SendMessage(m_edit1.m_hWnd,WM_GETTEXT,10,
		(LPARAM)ch1);
	::SendMessage(m_edit2.m_hWnd,WM_GETTEXT,10,
		(LPARAM)ch2);
	num1 = atoi(ch1);
	num2 = atoi(ch2);
	num3 = num1 + num2;
	itoa(num3,ch3,10);
	m_edit3.SendMessage(WM_SETTEXT,0,(LPARAM)ch3);

注意:
1.SendMessage是SDK下的函数,所以调用前需要加上::.
2.发送WM_GETTEXT消息时,wParam指明最大字节数,lParam填字符串的地址,但是需要强制类型转化。
3.发送WM_SETTEXT消息时,wParam不使用,填为0,lParam填字符串的地址,但是需要强制类型转化。
第七种方式:
直接给对画框的子控件发送消息:

	int num1,num2,num3;
	char ch1[10],ch2[10],ch3[10];

	SendDlgItemMessage(IDC_EDIT1,WM_GETTEXT,10,	(LPARAM)ch1);
	SendDlgItemMessage(IDC_EDIT2,WM_GETTEXT,10,	(LPARAM)ch2);
	num1 = atoi(ch1);
	num2 = atoi(ch2);
	num3 = num1 + num2;
	itoa(num3,ch3,10);
	SendDlgItemMessage(IDC_EDIT3,WM_SETTEXT,0,	(LPARAM)ch3);
使用成员函数,就省去了获取编辑框的工作。


如果想获取编辑框中的一部分文本,可以通过发送EM_SETSEL消息来获得,其中wParam表示起始位置,lParam表示结束位置。但是如果你想获取一部分文本,那么你的焦点必须设在这个文本框上:
	SendDlgItemMessage(IDC_EDIT3,EM_SETSEL,1,2);
	m_edit3.SetFocus();

总结起来,7中方法中:1、4、5比较常用。


下面我们看另外一个例子:对话框伸缩功能的实现。
这个功能其实也很常见,比如windowsXP下的画图中的颜色菜单中的编辑颜色,点击规定自定义颜色时,这个对话框就被扩充起来了。通常对于一些用户不太需要的功能,可以把它们放在隐藏的区域内,这样能让用户考虑主要的问题。
我们考虑如何设计这个问题:首先肯定是有一个按钮(名字为“收缩”),点击一下以后之后,对话框收缩一部分,按钮的名字改为“扩张”。我们先搞定点击之后名字转化的问题:

void CTestDlg::OnButton1() 
{
	// TODO: Add your control notification handler code here
	CString str;
	if(GetDlgItemText(IDC_BUTTON1,str), str == "收缩")
	{
		SetDlgItemText(IDC_BUTTON1,"扩张");
	}
	else
	{
		SetDlgItemText(IDC_BUTTON1,"收缩");
	}
}
接下来的才是重头戏,这种对话框分割的秘密在于:在原来的对话框上添加了一个看不见的,比较细的图像控件(一般是矩形),通过这个矩形确定点击收缩、扩张之后,对话框的位置。
我们先将这个图像控件改名为IDC_SEPERATOR,然后将它的样式改为下陷。

	static CRect rectLarge;
	static CRect rectSmall;
	if(rectLarge.IsRectNull())
	{

		CRect rectSeparator;
		GetWindowRect(&rectLarge);
		GetDlgItem(IDC_SEPARATOR)->GetWindowRect(&rectSeparator);

		rectSmall.left = rectLarge.left;
		rectSmall.top = rectLarge.top;
		rectSmall.right = rectLarge.right;
		rectSmall.bottom = rectSeparator.bottom;
	}

	if(str == "收缩")
	{
		SetWindowPos(NULL,0,0,rectSmall.Width(),rectSmall.Height(),
			SWP_NOMOVE | SWP_NOZORDER );
	}
	else
	{
		SetWindowPos(NULL,0,0,rectLarge.Width(),rectLarge.Height(),
			SWP_NOMOVE | SWP_NOZORDER );		
	}

在处理函数中,我们定义了3个矩形,rectLarge是对话框的原始尺寸,rectSmall是点击收缩以后对话框的尺寸,而rectSeparator则是我们添加的一条细线的尺寸。rectSmall的left、top、right都等于rectLarge,只是把它的button改为rectSeparator的button即可。
获取了窗口的大小,我们只需要使用SetWindowPos重新设置窗口的位置,大小就行了。

我们的程序有一点小瑕疵,当输入一个数字以后,按一下回车结果程序就窗口就关闭了,我们希望按下回车后能转到第二编辑框继续输入。这该怎么办呢?
首先解决第一个问题,为什么按一下回车就关闭了。我们看一下确定按钮的属性,在style里面看到,它是默认选中的。所以如果我们按了回车,就会由确定键的消息响应函数来处理。虽然我们没有写,但是基类中OnOK函数却写了,作用就是关闭对话框。
所以,如果我们希望实现自己的功能,那么得在派生类中重写OK的消息响应函数。我们为其添加响应函数时,类向导会默认的帮我们添加函数,调用基类的成分:
void CTestDlg::OnOK() 
{
	// TODO: Add extra validation here
	
	CDialog::OnOK();
}

如果我们把它注释起来,点击ok就不会关闭窗口了。
那么如何实现回车以后把输入焦点转移到第二个编辑框中呢?
有两种大的思路:1.让编辑框控件生成一个相关联的类,然后利用这个类来出来键盘消息;2.修改编辑框控件的窗口过程函数,在这个函数中判断,如果输入回车,则移动到下一个编辑框。
我们采用第二种思路。首先,改变窗口的消息处理函数的代码应该放在哪里?肯定不能放在create下面,因为此时对话框还没有创建完成呢。实际上,当对话框及其子控件创建完成,显示之前会发送消息:WM_INITDIALOG,我们为CTestDlg添加这个消息的响应函数。

WNDPROC prevProc;//声明先前的消息响应函数

//新的消息处理函数
LRESULT CALLBACK NewEditProc(
  HWND hwnd,      // handle to window
  UINT uMsg,      // message identifier
  WPARAM wParam,  // first message parameter
  LPARAM lParam   // second message parameter
)
{
	if(uMsg == WM_CHAR && wParam == 0x0d)
	{
		::SetFocus(GetNextWindow(hwnd,GW_HWNDNEXT));
		return 1;
	}
	else
	{
		return prevProc(hwnd,uMsg,wParam,lParam);
	}
}

其实这个函数并不复杂,如果消息是回车,那么把焦点设置为下一个窗口。如果不是,则调用原来的消息处理函数来处理。原来的处理函数是哪里来的呢?

BOOL CTestDlg::OnInitDialog() 
{
	CDialog::OnInitDialog();
	
	// TODO: Add extra initialization here
	prevProc = (WNDPROC)SetWindowLong(GetDlgItem(IDC_EDIT1)->m_hWnd,
		GWL_WNDPROC,(LONG)NewEditProc);
	return TRUE;  // return TRUE unless you set the focus to a control
	              // EXCEPTION: OCX Property Pages should return FALSE
}
SetWindowLong函数的返回值就是了!
唯一需要注意的是得给编辑框选上多行选项。因为没有这个选项,编辑框就只能接受一行消息,所以就不能接受回车键了。
我们也注意到,我们只是为第一个编辑框写了代码,所以第二个编辑框下按回车是不起作用的。下面介绍另一种获得窗口句柄的方法:GetWindow它返回与指定窗口有关系的窗口,所以也可以这样写:

	::SetFocus(::GetWindow(hwnd,GW_HWNDNEXT));
最后介绍一种获得获得窗口句柄的方法:GetNextDlgTabItem。这个函数返回一个指定控件前面后者后面的一个具有WS_TABSTOP风格的控件。那么什么是WS_TABSTOP风格呢?对于按钮的属性,都有制表站这一项,如果选上了,就有这个风格,如果没选,就没有。

		::SetFocus(::GetNextDlgTabItem(::GetParent(hwnd),hwnd,FALSE));
说了这么多,其实这些方法都很麻烦:你得为每个控件写一推代码,有没有简单的方法呢?
很简单,我们把响应回车键的默认函数改了就行了:

void CTestDlg::OnOK() 
{
	// TODO: Add extra validation here
	GetFocus()->GetNextWindow()->SetFocus();
//	CDialog::OnOK();
}

获取当前焦点,获取这个焦点的下个窗口,将焦点设置在这个窗口,一目了然。
但这个程序是有问题的:当你多输入几次回车以后,就崩溃了,因为最后一个窗口调用GetNextWindow()时,会得到一个空指针,对它进行SetFocus();就会出错。为了解决这个问题,可以使用GetNextDlgTabItem来代替。这个函数的作用是按照顺序查找具有tabstop属性的控件。在layout菜单的taborder下,可以看到他们的顺序。

void CTestDlg::OnOK() 
{
	// TODO: Add extra validation here
	GetNextDlgTabItem(GetFocus())->GetNextWindow()->SetFocus();
//	CDialog::OnOK();
}

最后再看看默认按钮:对于这个对话框,默认按钮时确定,但是可以通过修改属性来让别的按钮响应。






你可能感兴趣的:(孙鑫VC++深入详解)