Entrée utilisateur : exemple étendu

Nous allons combiner tout ce que nous avons appris sur l’entrée utilisateur pour créer un programme de dessin simple. Voici une capture d’écran du programme :

capture d’écran du programme de dessin

L’utilisateur peut dessiner des ellipses dans plusieurs couleurs différentes, puis sélectionner, déplacer ou supprimer des ellipses. Pour simplifier l’interface utilisateur, le programme ne permet pas à l’utilisateur de sélectionner les couleurs d’ellipse. Au lieu de cela, le programme parcourt automatiquement une liste prédéfinie de couleurs. Le programme ne prend pas en charge les formes autres que les ellipses. De toute évidence, ce programme ne gagnera aucun prix pour les logiciels graphiques. Toutefois, il s’agit toujours d’un exemple utile dont vous pouvez tirer des enseignements. Vous pouvez télécharger le code source complet à partir de l’exemple de dessin simple. Cette section couvre simplement quelques points saillants.

Les points de suspension sont représentés dans le programme par une structure qui contient les données d’ellipse (D2D1_ELLIPSE) et la couleur (D2D1_COLOR_F). La structure définit également deux méthodes : une méthode pour dessiner l’ellipse et une méthode pour effectuer des tests de positionnement.

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

Le programme utilise le même pinceau de couleur unie pour dessiner le remplissage et le contour de chaque ellipse, en modifiant la couleur si nécessaire. Dans Direct2D, la modification de la couleur d’un pinceau de couleur unie est une opération efficace. Par conséquent, l’objet pinceau de couleur unie prend en charge une méthode SetColor .

Les points de suspension sont stockés dans un conteneur de liste STL :

    list<shared_ptr<MyEllipse>>             ellipses;

Notes

shared_ptr est une classe de pointeur intelligent qui a été ajoutée à C++ dans TR1 et formalisée en C++0x. Visual Studio 2010 ajoute la prise en charge de shared_ptr et d’autres fonctionnalités C++0x. Pour plus d’informations, consultez Exploration des nouvelles fonctionnalités C++ et MFC dans Visual Studio 2010 dans MSDN Magazine. (Cette ressource n’est peut-être pas disponible dans certaines langues et certains pays.)

 

Le programme a trois modes :

  • Mode Dessin. L’utilisateur peut dessiner de nouvelles ellipses.
  • Mode de sélection. L’utilisateur peut sélectionner une ellipse.
  • Mode glisser. L’utilisateur peut faire glisser une ellipse sélectionnée.

L’utilisateur peut basculer entre le mode dessin et le mode de sélection à l’aide des mêmes raccourcis clavier que ceux décrits dans Tables accélérateurs. À partir du mode de sélection, le programme passe en mode glisser si l’utilisateur clique sur une ellipse. Il revient en mode sélection lorsque l’utilisateur relâche le bouton de la souris. La sélection actuelle est stockée en tant qu’itérateur dans la liste des points de suspension. La méthode MainWindow::Selection d’assistance retourne un pointeur vers l’ellipse sélectionnée, ou la valeur nullptr en l’absence de sélection.

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

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

Le tableau suivant récapitule les effets de l’entrée de la souris dans chacun des trois modes.

Entrée de la souris Mode Dessin Mode de sélection Mode glisser
Bouton gauche vers le bas Définissez la capture de la souris et commencez à dessiner une nouvelle ellipse. Relâchez la sélection actuelle et effectuez un test de positionnement. Si une ellipse est atteinte, capturez le curseur, sélectionnez l’ellipse, puis passez en mode glisser. Aucune action.
Déplacement de la souris Si le bouton gauche est vers le bas, redimensionnez l’ellipse. Aucune action. Déplacez l’ellipse sélectionnée.
Bouton gauche vers le haut Arrêtez de dessiner l’ellipse. Aucune action. Basculez vers le mode de sélection.

 

La méthode suivante dans la MainWindow classe gère les messages 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);
}

Les coordonnées de la souris sont passées à cette méthode en pixels, puis converties en dips. Il est important de ne pas confondre ces deux unités. Par exemple, la fonction DragDetect utilise des pixels, mais le dessin et les tests d’accès utilisent des DIPs. La règle générale est que les fonctions liées aux fenêtres ou à l’entrée de la souris utilisent des pixels, tandis que Direct2D et DirectWrite utilisent des dips. Testez toujours votre programme à un niveau de résolution élevé et n’oubliez pas de marquer votre programme comme étant sensible aux PPP. Pour plus d’informations, consultez Ppp et Device-Independent Pixels.

Voici le code qui gère les messages 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 logique de redimensionnement d’une ellipse a été décrite précédemment, dans la section Exemple : cercles de dessin. Notez également l’appel à InvalidateRect. Cela permet de s’assurer que la fenêtre est repeinte. Le code suivant gère les messages WM_LBUTTONUP .

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

Comme vous pouvez le voir, les gestionnaires de messages pour l’entrée de souris ont tous un code de branchement, en fonction du mode actuel. C’est une conception acceptable pour ce programme assez simple. Toutefois, cela peut rapidement devenir trop complexe si de nouveaux modes sont ajoutés. Pour un programme plus grand, une architecture MVC (model-view-controller) peut être une meilleure conception. Dans ce type d’architecture, le contrôleur, qui gère l’entrée utilisateur, est séparé du modèle, qui gère les données d’application.

Lorsque le programme change de mode, le curseur change pour envoyer des commentaires à l’utilisateur.

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

Enfin, n’oubliez pas de définir le curseur lorsque la fenêtre reçoit un message WM_SETCURSOR :

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

Résumé

Dans ce module, vous avez appris à gérer l’entrée de souris et de clavier ; comment définir des raccourcis clavier ; et comment mettre à jour l’image du curseur pour refléter l’état actuel du programme.