鼠标移动
鼠标移动时,Windows 会发布 WM_MOUSEMOVE 消息。 默认情况下, WM_MOUSEMOVE 转到包含光标的窗口。 可以通过 捕获 鼠标来替代此行为,下一部分将对此进行介绍。
WM_MOUSEMOVE消息包含与鼠标单击消息相同的参数。 lParam 的最低 16 位包含 x 坐标,接下来的 16 位包含 y 坐标。 使用 GET_X_LPARAM 和 GET_Y_LPARAM 宏从 lParam 中解包坐标。 wParam 参数包含按位 OR 标志,指示其他鼠标按钮的状态以及 SHIFT 和 Ctrl 键。 以下代码从 lParam 获取鼠标坐标。
int xPos = GET_X_LPARAM(lParam);
int yPos = GET_Y_LPARAM(lParam);
请记住,这些坐标以像素为单位,而不是与设备无关的像素, (DIP) 。 在本主题的后面部分,我们将介绍在两个单元之间转换的代码。
如果光标的位置相对于窗口发生更改,窗口也可以接收 WM_MOUSEMOVE 消息。 例如,如果光标位于窗口上方,并且用户隐藏窗口,则即使鼠标未移动,窗口也会收到 WM_MOUSEMOVE 消息。 此行为的一个后果是鼠标坐标可能不会在 WM_MOUSEMOVE 消息之间更改。
捕获鼠标在窗口外移动
默认情况下,如果鼠标移过工作区的边缘,窗口将停止接收 WM_MOUSEMOVE 消息。 但对于某些操作,可能需要跟踪超过此点的鼠标位置。 例如,绘图程序可能使用户能够将选择矩形拖动到窗口边缘之外,如下图所示。
若要在窗口边缘接收鼠标移动消息,请调用 SetCapture 函数。 调用此函数后,只要用户按住至少一个鼠标按钮,窗口将继续接收 WM_MOUSEMOVE 消息,即使鼠标在窗口外移动也是如此。 捕获窗口必须是前台窗口,一次只能有一个窗口作为捕获窗口。 若要释放鼠标捕获,请调用 ReleaseCapture 函数。
通常按以下方式使用 SetCapture 和 ReleaseCapture 。
- 当用户按下鼠标左键时,调用 SetCapture 开始捕获鼠标。
- 响应鼠标移动消息。
- 当用户松开鼠标左键时,请调用 ReleaseCapture。
示例:绘制圆圈
让我们通过允许用户使用鼠标绘制圆圈,从 模块 3 扩展 Circle 程序。 从 Direct2D 圆形示例 程序开始。 我们将修改此示例中的代码以添加简单绘图。 首先,向 类添加新成员变量 MainWindow
。
D2D1_POINT_2F ptMouse;
此变量存储用户拖动鼠标时鼠标向下的位置。 在构造函数中 MainWindow
,初始化 椭圆 和 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 需要与设备无关的像素 (DIP) 。 若要正确处理高 DPI 设置,必须将像素坐标转换为 DIP。 有关 DPI 的更多讨论,请参阅 DPI 和 Device-Independent 像素。 以下代码显示了一个将像素转换为 DIP 的帮助程序类。
class DPIScale
{
static float scale;
public:
static void Initialize(HWND hwnd)
{
float dpi = GetDpiForWindow(hwnd);
scale = dpi/96.0f;
}
template <typename T>
static D2D1_POINT_2F PixelsToDips(T x, T y)
{
return D2D1::Point2F(static_cast<float>(x) / scale, static_cast<float>(y) / scale);
}
};
float DPIScale::scale = 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(hwnd);
return 0;
若要从鼠标消息中获取 DIP 中的鼠标坐标,请执行以下操作:
- 使用 GET_X_LPARAM 和 GET_Y_LPARAM 宏获取像素坐标。 这些宏在 WindowsX.h 中定义,因此请记住在项目中包括该标头。
- 调用
DPIScale::PixelsToDips
以将像素转换为 DIP。
现在,将消息处理程序添加到窗口过程。
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;
最后,实现消息处理程序本身。
向左按钮向下键
对于向左按钮向下消息,请执行以下操作:
- 调用 SetCapture 开始捕获鼠标。
- 将鼠标单击的位置存储在 ptMouse 变量中。 此位置定义椭圆边框的左上角。
- 重置椭圆结构。
- 调用 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);
}
鼠标移动
对于鼠标移动消息,检查鼠标左键是否关闭。 如果是,请重新计算椭圆并重新绘制窗口。 在 Direct2D 中,椭圆由中心点和 x 和 y-radii 定义。 我们希望绘制一个椭圆,该椭圆适合由鼠标向下点 (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);
}
}
向左按钮向上
对于向左按钮向上的消息,只需调用 ReleaseCapture 即可释放鼠标捕获。
void MainWindow::OnLButtonUp()
{
ReleaseCapture();
}