修改鼠标光标的形状

来源:互联网 发布:英文字体设计软件 编辑:程序博客网 时间:2024/05/02 04:54

转自: http://blog.csdn.net/duxiuxing/archive/2007/05/07/1599771.aspx

 

 

 我还是比较习惯通过案例说明问题,需求案例描述如下:实现一个从 CDialog 派生的窗口类,要求当鼠标移动到窗口客户区的时候,修改鼠标指针的形状,使其显示一个小手的形状。

    为了后面阐述方便,我们假设该派生窗口类名称为 CMyDialog,鼠标光标对应资源为 IDC_CURSOR_HAND。

经常看到有的同僚会用下面这种不太好的方法来实现上面的需求:
(1)在 CMyDialog 中添加 WM_MOUSEMOVE 消息的映射函数 void CMyDialog::OnMouseMove();
(2)在 OnMouseMove() 中通过调用 SetCursor() 来改变光标形状,代码如下:

void CMyDialog::OnMouseMove(UINT nFlags, CPoint point) 
{
    
// TODO: Add your message handler code here and/or call default
    SetCursor(::LoadCursor(AfxGetResourceHandle(), MAKEINTRESOURCE(IDC_CURSOR_HAND)));
    CDialog::OnMouseMove(nFlags, point);
}


    这种实现方法主要存在两个缺陷:首先是闪烁问题,眼尖的人应该能看出在鼠标移动过程中,光标会有较明显的闪烁现象;其次是在鼠标点击,双击的时候,SetCursor() 的行为失效,光标形状会变回默认的指针形状。


    其实在 MFC 程序中,要修改鼠标光标形状有下面三种比较正统的方法可以用,所谓正统不是我吹的,它们出自 MSDN 技术文章:《HOWTO: Change the Mouse Pointer for a Window in MFC (Q131991)》。

Three Methods
Here are three ways an application can change the mouse pointer in a window:
(1)Override the CWnd::OnSetCursor() function. Call Windows API SetCursor() function to change the pointer.
(2)Register your own window class with the desired mouse pointer, override the CWnd::PreCreateWindow() function, and use the newly-registered window class to create the window.
(3)To show the standard hourglass pointer, an application can call the CCmdTarget::BeginWaitCursor(), which displays the hourglass, and call CmdTarget::EndWaitCursor() to revert back to the default pointer. This scheme works only for the duration of a single message. If the mouse is moved before a call to EndWaitCursor is made, Windows sends a WM_SETCURSOR message to the window underneath the pointer. The default handling of this message resets the pointer to the default type, the one registered with the class, so you need to override CWnd::OnSetCursor() for that window, and reset the pointer back to the hourglass.

    方法一和方法二主要针对于那些与需求案例类似的,希望在程序中定制鼠标光标形状的需求。这两种方法都可以实现把系统默认的鼠标光标(以下简称默认光标)形状修改为定制的光标形状。方法三主要是针对沙漏形状的鼠标光标(以下简称沙漏光标)的应用,沙漏光标是 Windows 操作系统里面一项比较重要和常见的用户体验,对于那些执行周期相对比较长的(通常是需要用户稍微等待一段时间的)操作,系统往往会在操作执行的过程中把默认光标修改为沙漏光标,以提示用户需要等待。 

    在后面的文章中,我将在 MSDN 技术文章的基础上,结合我自己的实践,通过实际可运行的代码来解释上面这三种方法。

 

 

 

 继续上一篇文章未完的话题,下面继续通过代码对之前提到的三种方法进行解释。

    在 CMyDialog 增加成员变量 HCURSOR m_hMyCurosr; 并在 CMyDialog 构造函数中将其初始化:

m_hMyCurosr = NULL;

    我的想法是通过 CMyDialog::m_hMyCurosr 来保存所要使用的鼠标光标形状,当其为 NULL 的时候,则使用系统默认的鼠标光标。另外在 CMyDialog 中增加 public 成员函数 SetMyCursor() 供外部调用(这个大家应该可以理解吧),其实现如下:

//////////////////////////////////////////////////////
//
// 函数名: CMyDialog::SetMyCursor
//
// 访问权: public
//
// 描述: 设置新的鼠标光标 
//
// 参数:
// hCursor 
// 新的鼠标光标句柄,为 NULL 表示使用系统默认鼠标光标形状
// 
// 返回值: 返回之前所使用的鼠标光标句柄
// 
//////////////////////////////////////////////////////
HCURSOR CMyDialog::SetMyCursor(HCURSOR hCursor)
{
    HCURSOR hRet 
= m_hMyCurosr;
    m_hMyCurosr 
= hCursor;
    
return hRet; 
}

方法一:在处理 WM_SETCURSOR 消息的时候调用 API 函数 SetCursor() 来修改鼠标光标形状。

    如果鼠标引起光标在某个窗口中移动,且鼠标输入没有被捕获时,该窗口就会收到 WM_SETCURSOR 消息,我们可以把 WM_SETCURSOR 理解成是专门用来设置鼠标光标形状的消息,在 VC6 中通过 ClassWizard 可以自动生成 WM_SETCURSOR 对应的消息函数 OnSetCursor(),其实现如下:

ON_WM_SETCURSOR()

BOOL CMyDialog::OnSetCursor(CWnd
* pWnd, UINT nHitTest, UINT message)
{
    
if ((HTCLIENT == nHitTest)
      
&& (m_hMyCurosr != NULL))
    {              
        ::SetCursor(m_hMyCurosr);
        
return TRUE;
    }
    
return CDialog::OnSetCursor(pWnd, nHitTest, message);

*要注意的是,当设置了鼠标光标以后,您应该让函数返回 TRUE 以防止系统再作缺省处理。

 

方法二:通过 AfxRegisterClass() 或者 AfxRegisterWndClass() 直接向系统注册一个指定鼠标光标的窗口类型。


   在注册窗口类型时所指定的这个鼠标光标又称为全程光标。 在 MFC 框架之下,我们通常把这一步操作放在 CWnd::PreCreateWindow() 函数中完成。

BOOL CMyDialog::PreCreateWindow(CREATESTRUCT& cs)
{
    
if (m_hMyCurosr != NULL)
    {
        cs.lpszClass 
= AfxRegisterWndClass(
            CS_DBLCLKS 
| CS_HREDRAW | CS_VREDRAW, // use any window styles
            m_hMyCurosr,
            (HBRUSH) (COLOR_WINDOW 
+ 1));         // background brush
    }
    
return CDialog::PreCreateWindow(cs)

     
    从最终实现的效果上看方法一和方法二没有什么区别。但从易用性和灵活性的角度上看,我本人则更加青睐于方法一。其易用性在于我不需要特别记住 AfxRegisterWndClass() 这个函数的用法;其灵活性在于能够在程序运行过程中精确地控制鼠标指针的形状,譬如说我把上面的需求案例变动一下:实现一个从 CDialog 派生的窗口类,要求当鼠标移动到窗口客户区中特定区域的时候,修改鼠标指针的形状,使其显示一个小手的形状。其中特定区域的大小由其成员变量 CRect m_rcChangeCursor 指定。

只需要把 OnSetCursor() 改写一下即可满足以上需求:

BOOL CMyDialog::OnSetCursor(CWnd* pWnd, UINT nHitTest, UINT message) 
{
    
// TODO: Add your message handler code here and/or call default
    if (m_hMyCurosr != NULL)
    {              
        CPoint pt(
00);
        ::GetCursorPos(
&pt);
        ScreenToClient(
&pt);
        
if (m_rcChangeCursor.PtInRect(pt))
        {
            ::SetCursor(m_hMyCurosr);
            
return TRUE;
        }
    }
    
    
return CDialog::OnSetCursor(pWnd, nHitTest, message);
}


在本篇结束之前,我们回过头谈一下 WM_SETCURSOR 消息的处理机制:
(a) 缺省的窗口过程首先发送 WM_SETCURSOR 消息到当前光标下的窗口或者控件;
(b) 如果窗口或者控件处理了 WM_SETCURSOR 消息(即返回 TRUE),则 Windows 不再作任何多余的事情,处理完消息便结束;
(c) 如果窗口或者控件不处理 WM_SETCURSOR 消息(例如返回 FALSE 或者调用 MFC 基类的默认处理),则继续让父窗口来处理 WM_SETCURSOR;
(d) 如果父窗口也不做任何处理,则使用全程光标;
(e) 如果没有全程光标,则使用当前系统默认的箭头光标。

 

方法三:沙漏光标为 Windows 操作系统本身默认支持之特性,微软已经把对沙漏光标的支持封装在 MFC 框架当中,开发人员可以不需要为此而在程序中引入 Cursor 资源。在 MFC 程序中,可以使用以下三种方式来操作沙漏光标:
(1)CWinApp::DoWaitCursor(); 
(2)CCmdTarget::BeginWaitCursor(),CCmdTarget::EndWaitCursor()和CCmdTarget::RestoreWaitCursor();
(3)CWaitCursor 封装类。

 通过 Debug 跟踪可知,(2)和(3)的封装最终还是通过调用(1)来实现的。它们存在以下的对应关系:

 

说明(1)(2)(3)打开沙漏光标CWinApp::DoWaitCursor(1)CCmdTarget::BeginWaitCursor()CWaitCursor 构造函数关闭沙漏光标CWinApp::DoWaitCursor(-1)CCmdTarget::EndWaitCursor()CWaitCursor 析构函数刷新沙漏光标CWinApp::DoWaitCursor(0)CCmdTarget::RestoreWaitCursor()CWaitCursor::Restore()

 

下面列举几个实例场景以作说明:

场景1:在开始执行长操作之前打开沙漏光标,在长操作操作结束之后关闭沙漏光标。长操作在这里是指那些相对需要比较长的时间才能执行完毕的操作,比如说大文件的复制粘贴。

void CMyDialog::DoSomeLengthyOperation()
{
    BeginWaitCursor();  
// or AfxGetApp()->DoWaitCursor(1) 
    
    
//... 执行长操作

    EndWaitCursor();    
// or AfxGetApp()->DoWaitCursor(-1)

如果在长操作的执行过程中,通过 DoModal() 方式显示了其他窗口的话,需要在 DoModal() 执行完毕之后进行沙漏光标的操作,以下代码修改自 MSDN :

void CMyDialog::DoSomeLengthyOperation( )
{
   CWaitCursor wait;   
// display wait cursor

   
// do some lengthy processing

   
// The dialog box will normally change the cursor to
   
// the standard arrow cursor.
   CSomeDialog dlg;
   dlg.DoModal( );

   
// It is necessary to call Restore here in order
   
// to change the cursor back to the wait cursor.
   wait.Restore( );

   
// do some more lengthy processing

   
// destructor automatically removes the wait cursor
}

 

场景2:有一些操作或者调用,用户不可以马上从 UI 上感觉到操作已经执行,此时我们可以把光标切换沙漏光标,并且保持显示1到2秒,以提示用户操作已被执行。我所遇到最经典的案例就是客户端程序调用 IE 进行网页的跳转操作。

void CMyDialog::JumpWebSite(LPCTSTR lpszUrl)
{
   CWaitCursor wait;

   ::ShellExecute(GetSafeHwnd(), _T(
"open"), _T("iexplore.exe"), lpszUrl, NULL, SW_SHOWNORMAL);
   
   
// 保持沙漏光标至少 1 秒
   ::Sleep(1000);        
}

在场景1和场景2里面,沙漏光标的打开和关闭都是在同一段连续的处理过程中进行的(在示例代码中,“同一段连续的处理过程中”其实就是“同一个函数里面”的意思),长操作会造成程序短时间内 UI 无响应,它们的实现原理实际上是在 UI 无响应之前(即长操作开始之前)先把光标置成沙漏,从而使得在 UI 无响应的这段时间内(即长操作执行过程中)光标一直呈沙漏形状。这种情况下,沙漏光标的显示时间很大程度上取决于长操作执行完毕所需要的时间,这是我们应该意识到的。

场景3:要实现对沙漏光标的精确控制,MSDN 提供了以下的示例代码:
step1:在 CMyDialog 中增加成员变量 BOOL m_bShowWaitCurosr,并在 CMyDialog 构造函数中将其初始化;

m_bShowWaitCurosr = FALSE;

step2:增加成员函数 ShowWaitCursor() 来控制沙漏光标的打开和关闭;

//////////////////////////////////////////////////////
//
// 函数名: CMyDialog::ShowWaitCursor
//
// 访问权: public
//
// 描述: 控制沙漏光标的打开和关闭
//
// 参数:
// bShow 
// TRUE:打开沙漏光标
// FALSE:关闭沙漏光标
// 
//////////////////////////////////////////////////////
void CMyDialog::ShowWaitCursor(BOOL bShow)
{    
    
if (bShow)
    {
        m_bShowWaitCurosr 
= TRUE;
        BeginWaitCursor();
    }
    
else
    {
        EndWaitCursor();    
        m_bShowWaitCurosr 
= FALSE;
    }
}

step3:处理 WM_SETCURSOR 消息。

ON_WM_SETCURSOR()

BOOL CMyDialog::OnSetCursor(CWnd
* pWnd, UINT nHitTest, UINT message)
{
    
if (m_bShowWaitCurosr)
    {              
        RestoreWaitCursor();
        
return TRUE;
    }
    
return CDialog::OnSetCursor(pWnd, nHitTest, message);

通过上一篇,我们已经知道 MFC 对沙漏光标的几种操作方式最终都是通过调用 CWinApp::DoWaitCursor() 来实现的。以下是 CWinApp::DoWaitCursor() 的实现代码:

void CWinApp::DoWaitCursor(int nCode)
{
    
// 0 => restore, 1=> begin, -1=> end
    ASSERT(nCode == 0 || nCode == 1 || nCode == -1);
    ASSERT(afxData.hcurWait 
!= NULL);
    AfxLockGlobals(CRIT_WAITCURSOR);
    m_nWaitCursorCount 
+= nCode;
    
if (m_nWaitCursorCount > 0)
    {
        HCURSOR hcurPrev 
= ::SetCursor(afxData.hcurWait);
        
if (nCode > 0 && m_nWaitCursorCount == 1)
            m_hcurWaitCursorRestore 
= hcurPrev;
    }
    
else
    {
        
// turn everything off
        m_nWaitCursorCount = 0;     // prevent underflow
        ::SetCursor(m_hcurWaitCursorRestore);
    }
    AfxUnlockGlobals(CRIT_WAITCURSOR);
}

 根据上面的代码,我们至少可以分析出以下知识点:
(1)afxData 是一个全局的结构,CWinApp 把沙漏光标的句柄存放在它一个成员变量 afxData.hcurWait 当中,同时 CWinApp::DoWaitCursor() 是一个虚函数,如果我们需要实现自定义沙漏光标的话,直接修改 afxData.hcurWait  的值或者重载 CWinApp::DoWaitCursor() 应该都是可以率先考虑的做法;


(2)CWinApp 对沙漏光标的打开、关闭和刷新操作,是通过其成员变量 CWinApp::m_nWaitCursorCount 来控制的,CWinApp 实际上是对沙漏光标作一个引用计数的管理,引用计数操作的类型由函数参数 nCode 的值决定;


(3)CWinApp::DoWaitCursor() 的代码实现中包含了对 API 函数 SetCursor() 的调用,这跟《修改鼠标光标的形状(二)》中所涉及的内容是相吻合的;

    *想查看某个 MFC 函数的实现代码,(我知道的)一般有以下三种方法,为弥补本人表达能力和部分读者理解能力的不足,我举例稍微说明一下,比方说要查 CWinApp::DoWaitCursor() 的实现代码:
方法1:通过 VC 的查找功能进行定位;
(1.1)先在通过主菜单 Edit->Find in Files... 调出查找窗口;



(1.2)Find What 里面填入查找关键字:CWinApp::DoWaitCursor;
(1.3)In Folder 里面填入查找 MFC 代码的存放路径:C:/PROGRAM FILES/MICROSOFT VISUAL STUDIO/VC98/MFC;
(1.4)其它查找选项根据实际情况填写;
(1.5)按“Find”按钮开始查找

方法2:安装 Visual Assist,利用它的代码跳转功能进行定位;
根据我多年的使用体会,Visual Assist 的确可以提高代码编写的速度,作为一名 VC 程序员,如果你还没有用过的话,推荐安装使用。在 www.VCKbase.com 上有个叫程序员工具箱的栏目,里面的“辅助开发工具”分类中有最新版本下载。

方法3:在代码中函数被调用的地方设置断点,通过 Debug 进行定位。
沿用之前的代码例子,在 CMyDialog::OnInitDialog() 函数中添加以下代码:

BOOL CMyDialog::OnInitDialog()
{
    CDialog::OnInitDialog();

    
// TODO: Add extra initialization here
    AfxGetApp()->DoWaitCursor(0);
    
    
return TRUE;  // return TRUE  unless you set the focus to a control
}

按 F9 在 DoWaitCursor() 所在行设置断点,Debug 模式下使程序运行到断点处,依次按下 F11、Shift+F11和F11,CWinApp::DoWaitCursor() 的实现代码就展现在你面前了。

原创粉丝点击