鼠标光标的一个“看起来”很神奇的问题

    技术2022-05-12  1

    Windows里最让人抓狂的问题,莫过于鼠标相关的BUG了(估计所有GUI程序都一样)。鼠标的BUG往往还不能直接打断点调试,只能结合日志信息,加上自己的经验一起来查,这就更是难上加难了。

     

    最近遇到一个问题,虽然很快解决了,但解决过程自觉还是很典型的。

     

    问题是这样的:我们的程序主窗口上有一些区域鼠标移上要变手形光标,移开恢复箭头光标。原来一直是正常的,但一个新版本开发了一周后合出来后,发现光标变成手形后,不再恢复了。

     

    我们的这个主窗口的C++类继承关系是这样的:

    CMainWindow -> CSkinFrameWindow -> CSkinWindow -> CWnd

     

    程序是在OnSetCursor里设置光标的,初步的猜测是OnSetCursor没有被调用到。由于WM_SETCURSOR消息是在WM_MOUSEMOVE事件前收到的,所以预计会收到很多次。于是在OnMouseMoveOnSetCursor里,都打上了日志。结果发现WM_SETCURSOR消息正常。

     

    这说明系统的鼠标消息机制没有出奇怪的问题。这给我了很大信心,至少不用查消息为什么没有发出来这样变态的问题了。这样就说明,一定是我们的代码里哪出现问题了。

    仔细看OnSetCursor的代码:

        BOOL result = FALSE;

        if (enmMouseType_Hand == m_eMouseType)

        {

            HCURSOR hCursor = ::LoadCursor(NULL, MAKEINTRESOURCE(IDC_HAND));

            SetCursor(hCursor);

            result = TRUE;

        }

        else if (enmMouseType_Disable == m_eMouseType)

        {

            HCURSOR hCursor = AfxGetApp()->LoadCursor(IDC_CURSOR_DISABLE_BIG);

            SetCursor(hCursor);

            result = TRUE;

        }

        else

        {

            result = __super::OnSetCursor(pWnd,nHitTest,message);

        }

        return result;

     

    继续打日志,发现m_eMouseType的值没有问题,在正确的时机都被正确的赋值了。因此,基本确认问题出在标红的一句,这一个分支就是处理把鼠标变成箭头的。但调试下来的结果是似乎这一句没起作用。

    在这一句打了断点后,发现,基类的OnSetCursor返回FALSE,这说明基类的默认处理没有修改鼠标光标。再看我们自己写的基类,发现没有OnSetCursor代码,因此这里调用的是CWnd::OnSetCursor()

     

    MSDN里查CWnd::OnSetCursor发现以下内容:

     

    The default implementation calls the parent window's OnSetCursor before processing. If the parent window returns TRUE, further processing is halted. Calling the parent window gives the parent window control over the cursor's setting in a child window.

    The default implementation sets the cursor to an arrow if it is not in the client area or to the registered-class cursor if it is.

     

    也就是说,默认的处理是:

    1.  如果光标不在客户区,就设置成箭头

    2.  否则设置成窗口类注册时指定的光标

     

    看来,是窗口类的问题了。用Beyond Compare查了一下最近的修改历史,发现我们的CMainWindow::Create()函数原来是调用基类的CSkinWindow::Create()来创建窗口的。而基类中注册窗口类时指定了箭头光标:

     

    BOOL CSkinWindow::Create(HWND hwndParent,

                             const RECT& rect,

                             DWORD dwStyle)

    {

        return CWnd::CreateEx(

            0,

            AfxRegisterWndClass(CS_DBLCLKS,::LoadCursor(NULL, IDC_ARROW)),

            L"",

            dwStyle,

            rect.left,

            rect.top,

            rect.right  - rect.left,

            rect.bottom - rect.top,

            hwndParent,

            NULL);

    }

     

     

    而改动之后,CMainWindow::Create()不再调用基类的这个Create,而是使用了另一个窗口类来创建窗口:

     

    BOOL CMainWindow::Create(RECT rcInitialPosition)

    {

        return CWnd::CreateEx(

            0,

            L"MainWindowClass",

            L"MainWindow",

            WS_OVERLAPPED | WS_VISIBLE| WS_OVERLAPPEDWINDOW| WS_CLIPCHILDREN,

            rcInitialPosition.left,

            rcInitialPosition.top,

            rcInitialPosition.right  - rcInitialPosition.left,

            rcInitialPosition.bottom - rcInitialPosition.top,

            NULL,

            NULL);

    }

     

    再在工程中查找"MainWindowClass"这个串,发现以下注册窗口类代码:

     

    BOOL CMainApp::InitInstance()

    {

        WNDCLASS wndclass;

        wndclass.hInstance = AfxGetInstanceHandle();

        wndclass.hbrBackground = (HBRUSH) (COLOR_WINDOW + 1);

        wndclass.cbClsExtra = 0;

        wndclass.cbWndExtra = 0;

        wndclass.hCursor = NULL;

        wndclass.hIcon = NULL;

        wndclass.lpszMenuName = NULL;

        wndclass.lpfnWndProc = ::DefWindowProc;

        wndclass.style = CS_HREDRAW | CS_VREDRAW;

        wndclass.lpszClassName = L"MainWindowClass";

        wndclass.hCursor = NULL;

        bRet = AfxRegisterClass(&wndclass);

     

        return CWinApp::InitInstance();

    }

     

    事情真相大白了,新的这个窗口类注册时没有指定光标,默认处理不会自作主张的把光标改成一个箭头,于是,本应当变成箭头的分支实际上没做任何处理。

     

    修改也很简单了,注册窗口类时,指定箭头光标就好:

    wndclass.hCursor = ::LoadCursor(NULL, IDC_ARROW);

     

    结论:

    1.  不要因为调用基类的OnSetCursor没起作用,就认为是系统的什么神奇问题。几乎所有情况下,Windows经受的测试都比你的代码多,一定是你的代码出的问题。

    2.  一定要有刨根问底的精神,不要在OnSetCursor那里想办法绕过,而是应当知道为什么一段你认为应当起作用的代码没起作用,什么情况下它才会发挥你想象中的作用。


    最新回复(0)