Ввод данных пользователем: расширенный пример

Давайте объединим все, что мы узнали о вводе пользователем, чтобы создать простую программу рисования. Ниже приведен снимок экрана программы:

screen shot of the drawing program

Пользователь может нарисовать многоточие в нескольких разных цветах, а также выбрать, переместить или удалить многоточие. Чтобы пользовательский интерфейс был простым, программа не позволяет пользователю выбирать многоточие цвета. Вместо этого программа автоматически перебирает предопределенный список цветов. Программа не поддерживает фигуры, отличные от многоточия. Очевидно, что эта программа не выиграет никаких наград за графическое программное обеспечение. Тем не менее, это по-прежнему полезный пример для изучения. Полный исходный код можно скачать из примера простого рисования. В этом разделе рассматриваются лишь некоторые основные моменты.

Многоточие представлено в программе структурой, содержащей многоточие (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 — это класс интеллектуального указателя, добавленный в C++ в TR1 и формализованный в C++0x. Visual Studio 2010 добавляет поддержку shared_pt r и других функций C++0x. Дополнительные сведения см. в статье "Изучение новых возможностей C++ и MFC" в Visual Studio 2010 в журнале MSDN. (Этот ресурс может быть недоступен в некоторых языках и странах.)

 

Программа имеет три режима:

  • Режим рисования. Пользователь может нарисовать новые многоточия.
  • Режим выбора. Пользователь может выбрать многоточие.
  • Режим перетаскивания. Пользователь может перетаскивать выделенный многоточие.

Пользователь может переключаться между режимом рисования и режимом выбора, используя те же сочетания клавиш, которые описаны в таблицах ускорителей. В режиме выбора программа переключается в режим перетаскивания, если пользователь щелкает многоточие. Он переключается обратно в режим выбора, когда пользователь отпускает кнопку мыши. Текущий выбор сохраняется в виде итератора в списке многоточий. Вспомогательный метод 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);
}

Координаты мыши передаются этому методу в пикселях, а затем преобразуются в diPs. Важно не путать эти два блока. Например, функция DragDetect использует пиксели, но при рисовании и тестировании попаданий используются diPs. Общее правило заключается в том, что функции, связанные с окнами или вводом мыши, используют пиксели, а Direct2D и DirectWrite использовать DIP. Всегда тестируйте программу в параметре с высоким разрешением и не забудьте пометить программу как результаты 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;

Итоги

В этом модуле вы узнали, как обрабатывать ввод с помощью мыши и клавиатуры; определение сочетаний клавиш; и как обновить изображение курсора, чтобы отразить текущее состояние программы.