4.2工具条的编程技术
本节将讨论一些与工具条有关的编程技术,主要包括命令处理、命令更新、按钮风格和工具条的隐藏/显示等技术。 4.2.1命令处理
要使菜单和工具条执行命令,光为它们指定命令ID是不行的,必须为每个命令ID定义命令处理函数。如果不为命令定义命令处理函数或下面将要提到的命令更新处理函数,则框架将自动使该命令对应的菜单项和按钮禁止(灰化),这就是4.1节中的工具条按钮和菜单项灰化的原因。
利用ClassWizard可以很方便地加入命令处理函数,请读者按以下步骤操作:
如图4.6所示,在Class name栏中选择CMainFrame,在Object IDs栏中选择ID_RECORD_START,在Messages栏中双击COMMAND项,则ClassWizard会弹出一个对话框询问命令处理函数的名字,使用其提供的函数名即可。按OK按钮后,函数OnRecordStart就被加入到了Member functions栏中。
仿照第2步,为ID_RECORD_STOP定义一个命令处理函数。
按OK按钮关闭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));
}
编译并运行Record,可以看到Start和Stop命令已经可以执行了。4.2.2命令更新
虽然Start和Stop命令可以执行了,但是还有一个不足之处。在没有开始录音之前,Stop命令应该是禁止的,也即对应的菜单项和按钮应是禁止的,这是因为此时没有必要执行该命令。录音开始后,Stop命令应该允许,而Start命令则应变为禁止。我们可以利用MFC的命令更新机制实现此逻辑功能。
在菜单下拉之前,或在工具条按钮处在空闲循环期间,MFC会发一个更新命令,这将导致命令更新处理函数的调用。命令更新处理函数可以根据情况,使用户接口对象(主要指菜单项和工具条按钮)允许或禁止。定义命令更新处理函数的方法如下:
图4.7 ClassWizard对话框
如图4.7所示,在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命令的逻辑功能已经实现了。
4.2.3按钮风格
在Record程序中,用户可以选择两种采样频率来录音。用户接口对象应该能反映出当前的采样频率。普通的工具条按钮在按下后会立刻弹起来,我们希望Record程序的频率选择按钮具有单选按钮的风格,即当用户选择了一个采样频率时,该采样频率对应的按钮一直处于按下的状态,而另一个频率选择按钮应处于弹起状态。
我们可以利用CCmdUI::SetCheck函数来实现这一功能,在命令更新函数中调用CCmdUI::SetCheck(TRUE)或CCmdUI::SetCheck(FALSE)可将用户接口对象设定为选中或不选中状态,当一个用户接口对象被选中时,相应的工具按钮会处于按下的状态,并且相应的菜单项的前面会加上一个选中标记。这里需要给CMainFrame类加一个布尔型成员变量以表明当前的采样频率。请读者在CMainFrame类内加入下面一行代码:
BOOL m_bHighQuality;
接下来请读者按清单4.4进行修改。
清单4.4
CMainFrame::CMainFrame()
{
// TODO: add member initialization code here
m_bWorking=FALSE;
m_bHighQuality=TRUE;
}
void CMainFrame::OnHighQuality()
{
// TODO: Add your command handler code here
m_bHighQuality=TRUE;
}
void CMainFrame::OnLowQuality()
{
// TODO: Add your command handler code here
m_bHighQuality=FALSE;
}
void CMainFrame::OnUpdateHighQuality(CCmdUI* pCmdUI)
{
// TODO: Add your command update UI handler code here
pCmdUI->SetCheck(m_bHighQuality);
}
void CMainFrame::OnUpdateLowQuality(CCmdUI* pCmdUI)
{
// TODO: Add your command update UI handler code here
pCmdUI->SetCheck(!m_bHighQuality);
}
m_bHighQuality的初值是TRUE,即缺省时是高频采样,对它的初始化工作在CMainFrame的构造函数中完成。m_bHighQuality的值在处理High quality和Low quality命令时会被更新以反映当前的状态。两个命令更新处理函数都调用了CCmdUI::SetCheck,该函数根据m_bHighQuality的值来更新命令接口对象,从而使工具条按钮具有了单选按钮的风格。
编译并运行Record,读者可以看到具有新风格的工具条按钮。当选择采样频率时,相应的菜单项前会出现一个选中标记,相应的工具条按钮会被按下。
4.2.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()
{
// TODO: Add your command handler code here
m_wndToolBar1.ShowWindow(m_wndToolBar1.IsWindowVisible()?
SW_HIDE:SW_SHOW);
RecalcLayout();
}
void CMainFrame::OnUpdateViewToolbar1(CCmdUI* pCmdUI)
{
// TODO: Add your command update UI handler code here
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菜单项选中或不选中。
编译并运行Record,现在Record程序已变得很有趣了。至此,读者已经掌握了工具条的一些实用编程技术。
4.3 状态栏的设计与实现
状态栏实际上是个窗口,一般分为几个窗格,每个窗格显示不同的信息。AppWizard会为应用程序自动创建一个状态栏,该状态栏包括几个窗格,分别用来显示状态栏提示和CAPS LOCK、NUM LOCK 、SCROLL LOCK键的状态。在MFC中,状态栏的功能由CStatusBar类实现。
创建一个状态栏需要以下几个步骤:
构建一个CStatusBar对象。
调用CStatusBar::Create创建状态栏窗口。
调用CStatusBar::SetIndicators函数分配窗格,并将状态栏的每一个窗格与一个字符串ID相联系。
相应的代码读者可以在Record工程的CMainFrame::OnCreate成员函数中找到。如清单4.6所示。
清单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就为时间的显示预留了空间。
提示:上述方法不能动态改变窗格宽度,并且有时是不精确的,当系统字体改变时,这种做法可能会导致一些误差。考虑到该方法简单直观,且一般情况下问题不大,故本文用它来举例。如果读者对动态、精确地指定窗格感兴趣,请参看Visual C++ 5.0随光盘提供的一个名为NPP的MFC例子(在samples/mfc/general/npp目录下)。 |
时间窗格显示的时间必须每隔一秒钟更新一次。更新时间窗格的正文可调用CStatusBar:: SetPaneText函数,要定时更新,则应利用WM_TIMER消息。在Windows中用户可以安装一个或多个计时器,计时器每隔一定的时间间隔就会自动发出一个WM_TIMER消息,而这个时间间隔可由用户指定。MFC的Window类提供了WM_TIMER消息处理函数OnTimer,我们应在该函数内进行更新时间窗格的工作。
请读者利用ClassWizard给CMainFrame类加入WM_TIMER的消息处理函数OnTimer和WM_CLOSE消息的处理函数OnClose,具体方法是在Class name栏中选择CMainFrame,在Object IDs栏中选择CMainFrame,在Messages栏中找到WM_TIMER和WM_CLOSE项,分别双击之然后按OK按钮退出ClassWizard。CMainFrame::OnClose函数是在关闭主框架窗口是被调用的,程序可以在该函数中做一些清除工作。
接下来请按清单4.8修改程序。
清单4.8 CMainFrame类的部分代码
int CMainFrame::OnCreate(LPCREATESTRUCT lpCreateStruct)
{
…
SetTimer(1,1000,NULL);
return 0;
}
void CMainFrame::OnTimer(UINT nIDEvent)
{
// TODO: Add your message handler code here and/or call default
CTime time;
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()
{
// TODO: Add your message handler code here and/or call default
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可更新状态栏窗格显示的正文。