Windows com C++

Criando aplicativos da área de trabalho com o Visual C++ 2012

Kenny Kerr

 

Kenny KerrCom toda a badalação em torno do Windows 8 e do que agora são conhecidos como aplicativos da Windows Store, recebi algumas perguntas sobre a relevância dos aplicativos da área de trabalho e se o C++ Padrão ainda é uma opção viável daqui para frente. Às vezes, é difícil responder a essas perguntas, mas o que posso dizer é que o compilador do Visual C++ 2012 está mais comprometido do que nunca com o C++ Padrão e continua sendo a melhor cadeia de ferramentas, na minha modesta opinião, para criar aplicativos da área de trabalho para o Windows, independentemente se for para o Windows 7, o Windows 8 ou até mesmo para o Windows XP.

Outra pergunta que inevitavelmente recebo é: qual seria a melhor maneira de lidar com o desenvolvimento de aplicativos da área de trabalho no Windows e por onde começar? Bem, na coluna deste mês, vou explorar os conceitos básicos da criação de aplicativos da área de trabalho com o Visual C++. Quando Jeff Prosise (bit.ly/WmoRuR) me apresentou a programação do Windows, o Microsoft Foundation Classes (MFC) era uma nova maneira promissora de criar aplicativos. Embora o MFC ainda esteja disponível, sua idade está começando a pesar, e a necessidade de alternativas modernas e flexíveis fez com que os programadores pesquisassem novas abordagens. Essa questão tornou-se ainda mais complexa devido à substituição de recursos de USER e GDI (msdn.com/library/ms724515) por Direct3D como a base primária por meio da qual o conteúdo é renderizado na tela.

Há anos, promovo a Active Template Library (ATL) e sua extensão, a Windows Template Library (WTL), como excelentes opções para a criação de aplicativos. No entanto, a idade começou a pesar até mesmo para essas bibliotecas. Com a substituição de recursos de USER e GDI, há ainda menos motivos para usá-las. Então, por onde começar? Pela API do Windows, é claro. Vou mostrar que criar uma janela da área de trabalho sem nenhuma biblioteca não é uma tarefa tão desanimadora quanto pode parecer a princípio. Em seguida, vou mostrar como você pode deixá-la um pouco mais parecida com C++, se desejar, com uma pequena ajuda da ATL e da WTL. A ATL e a WTL fazem muito mais sentido quando você tem uma boa ideia de como tudo funciona por trás dos modelos e macros.

A API do Windows

O problema com o uso da API do Windows para criar uma janela da área de trabalho é que há uma infinidade de maneiras de escrever o código — opções até demais, na verdade. Mesmo assim, há uma maneira simples de criar uma janela, e começa com o arquivo mestre include para Windows:

 

#include <windows.h>

Em seguida, você pode definir o ponto de entrada padrão para os aplicativos:

int __stdcall wWinMain(HINSTANCE module, HINSTANCE, PWSTR, int)

Se estiver escrevendo o código de um aplicativo de console, pode continuar usando a função de ponto de entrada principal do C++ Padrão, mas suponho que você não gostaria que uma caixa de console surja na tela sempre que seu aplicativo for iniciado. A função wWinMain apresenta um histórico de evolução. A convenção de chamada __stdcall esclarece a questão sobre a confusa arquitetura x86, que fornece algumas convenções de chamada. Se você estiver se concentrando em x64 ou ARM, isso não é importante porque o compilador do Visual C++ implementa apenas uma única convenção de chamada nessas arquiteturas — mas não custa verificar.

Os dois parâmetros HINSTANCE apresentam um histórico de evolução específico. Na época do Windows de 16 bits, o segundo HINSTANCE era o identificador para qualquer instância anterior do aplicativo. Isso permitia que um aplicativo se comunicasse com qualquer instância anterior desse aplicativo ou até mesmo alternasse para a instância anterior se o usuário o iniciasse mais uma vez acidentalmente. Hoje, esse segundo parâmetro é sempre um nullptr. Você também pode ter observado que nomeei o primeiro parâmetro como “module”, em vez de “instance”. Novamente, no Windows de 16 bits, instâncias e módulos eram duas coisas distintas. Todos os aplicativos compartilhariam o módulo que contém os segmentos de código, mas receberiam instâncias exclusivas contendo os segmentos de dados. Agora, os parâmetros HINSTANCE atuais e anteriores devem fazer mais sentido. O Windows de 32 bits apresentou espaços de endereço separados e, junto com eles, a necessidade de que cada processo mapeasse a própria instância/módulo, que na época era uma coisa só. Atualmente, é apenas o endereço base do executável. Na verdade, o vinculador do Visual C++ expõe esse endereço por meio de uma pseudovariável, que pode ser acessada ao declará-la da seguinte maneira:

extern "C" IMAGE_DOS_HEADER __ImageBase;

O endereço de __ImageBase terá o mesmo valor que o parâmetro HINSTANCE. É dessa maneira que a biblioteca de tempo de execução em C (CRT) obtém o endereço do módulo para passar para sua função wWinMain em primeiro lugar. É um atalho conveniente se você não quiser passar esse parâmetro wWinMain para o seu aplicativo. Lembre-se, no entanto, que essa variável aponta para o módulo atual, independentemente se for uma DLL ou um executável, portanto, é útil para carregar recursos específicos de módulo sem ambiguidade.

O próximo parâmetro fornece qualquer argumento da linha de comando, e o último parâmetro é um valor que deve ser passado para a função ShowWindow para a janela principal do aplicativo, supondo que você chame ShowWindow inicialmente. A ironia é que ela quase sempre será ignorada. Isso remonta para a maneira em que um aplicativo é iniciado via CreateProcess e outros para permitir que um atalho — ou algum outro aplicativo — defina se a janela principal de um aplicativo será minimizada, maximizada ou exibida normalmente no início.

Dentro da função wWinMain, o aplicativo deve registrar uma classe de janela. A classe de janela é descrita por uma estrutura WNDCLASS e registrada com a função RegisterClass. Esse registro é armazenado em uma tabela usando um par formado pelo ponteiro e nome da classe do módulo, permitindo que a função CreateWindow pesquise as informações de classe quando for a hora de criar a janela:

WNDCLASS wc = {}; wc.hCursor = LoadCursor(nullptr, IDC_ARROW); wc.hInstance = module; wc.lpszClassName = L"window"; wc.lpfnWndProc = []  (HWND window, UINT message, WPARAM wparam, LPARAM lparam) -> LRESULT {   ... }; VERIFY(RegisterClass(&wc));

Para que os exemplos não se prolonguem muito, usarei apenas a macro comum VERIFY como espaço reservado para indicar onde você precisará adicionar alguma identificação de erro a fim de gerenciar as falhas informadas pelas várias funções da API. Considere essas macros como espaços reservados para sua diretiva de tratamento de erros preferencial.

O código anterior é o mínimo necessário para descrever uma janela padrão. A estrutura WNDCLASS é inicializada com um par vazio de chaves. Isso garante que todos os membros da estrutura serão inicializados como zero ou nullptr. Os únicos membros que devem ser definidos são: hCursor, para indicar que ponteiro do mouse, ou cursor, deve ser usado quando o mouse estiver na janela; hInstance e lpszClassName, para identificar a classe de janela no processo; e lpfnWndProc, para apontar o procedimento de janela que processará as mensagens enviadas para a janela. Neste caso, estou usando uma expressão lambda para manter tudo alinhado, por assim dizer. Voltarei ao procedimento de janela em breve. A próxima etapa é criar a janela:

VERIFY(CreateWindow(wc.lpszClassName, L"Title",   WS_OVERLAPPEDWINDOW | WS_VISIBLE,   CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT,   nullptr, nullptr, module, nullptr));

A função CreateWindow espera alguns parâmetros, mas a maioria deles é padrão. O primeiro e o penúltimo parâmetro, como mencionei, representam juntos a chave que a função RegisterClass cria para permitir que CreateWindow encontre as informações de classe de janela. O segundo parâmetro indica o texto que será exibido na barra de título da janela. O terceiro indica o estilo da janela. A constante WS_OVERLAPPEDWINDOW é um estilo usado frequentemente que descreve uma janela de nível superior normal com uma barra de título com botões, bordas redimensionáveis e assim por diante. Combiná-la com a constante WS_VISIBLE instrui CreateWindow a continuar e mostrar a janela. Se você omitir WS_VISIBLE, precisará chamar a função ShowWindow antes que sua janela apareça na área de trabalho.

Os próximos quatro parâmetros indicam a posição inicial e o tamanho da janela, e a constante CW_USEDEFAULT usada em cada caso informa o Windows para escolher os padrões apropriados. Os próximos dois parâmetros fornecem o identificador da janela pai e do menu da janela, respectivamente (e nenhum dos dois são necessários). O parâmetro final fornece a opção de passar um valor com tamanho de apontador para o procedimento de janela durante a criação. Se tudo der certo, uma janela aparecerá na área de trabalho e um identificador de janela será retornado. Se tudo der errado, nullptr será retornado e a função GetLastError pode ser chamada para descobrir o porquê. Apesar de toda a conversa sobre as dificuldades de usar a API do Windows, acontece que criar uma janela é bem simples e se resume a:

WNDCLASS wc = { ... }; RegisterClass(&wc); CreateWindow( ... );

Assim que a janela for exibida, é importante que seu aplicativo comece a expedir mensagens o mais rápido possível — caso contrário, o aplicativo parecerá estar sem resposta. Basicamente, o Windows é um sistema operacional orientado por eventos e baseado em mensagens. Isso é particularmente verdadeiro na área de trabalho. Embora o Windows crie e gerencie a fila de mensagens, o aplicativo é responsável por retirá-las da fila e expedi-las, pois as mensagens são enviadas para um thread da janela, e não diretamente para a janela. Isso proporciona muita flexibilidade, mas um simples loop de mensagem não precisa ser complicado, como mostrado aqui:

MSG message; BOOL result; while (result = GetMessage(&message, 0, 0, 0)) {   if (-1 != result)   {     DispatchMessage(&message);   } }

O que talvez não seja surpreendente é que esse loop de mensagem aparentemente simples é implementado incorretamente com frequência. Isso ocorre porque a função GetMessage retorna um valor BOOL como protótipo, mas na verdade, é apenas um int. GetMessage retira ou recupera uma mensagem da fila de mensagens do thread de chamada. Isso pode acontecer para qualquer janela ou nenhuma janela, mas em nosso caso, o thread está bombeando mensagens para apenas uma única janela. Se a mensagem WM_QUIT for retirada da fila, GetMessage retornará zero, indicando que a janela desapareceu e terminou de processar as mensagens, e que o aplicativo deve ser interrompido. Se algo der terrivelmente errado, GetMessage pode retornar -1 e você pode chamar GetLastError novamente para obter mais informações. Caso contrário, todos os valores de retorno diferentes de zero de GetMessage indicam que uma mensagem foi retirada da fila e está pronta para ser expedida para a janela. Naturalmente, essa é a finalidade da função DispatchMessage. É claro que há muitas variantes para o loop de mensagem, e ter a capacidade de construir seu próprio lhe dá muitas opções de comportamento do aplicativo, entradas que ele aceitará e como elas serão traduzidas. Com exceção do ponteiro MSG, os parâmetros restantes de GetMessage podem ser usados para filtrar as mensagens opcionalmente.

O procedimento de janela começará a receber mensagens antes mesmo que a função CreateWindow retorne, por isso, é melhor que esteja pronto e aguardando. Mas como deve ser isso? Uma janela exige um mapa ou tabela de mensagens. Pode ser literalmente uma cadeia de instruções if-else ou uma grande instrução switch dentro do procedimento de janela. No entanto, isso pode se tornar complicado rapidamente, e muito esforço foi necessário em diferentes bibliotecas e estruturas para tentar gerenciar essa situação de alguma maneira. Na realidade, não é preciso que seja algo sofisticado, e uma simples tabela estática será suficiente em muitos casos. Primeiro, ajuda saber no que consiste uma mensagem em janela. Acima de tudo, há uma constante — como WM_PAINT ou WM_SIZE — que identifica exclusivamente a mensagem. Dois argumentos, por assim dizer, são fornecidos para cada mensagem, e são chamados WPARAM e LPARAM, respectivamente. Dependendo da mensagem, eles podem não fornecer informações. Por fim, o Windows espera que a manipulação de determinadas mensagens retorne um valor, que é chamado de LRESULT. A maioria das mensagens que seu aplicativo manipula, porém, deverá retornar zero, e não um valor.

Após essa definição, podemos criar uma tabela simples para a manipulação de mensagens usando esses tipos como blocos de construção:

typedef LRESULT (* message_callback)(HWND, WPARAM, LPARAM); struct message_handler {   UINT message;   message_callback handler; };

No mínimo, em seguida podemos criar uma tabela estática de manipuladores de mensagens, como mostra a Figura 1.

Figura 1 Uma tabela estática de manipuladores de mensagens

static message_handler s_handlers[] = {   {     WM_PAINT, [] (HWND window, WPARAM, LPARAM) -> LRESULT     {       PAINTSTRUCT ps;       VERIFY(BeginPaint(window, &ps));       // Dress up some pixels here!       EndPaint(window, &ps);       return 0;     }   },   {     WM_DESTROY, [] (HWND, WPARAM, LPARAM) -> LRESULT     {       PostQuitMessage(0);       return 0;     }   } };

A mensagem WM_PAINT chega quando a janela precisa de pintura. Isso acontece com bem menos frequência do que acontecia em versões anteriores do Windows, devido aos avanços em renderização e composição da área de trabalho. As funções BeginPaint e EndPaint são relíquias da GDI, mas são necessárias mesmo que você esteja desenhando com um mecanismo de renderização totalmente diferente. Isso acontece porque elas informam o Windows que você concluiu a pintura ao validar a superfície de desenho da janela. Sem essas chamadas, o Windows não consideraria a mensagem WM_PAINT respondida e sua janela receberia um fluxo constante de mensagens WM_PAINT desnecessariamente.

A mensagem WM_DESTROY chega depois que a janela desapareceu, informando que a janela está sendo destruída. Geralmente, isso é um indicador de que o aplicativo deve ser interrompido, mas que a função GetMessage dentro do loop de mensagem ainda está aguardando a mensagem WM_QUIT. Enfileirar essa mensagem é o trabalho da função PostQuitMessage. Seu parâmetro único aceita um valor que é passado via WPARAM de WM_QUIT, como uma maneira de retornar códigos de saída diferentes ao interromper o aplicativo.

A parte final do quebra-cabeça é implementar o verdadeiro procedimento de janela. Omiti o corpo da lambda que usei para preparar a estrutura WNDCLASS anteriormente, mas com o que você sabe agora, não deve ser difícil descobrir como ela poderia ser:

wc.lpfnWndProc =   [] (HWND window, UINT message,       WPARAM wparam, LPARAM lparam) -> LRESULT {   for (auto h = s_handlers; h != s_handlers +     _countof(s_handlers); ++h)   {     if (message == h->message)     {       return h->handler(window, wparam, lparam);     }   }   return DefWindowProc(window, message, wparam, lparam); };

O loop for procura um manipulador correspondente. Felizmente, o Windows fornece uma manipulação padrão para mensagens que você optar por não processar sozinho. Esse é o trabalho da função DefWindowProc.

E isso é tudo — se você chegou até aqui, conseguiu criar uma janela da área de trabalho usando a API do Windows!

O método baseado na ATL

O problema com essas funções da API do Windows é que elas foram desenvolvidas muito antes de o C++ se tornar o grande sucesso que é hoje e, portanto, não foram projetadas para acomodar facilmente uma visão do mundo orientada a objetos. Ainda assim, com a codificação inteligente ideal, essa API no estilo C pode ser transformada em algo um pouco mais adequado para a média de programadores de C++. A ATL fornece uma biblioteca de modelos de classe e macros que fazem exatamente isso, por isso, se precisar gerenciar mais do que algumas classes de janelas ou ainda utilizar os recursos de USER e GDI para a implementação de sua janela, não há motivo para não usar a ATL. A janela da seção anterior pode ser expressada com a ATL, como mostra a Figura 2.

Figura 2 Expressando uma janela em ATL

class Window : public CWindowImpl<Window, CWindow,   CWinTraits<WS_OVERLAPPEDWINDOW | WS_VISIBLE>> {   BEGIN_MSG_MAP(Window)     MESSAGE_HANDLER(WM_PAINT, PaintHandler)     MESSAGE_HANDLER(WM_DESTROY, DestroyHandler)   END_MSG_MAP()   LRESULT PaintHandler(UINT, WPARAM, LPARAM, BOOL &)   {     PAINTSTRUCT ps;     VERIFY(BeginPaint(&ps));     // Dress up some pixels here!     EndPaint(&ps);     return 0;   }   LRESULT DestroyHandler(UINT, WPARAM, LPARAM, BOOL &)   {     PostQuitMessage(0);     return 0;   } };

A classe CWindowImpl fornece o roteamento necessário das mensagens. CWindow é uma classe base que fornece vários wrappers de função de membro, principalmente para que você precise fornecer o identificador de janela explicitamente em cada chamada de função. Você pode ver isso em ação com as chamadas das funções BeginPaint e EndPaint nesse exemplo. O modelo CWinTraits fornece as constantes de estilo da janela que serão usadas durante a criação.

As macros são originadas como MFC e trabalham com CWindowImpl para corresponder as mensagens de entrada com as funções de membro apropriadas para manipulação. Cada manipulador é fornecido com a constante da mensagem como primeiro argumento. Isso pode ser útil se você precisar manipular uma variedade de mensagens com uma única função de membro. O padrão do parâmetro final é TRUE e permite que o manipulador decida em tempo de execução se realmente deseja processar a mensagem ou deixar que o Windows — ou até mesmo outro manipulador — cuide disso. Essas macros, juntamente com CWindowImpl, são bastante potentes e permitem que você manipule mensagens refletidas, encadeie mapas de mensagem juntos e assim por diante.

Para criar a janela, você deve usar a função de membro Create que sua janela herda de CWindowImpl e ela, por sua vez, chamará as boas e velhas funções RegisterClass e CreateWindow em seu nome:

Window window; VERIFY(window.Create(nullptr, 0, L"Title"));

Nesse ponto, o thread mais uma vez precisa começar a expedir as mensagens rapidamente, e o loop de mensagem da API do Windows da seção anterior será suficiente. A abordagem da ATL certamente se torna útil se você precisar gerenciar várias janelas em um único thread, mas com uma única janela de nível superior, é muito parecida com a abordagem da API do Windows da seção anterior.

WTL: Uma dose extra de ATL

Embora a ATL tenha sido projetada principalmente para simplificar o desenvolvimento de servidores COM e forneça apenas um modelo de manipulação de janelas simples — mas extremamente eficiente — a WTL consiste em uma infinidade de modelos de classe e macros adicionais, projetados especificamente para oferecer suporte para a criação de janelas mais complexas com base nos recursos de USER e GDI. Agora, a WTL está disponível no SourceForge (wtl.sourceforge.net), mas para um novo aplicativo que usa um mecanismo de renderização moderno, não oferece muitos benefícios. Ainda assim, existem alguns auxiliares úteis. No cabeçalho atlapp.h da WTL, você pode usar a implementação do loop de mensagem para substituir a versão feita manualmente que descrevi anteriormente:

CMessageLoop loop; loop.Run();

Embora seja simples soltá-la em seu aplicativo e usá-la, a WTL consome muita energia se você tiver necessidades sofisticadas de filtragem e roteamento de mensagens. A WTL também fornece atlcrack.h com macros projetadas para substituir a macro genérica MESSAGE_HANDLER fornecida pela ATL. Essas são meras conveniências, mas elas facilitam o processo de colocar uma nova mensagem em funcionamento, pois se encarregam de decifrar a mensagem, por assim dizer, e evitam qualquer suposição para descobrir como interpretar WPARAM e LPARAM. Um bom exemplo é WM_SIZE, que empacota a área de novo cliente da janela como as palavras de ordem inferior e superior de seu LPARAM. Com a ATL, a aparência pode ser a seguinte:

BEGIN_MSG_MAP(Window)   ...   MESSAGE_HANDLER(WM_SIZE, SizeHandler) END_MSG_MAP() LRESULT SizeHandler(UINT, WPARAM, LPARAM lparam, BOOL &) {   auto width = LOWORD(lparam);   auto height = HIWORD(lparam);   // Handle the new size here ...   return 0; }

Com a ajuda da WTL, isso é um pouco mais simples:

BEGIN_MSG_MAP(Window)   ...   MSG_WM_SIZE(SizeHandler) END_MSG_MAP() void SizeHandler(UINT, SIZE size) {   auto width = size.cx;   auto height = size.cy;   // Handle the new size here ... }

Observe a nova macro MSG_WM_SIZE que substituiu a macro MESSAGE_HANDLER genérica no mapa de mensagens original. A função de membro que manipula a mensagem também é mais simples. Como você pode ver, não há parâmetros desnecessários ou um valor de retorno. O primeiro parâmetro é apenas o WPARAM, que você pode inspecionar se precisar saber o que causou a alteração no tamanho.

A vantagem da ATL e da WTL é que elas são fornecidas como um conjunto de arquivos de cabeçalho que você pode incluir a seu critério. Você pode usar o que precisa e ignorar o restante. No entanto, como mostrei aqui, você pode ir bem longe sem depender de nenhuma dessas bibliotecas e simplesmente escrever o código de seu aplicativo usando a API do Windows. Acompanhe o próximo artigo, no qual vou mostrar uma abordagem moderna para renderizar os pixels na janela de seu aplicativo.

Kenny Kerré programador de computador, autor da Pluralsight e Microsoft MVP que mora no Canadá. Ele mantém um blog em kennykerr.ca e pode ser seguido no Twitter em twitter.com/kennykerr.

Agradecemos ao seguinte especialista técnico pela revisão deste artigo: Worachai Chaoweeraprasit