Input utente: esempio esteso

Combinare tutto ciò che abbiamo appreso sull'input utente per creare un semplice programma di disegno. Ecco una schermata del programma:

schermata del programma di disegno

L'utente può disegnare puntini di sospensione in diversi colori e selezionare, spostare o eliminare i puntini di sospensione. Per mantenere semplice l'interfaccia utente, il programma non consente all'utente di selezionare i colori con puntini di sospensione. Il programma viene invece eseguito automaticamente il ciclo tramite un elenco predefinito di colori. Il programma non supporta forme diverse dai puntini di sospensione. Ovviamente, questo programma non vincerà premi per il software grafico. Tuttavia, è ancora un esempio utile da imparare. È possibile scaricare il codice sorgente completo da Esempio di disegno semplice. Questa sezione illustra solo alcune evidenziazioni.

I puntini di sospensione sono rappresentati nel programma da una struttura che contiene i dati con puntini di sospensione (D2D1_ELLIPSE) e il colore (D2D1_COLOR_F). La struttura definisce anche due metodi: un metodo per disegnare i puntini di sospensione e un metodo per eseguire il hit test.

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

Il programma usa lo stesso pennello a tinta unita per disegnare il riempimento e la struttura per ogni ellisse, modificando il colore in base alle esigenze. In Direct2D modificare il colore di un pennello a tinta unita è un'operazione efficiente. L'oggetto pennello a tinta unita supporta quindi un metodo SetColor .

I puntini di sospensione vengono archiviati in un contenitore elenco STL:

    list<shared_ptr<MyEllipse>>             ellipses;

Nota

shared_ptr è una classe smart-pointer aggiunta a C++ in TR1 e formalizzata in C++0x. Visual Studio 2010 aggiunge il supporto per shared_ptr e altre funzionalità C++0x. Per altre informazioni, vedere Esplorazione delle nuove funzionalità C++ e MFC in Visual Studio 2010 inMSDN Magazine. Questa risorsa potrebbe non essere disponibile in alcune lingue e paesi.

 

Il programma ha tre modalità:

  • Modalità di disegno. L'utente può disegnare nuovi puntini di sospensione.
  • Modalità di selezione. L'utente può selezionare un'ellisse.
  • Modalità di trascinamento. L'utente può trascinare i puntini di sospensione selezionati.

L'utente può passare dalla modalità di disegno alla modalità di selezione usando le stesse scelte rapide da tastiera descritte in Tabelle acceleratori. Dalla modalità di selezione, il programma passa alla modalità di trascinamento se l'utente fa clic su un'ellisse. Torna alla modalità di selezione quando l'utente rilascia il pulsante del mouse. La selezione corrente viene archiviata come iteratore nell'elenco di puntini di sospensione. Il metodo MainWindow::Selection helper restituisce un puntatore ai puntini di sospensione selezionati oppure il valore nullptr se non è presente alcuna selezione.

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

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

La tabella seguente riepiloga gli effetti dell'input del mouse in ognuna delle tre modalità.

Mouse Input Modalità di disegno Modalità selezione Modalità di trascinamento
Pulsante sinistro verso il basso Impostare l'acquisizione del mouse e iniziare a disegnare un nuovo ellisse. Rilasciare la selezione corrente ed eseguire un hit test. Se viene colpito un puntino di sospensione, acquisire il cursore, selezionare i puntini di sospensione e passare alla modalità di trascinamento. Nessuna azione.
Spostamento del mouse Se il pulsante sinistro è in basso, ridimensionare i puntini di sospensione. Nessuna azione. Spostare i puntini di sospensione selezionati.
Pulsante sinistro su Arrestare il disegno dei puntini di sospensione. Nessuna azione. Passare alla modalità di selezione.

 

Il metodo seguente nella MainWindow classe gestisce i messaggi 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);
}

Le coordinate del mouse vengono passate a questo metodo in pixel e quindi convertite in DIP. È importante non confondere queste due unità. Ad esempio, la funzione DragDetect usa pixel, ma il disegno e il test di hit test usano IP. La regola generale è che le funzioni correlate alle finestre o all'input del mouse usano pixel, mentre Direct2D e DirectWrite usano indirizzi DIP. Testare sempre il programma in un'impostazione con valori DPI elevati e ricordare di contrassegnare il programma come con riconoscimento DPI. Per altre informazioni, vedere DPI e Device-Independent Pixel.

Ecco il codice che gestisce i messaggi di 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);
    }
}

La logica per ridimensionare un ellisse è stata descritta in precedenza nella sezione Esempio: Cerchi di disegno. Si noti anche la chiamata a InvalidateRect. Ciò garantisce che la finestra sia riinteduta. Il codice seguente gestisce i messaggi WM_LBUTTONUP .

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

Come si può notare, i gestori di messaggi per l'input del mouse hanno tutto il codice di branching, a seconda della modalità corrente. Questo è un design accettabile per questo programma abbastanza semplice. Tuttavia, potrebbe diventare troppo complesso se vengono aggiunte nuove modalità. Per un programma più ampio, un'architettura MVC (Model-View-Controller) potrebbe essere una progettazione migliore. In questo tipo di architettura, il controller, che gestisce l'input utente, viene separato dal modello, che gestisce i dati dell'applicazione.

Quando il programma cambia modalità, il cursore cambia per inviare commenti e suggerimenti all'utente.

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

Infine, ricordarsi di impostare il cursore quando la finestra riceve un messaggio di WM_SETCURSOR :

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

Riepilogo

In questo modulo si è appreso come gestire l'input del mouse e della tastiera; come definire i tasti di scelta rapida; e come aggiornare l'immagine del cursore per riflettere lo stato corrente del programma.