Benutzereingabe: erweitertes Beispiel

Wir kombinieren alles, was wir über Benutzereingaben gelernt haben, um ein einfaches Zeichenprogramm zu erstellen. Hier ist ein Screenshot des Programms:

Screenshot des Zeichenprogramms

Der Benutzer kann Auslassungspunkte in verschiedenen Farben zeichnen und Auslassungspunkte auswählen, verschieben oder löschen. Um die Benutzeroberfläche einfach zu halten, lässt das Programm dem Benutzer die Auswahl der Ellipsenfarben nicht zu. Stattdessen durchläuft das Programm automatisch eine vordefinierte Liste von Farben. Das Programm unterstützt keine anderen Formen als Ellipsen. Natürlich wird dieses Programm keine Auszeichnungen für Grafiksoftware gewinnen. Es ist jedoch immer noch ein nützliches Beispiel, aus dem man lernen kann. Sie können den vollständigen Quellcode unter Simple Drawing Sample herunterladen. In diesem Abschnitt werden nur einige Highlights behandelt.

Ellipsen werden im Programm durch eine Struktur dargestellt, die die Ellipsendaten (D2D1_ELLIPSE) und die Farbe (D2D1_COLOR_F) enthält. Die -Struktur definiert auch zwei Methoden: eine Methode zum Zeichnen der Ellipse und eine Methode zum Durchfü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 Volltonpinsel, um die Füllung und kontur für jede Ellipse zu zeichnen und die Farbe nach Bedarf zu ändern. In Direct2D ist das Ändern der Farbe eines Volltonpinsels ein effizienter Vorgang. Daher unterstützt das Volltonpinselobjekt eine SetColor-Methode .

Die Auslassungspunkte werden in einem STL-Listencontainer gespeichert:

    list<shared_ptr<MyEllipse>>             ellipses;

Hinweis

shared_ptr ist eine SmartPointer-Klasse, die in TR1 zu C++ hinzugefügt und in C++0x formalisiert wurde. Visual Studio 2010 bietet Unterstützung für shared_ptr und andere C++0x-Features. Weitere Informationen finden Sie unter Erkunden neuer C++- und MFC-Features in Visual Studio 2010 im MSDN Magazine. (Diese Ressource ist in einigen Sprachen und Ländern möglicherweise nicht verfügbar.)

 

Das Programm verfügt über drei Modi:

  • Zeichnungsmodus. Der Benutzer kann neue Auslassungspunkte zeichnen.
  • Auswahlmodus. Der Benutzer kann eine Ellipse auswählen.
  • Ziehmodus. Der Benutzer kann eine ausgewählte Ellipse ziehen.

Der Benutzer kann zwischen dem Zeichenmodus und dem Auswahlmodus wechseln, indem er die gleichen Tastenkombinationen verwendet, die unter Schnellinfotabellen beschrieben werden. Aus dem Auswahlmodus wechselt das Programm in den Ziehmodus, wenn der Benutzer auf eine Ellipse klickt. Er wechselt zurück in den Auswahlmodus, wenn der Benutzer die Maustaste loslässt. Die aktuelle Auswahl wird als Iterator in der Liste der Auslassungspunkte 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 sind die Auswirkungen der Mauseingabe in jedem der drei Modi zusammengefasst.

Mauseingabe Zeichnungsmodus Auswahlmodus Ziehmodus
Linke Schaltfläche nach unten Legen Sie die Mausaufnahme fest, und beginnen Sie, eine neue Ellipse zu zeichnen. Lassen Sie die aktuelle Auswahl los, und führen Sie einen Treffertest aus. Wenn eine Ellipse getroffen wird, erfassen Sie den Cursor, wählen Sie die Ellipse aus, und wechseln Sie in den Ziehmodus. Keine Aktion.
Mausbewegung Wenn die linke Schaltfläche nach unten ist, ändern Sie die Größe der Ellipse. Keine Aktion. Verschieben Sie die ausgewählte Ellipse.
Linke Schaltfläche nach oben Beenden Sie das Zeichnen der Ellipse. Keine Aktion. Wechseln Sie in den Auswahlmodus.

 

Die folgende Methode in der MainWindow -Klasse verarbeitet 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);
}

Mauskoordinaten werden in Pixeln an diese Methode übergeben und dann in DIPs konvertiert. Es ist wichtig, diese beiden Einheiten nicht zu verwechseln. Die DragDetect-Funktion verwendet beispielsweise Pixel, aber Zeichnung und Treffertests verwenden DIPs. Die allgemeine Regel ist, dass Funktionen im Zusammenhang mit Fenstern oder Mauseingaben Pixel verwenden, während Direct2D und DirectWrite DIPs verwenden. Testen Sie Ihr Programm immer mit einer Hohen DPI-Einstellung, und denken Sie daran, Das Programm als DPI-fähig zu markieren. Weitere Informationen finden Sie unter DPI und Device-Independent Pixel.

Hier ist der Code, der WM_MOUSEMOVE Nachrichten verarbeitet.

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 zum Ändern der Größe einer Ellipse wurde zuvor im Abschnitt Beispiel: Zeichnen von Kreisen beschrieben. Beachten Sie auch den Aufruf von InvalidateRect. Dadurch wird sichergestellt, dass das Fenster neu gezeichnet wird. Der folgende Code verarbeitet 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 Meldungshandler für die Mauseingabe je nach aktuellem Modus über Verzweigungscode. Das ist ein akzeptabler Entwurf für dieses recht einfache Programm. Es könnte jedoch schnell zu komplex werden, wenn neue Modi hinzugefügt werden. Für ein größeres Programm kann eine MVC-Architektur (Model-View-Controller) ein besserer Entwurf sein. Bei dieser Art von Architektur ist der Controller, der Benutzereingaben verarbeitet, vom Modell getrennt, das Anwendungsdaten verwaltet.

Wenn das Programm den Modus wechselt, ändert sich der Cursor, um dem Benutzer 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);
}

Denken Sie schließlich daran, den Cursor festzulegen, wenn das Fenster eine WM_SETCURSOR Meldung empfängt:

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

Zusammenfassung

In diesem Modul haben Sie gelernt, wie Sie mit Maus- und Tastatureingaben umgehen. Definieren von Tastenkombinationen; und wie das Cursorbild aktualisiert wird, um den aktuellen Status des Programms widerzuspiegeln.