当鼠标移动的时候,Windows 会发送 WM_MOUSEMOVE 消息。默认情况下,WM_MOUSEMOVE 会发送到光标所在的窗口,你可以覆盖这个默认逻辑,捕获鼠标窗口外的鼠标消息。

鼠标点击消息 类似,WM_MOUSEMOVE 消息也包含一些参数。lParam 参数的低16位为x坐标,下一个16位为y坐标。使用 GET_X_LPARAM 和 GET_Y_LPARAM 宏获取坐标信息。wParam 参数包含一些或运算的标记。

int xPos = GET_X_LPARAM(lParam);
int yPos = GET_Y_LPARAM(lParam);

上述获取的坐标也是像素坐标,非设备独立像素

在鼠标不动的情况下,窗口的改变也会导致 WM_MOUSEMOVE 消息的产生,例如,鼠标不动,隐藏当前窗口,一样会收到 WM_MOUSEMOVE 消息。换句话说,收到 WM_MOUSEMOVE 消息并不总意味着鼠标正在移动状态。

1. 捕获窗口外的鼠标

默认情况下,WM_MOUSEMOVE 消息会在鼠标移动窗口客户区之外后,停止接收。但是有一些操作,你可能仍然需要获取客户区之外的鼠标的位置信息。例如,一个绘图程序可能具有通过拖拽操作绘制椭圆的功能,如下图所示:

拖拽演示

这就需要通过调用 SetCapture 函数接收窗口边缘的鼠标消息。在这个函数调用后,只要用户按下至少一个鼠标按钮,你将会继续接收窗口之外的鼠标移动消息。捕获的窗口必须是前台窗口,并且每次只能捕获一个窗口,使用 ReleaseCapture 函数可以这种释放鼠标捕获行为。

下面是典型使用流程:

  1. 当用户按下鼠标左键,调用 SetCapture 函数捕获鼠标。

  2. 响应鼠标移动消息。

  3. 当用户释放左键的时候,调用 ReleaseCapture 函数。

2. 绘制圆形的案例

让我们从前面扩展 Circle 程序,让用户用鼠标画一个圆。从 Direct2D Circle Sample 程序开始。我们将修改此示例中的代码以添加简单的绘图。首先,向 MainWindow 类添加一个新的成员变量。

D2D1_POINT_2F ptMouse;

当用户拖动鼠标时,该变量存储鼠标向下的位置。在 MainWindow 构造函数中,初始化 ellipse 和 ptMouse 变量。

MainWindow() : pFactory(NULL), pRenderTarget(NULL), pBrush(NULL),
    ellipse(D2D1::Ellipse(D2D1::Point2F(), 0, 0)),
    ptMouse(D2D1::Point2F())
{
}

删除 MainWindow::CalculateLayout 方法的主体,这个例子不是必需的。

void CalculateLayout() { }

接下来,声明左侧按下、左侧释放以及鼠标移动消息的处理程序。

void    OnLButtonDown(int pixelX, int pixelY, DWORD flags);
void    OnLButtonUp();
void    OnMouseMove(int pixelX, int pixelY, DWORD flags);

鼠标坐标以物理像素为单位,但 Direct2D 需要与 设备无关的像素(DIPs)。要正确处理高 DPI 设置,必须将像素坐标转换为 DIP。有关 DPI 的更多讨论,请参阅 DPI 和设备无关的像素。以下代码显示了将像素转换为 DIP 的辅助类。

class DPIScale
{
    static float scaleX;
    static float scaleY;

public:
    static void Initialize(ID2D1Factory *pFactory)
    {
        FLOAT dpiX, dpiY;
        pFactory->GetDesktopDpi(&dpiX, &dpiY);
        scaleX = dpiX/96.0f;
        scaleY = dpiY/96.0f;
    }

    template <typename T>
    static D2D1_POINT_2F PixelsToDips(T x, T y)
    {
        return D2D1::Point2F(static_cast<float>(x) / scaleX, static_cast<float>(y) / scaleY);
    }
};

float DPIScale::scaleX = 1.0f;
float DPIScale::scaleY = 1.0f;

在创建 Direct2D 工厂对象 后,请在你的 WM_CREATE 处理函数中调用 DPIScale::Initialize。

case WM_CREATE:
    if (FAILED(D2D1CreateFactory(
            D2D1_FACTORY_TYPE_SINGLE_THREADED, &pFactory)))
    {
        return -1;  // Fail CreateWindowEx.
    }
    DPIScale::Initialize(pFactory);
    return 0;

要想从鼠标消息中获取 DIPs 中的鼠标坐标,请执行以下操作:

  • 使用 GET_X_LPARAM 和GET_Y_LPARAM 宏来获取像素坐标。这些宏是在 WindowsX.h 中定义的,所以记得在你的项目中包含这个头文件。

  • 调用 DPIScale::PixelsToDipsX 和 DPIScale::PixelsToDipsY 将像素转换为 DIPs。

现在将消息处理程序添加到您的 窗口过程函数

case WM_LBUTTONDOWN:
        OnLButtonDown(GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam), (DWORD)wParam);
        return 0;

    case WM_LBUTTONUP:
        OnLButtonUp();
        return 0;

    case WM_MOUSEMOVE:
        OnMouseMove(GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam), (DWORD)wParam);
        return 0;

最后,自己实现消息处理程序。

3. 左键按下

对于左键按下消息,需要执行下面的逻辑:

  1. 调用 SetCapture 开始捕捉鼠标。

  2. 将鼠标点击的位置存储在 ptMouse 变量中。该位置定义椭圆的边界框的左上角。

  3. 重置椭圆结构。

  4. 调用 InvalidateRect,该功能强制窗口被重新粉刷。

void MainWindow::OnLButtonDown(int pixelX, int pixelY, DWORD flags)
{
    SetCapture(m_hwnd);
    ellipse.point = ptMouse = DPIScale::PixelsToDips(pixelX, pixelY);
    ellipse.radiusX = ellipse.radiusY = 1.0f;
    InvalidateRect(m_hwnd, NULL, FALSE);
}

4. 移动鼠标

对于鼠标移动消息,检查鼠标左键是否按下。如果是,重新计算椭圆并重新绘制窗口。在 Direct2D 中,椭圆由中心点和x和y半径定义。我们要绘制一个适合由鼠标点(ptMouse)和当前光标位置(x,y)定义的边界框的椭圆,所以通过计算找到椭圆的宽、高以及位置信息。

代码中重新计算椭圆,然后调 InvalidateRect 重新绘制窗口。

void MainWindow::OnMouseMove(int pixelX, int pixelY, DWORD flags)
{
    if (flags & MK_LBUTTON)
    {
        const D2D1_POINT_2F dips = DPIScale::PixelsToDips(pixelX, pixelY);

        const float width = (dips.x - ptMouse.x) / 2;
        const float height = (dips.y - ptMouse.y) / 2;
        const float x1 = ptMouse.x + width;
        const float y1 = ptMouse.y + height;

        ellipse = D2D1::Ellipse(D2D1::Point2F(x1, y1), width, height);

        InvalidateRect(m_hwnd, NULL, FALSE);
    }
}

5. 释放左键

对于左键消息,只需调用 ReleaseCapture 即可释放捕获的鼠标。

void MainWindow::OnLButtonUp()
{
    ReleaseCapture();
}