Módulo I - Projetando e Registrando uma Classe |
|
No tutorial anterior aprendemos como pôr uma janelinha na tela. Só que era uma janela do tipo message box, que não permite adicionar certas funcionalidades como um menu por exemplo. Neste tutorial vamos aprender a criar uma janela "de verdade". Além disso, vamos explorar a "mecânica" da coisa.
|
| O PROJETO |
|
Como sempre, nosso primeiro passo é planejar nosso programa. O projeto é pequeno, em compensação... o tutorial é um mastodonte!
|
| A TEORIA DAS JANELAS |
|
A Interface Gráfica dos programas Windows, conhecida como GUI - Graphical User Interface, depende essencialmente de funções da API. O uso desta interface padrão beneficia usuários e programadores. Para os usuários, as GUIs dos programas Windows são todas parecidas facilitando a navegação. Para os programadores, os códigos da GUI estão disponíveis, testados e prontos para uso. A desvantagem para os programadores é a complexidade crescente envolvida. Para criar ou manipular qualquer objeto GUI, como janelas, menus ou ícones, os programadores precisam seguir regras rígidas definidas pelo sistema Windows. Logo abaixo estão os passos necessários para se criar uma janela no desktop (itens 1 a 9) e os finalmentes do tutorial (itens 10 a 12):
Vamos por partes que a coisa há de ficar clara. Um dos aspectos mais importantes é entender o sistema de comunicação do Windows. Antes de começar pra valer este tutorial que deve ficar um pouco longo, abra o QEditor do MASM32 e digite (ou copie e cole) o esqueleto de um programa que possa ser finalizado com ExitProcess: .386 .MODEL FLAT,STDCALL option casemap:none
include \masm32\include\windows.inc include \masm32\include\kernel32.inc includelib \masm32\lib\kernel32.lib
.CODE
|
| 1. O manipulador (handle) da instância do programa |
|
Como usuário do Windows, você sabe que pode rodar várias instâncias de um mesmo programa. Se você abrir a calculadora do Windows duas vezes, obterá duas janelas distintas, cada uma delas rodando uma instância do programa. Se você fizer cálculos na primeira janela, a segunda não é afetada. Portanto, cada uma das instâncias, aos olhos do sistema, é um aplicativo independente. Como é que o Windows consegue individualizar cada uma das instâncias? Através de um identificador ou manipulador de instância. Este manipulador é apenas um número que identifica a instância para o sistema. Por exemplo, "a janela da instância 1 precisa ser atualizada", "a janela da instância 2 foi ativada", "a janela da instância 2 foi minimizada", etc, permite que o sistema efetue as tarefas corretas. Se não existisse o número de identificação, bem... a bagunça seria inevitável. E se duas instâncias possuírem o mesmo número... o Windows vai dar pau.
Existe uma função da API que nos permite obter um manipulador de instância que não conflite com outros já existentes (as duas janelas que abrimos para a calculadora não são as únicas que estão abertas). Esta função é a GetModuleHandle, que faz parte da kernel32.lib. Aproveite e familiarize-se um pouco mais com o MASM32: clique no item de menu [Tools / API Library List] para ativar um ferramenta muito útil que nosso amigo hutch colocou à nossa disposição - a "API to Library list". Esta janelinha é o mapa da mina que relaciona uma grande quantidade de funções e suas bibliotecas correspondentes. Digite "getmoduleh" e você já estará na linha correspondente à função procurada: GetModuleHandle lib ==> kernel32.lib Nós (e o sistema) vamos precisar do manipulador de instância quando quisermos atualizar ou alterar alguma coisa na(s) janela(s) do programa. É um número que será usado numa porção de funções diferentes, portanto, precisamos preparar um lugar onde vamos guardar este identificador e que possa ser acessado de qualquer ponto do programa . Um endereço de memória que guarda um valor e que pode ser acessado de qualquer ponto do programa é chamado de VARIÁVEL GLOBAL. Ao invés de termos que lembrar que endereço é este, podemos dar um nome a ele. Por exemplo, você pode ir para a "casa do Zé" ou para a "rua dos bits, número 135" que dá na mesma, pois o Zé mora neste endereço.
Já que precisamos deixar a critério do sistema o número que será designado como manipulador de instância para o nosso programa, o valor da variável que conterá o manipulador da instância só pode ser obtido em tempo de execução, portanto não podemos (e não devemos!) inicializar esta variável. A seção para variáveis não inicializadas, conforme já foi visto no tutorial "Por onde começar", é a seção .DATA?. Nesta seção damos nomes às variáveis, indicamos seu tipo e informamos que não são inicializadas através de um ponto de interrogação (?).
Como saber que o tipo da variável deve ser DWORD? A referência da API nos diz que a função GetModuleHandle retorna um manipulador de módulo (módulo é igual a instância no win32) para o módulo especificado se o arquivo tiver sido mapeado no espaço de endereços do processo chamador e que o tipo do valor de retorno é HMODULE - apenas um dos muitos nomes que o Windows dá a DWORD. HMODULE GetModuleHandle(
);
Parâmetro lpModuleName Aponta para uma string terminada em zero com o nome de um módulo Win32 (uma .DLL ou arquivo .EXE). Se a extensão do nome do arquivo for omitida, a extensão default é .DLL. A string do nome do arquivo por ter um caracter finalizador ponto (.) para indicar que o nome do módulo não possui extensão. A string não precisa especificar um caminho (path). O nome é comparado (sem considerar maiúsculas e minúsculas) aos nomes dos módulos que já estejam mapeados no espaço de endereços do processo chamador. Se o parâmetro for NULL, GetModuleHandle retorna um manipulador do arquivo usado para criar o processo da chamada.
Então vamos digitar um pouco. Certifique-se de que os arquivos kernel32.inc e kernel32.lib constam da sua lista de include e includelib. Uma vez familiarizados com a função, podemos chamá-la com o parâmetro NULL. É tudo o que queremos: um manipulador de instância para o nosso programa. .386 .MODEL FLAT,STDCALL option casemap:none
include \masm32\include\windows.inc include \masm32\include\kernel32.inc
includelib \masm32\lib\kernel32.lib
.DATA?
.CODE
A diretiva invoke você já conhece do tutorial "O Folgado" e a função GetModuleHandle já foi mais do que explicada. A linha seguinte é que são elas: usa o mneumônico MOV com os operandos mInstancia e eax. Vamos por partes. Um mneumônico é um nome reservado de uma família de códigos operacionais que realizam tarefas semelhantes no processador. MOV pede ao processador para MOVer ou copiar um valor de uma localização para outra. Dê uma olhada no texto acessório "Códigos Operacionais" para entender melhor. EAX é um registrador de uso geral do processador, ou seja, é um tipo especial de memória DENTRO do processador e que serve para o armazenamento temporário de dados (a informação pode ser colocada num determinado instante e de lá retirada quando isso se fizer necessário). Existem vários registradores dentro do processador - para maiores detalhes leia o texto acessório "Registradores". Em todo caso, esta nova linha pode ser traduzida da seguinte maneira: MOVa o valor que se encontra no registrador EAX para a posição de memória referente à nossa variável mInstancia. Quando esta linha for executada, a variável global mInstancia é inicializada. O motivo pelo qual transferimos o valor de EAX para mInstancia é que o valor de retorno das funções da API são armazenados no registrador EAX. Neste caso, assim que se volta da função GetModuleHandle, o registrador EAX contém o valor do manipulador de instância solicitado.
|
| 2. Pegar as instruções da linha de comando |
|
Geralmente a linha de comando se resume no nome do programa que queremos executar, ou seja, não contém parâmetros adicionais. Existem alguns raros casos em que é necessário enviar um ou alguns parâmetros para que o programa funcione corretamente ou de forma personalizada. Somente nestes raros casos é que precisamos usar a função da API GetCommandLine, também da kernel32.lib. Apenas a título de ilustração vamos inserir esta chamada. .386 .MODEL FLAT,STDCALL option casemap:none
include \masm32\include\windows.inc include \masm32\include\kernel32.inc
includelib \masm32\lib\kernel32.lib
.DATA?
.CODE
|
| 3. Registrar a classe da janela |
|
O Windows só consegue criar objetos à partir de um modelo - é como se o sistema precisasse da "planta da casa" para que poder construir a "casa". Primeiro é preciso criar a "planta" e depois entregá-la (fazer o registro) para que o Windows possa usá-la como modelo. Normalmente a função padrão utilizada para esta dupla tarefa (criar e registrar) é a WinMain, que é chamada pelo sistema como ponto de entrada inicial para um aplicativo win32. A referência da API nos mostra o seguinte: int WINAPI WinMain(
);
Para poder utilizar esta função precisamos criar o protótipo da mesma. Se você esqueceu o que é um protótipo de função, refresque a memória relendo "O Folgado". Para criar o protótipo é necessário conhecer os tipos dos parâmetros que a função espera receber: HINSTANCE, LPSTR e int. Na verdade, todos eles são nomes diferentes que o Windows dá ao DWORD. Esta função recebe quatro parâmetros: o manipulador da instância do nosso programa, o manipulador de instância da instância anterior do nosso programa, a linha de comando e o estado da janela da primeira vez em que aparecer. Sob win32, NÃO existe uma instância anterior. Cada programa está sozinho no seu espaço de endereços, de modo que o valor de hPrevInstance será sempre 0 (NULL). Isto é uma sobra da época do win16 quando todas as instâncias de diversos programas rodavam no mesmo espaço de endereços e uma instância queria saber se era a primeira. Sob o win16, se hPrevInstance for NULL, então esta instância é a primeira. O nome da função também pode ser um da nossa escolha, portanto, nosso protótipo pode ser
O manipulador da instância do nosso programa já está armazenado em mInstancia, o manipulador da instância anterior é NULL, o ponteiro para a linha de comando já está em linhaComando e o modo de exibição pode ser o padrão (SW_SHOWDEFAULT). Nosso código fonte passe a ser o seguinte:
.386 .MODEL FLAT,STDCALL option casemap:none
include \masm32\include\windows.inc include \masm32\include\kernel32.inc
includelib \masm32\lib\kernel32.lib
gerenteJanela proto :DWORD, :DWORD, :DWORD, :DWORD
.DATA?
.CODE
A função gerenteJanela é um procedimento que separamos do corpo principal de código (que fica entre o par de rótulos "inicio:" e "end inicio"). Fiz isto apenas para destacar e individualizar este procedimento do resto do código. Se você quiser, não precisa criar o protótipo da função, não precisa fazer o invoke e nem criar o procedimento gerenteJanela. Pode simplesmente colocar todo o código que vem a seguir no corpo principal do código. Além disso, se você suprimiu o código correspondente à linha de comando, basta enviar um parâmetro NULL no lugar de linhaComando.
|
| 3a. Criar a classe da nossa janela |
|
A "planta" da classe da nossa janela é "desenhada" numa estrutura. Uma estrutura agrupa dados de tal forma que possam ser endereçados num bloco único (Leia mais sobre estruturas em "Trabalhando com Estruturas"). Usaremos uma estrutura predefinida no Windows, chamada WNDCLASSEX.
A estrutura WNDCLASSEX foi planejada para conter todas as informações de uma classe janela e a referência da API nos mostra o seguinte: typedef struct _WNDCLASSEX {
} WNDCLASSEX;
A seguir, a explicação para cada um dos membros desta estrutura:
Como só vamos precisar desta estrutura no procedimento da função gerenteJanela, vamos declará-la como variável LOCAL com o nome de ej (de estrutura janela - ou qualquer outro da sua escolha). A diretiva LOCAL aloca memória da pilha para esta variável e precisa estar situada imediatamente após a diretiva PROC. A sintaxe é LOCAL <nome da variável local>:<tipo da variável>. Vamos usar LOCAL ej:WNDCLASSEX, que pede ao MASM para alocar uma quantidade de memória de pilha correspondente ao tamanho da estrutura WNDCLASSEX para a variável de nome ej. A vantagem é que podemos referenciar ej no nosso código sem nos preocuparmos com o realinhamento da pilha, o que é mordomia pura. Uma desvantagem é que variáveis locais não podem ser usadas fora das funções onde foram criadas e que serão imediatamente destruídas quando retornamos ao chamador. Outra desvantagem é que variáveis locais não podem ser inicializadas automaticamente porque elas são apenas memória de pilha alocada dinamicamente na entrada da função. Precisamos atribuir seus valores manualmente após a diretiva LOCAL. Entre os membros da estrutura encontramos o que deve conter o ponteiro para o nome da classe que desejamos criar. Para preencher este requisito, precisamos inicializar uma variável na seção .DATA que contenha o nome da classe. .386 .MODEL FLAT,STDCALL option casemap:none
include \masm32\include\windows.inc include \masm32\include\kernel32.inc
includelib \masm32\lib\kernel32.lib
gerenteJanela proto :DWORD, :DWORD, :DWORD, :DWORD
.DATA
.DATA?
.CODE
A partir deste ponto, listarei apenas a porção da função gerenteJanela. O primeiro membro da estrutura é o cbSize, que deve conter o tamanho da estrutura. Podemos obter este valor usando o operador SIZEOF. Como estilo da janela usaremos CS_HREDRAW OR CS_VREDRAW. O terceiro membro, lpfnWndProc, é o mais importante de todos. O significado de lpfn é "long pointer to function", ou seja, ponteiro longo para função. No win32 não existem ponteiros "near" (perto) ou "far" (distante); devido ao modelo de memória FLAT, existem apenas ponteiros. Mas isto, novamente, é sucata da época do win16. Cada classe janela precisa estar associada a uma função que gerencie o comportamento das janelas criadas a partir desta classe. Esta função, que chamaremos de gerenteMensagem, é tão importante que será discutida em detalhes no módulo II deste tutorial. Por enquanto vamos atribuir valores aos primeiros membros da estrutura usando o mneumônico MOV:
O ej.hInstance, o próximo membro da estrutura, deve conter o manipulador da instância do programa. Podemos usar a variável global mInstancia ou o parâmetro mInst recebido pela função gerenteJanela pois ambos apontam para o mesmo endereço, ou seja, contém o mesmo valor. Como não é possível transferir diretamente o valor de uma posição de memória para outra posição de memória, e tanto ej.hInstance quanto mInst são posições de memória, será preciso usar o auxílio de um registrador. O mais fácil é utilizar o registrador da pilha: usar o mneumônico push para colocar o valor na pilha e o mneumônico pop para transferí-lo da pilha para ej.hInstance.
Para obter o manipulador do ícone e do cursor basta fazer uma chamada para LoadIcon e LoadCursor. A função LoadIcon carrega o recurso do ícone especificado a partir do executável associado a uma instância do aplicativo. HICON LoadIcon(
);
A função LoadCursor funciona como a anterior, apenas direcionada para o cursor. HCURSOR LoadCursor(
);
Em ambas as funções, HINSTANCE identifica uma instância do módulo cujo arquivo executável contém o ícone ou cursor que deve ser carregado. Como ainda não programamos os recursos do nosso aplicativo (vamos ver isto em tutoriais posteriores), nosso executável está "vazio" de recursos e HINSTANCE pode ser NULL. Neste caso serão usados os recursos do Windows e podemos usar os parâmetros default (veja mais detalhes na referência da API). Lembre-se de que o valor de retorno destas funções encontra-se no registrador EAX. A ordem de inicialização dos membros da estrutura WNDCLASSEX não é importante. Como estamos trabalhando o ícone do programa, aproveitaremos a chamada a LoadIcon e inicializaremos hIcon e hIconSm numa tacada.
A referência da API para WNDCLASSEX nos diz que, se usarmos uma cor para o membro hbrBackground, o valor da cor precisa ser um dos valores das cores padrão do sistema, acrescido de 1. Escolhemos a cor padrão COLOR_WINDOW, portanto usaremos COLOR_WINDOW+1. Como não projetamos os recursos, também não temos um menu para o nosso aplicativo - a string com o nome do menu, por enquanto, será NULL. E, finalmente, o nome da nossa classe de janela já foi definida na seção .DATA e o ponteiro para a string que contém o nome é OFFSET NomeClasse.
Foi extenso porém não foi complicado. Nossa estrutura WNDCLASSEX está com todos os valores inicializados, ou seja, nossa classe de janela está definida. Mantendo a comparação inicial, a "planta da casa" está pronta. Agora podemos registrá-la.
Esta função é responsável pelo gerenciamento das mensagens provenientes de todas as janelas criadas a partir da classe associada. O Windows enviará mensagens à função para notificá-la de eventos importantes (entradas de teclado, cliques do mouse, etc) relativos às janelas pelas quais é responsável e a função deve responder adequadamente a cada mensagem recebida.
|
| 3b. Registrar nossa classe de janela |
|
Após criar uma classe (a "planta de uma casa"), é obrigatório registrá-la para que o sistema nos permita usá-la como modelo para criar uma ou mais instâncias de objetos ("uma ou mais casas") baseados nesta classe. Como utilizamos a estrutura WNDCLASSEX para definir as caracteríticas da nossa classe de janela, para registrá-la precisamos usar a função que "faz par" com ela: a RegisterClassEx. Caso tivéssemos utilizado WNDCLASS, a função para registro seria RegisterClass. ATOM RegisterClassEx(
);
Tranquilo. Temos a estrutura pronta e a função pede um ponteiro. Usando invoke podemos usar o operador ADDR para fornecê-lo. Acontece que esta função faz parte da user32.lib e esta biblioteca, assim como o arquivo include correspondente, precisa ser incluída: ... include \masm32\include\windows.inc include \masm32\include\kernel32.inc include \masm32\include\user32.inc
includelib \masm32\lib\kernel32.lib includelib \masm32\lib\user32.lib ...
|
| 4. Criar a janela |
|
Se nossa classe foi aceita pelo sistema para registro, isto significa que possuímos um "alvará de construção". Podemos criar quantos objetos quisermos usando a classe registrada como modelo. Registra-se apenas uma vez, usa-se quantas vezes forem necessárias. A função para criar uma janela de acordo com a classe que registramos é do grupo Ex, ou seja, CreateWindowEx (WNDCLASSEX -> RegisterClassEx -> CreateWindowEx). Seria CreateWindow caso tivéssemos usado uma estrutura WNDCLASS. HWND CreateWindowEx(
);
Esta função pede um caminhão de parâmetros (12 ao todo, se você tiver o trabalho de contar). Então, vamos lá:
É explicação que não acaba mais e tudo isso só para chamar uma funçãozinha! Para todos os parâmetros já temos os valores, exceto para o título da janela. Este vocês já tiram de letra: basta inicializar uma variável na seção .DATA. Também vamos precisar do valor de retorno da função CreateWindowEx, que é o manipulador da instância da janela que acabamos de criar. Sem este manipulador, ou seja, sem o número identificador desta janela recém criada, não teremos como acessá-la. Como vamos precisar deste manipulador apenas no âmbito da função gerenteJanela, podemos declarar uma variável LOCAL para armazená-lo. ... .DATA
...
Você deve estar pensando "até que enfim a janela está na tela"... Ledo engano. A janela foi criada (a "casa foi construída"), está prontinha para uso, só que ninguém contou ao sistema que é para abrí-la ao público. No Módulo II deste tutorial vamos dar os retoques finais e iremos aprender como "pilotar" a janela.
|