第2章 消息映射与处理

在Windows中,消息分为鼠标消息、键盘消息、窗口消息、自定义消息。Visual C++把消息机制有效地封装起来,不需要写冗长的代码就可以很轻松地编写出各个不同的处理函数。本章将通过具体的例程介绍VC中相关的消息处理。

2.1 键盘消息及处理

按下一个键就会产生一条WM_KEYDOWN或WM_SYSKEYDOWN消息,并将被放到与有关键盘输入的窗口相应的线程消息队列中;释放一个键则会产生一条WM_KEYUP或WM_SYSKEYUP消息,同样也会被放到队列中。

实例22:基本键盘操作—判断按键消息

❑实例说明

本实例将演示基本的键盘消息的编程。实例实现的功能为,当用户按下了“Shift”键时,在视图窗口中显示提示信息“用户按下了Shift键!”,当用户释放了“Shift”键时,在视图窗口中显示提示信息“用户释放了Shift键!”,而当用户按下了“Shift”键后又按下了字符“B”键,在视图窗口中显示提示信息“用户同时按下Shift键和B键!”。程序的运行界面如图2.1所示。

图2.1 程序的运行界面

❑核心开发过程

(1)创建一个单文档的MFC工程,使用ClassWizard在视图类中添加WM_KEYDOWN、WM_KEYUP和WM_CHAR键盘消息映射和消息响应函数。

(2)在键盘消息响应函数中,判断按键的状态。代码如下:

        void CBaseKeyDemoView::OnKeyDown(UINT nChar, UINT nRepCnt, UINT nFlags)
        {
            // TODO: Add your message handler code here and/or call default
            if(nChar==VK_SHIFT)                            //判断"Shift"键是否被按下
            {
                //AfxMessageBox("dd");
                bShiftdown=TRUE;
                bShiftup=FALSE;
                Invalidate(TRUE);                          //显示信息
            }
                CView::OnKeyDown(nChar, nRepCnt, nFlags);
        }
        void CBaseKeyDemoView::OnKeyUp(UINT nChar, UINT nRepCnt, UINT nFlags)
        {
            // TODO: Add your message handler code here and/or call default
            if(nChar==VK_SHIFT)                            //判断"Shift"键是否被释放
            {
                //AfxMessageBox("dd");
                bShiftup=TRUE;
                Invalidate(TRUE);                          //显示信息
                bShiftdown=FALSE;
            }
            CView::OnKeyUp(nChar, nRepCnt, nFlags);
        }
        void CBaseKeyDemoView::OnChar(UINT nChar, UINT nRepCnt, UINT nFlags)
        {
            // TODO: Add your message handler code here and/or call default
            if((nChar==98)||(nChar==66))                   //判断是否敲击了字符键"B"键或"b"键
            {
                if(bShiftdown)
                {
                    bShiftB=TRUE;
                    bShiftdown=FALSE;
                    Invalidate(TRUE);                      //显示信息
                }
            }
            CView::OnChar(nChar, nRepCnt, nFlags);
        }

(3)在视图类的OnDraw函数中,根据按键的状态,显示相关的文本。代码如下:

        void CBaseKeyDemoView::OnDraw(CDC* pDC)
        {
            CBaseKeyDemoDoc* pDoc = GetDocument();
            ASSERT_VALID(pDoc);
            // TODO: add draw code for native data here
            if(bShiftdown)                                   //按下了"Shift"键
            {
                pDC->TextOut(20,20,"用户按下了Shift键!");
            }
            if(bShiftup)                                     //释放了"Shift"键
            {
                pDC->TextOut(20,20,"用户释放了Shift键!");
            }
            if(bShiftB)                                      //同时按下了"Shift"键和"B"键
            {
                pDC->TextOut(20,20,"用户同时按下Shift键和B键!");
                bShiftB=FALSE;
            }
        }

❑要点说明

在MFC中,ClassWizard封装的键盘消息共有下面3种。

• WM_KEYDOWN:某一键被按下。

• WM_KEYUP:某一键弹起。

• WM_CHAR:某一键按下又弹起,输入了一个字符。

当用户按下了键盘中的某一个键时,首先产生WM_KEYDOWN消息,进入其消息处理函数OnKeyDown。如果该键为字符键,之后还将产生WM_CHAR消息,进入消息处理函数OnChar。OnChar函数的参数nChar并不是虚键码,而是Windows字符集的字符代码,默认的为ASCII码。常见的字符及其ASCII码值如表2.1所示。

表2.1 常见的字符及其ASCII码值

实例23:在普通视图窗口中实现键盘字符的输入

❑实例说明

本实例将实现在单文档应用程序界面中,当用户通过键盘键入字符时,在视图窗口依次显示键入的字符。当用户按下“Enter”键时,进行换行输出。程序的运行界面如图2.2所示。

❑核心开发过程

(1)在视图类的头文件“KeyInputView.h”中,定义CPoint型变量,用于记录字符在视图窗口中的输出位置。

图2.2 程序的运行界面

        public:
            CPoint ptCharacter;                            //记录字符位置

在CKeyInputView类的构造函数中,初始化ptCharacter位置为(0,0)。

        CKeyInputView::CKeyInputView()
        {
            // TODO: add construction code here
            //初始位置设置在(0,0)
            ptCharacter.x=0;
            ptCharacter.y=0;
        }

(2)在WM_CHAR消息响应函数OnChar中,实现字符的显示以及换行。代码如下:

        void CKeyInputView::OnChar(UINT nChar, UINT nRepCnt, UINT nFlags)
        {
            // TODO: Add your message handler code here and/or call default
            if(nChar==13)                                    //按下了回车键
            {
                //换行
                ptCharacter.x=0;
                ptCharacter.y=ptCharacter.y+25;
            }
            else
            {
                CClientDC dc(this);
                dc.TextOut(ptCharacter.x,ptCharacter.y,(LPCTSTR)&nChar); //输出显示字符
                CSize textsize;
                textsize=dc.GetTextExtent((LPCTSTR)&nChar);          //获取当前字符大小
                //前进到下一个字符位置
                ptCharacter.x=ptCharacter.x+textsize.cx;
            }
            CView::OnChar(nChar, nRepCnt, nFlags);
        }

❑要点说明

本程序只是简单演示了WM_CHAR消息响应和按键字符的显示操作,并没有实现窗口的重绘。另外,实际程序如果涉及文本输入、编辑操作,可以通过使用Edit控件或者CEditView视图来实现。

实例24:创建和使用键盘插入符

❑实例说明

键盘插入符(Caret)是一个闪烁的位图(通常是一个细的垂直杠),它可使用户知道在窗口何处可进行有效的键盘输入。本实例将在前面实例的基础上,在当前键盘的输入位置显示一个自己创建的插入符,实例的运行界面如图2.3所示。

图2.3 程序的运行界面

❑核心开发过程(1)使用ClassWizard在视图类中添加WM_SETFOCUS消息映射和消息响应函数,在其中创建键盘插入符,并在当前的输入位置显示。代码如下:

        void CCaretKeyDemoView::OnSetFocus(CWnd* pOldWnd)
        {
            CView::OnSetFocus(pOldWnd);
            // TODO: Add your message handler code here
                CreateSolidCaret(3, 18);                    //创建插入符
                SetCaretPos (ptCharacter);                  //将插入符移到当前字符输入点
                ShowCaret ();                               //显示插入符
        }

(2)在OnChar函数中,实现在适当的时机显示、隐藏插入符。代码如下:

        void CCaretKeyDemoView::OnChar(UINT nChar, UINT nRepCnt, UINT nFlags)
        {
            // TODO: Add your message handler code here and/or call default
            if(nChar==13)                                             //按下了Enter键
            {
                //换行
                ptCharacter.x=0;
                ptCharacter.y=ptCharacter.y+25;
                SetCaretPos (ptCharacter);                            //将插入符移到键入点
                ShowCaret ();                                         //显示插入符
            }
            else
            {
                CClientDC dc(this);
                HideCaret ();                                         //隐藏插入符
                dc.TextOut(ptCharacter.x,ptCharacter.y,(LPCTSTR)&nChar);  //显示字符
                CSize textsize;
                textsize=dc.GetTextExtent((LPCTSTR)&nChar); //获取当前字符大小
                //前进到下一个字符位置
                ptCharacter.x=ptCharacter.x+textsize.cx;
                SetCaretPos (ptCharacter);                            //将插入符移到键入点
                ShowCaret ();                                         //显示插入符
            }
            CView::OnChar(nChar, nRepCnt, nFlags);
        }

❑要点说明

Windows总是把键盘消息送到拥有输入焦点的窗口。一般情况下一个应用程序有多个窗口,而键盘消息只能被一个窗口接收,接收键盘消息的窗口称为有“输入焦点”的窗口,具有输入焦点的窗口称为活动窗口。当某一个窗口成为活动窗口时,Windows会加亮显示其标题栏或窗口边框。

Windows用WM_SETFOCUS和WM_KILLFOCUS消息通知即将接收或失去输入焦点的窗口。当窗口获得键盘焦点时,就可以创建插入符了,若窗口没有焦点,就不能进行键盘输入。另外,插入符一旦创建起来,还要在窗口中对其进行定位和显示。

CWnd类提供了8个创建和管理键盘插入符的成员函数,各函数及实现的功能介绍如下。

• CreateCaret:使用用户提供的位图创建插入符。

• CreateGrayCaret:创建用户自定义大小的实心灰色插入符。

• CreateSolidCaret:创建用户自定义大小的实心黑色插入符。

• DestoryCaret:销毁插入符。

• ShowCaret:显示插入符。

• HideCaret:隐藏插入符。

• GetCaretPos:返回插入符的位置。

• SetCaretPos:移动插入符到窗口的某一位置。

实例25:使用程序模拟键盘输入

❑实例说明

键盘输入是对Windows程序的主要操作之一。对于复杂的或重复性的输入操作,需要通过程序来模拟键盘的输入。本实例将在编辑框中模拟键盘的输入,即当用户单击“模拟输入”按钮时,将创建一个定时器,间隔1秒在编辑框中模拟键盘,循环输入“hello world”字符。程序的运行界面如图2.4所示。

图2.4 程序的运行界面

❑核心开发过程

(1)在按钮的BN_CLICKED消息响应函数中,实现设置或取消定时器,并设置按钮文本。代码如下:

        void CSimulateKeyDlg::OnInput()
        {
            // TODO: Add your control notification handler code here
            static BOOL bChangeFlag=TRUE;
            if(bChangeFlag)
            {
                SetTimer(1,500,NULL);//设置定时器
                bChangeFlag =bChangeFlag ? FALSE:TRUE;
                GetDlgItem(IDC_INPUT)->SetWindowText("停止输入");//设置按钮文本
            }
            else
            {       GetDlgItem(IDC_INPUT)->SetWindowText("模拟输入");//设置按钮文本
                KillTimer(1);//取消定时器
                bChangeFlag =bChangeFlag ? FALSE:TRUE;
            }
            }

(2)使用ClassWizard,添加WM_TIMER消息响应函数,在函数中实现模拟按键操作。代码如下:

        void CSimulateKeyDlg::OnTimer(UINT nIDEvent)
        {
            // TODO: Add your message handler code here and/or call default
            static int count=0;
            BYTE keyname[11]={72,69,76,76,79,32,87,79,82,76,68};           //按键序列的虚拟键码
            if(nIDEvent==1)
            {
                m_ctlEdit.SetFocus();                                      //编辑框获得输入焦点
                keybd_event(keyname[count],0,0,0);                         //按键按下
                keybd_event(keyname[count],0,KEYEVENTF_KEYUP,0);           //按键抬起
                count++;
                if(count==12)
                {
                    m_ctlEdit.SetWindowText("");                           //情况编辑框
                    UpdateData(FALSE);
                    count=0;
                }
            }
                CDialog::OnTimer(nIDEvent);
        }

❑要点说明

使用API函数keybd_event可以触发WM_KEYDOWN或者WM_UP键盘消息,模拟键盘的输入。keybd_event函数的原型如下:

        VOID keybd_event(
          BYTE bVk,                                   //按键的虚拟键值
          BYTE bScan,                                 //扫描码,一般不用设置,用0代替即可
          DWORD dwFlags,                              //选项标志,KEYDOWN设置为0即可
          DWORD dwExtraInfo                           //附加特性,一般设置为0
        );

虚拟键码是Windows内部建立的设备无关的键盘代码,在Windows中不论使用什么类型的键盘,都将扫描代码翻译成同一的虚键码,这样应用程序就不用直接同硬盘等硬件打交道。Windows中常用的虚拟键码及其对应的按键如表2.2所示。

表2.2 Windows常用的虚拟键码及其对应的按键

实例26:在对话框中实现键盘消息响应

❑实例说明

用VC完成的对话框程序,在键盘消息响应和快捷键的实现上,没有提供直接的实现方式。原因是对话框里的控件需要首先对按键作出响应,如多行编辑框必须首先处理回车,不至于回车使对话框关闭。本实例将通过重载PreTranslateMessage函数,实现在对话框中,当用户按下按键时,显示相应按键的虚拟键码。程序的运行界面如图2.5所示(按了“a”键)。

图2.5 程序的运行界面

❑核心开发过程

使用ClassWizard重载PreTranslateMessage函数,进行消息预处理。实现代码如下:

        BOOL CDlgKeyMessageDlg::PreTranslateMessage(MSG* pMsg)
        {
            // TODO: Add your specialized code here and/or call the base class
            if(pMsg->message == WM_KEYDOWN)                          //如果用户按下了按键
            {
            CString strwParam;
            strwParam.Format("用户按键的虚拟键码为:%d ",pMsg->wParam);
            CDC* pDC = m_ctlframe.GetDC();                           //获取DC
            pDC->TextOut(2,2,strwParam);                             //显示文本
            ReleaseDC(pDC);                                          //释放DC
            }
            return CDialog::PreTranslateMessage(pMsg);
        }

❑要点说明

在对话框中,重载PreTranslateMessage函数,进行消息预处理,然后再添加WM_KEYDOWN、WM_KEYUP等消息的消息响应函数,就能实现对话框程序的键盘消息响应和快捷键功能。如在对话框中添加了WM_KEYDOWN消息响应函数OnKeyDown,如果要调用它,就可以在PreTranslateMessage函数中添加如下代码:

        BOOL CDlgKeyMessageDlg::PreTranslateMessage(MSG* pMsg)
        {
            // TODO: Add your specialized code here and/or call the base class
            if(pMsg->message == WM_KEYDOWN)             //如果用户按下了按键
            {
                OnKeyDown(pMsg->wParam, LOWORD(pMsg->lParam), HIWORD(pMsg->lParam));
            }
            return CDialog::PreTranslateMessage(pMsg);
        }

实例27:向其他应用程序(记事本)中发送键盘消息

❑实例说明

本实例实现向打开的记事本文件中,通过键盘消息,发送“HELLO”字符串,并通过发送“Ctrl”+“S”组合键命令保存记事本文件。实例的运行界面如图2.6所示。

图2.6 程序的运行界面

❑核心开发过程

发送字符串和保存文件的函数实现代码如下:

        void CSendNotepadMsgDlg::OnSend()               //发送字符串
        {
            // TODO: Add your control notification handler code here
            HWND hWnd = ::FindWindow("Notepad", NULL);
            if(hWnd)
            {
                HWND hEdit = FindWindowEx(hWnd, NULL, "Edit", NULL);
                if(hEdit)
                ::PostMessage(hEdit, WM_CHAR, 0x48, 0);          //发送'H'到notepad
                ::PostMessage(hEdit, WM_CHAR, 0x45, 0);          //发送'E'到notepad
                ::PostMessage(hEdit, WM_CHAR, 0x4C, 0);          //发送'L'到notepad
                ::PostMessage(hEdit, WM_CHAR, 0x4C, 0);          //发送'L'到notepad
                ::PostMessage(hEdit, WM_CHAR, 0x4F, 0);          //发送'O'到notepad
            }
            else
            {
                AfxMessageBox("请打开记事本文件!");
                return;
            }
        }
        void CSendNotepadMsgDlg::OnSave()                        //保存文件
        {
            // TODO: Add your control notification handler code here
            CWnd* pWnd =FindWindow("Notepad", NULL);
            if (pWnd->GetSafeHwnd())
            {
                pWnd->ShowWindow(SW_NORMAL);
                pWnd->SetForegroundWindow();
                keybd_event(VK_CONTROL, 0, 0, 0);                //按下"Ctrl"键
                keybd_event('S', 0, 0, 0);                       //按下"S"键
                keybd_event(VK_CONTROL,0,KEYEVENTF_KEYUP,0);//释放"Ctrl"键
                keybd_event('S', 0, KEYEVENTF_KEYUP, 0);         //释放"S"键
            }
            else
            {
                AfxMessageBox("未找到打开的记事本文件!");
                return;
            }
        }

❑要点说明

向其他应用程序中发送消息,查找接收消息的窗口句柄非常关键。使用FindWindow函数可以获取当前运行程序的主窗口句柄,而使用FindWindowEx函数则用于获取子窗口句柄。

2.2 鼠标消息及处理

鼠标消息是应用程序开发中常需要处理的消息,当鼠标移动、左键(右键)的按下或者松开、双击操作等都可以产生相应的鼠标消息。

实例28:基本鼠标操作—判断鼠标消息

❑实例说明

在MFC中,对常用的几种鼠标消息进行了封装,包括鼠标左键和右键的单击、释放、双击,鼠标的移动以及滚轮的滚动等操作。本实例将演示基本鼠标消息的响应,实现的功能是为当用户在视图窗口中按下鼠标左键,拖动鼠标时,在窗口中绘制一个随鼠标位置变化的椭圆,当释放鼠标键时,停止椭圆绘制。实例的运行界面如图2.7所示。

图2.7 程序的运行界面

❑核心开发过程

(1)在视图类中添加如下的成员变量:

        public:
            BOOL bDrag;                                      //是否在拉动
            CPoint ptDown;                                   //鼠标左键按下位置
            CPoint ptUp;                                     //鼠标左键释放位置

在视图类的构造函数中,对定义的成员变量进行初始化:

        CBaseMouseDemoView::CBaseMouseDemoView()
        {
            // TODO: add construction code here
            //初始化成员变量
            bDrag=false;
            ptDown.x=ptDown.y=0;
            ptUp.x=ptUp.y=0;
        }

(2)在视图类中添加绘制椭圆的成员函数DrawCircle,其实现代码如下:

        void CBaseMouseDemoView::DrawCircle()
        {
            Invalidate(false);
            CClientDC dc(this);                                     //获取DC
            CRect rect;
            GetClientRect(rect);                                    //获取客户窗口区域
            CBrush brush(RGB(255,255,255));
            dc.FillRect(rect,&brush);                               //填充背景色为白色
            dc.Ellipse(ptDown.x,ptDown.y,ptUp.x,ptUp.y);            //绘制椭圆
        }

(3)使用ClassWizard在视图类添加WM_LBUTTONDOWN、WM_LBUTTONUP和WM_MOUSEMOVE消息映射和消息处理函数。各函数的实现代码如下:

        void CBaseMouseDemoView::OnLButtonDown(UINT nFlags, CPoint point) //按下鼠标左键
        {
            // TODO: Add your message handler code here and/or call default
            bDrag=TRUE;
            ptUp=ptDown=point;                              //记录鼠标当前位置
            CView::OnLButtonDown(nFlags, point);
        }
        void CBaseMouseDemoView::OnLButtonUp(UINT nFlags, CPoint point) //释放鼠标左键
        {
            // TODO: Add your message handler code here and/or call default
            if(bDrag)
            {
                ptUp=point;
                DrawCircle();                               //画新圆
                bDrag=FALSE;
            }
            CView::OnLButtonUp(nFlags, point);
        }
        void CBaseMouseDemoView::OnMouseMove(UINT nFlags, CPoint point) //移动鼠标
        {
            // TODO: Add your message handler code here and/or call default
            if(bDrag)
            {
                ptUp=point;                                 //记录鼠标的当前位置
                DrawCircle();                               //画新圆
            }
            CView::OnMouseMove(nFlags, point);
        }

❑要点说明

鼠标消息的响应函数,如按下鼠标左键的消息响应函数的原型如下:

        afx_msg void OnLButtonDown(UINT nFlags,CPoint point );

其中,参数nFlags表明了当前一些按键的消息,其可取值及其含义介绍如下。

• MK_LBUTTON:按下了鼠标的左键。

• MK_MBUTTON:按下了鼠标的中键。

• MK_RBUTTON:按下了鼠标的右键。

• MK_CONTROL:按下了键盘上的“Ctrl”键。

• MK_SHIFT:按下了键盘上的“Shift”键。

• WM_RBUTTONDBLCLK:双击鼠标右键。

可以通过“位与”操作进行相关检测。在实际编程中,常使用nFlags参数指出消息生成时的鼠标键以及Shift和Ctrl的状态,如当按下鼠标左键时,同时检测“Shift”键和“Ctrl”键的状态,可采用下面的代码:

        void OnLButtonDown(UINT nFlags,CPoint point ) //按下了鼠标左键
        {
        if((nFlags &MK_CONTROL)&&( nFlags &MK_SHIFT)) //"Shift"和"Ctrl"键都被按下
        ...
        }

而参数point表示当前鼠标的设备坐标,坐标原点对应视图左上角。通过point参数可以将鼠标操作与屏幕显示对应起来。

实例29:创建并设置鼠标光标

❑实例说明

鼠标光标(Cursor)是鼠标与用户之间的接口,它指示鼠标的位置,随鼠标移动而移动,是鼠标的屏幕映像。在Visual C++创建的应用程序中,鼠标都采用系统默认的光标,用户也可以通过编程使用自己的光标。本实例将实现单击鼠标左键和右键时,分别显示自己创建的光标和系统光标。实例的运行界面如图2.8所示。

❑核心开发过程

(1)绘制光标资源

在Visual C++中,执行“Insert”→“Resource”菜单命令,在弹出的“Insert Resource”对话框中,选择“Cursor”,单击“New”按钮,即进入光标的编辑窗口。在光标编辑窗口中,可以绘制需要的光标图形。在绘制窗口中,有一个热点设置按钮,在它旁边显示了热点设置的坐标。单击这个按钮,在光标编辑器中出现一个十字光标,将十字中心放在需要设定的热点位置,单击鼠标左键即可。绘制光标窗口如图2.9所示。

图2.8 程序的运行界面

图2.9 编辑设计光标窗口

(2)在CMainFrame类的PreCreateWindow()函数中,通过API函数LoadImage函数将绘制的光标载入程序。代码如下:

        BOOL CMainFrame::PreCreateWindow(CREATESTRUCT& cs)
        {
            if( !CFrameWnd::PreCreateWindow(cs) )
                return FALSE;
            // TODO: Modify the Window class or styles here by modifying
            //将自定义的光标赋予m_cursor
            m_cursor=(HCURSOR)::LoadImage(cs.hInstance,MAKEINTRESOURCE(IDC_CURSOR1),
                                IMAGE_CURSOR,32,32, LR_CREATEDIBSECTION);
            return TRUE;
        }

(3)在视图类添加WM_LBUTTONUP消息响应函数,设置光标为自绘光标。代码如下:

        void CCursorMouseDemoView::OnLButtonUp(UINT nFlags, CPoint point)
        {
            // TODO: Add your message handler code here and/or call default
            CMainFrame* pMainframe;
            pMainframe=(CMainFrame*)AfxGetMainWnd();
            SetCursor(pMainframe->m_cursor);                    //设置光标为自绘光标
            CView::OnLButtonUp(nFlags, point);
        }

(4)在视图类添加WM_RBUTTONUP消息响应函数,载入和使用Windows系统提供的标准光标资源IDC_CROSS。代码如下:

        void CCursorMouseDemoView::OnRButtonUp(UINT nFlags, CPoint point)
        {
            // TODO: Add your message handler code here and/or call default
            HCURSOR cusor=AfxGetApp()->LoadStandardCursor(IDC_CROSS); //获取系统标准光标
            SetCursor(cusor);                                                 //设置光标
            CView::OnRButtonUp(nFlags, point);
        }

❑要点说明

Windows系统提供了19种标准光标,如IDC_APPSTARTING、IDC_ARROW、IDC_CROSS、IDC_WAIT等。如果要采用Windows系统提供的标准光标资源,必须首先通过函数LoadStandardCursor载入系统标准光标资源,而后通过函数SetCursor设置光标。在实际编程中,如果用户需要使光标“消失”,即隐藏光标,这时使用ShowCursor(false)语句实现。

实例30:在对话框中定义光标的热区

❑实例说明

本实例将实现为对话框中的控件定义光标热区,即当鼠标光标位于某个控件上时,改变鼠标的显示光标,并给出该控件的描述性信息。实例的运行界面如图2.10所示。

图2.10 程序的运行界面

❑核心开发过程

(1)向工程添加一手形光标资源,ID设置为IDC_MYHAND。(2)使用ClassWizard为对话框添加WM_SETCURSOR消息响应函数OnSetCursor,在该函数中根据鼠标所在窗口的ID设置相应的热区。函数实现代码如下:

        BOOL CCursorHotDemoDlg::OnSetCursor(CWnd* pWnd, UINT nHitTest, UINT message)
        {
            // TODO: Add your message handler code here and/or call default
            switch(pWnd->GetDlgCtrlID())                      //得到鼠标所在位置的控件的ID号
            {
                case IDC_BUTTON1:                             //鼠标位于按钮空间区域内
                {
                    //设置鼠标指针为"手"形指针
                    SetCursor(AfxGetApp()->LoadCursor(IDC_MYHAND));
                    //将提示文字可见
                    GetDlgItem(IDC_TEXT)->ShowWindow(SW_SHOW);
                    GetDlgItem(IDC_TEXT)->SetWindowText("光标所在位置为按钮框!");
                    return TRUE;
                }
                break;
                case IDC_EDIT1:                               //鼠标位于编辑框区域内
                {
                                                               //设置鼠标指针为"手"形指针
                    SetCursor(AfxGetApp()->LoadCursor(IDC_MYHAND));
                    // 将提示文字可见
                    GetDlgItem(IDC_TEXT)->ShowWindow(SW_SHOW);
                    GetDlgItem(IDC_TEXT)->SetWindowText("光标所在位置为编辑框!");
                    return TRUE;
                }
                break;
                case IDC_MONTHCALENDAR1:                      //鼠标位于日历控件区域内
                {
                    // 设置鼠标指针为"手"形指针
                    SetCursor(AfxGetApp()->LoadCursor(IDC_MYHAND));
                    // 将提示文字可见
                    GetDlgItem(IDC_TEXT)->ShowWindow(SW_SHOW);
                    GetDlgItem(IDC_TEXT)->SetWindowText("光标所在位置为日历控件!");
                    return TRUE;
                }
                break;
            default:                                          //鼠标离开了"热区"
                {
                    //将提示性文字隐藏
                    GetDlgItem(IDC_TEXT)->ShowWindow(SW_HIDE);
                    //将鼠标指针设置为标准的鼠标指针
                    SetCursor(AfxGetApp()->LoadStandardCursor(IDC_ARROW));
                    return TRUE;
                }
            }
                return CDialog::OnSetCursor(pWnd, nHitTest, message);
        }

实例31:创建和使用鼠标提示框

❑实例说明

当鼠标指针放在程序的某个控件上时,要想得知该控件的功能,最好的办法是弹出一个提示框来显示帮助信息。本实例将实现弹出鼠标提示框,给出鼠标指针所在位置控件的相关信息。实例的运行界面如图2.11所示。

❑核心开发过程

在Internet中,可以下载一个免费的类CMFECToolTip,实现浮动的鼠标提示框。它派生自CWnd类,具体实现代码本书不作介绍。下面简单介绍程序中如何调用该类实现鼠标提示框。(1)将CMFECToolTip类的头文件和实现文件复制到工程目录下,并添加到工程中。在头文件“MouseInfoTipDlg.h”中,声明CMFECToolTip类的对象。如下:

图2.11 程序的运行界面

        #include "MFECToolTip.h"
        ...
        protected:
            HICON m_hIcon;
            CMFECToolTip          m_toolTip;                                //声明对象

(2)在对话框的初始化函数OnInitDialog()中,为各个控件设置相关提示框的内容和背景、文本颜色。实现代码如下:

        BOOL CMouseInfoTipDlg::OnInitDialog()
        {
            CDialog::OnInitDialog();
            ...
            // TODO: Add extra initialization here
            m_toolTip.Create( this );                             //创建提示框对象
            CStringArray straInfo;
            //"添加"按钮提示框
            straInfo.RemoveAll();
            straInfo.Add( "向列表框中添加记录" );
            m_toolTip.AddControlInfo(IDC_ADD,straInfo,RGB(220,174,208),RGB(0,0,162) );
            //退出按钮提示框
            straInfo.RemoveAll();
            straInfo.Add( "关闭窗口" );
            m_toolTip.AddControlInfo(IDCANCEL,straInfo,RGB(220,174,208),RGB( 0,0,162));
            //编辑框提示框
            straInfo.RemoveAll();
            straInfo.Add( "输入书籍名称" );
            m_toolTip.AddControlInfo(IDC_EDIT1,straInfo,RGB(220,174,208),RGB( 0,0,162));
            //列表框提示框
            straInfo.RemoveAll();
            straInfo.Add( "显示添加的图书记录" );
            straInfo.Add( "实现自动排序:" );
            m_toolTip.AddControlInfo(IDC_LIST1,straInfo,RGB(220,174,208),RGB( 0,0,162) );
                return TRUE;   // return TRUE   unless you set the focus to a control
        }

(3)使用ClassWizard重载对话框的PreTranslateMessage函数,实现捕捉鼠标的WM_MOUSEMOVE消息,弹出鼠标提示框。代码如下:

        BOOL CMouseInfoTipDlg::PreTranslateMessage(MSG* pMsg)
        {
            // TODO: Add your specialized code here and/or call the base class
            if( pMsg->message == WM_MOUSEMOVE )                   //鼠标移动
            {
                POINT pt = pMsg->pt;
                ScreenToClient( &pt );                            //转换为客户区坐标
                m_toolTip.ShowToolTip( (CPoint)pt );              //显示提示框
            }
            return CDialog::PreTranslateMessage(pMsg);
        }

实例32:在视图窗口实现捕捉鼠标

❑实例说明

通常情况下,只有鼠标光标位于某一个窗口的客户区或非客户区时,该窗口的窗口函数才能接收鼠标消息。由于鼠标的随机性,难以保证光标始终不离开某一窗口,如果要使某个窗口能不间断地捕获鼠标消息,就必须对鼠标加以捕获,从而使Windows发送的所有鼠标消息均定向到某一个窗口,而不管鼠标光标位于何处。

本实例将实现鼠标的捕捉功能,即当用户在客户区窗口中单击鼠标左键后,该窗口实现对鼠标的捕捉,并记录鼠标的位置坐标,而当双击鼠标时,则释放捕捉。实例的运行界面如图2.12所示。

图2.12 程序的运行界面

❑核心开发过程

使用ClassWizard在视图类添加WM_LBUTTONDOWN、WM_LBUTTONUP和WM_LBUTTONDBLCLK消息映射和消息处理函数。各函数的实现代码如下:

        void CCaptureMouseView::OnLButtonDown(UINT nFlags, CPoint point)//实现鼠标捕捉
        {
            // TODO: Add your message handler code here and/or call default
            SetCapture();                                             //捕捉鼠标
            CString s;
            s.Format("用户按下鼠标左键的位置:X=%d,Y=%d",point.x,point.y);
            CClientDC dc(this);                                       //获得DC
            dc.TextOut(30,40,s);                                      //输出文本
            CView::OnLButtonDown(nFlags, point);
        }
        void CCaptureMouseView::OnLButtonUp(UINT nFlags, CPoint point)
        {
            // TODO: Add your message handler code here and/or call default
            CString s;
            s.Format("用户松开鼠标左键的位置:X=%d,Y=%d",point.x,point.y);
            CClientDC dc(this);                                       //获得DC
            dc.TextOut(30,80,s);                                      //输出文本
            CView::OnLButtonUp(nFlags, point);
        }
        void CCaptureMouseView::OnLButtonDblClk(UINT nFlags, CPoint point)   //释放鼠标捕捉
        {
            // TODO: Add your message handler code here and/or call default
            ReleaseCapture();                                         //释放鼠标
            CClientDC dc(this);                                       //获得DC
            dc.TextOut(30,40,"释放了鼠标,此时可以响应客户窗口以外的鼠标命令!");//输出文本
            CView::OnLButtonDblClk(nFlags, point);
        }

❑要点说明

使用Win32 API函数SetCapture()可以实现鼠标的捕获,其原型如下:

        HWND SetCapture(HWND hWnd);

hWnd为要捕捉鼠标消息的窗口句柄,这将导致Windows向窗口句柄为hWnd的窗口发送所有的鼠标消息,此时鼠标光标的坐标仍然相对于hWnd窗口客户区的左上角,因此此时鼠标位置值有可能为负值。

一旦某个窗口捕获了鼠标,其他窗口将无法得到鼠标消息,因此,当窗口不再需要捕获鼠标消息时,应及时使用ReleaseCapture( )函数将鼠标释放。

实例33:限制鼠标的作用区域只在客户窗口

❑实例说明

在程序设计中,有时为了限制用户的某些操作,需要将鼠标的活动限定在固定的区域内。本实例将实现限制鼠标的作用区域的功能。当用户在客户区窗口中单击鼠标左键时,鼠标的移动区域就被限制在客户窗口中。当双击鼠标左键时,将取消鼠标的区域限制。实例的运行界面如图2.13所示。

图2.13 程序的运行界面

❑核心开发过程

(1)在限制鼠标的活动区域之前,需要记录当前的鼠标获取区域。在视图类的PreCreateWindow函数中,通过下面的代码来实现:

        BOOL CClipCursorMouseView::PreCreateWindow(CREATESTRUCT& cs)
        {
            // TODO: Modify the Window class or styles here by modifying
            //   the CREATESTRUCT cs
            GetClipCursor(&oldrect);                      //获取原鼠标活动的有效区域
            return CView::PreCreateWindow(cs);
        }

(2)使用ClassWizard在视图类添加WM_LBUTTONDOWN和WM_LBUTTONDBLCLK消息映射和消息处理函数,分别实现限制和取消鼠标的活动区域。函数实现代码如下:

        void CClipCursorMouseView::OnLButtonDown(UINT nFlags, CPoint point)
        {
            // TODO: Add your message handler code here and/or call default
            SetCapture();                                    //捕捉鼠标
            CRect rect1;
            GetWindowRect(&rect1);                           //获取客户区窗口区域
            ClipCursor(&rect1);                              //将鼠标的活动区域限制在客户窗口
            CView::OnLButtonDown(nFlags, point);
        }
        void CClipCursorMouseView::OnLButtonDblClk(UINT nFlags, CPoint point)
        {
            // TODO: Add your message handler code here and/or call default
            ReleaseCapture();                                //释放鼠标
            ClipCursor(&oldrect);                            //恢复鼠标的活动区域
            CView::OnLButtonDblClk(nFlags, point);
        }

❑要点说明

除了前面介绍的鼠标捕捉和限制鼠标的活动区域外,还有一些常用的与鼠标相关的API函数,具体介绍如下。

• GetDoubleClickTime:获取鼠标双击有效的时间间隔。

• SetDoubleClickTime:设置鼠标双击有效的时间间隔。

• SwapMouseButton:交换鼠标左右键。

• GetCursorPos:获取鼠标的位置信息。

• SetCursorPos:设置鼠标的位置。

实例34:使用程序模拟鼠标动作

❑实例说明

对于复杂的或者重复性的鼠标输入操作,有时需要使用程序模拟鼠标的动作。这时可使用SetCursorPos函数将鼠标移动到某一位置,而后使用mouse_event函数发送鼠标动作消息。本实例将使用菜单命令触发模拟鼠标操作,即当执行“双击标题条”菜单命令时,鼠标将移动到标题条,而后执行双击操作;执行“单击关闭按钮”菜单命令时,鼠标将移动到关闭按钮,而后执行单击操作,关闭窗口。实例的运行界面如图2.14所示。

图2.14 程序的运行界面

❑核心开发过程

两个菜单项的WM_COMMAND消息响应函数的实现代码分别如下:

        void CMainFrame::OnDbclicked()                                //"双击标题条"菜单命令
        {
            // TODO: Add your command handler code here
            POINT lpPoint;
            CRect rect;
            CWnd *pParent=AfxGetApp()->GetMainWnd();                  //获取主窗口指针
            pParent->GetWindowRect(&rect);                            //获取主窗口的区域
            lpPoint.x=rect.left+60;
            lpPoint.y=rect.top+10;
            SetCursorPos(lpPoint.x,lpPoint.y);                        //将鼠标的位置移动到标题条上
            Sleep(1000);                                              //等待1s
            //双击标题条
            mouse_event(MOUSEEVENTF_LEFTDOWN,0,0,0,0);
            mouse_event(MOUSEEVENTF_LEFTUP,0,0,0,0);
            mouse_event(MOUSEEVENTF_LEFTDOWN,0,0,0,0);
            mouse_event(MOUSEEVENTF_LEFTUP,0,0,0,0);
        }
        void CMainFrame::OnLclick()                                   //"单击关闭按钮"菜单命令
        {
            // TODO: Add your command handler code here
            POINT lpPoint;
            CRect rect;
            CWnd *pParent=AfxGetApp()->GetMainWnd();                  //获取主窗口指针
            pParent->GetWindowRect(&rect);
            lpPoint.x=rect.right-10;
            lpPoint.y=rect.top+5;
            SetCursorPos(lpPoint.x,lpPoint.y);                        //将鼠标的位置移动到关闭按钮上
            Sleep(1000);                                              //等待1s
            //单击关闭按钮
            mouse_event(MOUSEEVENTF_LEFTDOWN,0,0,0,0);
            mouse_event(MOUSEEVENTF_LEFTUP,0,0,0,0);
        }

❑要点说明

使用API函数mouse_event可以触发鼠标消息,模拟鼠标的动作。mouse_event函数的原型如下:

        VOID mouse_event(
            DWORD dwFlags,                  //鼠标的动作
            DWORD dx,                       //鼠标的绝对X位置或相对移动量
            DWORD dy,                       //鼠标的绝对Y位置或相对移动量
            DWORD dwData,                   //一般设置为0
            ULONG_PTR dwExtraInfo           //一般设置为0
        );

2.3 其他消息处理

前面主要介绍了键盘消息、鼠标消息及其相关的操作,而窗口消息的使用在后面章节中会有详细的介绍,本节将给出一些其他常用的消息处理的实例。

实例35:创建和使用自定义消息

❑实例说明

在应用程序中,用户也可以创建自己的消息,在需要的时候发送和处理消息。本实例将自定义一个消息,进行消息映射,并定义相关的消息处理函数。当用户在对话框中按下按钮时,向窗口发送该消息。实例的运行界面如图2.15所示。

图2.15 程序的运行界面

❑核心开发过程

(1)在文件“OwnerMessageDlg.cpp”中,声明自定义消息,代码如下:

        #define WM_MYMESSAGE (WM_USER+100)                       //定义自定义消息

(2)在头文件“OwnerMessageDlg.h”中,声明消息响应函数,代码如下:

        afx_msg LRESULT OnMyMessage(WPARAM w,LPARAM l);      //添加消息响应函数

(3)在文件“OwnerMessageDlg.cpp”中,添加消息响应宏,代码如下:

        BEGIN_MESSAGE_MAP(COwnerMessageDlg, CDialog)
            //{{AFX_MSG_MAP(COwnerMessageDlg)
            ON_WM_SYSCOMMAND()
            ON_WM_PAINT()
            ON_WM_QUERYDRAGICON()
            ON_BN_CLICKED(IDC_BUTTON1, OnButton1)
            ON_MESSAGE(WM_MYMESSAGE,OnMyMessage)               //添加消息响应宏
            //}}AFX_MSG_MAP
        END_MESSAGE_MAP()

(4)在文件“OwnerMessageDlg.cpp”中,实现消息响应函数,代码如下:

        LRESULT COwnerMessageDlg::OnMyMessage(WPARAM wParam, LPARAM lParam)
        {
            AfxMessageBox("自定义消息响应成功!");
            return 1;
        }

(5)在按钮的BN_CLICKED消息响应函数中,触发用户自定义函数,代码如下:

        void COwnerMessageDlg::OnButton1()
        {
            // TODO: Add your control notification handler code here
            PostMessage(WM_MYMESSAGE, IDC_BUTTON1);
        }

❑要点说明

Windows消息的投递有两种方式:PostMessage和SendMessage。二者的区别主要在于是否等待其他程序消息处理:

• PostMessage只是把消息放入队列,不管其他程序是否处理都继续执行;

• SendMessage必须等待其他程序处理消息后才继续执行。

这两个函数的返回值也不同,PostMessage的返回值表示PostMessage函数执行是否正确,而SendMessage的返回值表示其他程序处理消息后的返回值。

实例36:使用命令范围添加消息处理函数

❑实例说明

在MFC中,可以使用ClassWizard为每一个控件、菜单命令项、工具按钮等添加消息映射和消息处理函数。而使用VC提供的范围消息映像宏,则可以实现为ID在某一范围内的对象添加同一个消息处理函数。

本实例将使用ON_COMMAND_RANGE宏和ON_UPDATE_COMMAND_UI_RANGE宏,为在某一范围内的4个菜单命令项,分别添加同一命令处理和界面更新函数。实例的运行界面如图2.16所示。

图2.16 程序的运行界面

❑核心开发过程

(1)在视图类的头文件中,手工添加菜单项组的命令函数和界面函数,代码如下:

        protected:
            afx_msg void OnMenuItemCommandRange(UINT nID);                    //命令函数
            afx_msg void OnUpdateMenuItemCommandRange(CCmdUI* pCCmdUI);       //界面函数

(2)在视图类的实现文件中,为菜单项组通过使用ON_COMMAND_RANGE宏和ON_UPDATE_COMMAND_UI_RANGE宏,实现消息映射。代码如下:

        BEGIN_MESSAGE_MAP(CCommandRangeDemoView, CView)
            //{{AFX_MSG_MAP(CCommandRangeDemoView)
                // NOTE - the ClassWizard will add and remove mapping macros here.
                //     DO NOT EDIT what you see in these blocks of generated code!
            //}}AFX_MSG_MAP
            // Standard printing commands
            ON_COMMAND(ID_FILE_PRINT, CView::OnFilePrint)
            ON_COMMAND(ID_FILE_PRINT_DIRECT, CView::OnFilePrint)
            ON_COMMAND(ID_FILE_PRINT_PREVIEW, CView::OnFilePrintPreview)
            //命令消息映射
            ON_COMMAND_RANGE(ID_MENUITEM1,ID_MENUITEM4,OnMenuItemCommandRange)
            ON_UPDATE_COMMAND_UI_RANGE(ID_MENUITEM1,ID_MENUITEM4,OnUpdateMenuItem
        CommandRange)                                                  //界面消息映射
        END_MESSAGE_MAP()

(3)在菜单项组的消息响应函数中,根据菜单ID的不同,执行不同的处理方式。代码如下:

        void CCommandRangeDemoView::OnMenuItemCommandRange(UINT nID)
        {
            switch(nID)
            {
                case ID_MENUITEM1:
                    m_radio=1;
                    AfxMessageBox("命令1菜单项执行成功!");
                    break;
                case ID_MENUITEM2:
                    m_radio=2;
                    AfxMessageBox("命令2菜单项执行成功!");
                    break;
                case ID_MENUITEM3:
                    m_radio=3;
                    AfxMessageBox("命令3菜单项执行成功!");
                    break;
                case ID_MENUITEM4:
                    m_radio=4;
                    AfxMessageBox("命令4菜单项执行成功!");
                    break;
            }
        }
        void CCommandRangeDemoView::OnUpdateMenuItemCommandRange(CCmdUI* pCCmdUI)
        {
            switch(pCCmdUI->m_nID)
            {
                case ID_MENUITEM1:
                    pCCmdUI->SetRadio(m_radio==1);
                    break;
                case ID_MENUITEM2:
                    pCCmdUI->SetRadio(m_radio==2);
                    break;
                case ID_MENUITEM3:
                    pCCmdUI->SetRadio(m_radio==3);
                    break;
                case ID_MENUITEM4:
                    pCCmdUI->SetRadio(m_radio==4);
                    break;
            }
        }

❑要点说明

命令范围宏一般用来处理一组命令消息,这些消息处理的差别只在于被调用的命令的不同。这些宏可以大大简化类,否则类中就需要很多调用相同函数的多个消息处理函数。

实例37:使用定时器实时显示当前时间

❑实例说明

定时器(Timer)是Windows应用程序的一个有用的工具,它的主要用途是按程序员设定定时产生WM_TIMER消息,程序中可以通过处理WM_TIMER消息完成定时任务。本实例将创建一个定时器,实现每秒触发一次,在客户窗口中实时显示当前的时间。实例的运行界面如图2.17所示。

图2.17 程序的运行界面

❑核心开发过程(1)使用ClassWizard为视图类添加WM_CREATE消息响应函数,在其中设置定时器。代码如下:

        int CTimerDemoView::OnCreate(LPCREATESTRUCT lpCreateStruct)
        {
            if (CView::OnCreate(lpCreateStruct) == -1)
                return -1;
            // TODO: Add your specialized creation code here
            SetTimer(2,1000,NULL);                        //设置定时器,每秒触发一次
            return 0;
        }

(2)使用ClassWizard为视图类添加WM_TIMER消息响应函数,在其中实现显示当前的系统时间。代码如下:

        void CTimerDemoView::OnTimer(UINT nIDEvent)
        {
            // TODO: Add your message handler code here and/or call default
            if(nIDEvent==2)
            {
            CClientDC dc(this);                             //获得DC
            CFont myfont;
            CFont*oldfont;
            LOGFONT font;
            memset(&font,0,sizeof(LOGFONT));                //为字体结构赋初值,默认值
            font.lfHeight=50;                               //字体高度为50
            font.lfWeight=600;
            strcpy(font.lfFaceName,"Arial");
            myfont.CreateFontIndirect(&font);                      //创建新字体
            oldfont=dc.SelectObject(&myfont);                      //将新字体选入设备环境*/
            CTime m_time;
            m_time=CTime::GetCurrentTime();                        //获取当前时间日期
            CString str=m_time.Format("当前时间:%H:%M:%S");       //格式化
            dc.TextOut(40,70,str);                                 //显示时间
            dc.SelectObject(oldfont);                              //恢复设备环境中的旧字体
            }
            CView::OnTimer(nIDEvent);
        }

(3)使用ClassWizard为视图类添加WM_DESTROY消息响应函数,实现销毁定时器。代码如下:

        void CTimerDemoView::OnDestroy()
        {
            CView::OnDestroy();
            // TODO: Add your message handler code here
            KillTimer(2);                                             //销毁定时器
        }

❑要点说明

用户可以通过SetTimer函数设置多个时钟,当时间到时,系统就产生WM_TIMER消息,并通过参数告诉用户哪个时钟的时间到了。使用这种方式,用户进行周期性的处理工作非常方便。

SetTimer函数为API函数,在程序的任何位置都可以直接调用。如下面的调用:

        SetTimer(5,4000,NULL)

表明计时器的ID号为5,间隔4000ms,即系统每隔4秒自动发送WM_TIMER消息。

计时器属于系统资源,使用完应及时销毁。调用KillTimer函数后,消息队列中所有未处理的WM_TIMER消息都将被消除。如销毁上面创建的计时器,可采用下面的代码:

        KillTimer(5);

在MFC Class Wizard中,封装了WM_TIMER消息,程序员可以使用MFC Class Wizard对话框为应用程序添加WM_TIMER消息的处理函数OnTimer。

实例38:使用定时器显示毫秒级的时间

❑实例说明

使用SetTimer函数可以设置精确到毫秒的时间控制,然而CTime::GetCurrentTime()函数只能获取精确到秒的系统时间。本实例将使用_timeb结构和_ftime函数实现毫秒级的系统时间,并实时显示。实例的运行界面如图2.18所示。

图2.18 程序的运行界面

❑核心开发过程

(1)在对话框的初始化函数OnInitDialog()中,设置定时器,每毫秒触发一次。代码如下:

        BOOL CMilliSecondDemoDlg::OnInitDialog()
        {
            CDialog::OnInitDialog();
        ...
            // TODO: Add extra initialization here
            SetTimer(1,1,NULL);
//设置定时器,每毫秒触发一次
            return TRUE;   // return TRUE   unless you set the focus to a control
        }

(2)使用ClassWizard为对话框添加WM_TIMER消息响应函数,实现显示当前的系统时间。代码如下:

        void CMilliSecondDemoDlg::OnTimer(UINT nIDEvent)
        {
            // TODO: Add your message handler code here and/or call default
            struct _timeb timebuffer;
            char *timeline;
            //获得毫秒级的时间
            _ftime( &timebuffer );
            timeline = ctime(&(timebuffer.time));
            //格式化时间
            m_strTime.Format("当前时间是:%.19s.%hu %s",timeline,timebuffer.millitm,
        &timeline[20]);
            UpdateData(FALSE);                                      //显示时间
            CDialog::OnTimer(nIDEvent);
        }

要使用_ftime函数,需要包含如下的头文件:

        #include <sys/timeb.h>
        #include <time.h>

❑要点说明

_ftime函数获取的系统时间信息存储在_timeb结构体中。_timeb结构体中的time变量记录了系统时间的秒级信息,而millitm变量则记录了毫秒级的信息。