Informática Numaboa - Tutoriais e Programação
Janela Numaboíssima (masm)
Qui 16 Abr 2009 00:00 |
- Detalhes
- Categoria: Assembly Numaboa (antigo oiciliS)
- Atualização: Domingo, 21 Junho 2009 18:46
- Autor: vovó Vicki
- Acessos: 11438
Neste tutorial vamos sair do feijão com arroz da janela retangular do Windows e criar um visual totalmente novo. Para criar um gráfico que servirá de pele (skin) para a nossa janela, vamos finalmente programar "de verdade" em assembly.
Os mnemônicos utilizados neste tutorial são os de uso mais frequente - é bom a gente começar a se acostumar com eles. Caso você não saiba quais são, dê uma chegadinha na Oficina de Informática e leia o tutorial Curso Relâmpago de Assembly.
Mais sobre o contexto de dispositivo
Uma das principais características da API do Windows é a sua independência de dispositivos - os aplicativos win32 podem desenhar e imprimir numa grande variedade deles. O software que dá apoio a essa independência são duas DLLs: a GDI.DLL (Graphics Device Interface - interface de dispositivos gráficos) e um driver de dispositivo (device driver). O driver é próprio do dispositivo usado pelo aplicativo. Por exemplo, se o aplicativo for desenhar na área cliente da sua janela num monitor VGA, a biblioteca usada será a VGA.DLL; se for desenhar numa impressora, será SuaImpressora.DLL.
O aplicativo precisa indicar para a GDI qual driver deve ser carregado e, depois de carregado, precisa preparar o dispositivo para a operação de desenho: escolher a espessura e a cor das linhas, o padrão e a cor do pincel, a fonte, a região de corte, etc. Estas tarefas são efetuadas através de um contexto de dispositivo.
Um contexto de dispositivo é uma estrutura que define um conjunto de objetos gráficos e seus respectivos atributos, além dos modos gráficos que afetam a saída. Para as operações de desenho e pintura, os objetos gráficos incluem uma caneta para desenhar linhas, um pincel para pintar e preencher, um bitmap para copiar ou fazer a rolagem de porções da tela, uma paleta para definir o conjunto de cores disponíveis, uma região para cortes e outras operações e um caminho. Diferentemente de outras estruturas win32, um aplicativo nunca pode acessar diretamente um contexto de dispositivo - sempre atua indiretamente sobre a estrutura através de chamadas a diversas funções.
Os contextos de dispositivo de memória
Neste tutorial o interesse maior é nos contextos de dispositivo de memória. Estes contextos armazenam imagens do tipo bitmap para um dispositivo em particular. O formato de cor para o bitmap criado é compatível com o dispositivo associado, por isto este tipo de contexto de dispositivo também é conhecido como contexto de contexto compatível.
O bitmap original num contexto de dispositivo de memória é apenas marcador e sua dimensão é de 1 x 1 pixel. Para que um aplicativo possa começar a desenhar, primeiro é preciso selecionar um objeto bitmap para o contexto, indicando as dimensões (largura e altura) apropriadas. Só depois disto é que o aplicativo pode começar a usar o contexto para armazenar imagens. É coisa do tipo "abrir o buraco" para depois "plantar" o gráfico. Não confunda objeto bitmap (o buraco) com a imagem gráfica (a planta).
Quando um aplicativo cria um contexto de dispositivo, o Windows lhe atribui automaticamente um conjunto de objetos padrão (caneta, pincel, paleta, região). Só não existe como padrão um bitmap e um caminho (para formas geométricas). Um aplicativo também pode criar um novo objeto e incluí-lo no contexto de dispositivo.
Os tipos de contexto de dispositivo existentes são: display (pintura em vídeo), printer (pintura em impressora), memory (operações de pintura em bitmaps) e information (dispositivos de dados).
Um pouco sobre bitmaps
A tradução literal de Bitmap é mapa de bits, ou seja, a cor de cada pixel é identificada por um conjunto de bits. O conjunto de cores designadas para um determinado gráfico é chamado de paleta de cores. Um bitmap de 16 cores possui uma paleta com 16 cores possíveis, portanto, precisamos de 4 bits para poder numerar cada uma das cores. Veja o exemplo abaixo:
Decimal | Hexa | Binário | Cor |
0 | 0 | 0000 | preto |
1 | 1 | 0001 | vermelho escuro |
2 | 2 | 0010 | verde escuro |
3 | 3 | 0011 | amarelo escuro |
4 | 4 | 0100 | azul escuro |
5 | 5 | 0101 | magenta escuro |
6 | 6 | 0110 | turquesa escuro |
7 | 7 | 0111 | cinza escuro |
8 | 8 | 1000 | cinza claro |
9 | 9 | 1001 | vermelho claro |
10 | A | 1010 | verde claro |
11 | B | 1011 | amarelo claro |
12 | C | 1100 | azul claro |
13 | D | 1101 | magenta claro |
14 | E | 1110 | turquesa claro |
15 | F | 1111 | branco |
Com a paleta definida, podemos montar um bitmap sob a ótica de um programador: basta mapear a sequência de pixels atribuindo-lhes os valores das suas cores. No exemplo abaixo, o gráfico é uma área retangular de 9 x 9 pixels e mostra uma casinha. Note que as linhas são mapeadas de baixo para cima porque se trata de um DIB (device independent bitmap - bitmap independente de dispositivo), um tipo muito comum de bitmap. Na coluna "Mapeado em Hexa" encontram-se os valores da cor de cada um dos pixels de acordo com o padrão da paleta de cores. Poderíamos ter adicionado uma coluna com o mapeamento em binário o que, na verdade, seria a representação "real" do bitmap e é de onde deriva o nome deste tipo de imagem.
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
As janelas como bitmap
As janelas nada mais são do que áreas retangulares numa região da tela. Cada uma possui seu próprio modelo de contexto de dispositivo, que contém os objetos gráficos usados para desenhá-la e pintá-la. Portanto, cada janela possui um bitmap que lhe confere a aparência. Se a área retangular estiver totalmente preenchida por pixels coloridos, é claro que a janela terá o formato retangular. Se a área retangular tiver pixels transparentes, apenas as áreas de pixels visíveis é que irão compor o formato da janela. Sabendo disto, podemos desenhar janelas com "buracos", com contornos arredondados ou outras características visuais partindo de uma área retangular e tornando porções desta área "invisíveis" ou transparentes. Precisamos apenas fornecer o bitmap adequado ao dispositivo de contexto modelo janela.
Use o programa gráfico da sua preferência para criar o gráfico que servirá de máscara para a janela. Use a mesma cor em todas as áreas do gráfico que você quer que "desapareçam", pois esta cor é que será transformada em "transparente". É claro que precisa ser uma cor diferente de todas as que você usar para as áreas não transparentes. ANOTE as dimensões do gráfico em pixels e salve-o em formato bitmap (.bmp). O gráfico do exemplo tem 350 x 200 pixels e a cor magenta foi escolhida para ser a transparente:
Bitmap numaBoa.bmp | Aparência da janela |
Este bitmap, para ser incorporado ao executável, precisa ser adicionado ao arquivo de recursos. No tutorial "Usando Recursos" você encontra uma descrição detalhada de como proceder (não se esqueça de compilar o arquivo de recursos). A referência a este gráfico no arquivo RSRC.RC será a seguinte:
Criando nossa classe janela
Nossa janela deverá ter as mesmas dimensões que o bitmap que servirá de máscara. Podemos inicializar duas constantes com estes valores ou usar os valores quando formos criar a classe. Vamos usar a primeira opção. O fundo da janela pode (e deve) ser NULL. IMPORTANTE é que a janela seja do tipo popup.
Interceptando o WM_CREATE
No momento em que esta janela for criada, queremos que ela faça uso do nosso bitmap e não do padrão do sistema. Faremos o trabalho de pintura para que ele fique de acordo com o nosso projeto.
Carregando o bitmap
A primeira providência é carregar o bitmap desejado. Para isto vamos usar a função LoadBitmap (também descrita em "Usando Recursos") e guardar o manipulador do bitmap na variável global mBitmap.
Criando um contexto de dispositivo de memória
Para podermos manipular nosso gráfico antes de apresentá-lo na tela é preciso criar um contexto de dispositivo do tipo memória. É como se fosse o rascunho de trabalho. A função que cria automaticamente um contexto de dispositivo de memória é a CreateCompatibleDC, que pede como parâmetro o manipulador do dispositivo de saída. Se este parâmetro for NULL, a função cria um contexto de dispositivo de memória compatível com a tela atual do aplicativo. O manipulador obtido será guardado na variável local mCMMem.
Inserindo o bitmap no contexto modelo de memória
Agora é necessário inserir o objeto bitmap que contém o gráfico da nossa janela no contexto de dispositivo de memória para que possamos utilizá-lo como modelo de pintura. A função SelectObject faz o trabalho:
Obtendo as coordenadas da área da janela
As coordenadas de tela do canto superior esquerdo e inferior direito da janela servem de referência para definir a área retangular total da janela. Chamando a função GetWindowRect obtemos os quatro valores que serão armazenados numa estrutura do tipo RECT.
De posse destas coordenadas, vamos chamar a função grafiti. Esta é uma função própria (não é da API do Windows), cheia de emoções
Criando a Região Gráfica
Agora vamos entrar no "filé" do projeto. Todas as condições necessárias para podermos pintar nossa janela estão preparadas. Agora pegue a lata de tinta e dá-lhe pintura! Para efetuar este serviço, vamos criar uma função própria, a grafiti.
Declarando uma função própria
A função grafiti precisa de três parâmetros para trabalhar, todos do tipo DWORD: o manipulador do contexto de dispositivo de memória, a altura e a largura da janela. Vamos escrever seu protótipo, seu cabeçalho e seu final:
A palavra-chave USES
Esta declaração do procedimento contém uma novidade: foi incluída a palavra-chave USES.
Quando você programa para Win32, é preciso conhecer algumas regras importantes. Uma dessas regras é que o Windows usa ESI, EDI, EBP e EBX internamente e não espera que os valores destes registradores sejam alterados. Portanto, lembre-se da primeira regra: se você usar qualquer um destes quatro registradores numa função callback, não esqueça de restaurar seus valores originais antes de devolver o controle ao Windows. Uma função callback é uma função sua que é chamada pelo Windows. Isto não significa que você não possa utilizar estes quatro registradores. Apenas certifique-se de que seus valores sejam restaurados antes de devolver o controle ao Windows.
O MASM possui uma palavra-chave opcional que pode ser usada com PROC. Esta palavra-chave faz com que o assembler gere código para "pushar" para a pilha o valor dos registradores que devem ser preservados (e que serão alterados no procedimento) e para reavê-los da pilha com pop quando o procedimento retorna. USES aceita uma lista de registradores separados por um espaço.
Identificando a cor transparente
Partimos do princípio de que o primeiro pixel (o das coordenadas 0,0) tenha a cor que queremos transparente. O que precisamos é identificar todos os pixels que tenham uma cor diferente da cor que deve ficar transparente e fazer uma cópia dos mesmos. Ficando no exemplo acima, seria como se usássemos nosso bitmap original e fizéssemos apenas a cópia dos pixels diferentes de preto:
Pixels Originais | Cópia dos Pixels | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
Para obter a cópia desejada, vamos analisar todos os pixels do bitmap percorrendo todas as linhas, coluna a coluna. O registrador EDI mostrará o número da coluna e o ESI o número da linha. Para usá-los no nosso procedimento, a primeira providência será zerá-los com XOR. O valor da cor transparente, em RGB (red, green, blue), ficará na variável local corT.
A função GetPixel, da gdi.lib, obtém o valor RGB (red, green, blue) do pixel especificado pelas coordenadas.
Verificando cada pixel
Com a ajuda de rótulos, vamos fazer primeiro um loop para percorrer todos os pixels do bitmap e comparar a cor do pixel atual com a cor transparente.
Copiando os pixels não transparentes
No nosso exemplo, na segunda fileira, quando alcançarmos a coluna 4, encontraremos o primeiro pixel vermelho. Na coluna 6 encontra-se o último pixel vermelho desta série. Portanto, teremos que copiar os pixels 4, 5 e 6 desta fileira. Para determinar as coordenadas desta região numere as linhas horizontais e verticais a partir do ponto 0,0. A segunda linha horizontal é a linha 1 (x = 1) e a quarta linha vertical é a linha 3 (y = 3). Estas são as coordenadas do canto superior esquerdo da região (1,3). Repetindo o raciocínio para o último pixel vermelho desta fileira, verificamos que seu canto inferior direito é delimitado pela terceira linha horizontal (linha 2) e pela sétima linha vertical (linha 6). Suas coordenadas são x = 2 e y = 3).
No esquema abaixo estão as coordenadas de cada região que deverá ser copiada:
0,0 |
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
A lógica do nosso procedimento será a seguinte:
- Percorrer cada fileira analisando bit por bit.
- Quando encontrar o primeiro bit não transparente, marcar as coordenadas do canto superior esquerdo.
- Continuar até encontrar o próximo bit transparente (acabou
a sequência dos coloridos) ou o fim da fileira:
- marcar as coordenadas do canto inferior direito
- fazer a cópia dos bits coloridos usando as coordenadas obtidas
- se for a primeira cópia, esta inicializará o "copião"
- se não for a primeira cópia, adicioná-la ao "copião"
- Retornar o copião
Os pontos vitais desta rotina são: identificar uma sequência de pixels não transparentes e determinar a primeira cópia (que servirá de "copião"). Para isto faremos uso de duas variáveis locais. A variável temCor nos indicará se o pixel analisado é transparente ou não e a variável fazerCopiao indicará se o "copião" já foi inicializado.
Convencionaremos que o primeiro pixel seja da cor que queremos transparente - é o pixel indicador de transparência. Sabendo disto, no início do processo a variável temCor deve ser falsa (FALSE) e depois sempre deve refletir o estado do pixel que está sendo analisado.
A primeira cópia de pixels não transparentes servirá para inicializar o "copião"; as cópias subsequentes serão apenas adicionadas ao mesmo. Portanto, no início do processo, precisamos indicar que o "copião" precisa ser feito, ou seja, fazCopiao deve ser verdadeiro (TRUE).
No esquema abaixo, siga os pontos () e acompanhe a lógica:
Fileira 1 (linha 0) |
fazCopiao | temCor | esi (linha) | Faz cópia de ebx,esi edi,esi+1 |
edi | ebx | ||||||||
| TRUE | FALSE | 0 | 3 -> 4 | ||||||||||
| TRUE | FALSE -> TRUE | 0 | 4 -> 5 | 4 | |||||||||
| TRUE -> FALSE | TRUE -> FALSE | 0 | 4,0 e 5,1 | 5 | |||||||||
| FALSE | FALSE | 0 | 5 -> 6 |
Fileira 2 (linha 1) |
fazCopiao | temCor | esi (linha) | Faz cópia de ebx,esi edi,esi+1 |
edi | ebx | ||||||||
| FALSE | FALSE | 1 | 2 -> 3 | ||||||||||
| FALSE | FALSE -> TRUE | 1 | 3 -> 4 | 3 | |||||||||
| FALSE | TRUE | 1 | 4 -> 5 | ||||||||||
| FALSE | TRUE | 1 | 5 -> 6 | ||||||||||
| FALSE | TRUE -> FALSE | 1 | 3,1 e 6,2 | 6 | |||||||||
(adiciona ao copião) |
Para abrigar as cópias das regiões com pixels não transparentes vamos precisar de um contexto de dispositivo que funcionará como um temporário, o CMtemp, e de um contexto de dispositivo para o "copião", o CMcopiao. Agora é possível completar nosso processo:
Regiões
Foram usadas algumas funções que ainda não foram vistas. A CreateRectRgn (que cria uma região retagular), a CombineRgn (que funde duas regiões retangulares) e a DeleteObject, todas da GDI32.DLL.
Uma região é um retângulo, polígono ou elipse (ou a combinação de duas ou mais destas formas) que pode ser preenchida, pintada, invertida, emoldurada e que pode ser usada para testar a localização do cursor (chamado de hit testing). CreateRectRgn cria uma região retangular, CreateEllipticRgn cria uma região elíptica, e assim por diante. As regiões assim criadas podem ser inseridas num contexto de dispositivo para que o aplicativo possa operar sobre elas.
A função CreateRectRgn cria uma região retangular e retorna um manipulador para a região criada:
A função CombineRgn combina duas regiões e armazena o resultado numa terceira região:
O modo de combinação define o resultado desejado e pode ser RGN_AND, RGN_COPY, etc. No nosso exemplo foi usado o modo RGN_OR porque queremos "somar" as regiões. Veja abaixo:
Devolvendo o controle ao gerente de mensagens
Após ter percorrido um a um os pixels do nosso bitmap e "montado" uma região que contém apenas os pixels não transparentes, vamos devolver o controle ao gerente de mensagens para que possa fazer uso desta máscara na janela.
Incorporando a região gráfica
Retornando da função grafiti, o manipulador para a região com o bitmap trabalhado, onde vazamos todos os pixels da cor que foi determinada para ser transparente, se encontra no registrador EAX. Existe uma função da API para incorporar a região gráfica a uma janela. É a SetWindowRgn:
Através desta função podemos trocar o bitmap original (aquele que nós carregamos e transferimos para o mBitmap) pelo novo bitmap obtido através da função grafiti. Fazendo a troca, termina o nosso serviço de criar a janela. Certifique-se de ter liberado o contexto de dispositivo para que outros aplicativos possam utilizá-lo. Além disso, o contexto de dispositivo de memória não é mais necessário e DEVE ser eliminado com DeleteDC.
Pintando a janela com a máscara
Assim que uma janela é criada, ato contínuo ela começa a ser pintada. Precisamos monitorar a pintura para garantir que nosso bitmap "transparentado", obtido a duras penas, seja utilizado como máscara da nossa janela. A primeira providência é iniciar um sessão de pintura com BeginPaint e declarar uma variável local para receber o manipulador do contexto de dispositivo obtido e uma do tipo PAINTSTRUCT para receber as informações sobre a pintura. Depois precisamos obter a área retangular da janela com a função GetWindowRect e criar um contexto de dispositivo compatível com CreateCompatibleDC (se tiver dúvidas, veja novamente no tutorial "Pintando Texto"). Em seguida selecionamos o bitmap com SelectObject.
Lembre-se de que todo BeginPaint precisa terminar com um EndPaint para liberar o contexto de dispositivo para outras operações.
Transferindo blocos de bits
Agora é só transferir as "cores dos pixels" para a área retangular da nossa janela e tchan, tchan, tchan, tchan... vestimos nossa janela com a máscara desejada. A função responsável pela transferência dos blocos de bits referentes às cores dos pixels é a BitBlt (BIT-BLock Transfer) que exige um caminhão de parâmetros:
Como último parâmetro da função BitBlt usamos o código SRCCOPY (source copy - cópia da fonte), indicando que queremos uma simples cópia. Eliminamos mCMMem porque sempre é bom livrar um pouco de memória e... NOSSA JANELA ESTÁ PRONTA!!!
Compile o arquivo de recursos, compile e linke o código fonte. Divirta-se com o executável. É aí que aparecem alguns "senões": a janela não oferece nenhuma possibilidade visível de ser fechada e a janela não pode ser arrastada. Por enquanto, nos testes, é preciso usar Alt+F4 para fechá-la.
Permitindo o arraste da janela
O arraste da janela é efetuado mantendo o botão esquerdo do mouse pressionado enquanto se arrasta a janela. Pressionar um botão do mouse começa sempre com "abaixar", ou seja, BUTTON DOWN. Cada vez que o botão esquerdo do mouse é "abaixado", a janela recebe uma mensagem WM_LBUTTONDOWN (o L vem de LEFT, esquerdo). Vamos caçar esta mensagem e reenviá-la com os nossos parâmetros:
A mensagem WM_NCLBUTTONDOWN é enviada quando o usuário pressiona o botão esquerdo do mouse com o cursor FORA da área cliente da janela. Como estamos trabalhando com a área total da janela, o cursor está sempre fora da área cliente, portanto... vamos capturar a mensagem de "botão abaixado" e corrigí-la para "fora da área cliente". Para isto, precisamos conhecer melhor a mensagem WM_NCLBUTTONDOWN:
O hit-test (teste do ponto clicado) é um valor retornado pela DefWindowProc (o gerente de mensagens padrão do Windows) como resultado do processamento de uma mensagem WM_NCHITTEST. Esta mensagem é enviada para uma janela quando o cursor se movimenta ou quando um botão do mouse é pressionado ou solto. Nós não interceptamos esta mensagem, portanto ela entrou no processamento padrão do DefWindowProc. Dos diversos valores de retorno do processamento de uma mensagem WM_NCHITTEST, aquele que nos interessa é o HTCAPTION. Este valor indica que o clique ocorreu na barra de título da janela, ou seja, a única área da janela que permite o arrasto. Vamos "enganar" o sistema e informar que todo e qualquer clique ocorre na barra de título
A posição do cursor não é importante, pois não dependemos dela. Podemos simplesmente informar o valor 0 (coordenadas 0,0 da janela).
Fechando a janela com um duplo clique
Já que estamos interceptando mensagens do mouse, vamos associar um duplo-clique do botão esquerdo do mouse com o fechamento da janela. Mas, para que a janela aceite cliques duplos, é preciso dar-lhe esta característica ao definirmos sua classe:
Com a janela preparada, podemos interceptar a mensagem WM_LBUTTONDBLCLK (Left BUTTON DouBLe CLicK - duplo clique do botão esquerdo) e trocá-la pela mensagem WM_DESTROY, que não precisa de parâmetros específicos e fecha o aplicativo:
Serviço de faxina
Quando escrevemos um código fonte, sempre é aconselhável fazer uma revisão final para verificar se existem variáveis definidas e não inicializadas/usadas, se criamos algum objeto que não é eliminado automaticamente pelo sistema após o seu uso ou se iniciamos um processo que necessita de fechamento. É o caso do BeginPaint que precisa do EndPaint correspondente e do contexto de dispositivo que precisa ser liberado para o uso geral. Destes nós já cuidamos.
Um objeto Bitmap também precisa ser eliminado, portanto, ponha na sua lista de faxina: após um LoadBitmap SEMPRE precisa existir um DeleteObject para o(s) bitmap(s) carregado(s). Se não fizermos esta "limpeza" antes de encerrarmos o aplicativo, vai sobrar lixo e, geralmente, dos grandes.
Nós usamos a função LoadBitmap quando interceptamos a mensagem WM_CREATE e o manipulador do bitmap foi armazenado em mBitmap. Enquanto o aplicativo estiver rodando, precisamos deste bitmap para fazer a pintura da máscara da janela (interceptando WM_PAINT), portanto, não podemos eliminá-lo durante a execução. Só nos resta a alternativa de usar a DeleteObject na saída do programa, ou seja, quando interceptamos a mensagem WM_DESTROY:
É isso aí, moçada. A novela chegou ao fim. Compilem o executável e bom divertimento
Download
Tutorial para download
Este tutorial, juntamente com o código fonte, imagens e o executável está na seção de Downloads / Tutoriais / Assembly Numaboa, mas você também pode baixá-lo aqui.
Fontes
Este tutorial se baseia no exemplo "CUSTOM WINDOWS SHAPE" escrito por mob (também conhecido como drcmda), incluído no pacote MASM32 do hutch.