“Hello, world” no estilo Windows

Algumas pessoas têm me perguntado se a programação para Windows é muito diferente daquilo que elas aprendem nos cursos básicos. Bem… sinto dizer-lhes: É muito diferente! Em primeiro lugar, o famoso ponto de entrada main(), não é esse. No caso do Windows é WinMain(), com uma outra série de argumentos (nada de argc ou argv). A assinatura da função é essa:

int CALLBACK WinMain(HINSTANCE hInstance, 
                     HINSTANCE hPreviousInstance, 
                     LPSTR lpszCmdLine, 
                     int nCmdShow);

Para entender o que significa esses tipos HINSTANCE, temos que recorrer a analogia com arquivos. Quando você abre um arquivo usando uma syscall clássica (open()), obtém um valor inteiro que é chamado descritor do arquivo. Mas na nomenclatura da Microsoft, isso é chamado de handle, ou “manipulador”, em inglês… A maioria das funções da API do Windows funciona com “handles” e eles são referências aos objetos que você pode usar no Windows.

Handles são exatamente como file descriptors. Eles são valores numéricos que não tem nenhum significado especial (não são ponteiros para objetos, por exemplo) a não ser o valor em si…

Por exemplo: Um processo possui um handle correspondente a instância do mesmo (HINSTANCE). Note que o mesmo processo pode ser executado mais que uma vez (você pode abrir duas janelas da mesma aplicação – experimente com o Notepad, por exemplo). Assim, o “handle” da instância sendo executada é passada para WinMain em hInstance, acima… O segundo “handle”, hPreviousInstance, é meramente histórico… Na Win16 API os processos eram colocados em uma lista e o handle para a instância anterior da aplicação era informada em hPreviousInstance… No Win32 em diante esse argumento sempre receberá NULL (ou 0).

Ao invés de receber uma lista de argumentos (como acontece com o array argv, na função main()) o nosso ponto de entrada recebe um ponteiro simples para uma string contendo a linha de comando. O tipo LPSTR pode ser entendido como um “Long Pointer to STRing“, ou seja, um “char *“… O Termo “long” aqui também é histórico.

O quarto parâmetro é uma constante que o Windows envia para a aplicação informando-a de como o usuário quer que a primeira janela seja mostrada (ou se ela será inicialmente escondida!)… Geralmente a constante usada é SW_SHOW. E aqui vale uma explicação: Os header files do Windows definem um “porrilhão” (medida universal de grandeza) de constantes simbólicas, muitas delas são bitmaps, ou seja, podem ser “adicionadas” a outras via operador ‘|’ (OR).

A função WinMain() retornará um valor inteiro que deve ser 0 ou um valor oriundo da mensagem WM_QUIT (mais tarde explico)… Ou seja, geralmente esse valor será 0 e ele não tem nada a ver com “ERRORLEVEL” que, talvez, você tenha aprendido no DOS… Windows não lida com ERRORLEVEL.

Abaixo, temos um programa mínimo. Esse não será nosso “hello, world” definitivo porque não é como uma aplicação Windows realmente funciona:

#include <windows.h>

int CALLBACK WinMain(HINSTANCE hInst, HINSTANCE hPrev, 
                     LPSTR lpszCmdLine, int nCmdShow)
{
  MessageBox(NULL, "Hello, world!", "Mensagem", MB_OK);
  return 0;
}

Hello, Windows!

Uma aplicação Windows é composta de… janelas… o que mais seria? Uma janela é um objeto acessível através de um “handle”, obtido através de uma chamada à função CreateWindow() da Win32 API, mas antes, temos que informar à função qual é a “classe” da janela, ou seja, suas características iniciais… Além da “classe” definida para a sua janela principal, existem “classes” pré definidas para outros tipos de “sub janelas” como “button”, “combobox”, “edit”, “listbox” e “scrollbar”… Sim! Esses objetos também são janelas e são “criados” com a função CreateWindow().

A diferença é que sua janela receberá um ponteiro para uma função de manipulação de mensagens… Por exemplo, quando você clica no botão de fechar a janela, a mensagem WM_DESTROY (outro símbolo que é um valor inteiro!) é enviada para o seu “procedimento de janela” que, por sua vez, envia a mensagem WM_QUIT para ele… A aplicação, que está em loop, ao receber WM_QUIT, encerra este loop e é terminada.

As mensagens geralmente são constantes nomeadas como WM_ de “Window Message”… Existem algumas mensagens especiais para classe de janelas específicas. Por exemplo, BN_CLICKED (de “Button Notification: Clicked”), mas não as usamos com frequência…

Abaixo, temos um “hello, world” mais completo (sem o “hello, world” ainda…):

#include <windows.h>

LRESULT CALLBACK WindowMessagesHandler(HWND, UINT, 
                                       WPARAM, LPARAM);

int CALLBACK WinMain(HINSTANCE hInst, HINSTANCE hPrev,
                     LPSTR lpszCmdLine, int nCmdShow)
{
  static const char *szWindowClass = "MyMainWindowClass";
  HWND hWnd; /* Handle da janela. */
  MSG msg;
  WNDCLASS wc = {
    /* Se os tamanhos horizontal e vertical forem modificados,
       redesenha a janela. E ela aceita double clicks! */
    .style = CS_HREDRAW | CS_VREDRAW | CS_DBLCLKS,

    /* Este é o tratador de mensagens da nossa janela. */
    .lpfnWndProc = WindowMessagesHandler,

    /* Essa classe vale para essa instância! */
    .hInstance = hInst,

    /* O ícone da janela, o cursor e a cor de fundo! */
    .hIcon = LoadIcon(NULL, IDI_APPLICATION),
    .hCursor = LoadCursor(NULL, IDC_ARROW),
    .hbrBackground = (HBRUSH)(COLOR_WINDOW + 1),

    /* Uma classe customizada deve ter um nome único! */
    .lpszClassName = szWindowClass

    /* Existem outros campos que podem ser inicializados... */
  };

  /* Registra a nova classe. */
  RegisterClass(&wc);

  /* Cria a janela principal. */
  if (!(hWnd = CreateWindow(
                 szWindowClass, /* Usa a classe. */
                 "Minha janela" /* Barra de título. */
                 WS_OVERLAPPEDWINDOW, /* Estilo da janela. */
                 /* Deixa o Windows escolher (x,y). */
                 CW_USEDEFAULT, CW_USEDEFAULT,
                 /* Tamanho e altura. */
                 640, 480,
                 NULL, /* Não tem janela "parente". */
                 NULL, /* Não tem menu. */
                 hInstance, /* Atrelada a essa instância. */
                 NULL)))  /* Não tem parâmetros extras. */
  {
    /* Ocorreu um erro! */
    MessageBox(NULL, "Não consigo criar a janela principal!",
               "Erro", MB_OK | MB_ICONERROR);
    return 0;
  }

  /* Começa a enviar mensagens e mostra a janela. */
  UpdateWindow(hWnd);
  ShowWindow(hWnd, nCmdShow); /* Usa nCmdShow de WinMain() */

  /* Entra no loop de coleta de mensagens. */
  while (GetMessage(&msg, NULL, 0, 0))
  {
    /* Traduz mensagens vindas do teclado. */
    TranslateMessage(&msg);

    /* Despacha mensagem para o tratador. */
    DispatchMessage(&msg);
  }

  /* Ao sair do loop, encerra a aplicação. */
  return msg.wParam;
}

/* Eis o tratador de nossa janela. */
LRESULT CALLBACK WindowMessagesHandler(HWND hWnd, UINT uMsg,
                                       WPARAM wParam, 
                                       LPARAM lParam)
{
  /* Estamos tratando apenas WM_DESTROY aqui.
     Deixo todos os outros tratamentos para o próprio
     Windows! */
  switch (uMsg)
  {
    case WM_DESTROY:
      PostQuitMessage(0);
      break;

    default:
      return DefWindowProc(hWnd, uMsg, wParam, lParam);
  }

  return 0L;
}

Note que, na criação da janela, tem uma coisa estranha acontecendo com a especificação do “pincel” (brush) usado para pintar a área cliente da janela… Ao invés de obtermos um “handle” (como seria esperado) usamos a constante COLOR_WINDOW somada a 1… De fato, a forma “correta” de obter o pincel seria obter um pincel que o Windows mantém “em estoque”, disponível para todas as aplicações:

  .hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH),

Mas o Windows nos permite o tipo de “atalho” que mostrei acima… COLOR_WINDOW é a constante do pincel usado para pintar a área cliente (de acordo com o esquema de cores definido pelo usuário!).

Outra coisa estranha são esses parâmetros wParam e lParam no tratador de mensagens. De novo, são históricos: No Win16 o primeiro, wParam, era mapeado para o tipo short, de 16 bits, e o segundo, lParam, para o tipo long, de 32… Atualmente eles têm o mesmo tamanho… Eles estão ai porque uma mensagem, uMsg, pode conter dados adicionais. Por exemplo: Quando você cria uma janela, o Windows envia uma mensagem WM_CREATE e, no parâmetro lParam ele coloca um ponteiro para uma estrutura do tipo CREATESTRUCT… Dependendo da mensagem, wParam e lParam serão preenchidos, pelo Windows, com dados adicionais.

Para compilar o código (no Linux), para Win32, basta fazer:

$ i586-mingw32msvc-cc -O3 -s -o myapp.exe myapp.c -Wl,-subsystem=windows

Estou compilando e linkando na mesma linha de comando aqui, mas você pode fazer separadamente (e isso será necessário mais adiante!)… No caso do uso do MinGW64, substitua o i586-ming32msvc-cc por x86_64-w64-mingw32-gcc. A vantagem da versão x86-64 é que o código usará o conjunto de instruções de 64 bits (óbviamente) e ela não precisa de um runtime… É imperativo distribuir uma DLL chamada mingwm10.dll, que é distribuída em /usr/share/doc/mingw32-runtime/mingwm10.dll.gz. Descopacte-o e distribua junto com seu executável:

$ cp /usr/doc/share/mingw32-runtime/mingwm10.dll.gz ./
$ gunzip mingwm10.dll.gz

Se você está trabalhando com Windows, basta instalar o MinGW32 e usar o gcc.exe. Mas, note, mingwm10.dll ainda continua sendo necessário… Do meu lado, sempre o coloco em %windir%/system32. Ahhh… EU nunca consegui fazer o MinGW64 funcionar direito no Windows!! Boa sorte!

Executando o programinha winapp.exe compilado, temos algo assim:

Nossa janela.
Nossa janela.

Nada de especial, huh?

Cadê o “Hello, world”?

Como colocamos esse texto na janela? Infelizmente não podemos fazer como nos programinhas em C padrão e usar um printf()… Temos que desenhar o texto… Isso pode ser feito no ato do redesenho da janela. Sempre que a janela for redesenhada, uma mensagem WM_PAINT é enviada ao procedimento tratador de mensagens da janela. Assim, basta adicionarmos um tratador em WindowMessagesHandler(), assim:

LRESULT CALLBACK WindowMessagesHandler(HWND hWnd, UINT uMsg,
                                       WPARAM wParam,
                                       LPARAM lParam)
{
  PAINTSTRUCT ps;
  RECT rc;

  switch (uMsg)
  {
    case WM_PAINT:
      BeginPaint(hWnd, &ps);

        /* Pega as coordenadas do retângulo da área cliente */
        GetClientRect(hWnd, &rc);

        /* Desenha o texto usando a fonte atual. */
        DrawText(ps.hdc, "Hello, world", -1, &rc, 
                 DT_CENTER | DT_VCENTER | DT_SINGLELINE);

      EndPaint(hWnd, &ps);
      break;

    case WM_DESTROY:
      PostQuitMessage(0);
      break;

    default:
      return DefWindowProc(hWnd, uMsg, wParam, lParam);
  }

  return 0L;
}

Usar BeginPaint/EndPaint é um jeito de fazer a coisa… A necessidade da estrutura PAINTSTRUCT está no fato de que estamos desenhando numa janela cujas características gráficas podem variar de computador para computador (você pode ter um monitor CRT de 17″ com “resolução” de 72 dpi e área útil máxima de 1024×768 e outro monitor, LCD, de 25″ com resolução de 96 dpi e Full HD, por exemplo!). Para resolver esse problema o Windows usa a abstração chamada de “contexto de dispositivo” (DC)… Assim, qualquer dispositivo gráfico, incluindo impressoras, são usados da mesma maneira, através de um “handle” de contexto de dispositivo… No caso acima, BeginPaint obtem informações sobre a janela e coloca na estrututra PAINTSTRUCT, incluindo o “hdc”.

Outra forma de fazer a mesma coisa seria usar a função GetWindowDC da API… Mas, sempre que um handle for obtido, é prudente “livrar-nos” dele assim que ele não for mais necessário. Dessa forma, substituiríamos BeginPaint por GetWindowDC e EndPaint por ReleaseDC… O que dá no mesmo, poupando apenas o uso da estrutura (existem pequenos problemas com usar um DC dessa forma… A documentação, na MSDN, nos diz que GetWindowDC foi feita para casos especiais!).

No exemplo acima, por causa dos flags passados para DrawText, você pode modificar o tamanho da janela e verá que o texto será desenhado sempre no meio da área cliente:

Hello!
Hello!

Sei que existem vários perguntas que o novato queira fazer sobre isso tudo… Por exemplo: Como eu coloco botões, edit boxes etc, em minhas janelas? Como mudo a fonte? Como coloco meu próprio ícone na aplicação? Falarei sobre isso num outro texto… No momento, é útil que vocẽ dê uma olhada na MSDN Library e use-a para consultar as funções, constantes e outros detalhes da Win32 API. Por exemplo, dê uma olhada na função CreateWindow(): (aqui). Além de todas as informações sobre a função e seus parâmetros, você verá comentários e requerimentos (qual header incluir e/ou DLL importar)… Note que CreateWindow() exige o header winuser.h, mas eu usei aqui windows.h… Acontece que windows.h é um header que agrega alguns outros headers padrão… mas não faz mal iniciar sua aplicação com:

#include <windows.h>
#include <winuser.h>

Se quiser manter alguma coerência com a documentação do MSDN…

Anúncios