Condividi tramite


Input utente: esempio esteso

Si combinano tutti gli elementi appresi sull'input dell'utente per creare un semplice programma di disegno. Ecco una schermata del programma:

screenshot del programma di disegno

L'utente può disegnare puntini di sospensione in diversi colori e selezionare, spostare o eliminare puntini di sospensione. Per mantenere l'interfaccia utente semplice, il programma non consente all'utente di selezionare i colori dell'ellisse. Al contrario, il programma scorre automaticamente un elenco predefinito di colori. Il programma non supporta forme diverse dai puntini di sospensione. Ovviamente, questo programma non vincerà alcun premio per il software grafico. Tuttavia, è ancora un esempio utile da cui imparare. È possibile scaricare il codice sorgente completo da Esempio di disegno semplice. Questa sezione illustra solo alcuni punti salienti.

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

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 il contorno per ogni ellisse, modificando il colore in base alle esigenze. In Direct2D, la modifica del 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 di puntatore intelligente 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 l'articolo di MSDN Magazine Sull'esplorazione delle nuove funzionalità C++ e MFC in Visual Studio 2010.

 

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 un'ellisse selezionata.

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 dei puntini di sospensione. Il metodo MainWindow::Selection helper restituisce un puntatore all'ellisse selezionata 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à disegno Modalità di 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 raggiunta un'ellisse, acquisire il cursore, selezionare l'ellisse e passare alla modalità di trascinamento. Nessuna azione.
Spostamento del mouse Se il pulsante sinistro è in basso, ridimensionare l'ellisse. Nessuna azione. Spostare l'ellisse selezionata.
Pulsante sinistro verso l'alto Smettere di disegnare l'ellisse. Nessuna azione. Passare alla modalità di selezione.

 

Il metodo seguente nella MainWindow classe gestisce WM_LBUTTONDOWN messaggi.

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 l'hit testing usano DIP. La regola generale è che le funzioni correlate alle finestre o all'input del mouse usano pixel, mentre Direct2D e DirectWrite usano DIP. Testare sempre il programma con un'impostazione con valori DPI elevati e ricordarsi di contrassegnare il programma come compatibile con DPI. Per altre informazioni, vedere DPI e Device-Independent Pixel.

Ecco il codice che gestisce WM_MOUSEMOVE messaggi.

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. In questo modo si garantisce che la finestra sia riavvolta. Il codice seguente gestisce WM_LBUTTONUP messaggi.

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 codice di diramazione, 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 dell'utente, è separato dal modello, che gestisce i dati dell'applicazione.

Quando il programma cambia modalità, il cursore cambia per fornire feedback 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 in modo da riflettere lo stato corrente del programma.