Compartir a través de


Entrada de usuario: ejemplo ampliado

Vamos a juntar todo lo que hemos aprendido sobre la entrada del usuario para crear un programa de dibujado sencillo. Esta es una captura de pantalla del programa:

captura de pantalla del programa de dibujado

El usuario puede dibujar elipses con varios colores diferentes y seleccionar, mover o eliminar elipses. Para que la interfaz de usuario sea básica, el programa no permite al usuario seleccionar los colores de los elipses. En su lugar, el programa pasa automáticamente por una lista predefinida de colores. El programa no admite ninguna otra forma que no sean elipses. Obviamente, este programa no es un software de diseño gráfico que vaya a gana ningún premio. Sin embargo, es un ejemplo útil para aprender. Puede descargar el código fuente completo de Ejemplo de dibujo sencillo. En esta sección solo se abordarán algunos puntos destacados.

Las elipses se representan en el programa mediante una estructura que contiene los datos de las elipses (D2D1_ELLIPSE) y el color (D2D1_COLOR_F). La estructura también define dos métodos: un método para dibujar la elipse y un método para realizar las pruebas de pulsación.

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

El programa usa el mismo pincel de color sólido para dibujar el relleno y el contorno de cada elipse, cambiando el color según sea necesario. En Direct2D, cambiar el color de un pincel de color sólido es una operación eficiente. Así pues, el objeto del pincel de color sólido admite el método SetColor.

Las elipses se almacenan en un contenedor list STL:

    list<shared_ptr<MyEllipse>>             ellipses;

Nota:

shared_ptr es una clase de puntero inteligente que se ha agregado a C++ en TR1 y se ha formalizado en C++0x. Visual Studio 2010 incluye la funcionalidad de shared_ptr y otras características de C++0x. Para obtener más información, consulte el artículo de MSDN Magazine Descubrir nuevas características de C++ y MFC en Visual Studio 2010.

 

El programa tiene tres modos:

  • Modo de dibujo. El usuario puede dibujar nuevas elipses.
  • Modo de selección. El usuario puede seleccionar una elipse.
  • Modo de arrastrar. El usuario puede arrastrar una elipse seleccionada.

El usuario puede cambiar entre el modo de dibujo y el modo de selección mediante los mismos métodos abreviados de teclado descritos en Tablas de aceleradores. En el modo de selección, el programa cambia al modo de arrastrar si el usuario hace clic en una elipse. Se vuelve a pasar al modo de selección cuando el usuario suelta el botón del ratón. Lo que se seleccione se almacena como iterador en la lista de elipses. El método auxiliar MainWindow::Selection devuelve un puntero a la elipse seleccionada o el valor nullptr si no se ha seleccionado nada.

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

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

En la tabla siguiente se resumen los efectos de la acción del ratón en cada uno de los tres modos.

Entrada de mouse Modo de dibujo Modo de selección Modo de arrastrar
Botón izquierdo pulsado Activa el modo de captura del ratón y empieza a dibujar una nueva elipse. Anula lo seleccionado y realiza una prueba de pulsación. Si se pulsa una elipse, se captura el cursor, se selecciona la elipse y se pasa al modo de arrastrar. No se requiere ninguna acción.
Movimiento del ratón Si el botón izquierdo está inactivo, se cambia el tamaño de la elipse. No se requiere ninguna acción. Se mueve a la elipse seleccionada.
Botón izquierdo levantado Se deja de dibujar la elipse. No se requiere ninguna acción. Se cambia al modo de selección.

 

El método siguiente en la clase MainWindow controla los mensajes 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);
}

Las coordenadas del ratón se pasan a este método en píxeles y luego pasan a DIP. Es importante no confundir estas dos unidades. Por ejemplo, la función DragDetect usa píxeles, pero el dibujo y las pruebas de pulsación usan DIP. Lo normal es que las funciones relacionadas con las ventanas o la acción del ratón usen píxeles, mientras que Direct2D y DirectWrite usen DIP. Pruebe siempre el programa en una configuración de valores altos de PPP y recuerde establecer el programa como compatible con PPP. Para obtener más información, consulte PPP y píxeles independientes del dispositivo.

Este es el código que controla los mensajes 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 lógica para cambiar el tamaño de una elipse se ha descrito anteriormente, en la sección Ejemplo: Dibujo de círculos. Tenga en cuenta también la llamada a InvalidateRect. Esto garantiza que la ventana se vuelva a pintar. El código siguiente controla los mensajes WM_LBUTTONUP.

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

Como puede ver, los controladores de mensajes de la acción del ratón tienen código ramificado, según el modo actual. Es un diseño aceptable para este programa bastante simple. Sin embargo, podría volverse demasiado complejo si se agregan nuevos modos. En un programa más complejo, una arquitectura Modelo-vista-controlador (MVC) podría ser un mejor diseño. En este tipo de arquitectura, el controlador, que controla la entrada del usuario, se desvincula del modelo, que administra los datos de la aplicación.

Cuando el programa cambia los modos, el cursor cambia para avisar al usuario.

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

Por último, recuerde ajustar el cursor cuando la ventana reciba un mensaje WM_SETCURSOR:

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

Resumen

En este módulo, ha aprendido a controlar la acción del ratón y del teclado, cómo definir métodos abreviados de teclado y cómo actualizar la imagen del cursor para reflejar el estado actual del programa.