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:
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.