Partager via


Entrée utilisateur : exemple étendu

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

capture d’écran du programme de dessin

L’utilisateur peut dessiner des ellipses de plusieurs couleurs différentes, et sélectionner, déplacer ou supprimer des ellipses. Pour garder l’interface utilisateur simple, le programme ne permet pas à l’utilisateur de sélectionner les couleurs des ellipses. Au lieu de cela, le programme parcourt automatiquement une liste prédéfinie de couleurs. Le programme ne prend en charge aucune autre forme que les ellipses. Évidemment, ce programme ne remportera aucun prix pour les logiciels graphiques. Cependant, c’est toujours un exemple utile pour apprendre. Vous pouvez télécharger le code source complet à partir de Exemple de dessin simple. Cette section couvrira seulement quelques points forts.

Les ellipses sont représentées dans le programme par une structure contenant les données de l’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 un test de frappe.

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, changeant la couleur selon les besoins. Dans Direct2D, changer la couleur d’un pinceau de couleur unie est une opération efficace. Ainsi, l’objet pinceau de couleur unie prend en charge une méthode SetColor.

Les ellipses sont stockées dans un conteneur list de la STL :

    list<shared_ptr<MyEllipse>>             ellipses;

Remarque

shared_ptr est une classe de pointeur intelligent qui a été ajoutée à C++ dans TR1 et formalisée dans C++0x. Visual Studio 2010 ajoute la prise en charge de shared_ptr et d’autres fonctionnalités C++0x. Pour plus d’informations, voir l’article du MSDN Magazine Exploring New C++ and MFC Features in Visual Studio 2010.

 

Le programme a trois modes :

  • Mode dessin. L’utilisateur peut dessiner de nouvelles ellipses.
  • Mode sélection. L’utilisateur peut sélectionner une ellipse.
  • Mode déplacement. L’utilisateur peut déplacer une ellipse sélectionnée.

L’utilisateur peut passer du mode dessin au mode sélection en utilisant les mêmes raccourcis clavier décrits dans Tables d’accélérateurs. À partir du mode sélection, le programme passe en mode déplacement si l’utilisateur clique sur une ellipse. Il repasse en mode sélection lorsque l’utilisateur relâche le bouton de la souris. La sélection actuelle est stockée comme un itérateur dans la liste des ellipses. La méthode auxiliaire MainWindow::Selection renvoie un pointeur vers l’ellipse sélectionnée, ou la valeur nullptr s’il n’y a pas 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ées de la souris Dessin des cartes Mode de sélection Mode déplacement
Bouton gauche enfoncé Définir la capture de la souris et commencer à dessiner une nouvelle ellipse. Libérer la sélection actuelle et effectuer un test de positionnement. Si une ellipse est touchée, capturer le curseur, sélectionner l’ellipse et passer en mode déplacement. Aucune action.
Déplacement de la souris Si le bouton gauche est enfoncé, redimensionner l’ellipse. Aucune action. Déplacer l’ellipse sélectionnée.
Bouton gauche relâché Arrêter de dessiner l’ellipse. Aucune action. Passer en mode sélection.

 

La méthode suivante dans la classe MainWindow 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 le test de frappe 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 avec un paramètre DPI élevé et n’oubliez pas de marquer votre programme comme étant compatible DPI. Pour plus d’informations, veuillez consulter la section DPI et pixels indépendants de l’appareil.

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 pour redimensionner une ellipse a été décrite précédemment, dans la section Exemple : Dessiner des cercles. Notez également l’appel à InvalidateRect. Cela garantit 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 la souris ont tous du code conditionnel, selon le mode actuel. C’est une conception acceptable pour ce programme relativement simple. Cependant, cela pourrait rapidement devenir trop complexe si de nouveaux modes étaient ajoutés. Pour un programme plus grand, une architecture modèle-vue-contrôleur (MVC) pourrait ê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 de l’application.

Lorsque le programme change de mode, le curseur change pour donner un retour à 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);
}

Et 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 comment gérer l’entrée de la souris et du clavier ; comment définir des raccourcis clavier ; et comment mettre à jour l’image du curseur pour refléter l’état actuel du programme.