Поделиться через


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

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

Снимок экрана программы рисования

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

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

 

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

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

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

Итоги

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