VC6讲座(十八)
处理键盘和鼠标事件
上一讲中我们学习了在MFC程序中使用定时器的方法,并成功地为Schedule添加了报警功能,现在的Schedule已具备了我们在第四讲中提出的基本功能,但是它使用起来仍然不太方便,添加、编辑和删除事件条目的操作都只能通过选取菜单中相应的命令或工具栏上相应的按钮来进行,Schedule还不能接受通过键盘输入的命令,对鼠标的支持也不够强,因此在本讲中,我准备介绍一下处理键盘和鼠标事件的方法,为Schedule添加相应的消息处理函数,并设计一个弹出式菜单,让它作为Schedule的关联菜单。
? 添加处理键盘输入事件的函数
按照Windows程序设计的基本要求,一个WIN32应用程序应该能够处理来自键盘的用户输入,这些键盘输入将以消息的形式被发送到应用程序的窗口处理函数中。在设计MFC程序时,我们不用直接编写窗口处理函数,但需要利用ClassWizard来添加相应的消息处理函数。
现在让我们调出ClassWizard,选中CScheduleView类,准备把处理键盘输入的函数放在视类中,不过此时可以发现在Messages列表框中列出了多种与键盘输入有关的消息,例如WM_CHAR、WM_KEYDOWN、WM_KEYUP、WM_SYSKEYDOWN和WM_SYSKEYUP等等,面对这么多的消息,我们应该处理哪一条呢?
上述几种消息可以分成两类,一类被称为按键消息(Keystroke Messages),即当用户按下和松开某个键时拥有输入焦点的程序收到的消息,包括有WM_KEYDOWN、WM_KEYUP等四种,它们通常被用来处理键盘命令,另一类被称为字符消息(Character Messages),窗口处理函数在接收到第一类按键消息后,一般都要调用TranslateMessage()函数,该函数检查按键是否对应着一个ASCII字符,如果是的话,就额外放置一条字符消息(即
WM_CHAR)到窗口的消息队列之中,字符消息通常被用来处理文字输入。由此可见,用户的绝大部分按键都将引发按键消息,而按键消息中有一大部分将再次引发一条WM_CHAR消息。 按键消息中的WM_SYSKEYDOWN是在用户按下了F10键,或者按下Alt加上其它一个键时才引发的,这两种按键组合被称为系统键,它们的用途都是激活菜单。WM_KEYDOWN则是在用户按下非系统键时引发的,即用户在按下除F10以外的键时没有同时按下Alt键。 在决定处理哪种消息之前,我们首先要明确Schedule准备接受什么样的键盘命令,我设想用Ins、Enter和Del三个键来分别执行添加、编辑和删除事件条目的命令,这三个键中的Ins和Del都没有ASCII字符与之相对应,它们也都不是系统键,因此我们应选择处理WM_KEYDOWN或WM_KEYUP消息,而不是其它三种消息,又由于在实际应用中,绝大多数程序都选择处理WM_KEYDOWN,我们也就遵守这个原则。
1
于是我们为CScheduleView添加一个处理WM_KEYDOWN消息的函数OnKeyDown(),然后编写以下的实现代码:
void CScheduleView::OnKeyDown(UINT nChar, UINT nRepCnt, UINT nFlags) {
// TODO: Add your message handler code here and/or call default switch(nChar) { //根据按键来调用不同的命令处理函数
case VK_INSERT:
GetDocument()->OnAddtask(); break; case VK_DELETE:
GetDocument()->OnDeletetask(); break; case VK_RETURN:
GetDocument()->OnEdittask(); break; default:
break;
}
CListView::OnKeyDown(nChar, nRepCnt, nFlags); }
在上面代码中出现的“VK_*”是Virtual-Key代码,所谓Virtual-Key代码是指Windows对各种键盘上的按键进行的统一编码,其中对应着Ins、Enter和Del三个键的Virtual-Key分别为VK_INSERT、VK_RETURN和VK_DELETE。OnKeyDown()在接收到上述三种按键时,将直接调用CScheduleDoc中的三个菜单命令处理函数。
2
由于Schedule接受的键盘命令比较简单,都直接对应着菜单命令,因此除了处理WM_KEYDOWN消息外,我们还可以为相应的菜单项设置加速键(Accelerator),或者为程序注册三个热键(Hotkey)并处理WM_HOTKEY消息。在所有的三种方法中,设置加速键是最为简便的,我们使用的方法其次,而注册热键的方法最为复杂,关于热键的含义及其详细的使用方法请大家自己参考MSDN库。
? 添加处理鼠标输入事件的函数
在为Schedule增添了接受键盘命令的代码后,我们还想让用户能够通过双击鼠标左键或单击右键来完成一些操作,例如当用户对着某个事件条目双击左键时,我们可以理解成他想编辑该条目,此时Schedule应能自动弹出编辑对话框,当用户单击右键时,我们可以理解成他想调出一个关联菜单,以便从中选择合适的命令。
当用户在客户区内单击或双击鼠标时,所引发的消息只会发送给相应的视类(即
CScheduleView),而不会发送给APP类、文档类和框架类,因此处理鼠标输入事件的函数也必须放在视类中。
同样地,我们调出ClassWizard,然后为CScheduleView添加一个处理
WM_LBUTTONDBLCLK消息的处理函数OnLButtonDblClk(),并输入下面的实现代码:
void CScheduleView::OnLButtonDblClk(UINT nFlags, CPoint point) {
// TODO: Add your message handler code here and/or call default GetDocument()->OnEdittask();
CListView::OnLButtonDblClk(nFlags, point); }
上述代码基本上能够实现前面提到的功能,但是在Schedule使用了CListView的特殊情况下,更好的做法是处理List控件向父窗口(也就CScheduleView)发送的通知消息NM_DBLCLK,因为该消息的参数中有一个参数直接指明了用户双击的对象是哪个条目,而WM_LBUTTONDBLCLK消息只传递了双击时鼠标所在的位置。下面我们就再来为
CScheduleView添加一个处理NM_DBLCLK通知消息的处理函数OnDblclk(),并输入下面的实现代码:
void CScheduleView::OnDblclk(NMHDR* pNMHDR, LRESULT* pResult) {
if( ((LPNMLISTVIEW) pNMHDR)->iItem != -1 )
GetDocument()->OnEdittask();
3
*pResult = 0; }
这个函数比较有意思,它的第一个参数本来是一个指向
NMHDR结构的指针,然而在函数体中我们却将之强制转换成指向一个NMLISTVIEW结构的指针,为什么呢?原因是4.71版及更高版本的List控件(IE 4.0之后的List控件都满足版本要求)在发送通知消息NM_DBLCLK的时,实际上传递的是一个NMLISTVIEW结构的指针,但标准的NM_DBLCLK消息传递的却是NMHDR结构的指针,ClassWizard在生成函数框架时是按照标准消息格式来处理的。NMLISTVIEW结构除了在开头部分包含了一个NMHDR结构外,它还提供了被双击项目的相关信息,如果它的iItem成员等于-1,则表示用户不是对着一个项目双击的,如果是这样的话,我们也就不必调用OnEdittask(),从而避免了出现不必要的操作提示信息。实际上,OnLButtonDblClk()也可以实现OnDblclk()的这个功能,但需要将相关代码改为:
if( GetListCtrl().GetSelectedCount() == 1 )
GetDocument()->OnEdittask();
现在这两个函数的功能就基本一致了,朋友们可以任意保留其中一个函数,而把另一个函数删掉,当然,今后在编写其它程序时OnLButtonDblClk()和OnDblclk()就不一定能做到等价了。另外,大家不妨进一步研究一下WM_LBUTTONDBLCLK和NM_DBLCLK之间的关系,解释为什么用户的一次双击能引发这两种消息,它们的发生顺序又如何等等。
接下来我们再次利用ClassWizard为CScheduleView添加一个处理WM_RBUTTONDOWN消息的函数OnRButtonDown(),准备在其中弹出一个关联菜单。
? 编辑和使用弹出式菜单
图18-1:设计时的弹出式菜单
关联菜单实质上是一种弹出式菜单,通常情况下,弹出式菜单相当于主菜单的一个子菜单项。我们打开资源编辑器,添加一个新的菜单资源,修改其ID为IDR_TASKMENU,然后在第一个顶级菜单项下面添加三个命令,它们与程序主菜单的“安排”子菜单下面的命令完全一样,包括ID、Caption和Prompt。编辑完毕后,以Popup方式来查看IDR_TASKMENU,其效果应如图18-1所示。
现在我们来为OnRButtonDown()编写实现代码:
void CScheduleView::OnRButtonDown(UINT nFlags, CPoint point) {
CMenu tmpMenu;
4
CMenu* pSubMenu;
tmpMenu.LoadMenu(IDR_TASKMENU); //装载菜单资源 pSubMenu=tmpMenu.GetSubMenu(0); //获得子菜单的指针
ClientToScreen(&point); //将客户区的坐标位置转换成屏幕坐标位置 pSubMenu->TrackPopupMenu( TPM_RIGHTBUTTON|TPM_LEFTALIGN|TPM_TOPALIGN,
point.x, point.y, this,0 ); //显示弹出式菜单 tmpMenu.DestroyMenu(); //释放菜单资源 }
在上面的代码中,LoadMenu()用于装载菜单资源IDR_TASKMENU,由于我们使用了局部变量来存放不属于任何一个窗口的菜单对象,所以在退出OnRButtonDown()函数之前必须释放相应的菜单资源,否则多次调用该函数之后,程序将会占掉不少的系统资源。
图18-2:运行时的弹出式菜单
ClientToScreen()的作用是将客户区的坐标位置转换成屏幕坐标位置,因为WM_RBUTTONDOWN消息中的鼠标位置参数是相对于客户区的,而TrackPopupMenu()函数的参数必须是基于屏幕坐标系的,如果不经过一次转换,菜单弹出的位置就不知道会偏到哪里去了。
现在我们来运行一下Schedule,在客户区内单击鼠标右键,就会弹出如图18-2所示的关联菜单,选择命令之后它就会调用相应的处理函数。
到本讲为止,Schedule的编程工作也就要告一段落了,Schedule只是我用来讲解VC编程的一个实例,它的功能很单一,但仍有一定的实用价值。已经用惯了各种优秀软件的朋友肯定不会满足于现在的Schedule,因为它现在只是一只丑小鸭而已,要把它变成一只天鹅还需要我们更多的辛勤劳动,我在下面列出了一些可能的改进,希望已经喜爱上VC编程的朋友们自已动手来进一步修饰Schedule。
1.删除Schedule中一些不必要的菜单命令和工具栏上的按钮;
2.根据用户选中事件条目的情况自动改变某些菜单命令的有效状态,例如没有选中任何条目时,“编辑条目”应处于不能选择的无效状态,它所对应的工具按钮也应无效; 3.让Schedule能够象网络蚂蚁那样在最小化时隐藏自己的窗口,在任务栏的通知区域内放置一个图标,并设计相关的菜单;
5
4.修改Schedule的用户界面,设计更好的图标和位图;
5.修改添加和编辑事件条目的对话框,让用户能更直观地设置时间; 6.设计一个启动画面,把某些选项保存到注册表之中; 7.为Schedule增添打印功能; ? ?等等
在下一讲中,我准备介绍一下VC6的部件库(Components),看看如何把一些可重用6
的代码添加到应用程序中去。