Compartilhar via


Entrada do usuário: exemplo estendido

Vamos combinar tudo o que aprendemos sobre a entrada do usuário para criar um programa de desenho simples. Aqui está uma captura de tela do programa:

Captura de tela do programa de desenho

O usuário pode desenhar elipses em várias cores diferentes e selecionar, mover ou excluir elipses. Para manter a interface do usuário simples, o programa não permite que o usuário selecione as cores da elipse. Em vez disso, o programa percorre automaticamente uma lista predefinida de cores. O programa não suporta nenhuma forma além de elipses. Obviamente, este programa não ganhará nenhum prêmio de software gráfico. No entanto, ainda é um exemplo útil de aprendizado. Você pode baixar o código fonte completo da amostra de desenho simples. Esta seção abordará apenas alguns destaques.

As elipses são representadas no programa por uma estrutura que contém os dados da elipse (D2D1_ELLIPSE) e a cor (D2D1_COLOR_F). A estrutura também define dois métodos: um método para desenhar a elipse e um método para executar o teste de clique.

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

O programa usa o mesmo pincel de cor sólida para desenhar o preenchimento e o contorno de cada elipse, alterando a cor sempre que necessário. Em Direct2D, alterar a cor de um pincel de cor sólida é uma operação eficiente. Portanto, o objeto pincel de cor sólida dá suporte a um método SetColor.

As elipses são armazenadas em um container de lista STL:

    list<shared_ptr<MyEllipse>>             ellipses;

Observação

shared_ptr é uma classe de ponteiro inteligente que foi adicionada ao C++ em TR1 e formalizada em C++0x.. O Visual Studio 2010 adiciona suporte para shared_pte outros recursos do C++0x. Para obter mais informações, consulte o artigo da MSDN Magazine Explorando novos recursos do C++ e do MFC no Visual Studio 2010.

 

O programa tem três modos:

  • Modo de desenho. O usuário pode desenhar novas elipses.
  • Modo de seleção. O usuário pode selecionar uma elipse.
  • Modo de arrastar. O usuário pode arrastar uma elipse selecionada.

O usuário pode alternar entre o modo de desenho e o modo de seleção usando os mesmos atalhos do teclado descritos em tabelas de aceleradores. No modo de seleção, o programa alterna para o modo de arrastar se o usuário clicar em uma elipse. Ele volta para o modo de seleção quando o usuário solta o botão do mouse. A seleção atual é armazenada como um iterador na lista de elipses. O método auxiliar retorna um ponteiro para a elipse selecionada ou o valor nullptr se não houver seleção. MainWindow::Selection retorna um ponteiro para a elipse selecionada ou o valor nullptr se não houver seleção.

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

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

A tabela a seguir resume os efeitos da entrada do mouse em cada um dos três modos.

Entrada por mouse Modo de Desenho Modo de Seleção Modo de arrastar
Botão esquerdo para baixo Defina a captura do mouse e comece a desenhar uma nova elipse. Libere a seleção atual e execute um teste de clique. Se uma elipse for tocada, capture o cursor, selecione a elipse e mude para o modo de arrastar. Nenhuma ação.
Movimento do mouse Se o botão esquerdo estiver pressionado, redimensione a elipse. Nenhuma ação. Mova a elipse selecionada.
Botão esquerdo para cima Pare de desenhar a elipse. Nenhuma ação. Mudar para o modo de seleção.

 

O método a seguir na classe MainWindow lida com mensagens 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);
}

As coordenadas do mouse são passadas para esse método em pixels e, em seguida, são convertidas em DIPs. É importante não confundir essas duas unidades. Por exemplo, a função DragDetect usa pixels, porém o desenho e o teste de clique usam DIPs. A regra geral é que as funções relacionadas a janelas ou entrada do mouse usam pixels, enquanto Direct2D e DirectWrite usam DIPs. Sempre teste seu programa em uma configuração de alta DPI e lembre-se de marcar seu programa como compatível com DPI. Para obter mais informações, consulte DPI e Pixels independentes do dispositivo.

Aqui está o código que lida com as mensagens 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);
    }
}

A lógica para redimensionar uma elipse foi descrita anteriormente, na seção Exemplo: desenhando círculos. Observe também a chamada para InvalidateRect. Isso garante que a janela seja repintada. O código a seguir lida com mensagens WM_LBUTTONUP.

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

Como você pode ver, todos os manipuladores de mensagens para entrada do mouse têm código de ramificação, dependendo do modo atual. Esse é um design aceitável para esse programa relativamente simples. No entanto, ele pode rapidamente tornar-se muito complexo se novos modos forem adicionados. Para um programa maior, uma arquitetura (MVC) pode ter um design melhor. Nesse tipo de arquitetura, o controlador que lida com a entrada do usuário, é separado do modelo que gerencia os dados do aplicativo.

Quando o programa alterna os modos, o cursor muda para fornecer comentários ao usuário.

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

E, finalmente, lembre-se de definir o cursor quando a janela receber uma mensagem WM_SETCURSOR:

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

Resumo

Neste módulo, você aprendeu a lidar com a entrada do mouse e do teclado, como definir atalhos de teclado, e como atualizar a imagem do cursor para refletir o estado atual do programa.