Adicionando botões e editboxes na sua aplicação Windows – Janelas filhas

O simples “hello, world” não é lá grandes coisas. Você pode querer fazer algo mais sério com sua aplicação, torná-la mais interativa. Aqui vou mostrar, rapidamente, como criar uma aplicação simples que mostra, numa “message box”, um texto digitado numa “edit box” depois que um botão é pressionado. Vou colocar um recurso adicional que permitirá que a aplicação execute em apenas uma instância. Começemos com nossa função WinMain():

/* main.c */
#include <windows.h>

/* Protótipo do procedimento de janela.
   Será implementada em winproc.c */
LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);

int CALLBACK WinMain(HINSTANCE hInst,
                     HINSTANCE hPrevInst,
                     LPSTR lpszCmdLine,
                     int nCmdShow)
{
  static LPCSTR szClassName = "SimpleAppClass";
  HWND hWnd;
  MSG msg;
  WNDCLASS wc =
  {
    .style = CS_HREDRAW | CS_VREDRAW,
    .lpfnWndProc = WndProc,
    .hInstance = hInst,
    .hIcon = LoadIcon(NULL, IDI_APPLICATION),
    .hCursor = LoadCursor(NULL, IDC_ARROW),
    .hbrBackground = (HBRUSH)(COLOR_BTNFACE + 1),
    .lpszClassName = szClassName
  };

  /* Isso impede que outra instância seja
     executada. Lembre-se que hPrevInst não 
     tem uso no Win32. Usei aqui o utilitário
     uuidgen (do linux) para criar uma string 
     única, para ser usada no mutex nomeado:

     $ uuidgen -r
     b27ec0f9-8922-4b44-a894-7610d01dad66

     Se CreateMutex falhar, quer dizer que um
     mutex do mesmo nome já foi criado antes!

     Obs: Poderia ter feito o mesmo para o nome
          da classe de janela! */
  #define MUTEX_UUID "b27ec0f9-8922-4b44-a894-7610d01dad66"
  if (!CreateMutex(NULL, TRUE, MUTEX_UUID))
    return 0;

  RegisterClass(&wc);

  /* Este estilo criará uma janela "normal" que 
     não pode ser maximizada ou ter seu tamanho 
     alterado. */
#define WINDOW_STYLE \
  WS_OVERLAPPED  | \
  WS_SYSMENU     | \
  WS_VISIBLE     | \
  WS_BORDER      | \
  WS_MINIMIZEBOX | \
  WS_CAPTION

  if (!(hWnd = CreateWindowEx(0,
                              szClassName,
                              "My Simple App",
                              WINDOW_STYLE,
                              CW_USEDEFAULT, CW_USEDEFAULT,
                              320, 100,
                              NULL, NULL,
                              hInst,
                              NULL)))
  {
    /* Trata erro se não conseguiu criar a janela. */
    MessageBox(NULL, 
      "Erro ao tentar criar a janela da aplicação.",
      "Erro", MB_OK | MB_ICONERROR);
    return 0;
  }

  UpdateWindow(hWnd);
  ShowWindow(hWnd, SW_SHOW); /* Sempre mostra! */

  while (GetMessage(&msg, NULL, 0, 0))
  {
    TranslateMessage(&msg);
    DispatchMessage(&msg);
  }

  return msg.wParam;
}

A primeira coisa a observar é o uso da função CreateMutex da Win32 API para criar um objeto, mutex, único para todo o sistema. Infelizmente o handle hPrevInst não pode ser usado para determinarmos se outra instância deste processo já encontra-se em execução. Isso só funcionava no Win16 (até o Windows 3.11 e, se não me engano, no famigerado Windows Millenium!). Usar um mutex nomeado que tem um proprietário inicial (o segundo parâmetro, TRUE, diz isso) é uma forma simples e rápida de fazer isso… Se a função retornar NULL isso significa que houve um erro na criação do mutex que, no caso, só pode significar que ele já existe e pertence a outro processo.

Outra diferença do nosso WinMain do “hello, world” é que uso aqui estilos de janela específicos para atingir um efeito. Ao invés de usar a constante WS_OVERLAPPEDWINDOW, que é um atalho para outras constantes de estilos de janela, monto o bitmap do novo estilo manualmente, para que a janela não possa ter seu tamanho alterado, todo o resto fica igual ao WS_OVERLAPPEDWINDOW…

E, para evitar que o usuário possa instanciar a aplicação maximizada, não uso o argumento nCmdShow de WinMain na função ShowWindow. Ao invés disso forço a barra dizendo que a janela será mostrada do jeito que está definida em CreateWindowEx (SW_SHOW).

Ahhh… repare que na classe da janela uso o atalho para o pincel da cor de um botão (COLOR_BTNFACE+1). Essa parece ser a cor de fundo preferida da maioria das aplicações desde o Windows 95… Outro detalhe na estrutura da classe é o estilo. Note que deixei apenas CS_HREDRAW e CS_VREDRAW, eliminando o CS_DBLCLKS, já que não trataremos de clicks duplos em nossa aplicação. A necessidade do redesenho horizontal e vertical deve-se ao fato de que a janela pode ser movida nessas direções, mesmo que não possa ser redimensionada nelas.

Nossa aplicação contém um “label”, uma “editbox” e um botão, dai, na mensagem WM_CREATE (enviada por CreateWindowEx na criação de nossa janela) criaremos as janelas filhas:

#include <windows.h>
#include <stdio.h>

#define ID_STATIC1 100
#define ID_EDIT1   101
#define ID_BUTTON1 102

LRESULT CALLBACK WndProc(HWND hWnd,
                         UINT uMsg,
                         WPARAM wParam,
                         LPARAM lParam)
{
  /* Criarei 3 janelas filhas
     (label, editbox e botão). */
  static LPSTR ChildClass[3] = { "static", 
                                 "edit", 
                                 "button" };
  static POINT ChildPos[3] = { { 10, 10 }, 
                               { 60, 10 }, 
                               { 10, 40 } };
  static POINT ChildSize[3] = { { 50, 20 }, 
                                { 240, 20 }, 
                                { 60, 20 } };
  static DWORD ChildStyleEx[3] = { 0, 
                                   WS_EX_CLIENTEDGE, 
                                   0 };
  static DWORD ChildStyle[3] = { 
    /* SS_ = "Static Style" */
    WS_VISIBLE | WS_CHILD | SS_SIMPLE,

    /* ES_ = "Editbox Style" */
    WS_VISIBLE | WS_CHILD | ES_LEFT,

    /* BS_ = "Button Style" */
    WS_VISIBLE | WS_CHILD | BS_PUSHBUTTON 
  };
  static LPSTR ChildCaption[3] = { "Texto:", "", "Mostrar" };
  static int   ChildId[3] = { ID_STATIC1, 
                              ID_EDIT1, 
                              ID_BUTTON1 };
  static HWND  hChildWnd[3];

  int i;

  switch (uMsg)
  {
  case WM_CREATE:
    for (i = 0; i < 3; i++) { /* Note hWnd no argumento hWndParent de CreateWindowEx! */ hChildWnd[i] = CreateWindowEx( ChildStyleEx[i], ChildClass[i], ChildCaption[i], ChildStyle[i], ChildPos[i].x, ChildPos[i].y, ChildSize[i].x, ChildSize[i].y, hWnd, (HMENU)ChildId[i], ((LPCREATESTRUCT)lParam)->hInstance,
                       NULL);

      if (!hChildWnd[i])
      {
        char buf[128];
        sprintf(buf, "Erro criando janela filha (id=%d).", 
          ChildId[i]);

        MessageBox(hWnd, 
                   buf, 
                   "Error", MB_OK | MB_ICONERROR);
        return -1; /* Faz CreateWindowEx em WinMain falhar! */
      }
    }

    /* EditBox aceitará, no máximo 100 chars. 
       Manda a mensagem EM_SETLIMITTEXT para informar
       a EditBox disso! */
    SendMessage(hChildWnd[1], EM_SETLIMITTEXT, 100, 0);

    return 0;

  case WM_COMMAND:
    if (LOWORD(wParam) == ID_BUTTON1)
    {
      char buf[128];

      /* Pede para a editbox colocar seus dados 
         no nosso buffer. */
      SendMessage(hChildWnd[1], WM_GETTEXT, 
                  100, (LPARAM)buf);

      MessageBox(hWnd, buf, "Mensagem", MB_OK);

      return 0;
    }
    break;

  case WM_DESTROY:
    PostQuitMessage(0);
    return 0L;
  }

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

Nossa pequena aplicação será compilada de acordo com o seguinte makefile (para x86_64):

# Makefile
#
# Subtitua CC para i586-mingw32msvc-gcc se você quer 
# compilar para o modo i386.
#
CC=x86_64-w64-mingw32-gcc

app.exe: main.o winproc.o
	$(CC) -s -o $@ $^ -Wl,-subsystem=windows -lgdi32

%.o: %.c
	$(CC) -O3 -c -o $@ $<

O motivo pelo uso da biblioteca gdi32.dll ficará claro adiante… Depois de compilada e executada, nossa aplicaçãozinha ficará assim:

Estamos quase lá!
Estamos quase lá!

Ficou bonitinho, mas temos alguns problemas: O tamanho vertical da editbox parece fora do padrão (fácil de mudar!) e as fontes usadas nas janelas filhas estão erradas, para o padrão usado nas aplicações do seu Windows, não é?! Corrigirei isso mais adiante.

Olhando para o código fonte acima, note que, para a editbox, usei o estilo estendido WS_EX_CLIENTEDGE para dar a aparência de 3D, como é usual… Sem esse estilo a editbox seria uma caixa branca sem bordas. Cada janela filha é criada do mesmo jeito que as janelas comuns, via CreateWindowEx, onde o parâmetro hWndParent contém o handle da janela mãe… Note que todas as janelas filhas têm o estilo WS_CHILD e também WS_VISIBLE. Este último, se não constar, fará o controle ser criado, colocado na posição solicitada, mas não será mostrado.

No fim da criação das janelas filhas envio diretamente (não enfileiro) uma mensagem EM_SETLIMITTEXT para a editbox contendo o tamanho máximo de caracteres que ela suportará. Caso contrário, o tamanho máximo será enorme! Neste ponto vale esclarecer que existem duas maneiras de enviar mensagens para janelas: Uma através da função SendMessage e outra através da função PostMessage. A diferença entre elas é que a primeira envia a mensagem diretamente para a janela do handle fornecido. Já “postar” uma mensagem significa colocá-la numa fila… Lembra-se do GetMessage lá do WinMain? Essa função coleta a última mensagem que consta da fila de mensagens associada à thread principal da sua aplicação. Quando a fila está vazia GetMessage simplesmente não retorna, entregando o controle ao Windows… Quando uma mensagem é recuperada da fila ela precisa ser “traduzida” (caso a mensagem seja relacionada ao teclado, por exemplo WM_KEYUP) e depois “despachada” para o procedimento de janela correspondente ao handle da janela para a qual a mensagem foi enfileirada.

Mas, quando queremos enviar a mensagem diretamente ao procedimento de janela, ao invés de “postá-la” (post), a enviamos (send)! Aliás, no tratador da mensagem WM_DESTROY a chamada da função PostQuitMessage(0) é apenas um atalho para:

PostMessage(NULL, WM_QUIT, 0, 0);

Ou seja, WM_QUIT é colocada na fila de mensagens. Como não interessa para qual janela essa mensagem seja direcionada (já que GetMessage vai retornar 0), então ela passa NULL para o argumento hWnd.

Quanto ao comportamento do botão, a mensagem WM_COMMAND, quando recebida, testa se wParam contém o ID do mesmo. Se for o caso, o tratador obtém a mensagem contida na editbox enviando a mensagem WM_GETTEXT para ela e a mostra, usando MessageBox e retorna 0… Caso contrário, entrega o controle a DefWindowProc, como deveria fazer.

Com relação à semântica do tratador de mensagem, é útil consultar a MSDN Library para saber o que o Windows espera que você faça com as mensagens específicas…

Acertanto as fontes…

Modificar o tamanho da editbox é simples, mude lá em WM_CREATE, no array ChildSize… Agora, precisamos acertar as fontes que aparecem nos controles. Nada mais simples: Basta enviar uma mensagem WM_SETFONT para cada um deles com a fonte correta. Felizmente, para nós, a função GetStockObject, contida na gdi32.dll (e, por isso a incluí no linker), consegue obter o handle da fonte configurada como default para ser usada na GUI. Assim, basta adicionar essa chamada no loop da criação das janelas filhas:

 ...
  case WM_CREATE:
    for (i = 0; i < 3; i++) { 
      ...

      if (!hChildWnd[i])
      {
        ...
      }

      /* Insira isso aqui! */
      SendMessgae(hChildWnd[i], 
                  WM_SETFONT,
                  (WPARAM)GetStockObject(DEFAULT_GUI_FONT),
                  TRUE);
    }

Voilà!

Anúncios