用户输入:扩展示例

让我们结合我们学到的有关用户输入的所有内容来创建一个简单的绘图程序。 下面是程序的屏幕截图:

绘图程序的屏幕截图

用户可以绘制多种不同颜色的省略号,以及选择、移动或删除省略号。 为了保持 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 和 Device-Independent 像素

下面是处理 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;

总结

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