前面说了鼠标的点击和移动,下面讲一下鼠标的其它操作。

1. 拖拽UI元素

如果你的应用程序界面支持元素拖拽功能,那么当你按下鼠标的时候需要调用 DragDetect 函数。这个函数会检测用户是否正在实现一个拖拽的手势,如果是函数就会返回 TURE。下面展示了函数的使用方法:

case WM_LBUTTONDOWN:
    {
        POINT pt = { GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam) };
        if (DragDetect(m_hwnd, pt))
        {
            // Start dragging.
        }
    }
    return 0;

这个函数的大致原理:当程序支持拖拽的时候,你需要判断用户的鼠标动作是否为拖拽行为,判断的方法就是监视鼠标的移动范围,如果鼠标在点击后超过了一定的移动距离,则代表拖拽,而这个移动距离非常关键,DragDetect 函数就是来判断鼠标是否超过这个移动距离的,超过则返回 TURE。

除非你的程序支持拖拽,否则不要尝试一直调用 DragDetect 函数,因为调用这个函数后,会无法收到鼠标按键释放消息(WM_LBUTTONUP)。

2. 限制鼠标指针

有时候你可能想将鼠标指针限制在客户区或者客户区的某一部分,ClipCursor 函数可以将鼠标指针限制到一个矩形区域。这个矩形区域的坐标系是屏幕坐标系,而不是客户区坐标系,简单的说原点 (0,0) 在屏幕的左上角,而非窗口的左上角。可以调用 ClientToScreen 将客户区的坐标转换为屏幕区的坐标。

下面是限制光标的代码:

// Get the window client area.
RECT rc;
GetClientRect(m_hwnd, &rc);

// Convert the client area to screen coordinates.
POINT pt = { rc.left, rc.top };
POINT pt2 = { rc.right, rc.bottom };
ClientToScreen(m_hwnd, &pt);
ClientToScreen(m_hwnd, &pt2);
SetRect(&rc, pt.x, pt.y, pt2.x, pt2.y);

// Confine the cursor.
ClipCursor(&rc);

ClipCursor 的参数是一个 RECT 结构体,但是 ClientToScreen 接收的是一个 POINT 结构体,所以代码中将矩形的左上角的点和右下角的点进行转换,然后将转换后的两个点生成一个矩形,最后传入到 ClipCursor 函数中。

如果想要移除限制,再次调用 ClipCursor,参数传 NULL 即可。

ClipCursor(NULL);

3. 鼠标事件跟踪:悬停 和 离开

默认情况下还有两个鼠标消息是被禁用的,但是在一些程序中它们可能很有用:

  • WM_MOUSEHOVER 该消息是当鼠标在客户区悬停不动的时候发送的。

  • WM_MOUSELEAVE 该消息是当鼠标离开客户区发送的。

可以调用 TrackMouseEvent 函数开启这两个消息的接收。

TRACKMOUSEEVENT tme;
tme.cbSize = sizeof(tme);
tme.hwndTrack = hwnd;
tme.dwFlags = TME_HOVER | TME_LEAVE;
tme.dwHoverTime = HOVER_DEFAULT;
TrackMouseEvent(&tme);

函数需要传入一个 TRACKMOUSEEVENT 结构体,该结构体中包含很多成员变量。dwFlags 是一个标记位集合,该标记为指定了跟踪的鼠标事件类型,可以选择 WM_MOUSEHOVER 和 WM_MOUSELEAVE 事件中的一个,或者二者都选。dwHoverTime 指定悬停判断的时间,单位为毫秒,只有超过这个时间范围,才会触发悬停事件。HOVER_DEFAULT 是系统默认的一个悬停时间设置。

当你收到其中一个消息后,TrackMouseEvent 函数将被重置。如果你想继续接收消息,则需要再次设置。不过再次设置该函数之前,你需要确保鼠标真正的移动过,否则将会不断的触发这两个消息。举个例子:如果鼠标当前已经处于悬停状态,现在你收到了 WM_MOUSEHOVER 消息,这时候逻辑上你并不需要再次调用 TrackMouseEvent 判断是否鼠标还是悬停状态,除非你收到鼠标移动消息之后。如果你再次调用,则处于悬停状态的鼠标还会再次触发这个消息,整个程序逻辑会在鼠标移动前陷入一个死循环中。

下面是示例代码:

class MouseTrackEvents
{
    bool m_bMouseTracking;

public:
    MouseTrackEvents() : m_bMouseTracking(false)
    {
    }

    void OnMouseMove(HWND hwnd)
    {
        if (!m_bMouseTracking)
        {
            // Enable mouse tracking.
            TRACKMOUSEEVENT tme;
            tme.cbSize = sizeof(tme);
            tme.hwndTrack = hwnd;
            tme.dwFlags = TME_HOVER | TME_LEAVE;
            tme.dwHoverTime = HOVER_DEFAULT;
            TrackMouseEvent(&tme);
            m_bMouseTracking = true;
        }
    }
    void Reset(HWND hwnd)
    {
        m_bMouseTracking = false;
    }
};

下面是窗口过程的逻辑处理:

LRESULT MainWindow::HandleMessage(UINT uMsg, WPARAM wParam, LPARAM lParam)
{
    switch (uMsg)
    {
    case WM_MOUSEMOVE:
        mouseTrack.OnMouseMove(m_hwnd);  // Start tracking.

        // TODO: Handle the mouse-move message.

        return 0;

    case WM_MOUSELEAVE:

        // TODO: Handle the mouse-leave message.

        mouseTrack.Reset(m_hwnd);
        return 0;

    case WM_MOUSEHOVER:

        // TODO: Handle the mouse-hover message.

        mouseTrack.Reset(m_hwnd);
        return 0;

    }
    return DefWindowProc(m_hwnd, uMsg, wParam, lParam);
}

鼠标事件跟踪需要系统增加额外的处理,如果你不需要最好禁用它们。

下面这个函数可以查询默认的悬停超时时间:

UINT GetMouseHoverTime()
{
    UINT msec;
    if (SystemParametersInfo(SPI_GETMOUSEHOVERTIME, 0, &msec, 0))
    {
        return msec;
    }
    else
    {
        return 0;
    }
}

4. 鼠标滚轮

下面这个函数可以判断鼠标是否存在滚轮:

BOOL IsMouseWheelPresent()
{
    return (GetSystemMetrics(SM_MOUSEWHEELPRESENT) != 0);
}

如果用户旋转滚轮,操作系统将会收到 WM_MOUSEWHEEL 消息。lParam 参数包含了一个叫做delta的整数值,表示滚轮旋转的距离。delta 的单位任意,其中 120 单位定义为执行一次动作的旋转阀值。当然判断动作的阀值大小还要程序来决定。例如,一个文本编辑程序,120单位可以定义为滚动一行,也可以定义为滚动一页,具体还要看程序的逻辑。

delta 是一个有符号整形,符号代表方向:

  • 正数,向前滚,离开用户方向。

  • 负数,向后滚,靠近用户方向。

可以使用 GET_WHEEL_DELTA_WPARAM 宏获取 delta 的大小:

int delta = GET_WHEEL_DELTA_WPARAM(wParam);

如果鼠标有一个高分辨率的滚轮,delta 的值可能小于120。你可以将这个值映射到你程序的逻辑中,例如120代表滚动一行,小于120则滚动半行,甚至更小。