Partilhar 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 de elipse. Em vez disso, o programa percorre automaticamente uma lista predefinida de cores. O programa não suporta outras formas além de elipses. Obviamente, este programa não ganhará nenhum prêmio para software gráfico. No entanto, continua a ser um exemplo útil para aprender. Você pode baixar o código-fonte completo do Simple Drawing Sample. Esta seção abordará apenas alguns destaques.

As elipses são representadas no programa por uma estrutura que contém os dados de 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 acertos.

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 contorno para cada elipse, alterando a cor conforme necessário. No Direct2D, alterar a cor de um pincel de cor sólida é uma operação eficiente. Assim, o objeto de pincel de cor sólida suporta um métodoSetColor.

As reticências são armazenadas em uma lista de STL contêiner:

    list<shared_ptr<MyEllipse>>             ellipses;

Observação

shared_ptr é uma classe de ponteiro inteligente que foi adicionada ao C++ no TR1 e formalizada no C++0x. O Visual Studio 2010 adiciona suporte para shared_ptr e outros recursos do C++0x. Para obter mais informações, consulte o artigo da MSDN Magazine Explorando novos recursos C++ e 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 de teclado descritos em Tabelas Aceleradoras. No modo de seleção, o programa muda 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 libera o botão do mouse. A seleção atual é armazenada como um iterador na lista de elipses. O método auxiliar 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 do 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. Solte a seleção atual e execute um teste de acerto. Se uma elipse for atingida, capture o cursor, selecione a elipse e alterne para o modo de arrastar. Nenhuma ação.
Movimento do rato Se o botão esquerdo estiver para baixo, redimensione a elipse. Nenhuma ação. Mova a elipse selecionada.
Botão esquerdo para cima Pare de desenhar a elipse. Nenhuma ação. Mude para o modo de seleção.

 

O método a seguir na classe MainWindow manipula 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, convertidas em DIPs. É importante não confundir estas duas unidades. Por exemplo, a funçãoDragDetect usa pixels, mas o desenho e o teste de acerto usam DIPs. A regra geral é que as funções relacionadas à entrada do Windows ou do mouse usam pixels, enquanto Direct2D e DirectWrite usam DIPs. Sempre teste seu programa em uma configuração de DPI alto e lembre-se de marcar seu programa como compatível com DPI. Para obter mais informações, consulte DPI e Device-Independent Pixels.

Aqui está o código que lida com WM_MOUSEMOVE mensagens.

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 manipula 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 este programa bastante simples. No entanto, pode rapidamente tornar-se demasiado complexo se forem adicionados novos modos. Para um programa maior, uma arquitetura MVC (model-view-controller) pode ser 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 muda de modo, o cursor muda para dar feedback 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 de mouse e teclado; como definir atalhos de teclado; e como atualizar a imagem do cursor para refletir o estado atual do programa.