Benutzereingabe: erweitertes Beispiel

Lassen Sie uns nun alles kombinieren, was wir über Benutzereingaben gelernt haben, um ein einfaches Programm für Zeichnungen zu erstellen. Dies ist ein Screenshot des Programms:

Screenshot des Programms für Zeichnungen

Benutzer*innen können Ellipsen in verschiedenen Farben zeichnen und Ellipsen auswählen, verschieben oder löschen. Um die Benutzeroberfläche einfach zu halten, lässt das Programm die Auswahl der Farben für die Ellipsen durch Benutzer*innen nicht zu. Stattdessen nutzt das Programm automatisch eine vordefinierte Liste von Farben. Das Programm unterstützt keine anderen Formen als Ellipsen. Offensichtlich wird dieses Programm keine Auszeichnungen als Grafiksoftware gewinnen. Es ist jedoch immer noch ein nützliches Beispiel, um zu lernen. Sie können den vollständigen Quellcode aus Beispiel für eine einfache Zeichnung herunterladen. In diesem Abschnitt werden nur einige wesentliche Punkte behandelt.

Ellipsen werden im Programm durch eine Struktur mit den Daten (D2D1_ELLIPSE) und der Farbe (D2D1_COLOR_F) der Ellipse dargestellt. Die Struktur definiert auch zwei Methoden: eine Methode zum Zeichnen der Ellipse und eine Methode zum Ausführen von Treffertests.

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;
    }
};

Das Programm verwendet den gleichen Volltonfarbenpinsel, um die Füllung und die Kontur für jede Ellipse zu zeichnen. Die Farbe wird dabei wie erforderlich geändert. In Direct2D ist das Ändern der Farbe eines Volltonfarbenpinsels ein effizienter Vorgang. Das Volltonfarbenpinsel-Objekt unterstützt die Methode SetColor.

Die Ellipsen werden im STL-Container list gespeichert:

    list<shared_ptr<MyEllipse>>             ellipses;

Hinweis

shared_ptr ist eine intelligente Zeigerklasse, die in TR1 zu C++ hinzugefügt und in C++0x formalisiert wurde. Visual Studio 2010 fügt Unterstützung für shared_ptr und andere C++0x-Funktionen hinzu. Weitere Informationen finden Sie im MSDN Magazine-Artikel Im Blickpunkt: Neue C++- und MFC-Features in Visual Studio 2010.

 

Das Programm verfügt über drei Modi:

  • Zeichnungsmodus. Benutzer*innen können neue Ellipsen zeichnen.
  • Auswahlmodus. Benutzer*innen können eine Ellipse auswählen.
  • Verschiebungsmodus. Benutzer*innen kann eine ausgewählte Ellipse verschieben.

Benutzer*innen können zwischen dem Zeichnungsmodus und dem Auswahlmodus wechseln, indem sie die in Zugriffstastentabellen beschriebenen Tastenkombinationen verwenden. Im Auswahlmodus wechselt das Programm in den Verschiebungsmodus, wenn Benutzer*innen auf eine Ellipse klicken. Das Programm kehrt zum Auswahlmodus zurück, wenn die Benutzer*innen die Maustaste loslassen. Die aktuelle Auswahl wird als Iterator in der Liste der Ellipsen gespeichert. Die Hilfsmethode MainWindow::Selection gibt einen Zeiger auf die ausgewählte Ellipse oder den Wert nullptr zurück, wenn keine Auswahl vorhanden ist.

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

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

In der folgenden Tabelle finden Sie eine Übersicht über die Auswirkungen von Mauseingaben in jedem der drei Modi.

Mauseingabe Zeichnungsmodus Selection Mode Verschiebungsmodus
Linke Taste unten Legt die Mauserfassung fest und beginnt mit der Zeichnung einer neuen Ellipse. Gibt die aktuelle Auswahl frei und führt einen Treffertest durch. Wenn eine Ellipse getroffen wird, erfolgen Cursorerfassung, Auswahl der Ellipse und der Wechsel in den Verschiebungsmodus. Keine Aktion.
Mausbewegung Wenn die linke Maustaste gedrückt ist, wird die Größe der Ellipse geändert. Keine Aktion. Verschiebt die ausgewählte Ellipse.
Linke Taste oben Beendet das Zeichnen der Ellipse. Keine Aktion. Wechselt in den Auswahlmodus.

 

Die folgende Methode in der Klasse MainWindow behandelt WM_LBUTTONDOWN-Nachrichten.

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);
}

Die Mauskoordinaten werden in Pixeln an diese Methode übergeben und dann in DIPs konvertiert. Es ist wichtig, diese beiden Einheiten nicht zu verwechseln. Beispielsweise verwendet die Funktion DragDetect Pixel, während für Zeichnungen und Treffertests DIPs verwendet werden. Als allgemeine Regel verwenden Funktionen im Zusammenhang mit Fenster- oder Mauseingaben Pixel, während Direct2D und DirectWrite DIPs verwenden. Testen Sie Ihr Programm stets bei einem hohen DPI-Wert, und denken Sie daran, Ihr Programm als DPI-kompatibel zu kennzeichnen. Weitere Informationen finden Sie unter DPI und geräteunabhängige Pixel.

Dies ist der Code, der WM_MOUSEMOVE-Nachrichten behandelt.

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);
    }
}

Die Logik für die Änderung der Größe einer Ellipse wurde zuvor im Abschnitt Beispiel: Zeichnen von Kreisen beschrieben. Beachten Sie auch den Aufruf von InvalidateRect. Dies stellt die Neuzeichnung des Fensters sicher. Der folgende Code behandelt WM_LBUTTONUP-Nachrichten.

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

Wie Sie sehen können, verfügen die Nachrichtenhandler für Mauseingaben Branchingcode, abhängig vom aktuellen Modus. Dies ist ein akzeptables Design für dieses ziemlich einfache Programm. Es könnte jedoch schnell zu komplex werden, wenn weitere Modi hinzugefügt werden. Für ein umfangreicheres Programm könnte eine Model-View-Controller (MVC)-Architektur die bessere Wahl sein. In dieser Art von Architektur wird der Controller, der Benutzereingaben verarbeitet, vom Modell getrennt, das die Anwendungsdaten verwaltet.

Wenn das Programm den Modus wechselt, wird der Cursor geändert, um Benutzer*innen Feedback zu geben.

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);
}

Schließlich müssen Sie den Cursor für den Fall festlegen, dass das Fenster eine WM_SETCURSOR-Nachricht empfängt:

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

Zusammenfassung

In diesem Modul haben Sie erfahren, wie Sie Maus- und Tastatureingaben behandeln. wie Sie Tastenkombinationen definieren und wie Sie das Cursorbild aktualisieren, um den aktuellen Status des Programms anzugeben.