用户输入:扩展示例

让我们结合所学的用户输入知识,来创建简单的绘图程序。 以下是该程序的屏幕截图:

绘图程序的屏幕截图

用户可以使用多种不同颜色绘制椭圆,以及选择、移动或删除椭圆。 为了保持 UI 的简洁,程序不允许用户选择椭圆颜色。 相反,程序会自动循环访问预定义的颜色列表。 程序不支持椭圆以外的任何形状。 显然,此程序不会赢得任何图形软件奖。 但是,这仍然是一个值得借鉴的例子。 可以从简单绘图示例下载完整的源代码。 本部分只介绍一些亮点。

椭圆通过包含椭圆数据 (D2D1_ELLIPSE) 和颜色 (D2D1_COLOR_F) 的结构在程序中表示。 该结构还定义了两种方法:绘制椭圆的方法,以及执行命中测试的方法。

struct MyEllipse
{
    D2D1_ELLIPSE    ellipse;
    D2D1_COLOR_F    color;

    void Draw(ID2D1RenderTarget *pRT, ID2D1SolidColorBrush *pBrush)
    {
        pBrush->SetColor(color);
        pRT->FillEllipse(ellipse, pBrush);
        pBrush->SetColor(D2D1::ColorF(D2D1::ColorF::Black));
        pRT->DrawEllipse(ellipse, pBrush, 1.0f);
    }

    BOOL HitTest(float x, float y)
    {
        const float a = ellipse.radiusX;
        const float b = ellipse.radiusY;
        const float x1 = x - ellipse.point.x;
        const float y1 = y - ellipse.point.y;
        const float d = ((x1 * x1) / (a * a)) + ((y1 * y1) / (b * b));
        return d <= 1.0f;
    }
};

程序使用相同的纯色笔刷为每个椭圆绘制填充和轮廓,并根据需要更改颜色。 在 Direct2D 中,更改纯色画笔的颜色是一项高效的操作。 因此,纯色画笔对象支持 SetColor 方法。

椭圆存储在 STL 列表容器中:

    list<shared_ptr<MyEllipse>>             ellipses;

注意

shared_ptr 是一个智能指针类,已添加到 TR1 中的 C++,并在 C++0x 中正式化。 Visual Studio 2010 添加了对 shared_ptr 和其他 C++0x 功能的支持。 有关详细信息,请参阅 MSDN 杂志文章探索 Visual Studio 2010 中新的 C++ 和 MFC 功能

 

该程序有三种模式:

  • 绘图模式。 用户可以绘制新的椭圆。
  • 选择模式。 用户可以选择椭圆。
  • 拖动模式。 用户可以拖动所选的椭圆。

用户可以使用快捷键表中所述的相同键盘快捷方式在绘图模式和选择模式之间进行切换。 在选择模式下,如果用户点击椭圆,程序就会切换到拖动模式。 当用户释放鼠标按钮时,它会切换回选择模式。 当前选择以迭代器的形式存储在椭圆列表中。 帮助程序方法 MainWindow::Selection 返回指向所选椭圆的指针,如果没有选择,则返回值 nullptr

    list<shared_ptr<MyEllipse>>::iterator   selection;
     
    shared_ptr<MyEllipse> Selection() 
    { 
        if (selection == ellipses.end()) 
        { 
            return nullptr;
        }
        else
        {
            return (*selection);
        }
    }

    void    ClearSelection() { selection = ellipses.end(); }

下表汇总了这三种模式中鼠标输入的影响。

鼠标输入 绘图模式 选择模式 拖动模式
按下左键 设置鼠标捕获并开始绘制新的椭圆。 释放当前选择并执行命中测试。 如果命中椭圆,请捕获光标,选择省略号,然后切换到拖动模式。 无操作。
鼠标移动 如果按下左键,则调整椭圆的大小。 无操作。 移动选定的椭圆。
松开左键 停止绘制椭圆。 无操作。 切换到选择模式。

 

MainWindow中的以下方法处理 WM_LBUTTONDOWN 消息。

void MainWindow::OnLButtonDown(int pixelX, int pixelY, DWORD flags)
{
    const float dipX = DPIScale::PixelsToDipsX(pixelX);
    const float dipY = DPIScale::PixelsToDipsY(pixelY);

    if (mode == DrawMode)
    {
        POINT pt = { pixelX, pixelY };

        if (DragDetect(m_hwnd, pt))
        {
            SetCapture(m_hwnd);
        
            // Start a new ellipse.
            InsertEllipse(dipX, dipY);
        }
    }
    else
    {
        ClearSelection();

        if (HitTest(dipX, dipY))
        {
            SetCapture(m_hwnd);

            ptMouse = Selection()->ellipse.point;
            ptMouse.x -= dipX;
            ptMouse.y -= dipY;

            SetMode(DragMode);
        }
    }
    InvalidateRect(m_hwnd, NULL, FALSE);
}

鼠标坐标以像素为单位传递给此方法,然后转换为 DIP。 请务必不要混淆这两个单位。 例如,DragDetect 函数使用像素,但绘图和命中测试使用 DIP。 一般规则是,与窗口或鼠标输入相关的函数使用像素,而 Direct2D 和 DirectWrite 使用 DIP。 始终在高 DPI 设置下测试程序,并记住将程序标记为 DPI 感知。 有关详细信息,请参阅 DPI 和与设备无关的像素

下面是处理 WM_MOUSEMOVE 消息的代码。

void MainWindow::OnMouseMove(int pixelX, int pixelY, DWORD flags)
{
    const float dipX = DPIScale::PixelsToDipsX(pixelX);
    const float dipY = DPIScale::PixelsToDipsY(pixelY);

    if ((flags & MK_LBUTTON) && Selection())
    { 
        if (mode == DrawMode)
        {
            // Resize the ellipse.
            const float width = (dipX - ptMouse.x) / 2;
            const float height = (dipY - ptMouse.y) / 2;
            const float x1 = ptMouse.x + width;
            const float y1 = ptMouse.y + height;

            Selection()->ellipse = D2D1::Ellipse(D2D1::Point2F(x1, y1), width, height);
        }
        else if (mode == DragMode)
        {
            // Move the ellipse.
            Selection()->ellipse.point.x = dipX + ptMouse.x;
            Selection()->ellipse.point.y = dipY + ptMouse.y;
        }
        InvalidateRect(m_hwnd, NULL, FALSE);
    }
}

前面在“示例:绘制圆章节介绍了重设椭圆大小的逻辑。 另请注意对 InvalidateRect 的调用。 这可确保重新绘制窗口。 以下代码处理 WM_LBUTTONUP 消息。

void MainWindow::OnLButtonUp()
{
    if ((mode == DrawMode) && Selection())
    {
        ClearSelection();
        InvalidateRect(m_hwnd, NULL, FALSE);
    }
    else if (mode == DragMode)
    {
        SetMode(SelectMode);
    }
    ReleaseCapture(); 
}

可以看到,鼠标输入的消息处理程序都具有分支代码,具体取决于当前模式。 对于这个相当简单的程序来说,这样的设计是可以接受的。 但是,如果添加新模式,它可能会很快变得过于复杂。 对于较大的程序,模型视图控制器 (MVC) 体系结构可能是更好的设计。 在这种体系结构中,处理用户输入的控制器与管理应用程序数据的模型分离开来。

当程序切换模式时,光标会更改,以便向用户提供反馈。

void MainWindow::SetMode(Mode m)
{
    mode = m;

    // Update the cursor
    LPWSTR cursor;
    switch (mode)
    {
    case DrawMode:
        cursor = IDC_CROSS;
        break;

    case SelectMode:
        cursor = IDC_HAND;
        break;

    case DragMode:
        cursor = IDC_SIZEALL;
        break;
    }

    hCursor = LoadCursor(NULL, cursor);
    SetCursor(hCursor);
}

最后,请记住在窗口收到 WM_SETCURSOR 消息时设置光标:

    case WM_SETCURSOR:
        if (LOWORD(lParam) == HTCLIENT)
        {
            SetCursor(hCursor);
            return TRUE;
        }
        break;

总结

在本模块中,您了解到如何处理鼠标和键盘输入;如何定义键盘快捷方式;以及如何更新游标图像以反映程序的当前状态。