quinta-feira, 19 de maio de 2011

Programação de GUIs utilizando GTK - Parte 3

.
Back to life... :-)

Vamos estudar detalhadamente o programa "aloumundo" apresentado no post anterior. Também veremos alguns detalhes relacionados com a consulta a documentação da GTK. Por enquanto, estes artigos ainda serão baseados na GTK2, mas tentarei mostrar comparações com a nova GTK3, na medida do possível.

Falando em GTK3, algumas observações:
  • houve quebra da compatibilidade entre GTK2 e GTK3. Ou seja, não teremos 100% das coisas que funcionam em GTK2 funcionando diretamente (sem modificações) na GTK3;
  • devido a essa quebra de compatibilidade, é possível instalar no seu sistema as duas bibliotecas separadamente, e usar (na compilação) conforme o caso. Para instalar a nova GTK3 (para desenvolvimento) e suas dependências, basta instalar o pacote libgtk-3-dev;
  • já que estamos no gerenciador de pacotes... vamos instalar também devhelp (acesso a documentação das bibliotecas), libgtk2.0-doc (documentação offline da biblioteca GTK2), e libgtk3.0-doc (idem... GTK3);
  • para compilar com GTK3, basta alterar o parâmetro de pkg-config (veja post anterior) para "gtk+-3.0":


O programa funciona perfeitamente, a janela é criada e tals... mas note que, diferente do que acontece se compilarmos com GTK2, aparece uma mensagem de erro na execução:

Gtk-Message: Failed to load module "canberra-gtk-module"

Para resolver isso, basta instalar o pacote "libcamberra-gtk3-module" e suas dependências.

Mais do que simplesmente resolver um problema técnico imediato (um erro de compilação/execução), o fato acima gera a seguinte reflexão: "será que, ao instalarmos a biblioteca de desenvolvimento libgtk3-dev, já não deveria ser instalada também, automaticamente (como dependência), a biblioteca libcamberra-gtk3-module?" Ou seja, será que os desenvolvedores/empacotadores da GTK3 "comeram mosca" nessa questão ?

Feita esta reflexão, você pode ter 2 ações possíveis: reclamar da vida, reclamar dos desenvolvedores, dizer que a coisa tá mal feita, e ficar esperando alguém corrigir... ou ... entrar verdadeiramente no espírito do desenvolvimento colaborativo do software livre, e 1) identificar se isso é realmente um bug e 2) caso seja um bug, contribuir para seu conserto, que vai desde informar esse fato em uma lista de discussão apropriada ou em um sistema de gerenciamento de bugs, até efetivamente mudar o código. Nessa linha de ação, enviei um email para a lista "gnome-love" (específica para auxílio a novatos) para receber o feedback dos mais experientes sobre o fato. Depois coloco aqui a(s) resposta(s).


Segue então, o programa que iremos analisar:

1:  #include <gtk/gtk.h>
2:
3: int main (int argc, char *argv[])
4: {
5:
6: GtkWidget *window;
7:
8: gtk_init (&argc, &argv);
9:
10: window = gtk_window_new (GTK_WINDOW_TOPLEVEL);
11: gtk_window_set_title (GTK_WINDOW (window), "Alou, mundo!!");
12:
13: gtk_widget_show (window);
14:
15: gtk_main ();
16: return 0;
17:
18: }
19:

Para desenvolver com GTK, precisamos incluir (#include) o arquivo gtk.h. Apesar de gtk ser composta por várias bibliotecas de apoio, não precisamos indicá-las todas, o próprio gtk.h já faz isso pra gente. Claro que você precisará indicar outras bibliotecas (não relacionadas com GTK) que você quiser usar no seu programa.

Vamos então, nesse programa, criar e apresentar ao usuário uma simples janela. Precisamos então de uma variável para ser a referência desta janela. Criamos então a variável window, como um ponteiro para GtkWidget. A explicação de porque estamos criando esta variável como ponteiro para GtkWidget, e não GtkWindow, está no primeiro post desta série. Mas para relembrar rapidamente, todo objeto em gtk é construído a partir do objeto básico GtkWidget, utilizando o recurso de herança do paradigma de orientação a objetos. Logo, uma GtkWindow É um GtkWidget.

A função gtk_init é reponsável pela inicialização do ambiente GTK. Deve ser chamada antes de qualquer outra.

Vamos então, enfim, criar a janela. Atente para o fato que, neste passo, iremos criar a janela EM MEMÓRIA, ou seja, na verdade não vamos criar "o objeto gráfico janela, na tela", mas sim, criar as estruturas de dados em memória que vão armazenar e controlar o funcionamento deste futuro objeto gráfico. A função que faz isso é a gtk_window_new. Vamos ver na documentação qual é o protótipo desta função (ou seja, quais parâmetros precisamos passar para ela executar seu trabalho, e o que ela nos retorna). Vou utilizar para isso o DEVHELP, acessando dentro dele o "GTK+ 2 reference manual":


Para usar a função, então, precisaremos indicar um "GtkWindowType". O que é isso? É um parâmetro que diz que tipo de janela iremos criar, se uma janela principal (com tudo que tem direito, bordas, redimensionamento, título, etc...) ou uma janela "básica", sem esses recursos. Se clicarmos no próprio link "GtkWindowType, o DEVHELP vai nos mostrar que dados são válidos para este parâmetro (ou seja, quais os valores que estão definidos na enum GtkWindowType).


Podemos então passar para a função o valor GTK_WINDOW_TOPLEVEL (para uma janela normal, com tudo) ou GTK_WINDOW_POPUP (a janela básica "no osso"). O DEVHELP mostra uma descrição básica para esses valores.

Voltando a definição da função gtk_window_new, vemos que ela retorna um ponteiro para o tipo GtkWidget. Ou seja, vamos armazenar este retorno (que é efetivamente a referência para os dados da janela criados na memória) na variável window, criada anteriormente.

Vamos agora definir o texto que vai aparecer na barra de título da janela (Não esqueça que nossa janela ainda é um "projeto" de janela, na memória, e não um objeto gráfico bonitinho na tela do computador... ). Para isso utilizaremos a função gtk_window_set_title. Lá vamos nós novamente no DEVHELP...


A função não retorna nada (void), mas precisa de 2 parâmetros: um ponteiro para GtkWindow (referência da janela que vamos definir o texto na barra de título) e um ponteiro para "const gchar".

Como diria Jack, "vamos por partes": armazenamos a referência da janela num ponteiro para GtkWidget, e agora você diz que quer um ponteiro para GtkWindow? Bem, se você relembrar o primeiro post dessa série, verá que a hierarquia de objetos (widget hierarchy) começa em GtkWidget e, neste caso específico, termina em GtkWindow. Ou seja, uma GtkWindow É uma GtkWidget, então podemos usar uma variável no lugar da outra sem problemas! (ou quase...)

É "quase" sem problemas porque a linguagem C não foi construída levando em consideração o paradigma da orientação a objetos. Desta forma, algumas coisas não são tão transparentes como seriam numa linguagem especificamente projetada para este paradigma. No nosso caso, poderemos usar a variável window, que é GtkWidget, em um lugar que se espera uma GtkWindow, mas teremos que indicar que uma pequena conversão deverá ser feita. Isto é feito utilizando-se uma "função" que faz esse tipo de conversão pra gente. Neste caso, fica:

gtk_window_set_title ( GTK_WINDOW( window ) , ...

A função que "converte" algo em um GtkWindow é... GTK_WINDOW() !

(estou colocando "converte" entre aspas porque não é uma conversão propriamente dita, é mais uma indicação de tipo)

Mais devagar:
  • a variável window é um GtkWidget;
  • a "função" GTK_WINDOW( window ) indica que a variável window agora deverá ser tratada como uma GtkWindow. Nenhum problema aqui, pois como vimos, pelo paradigma de orientação a objetos, uma GtkWidget É um GtkWindow!
  • como window agora será tratada como um GtkWindow, podemos passar ela para a função gtk_window_set_title sem problemas!

Cuidado, se você consultar o livro de Andrew Krause, ele usa o termo "cast" para indicar essa operação de "conversão":
"... you can cast an object as a GtkWindow with GTK_WINDOW() ..."
Cuidado para não confundir com o cast básico da linguagem C, que tem outra sintaxe (tipo para o qual vamos converter entre parênteses, como em ... = (int) ... ).

Alguns "casts" disponíveis:

  • para GObject: G_OBJECT()
  • para GtkObject: GTK_OBJECT()
  • para GtkWidget: GTK_WIDGET() - Como sempre criaremos objetos do tipo GtkWidget, este "cast" raramente será usado.
  • para GtkContainer: GTK_CONTAINER()
  • para GtkBin: GTK_BIN()

... ou seja, uma função cujo nome é o tipo para o qual queremos "converter" em maiúsculas. Simples assim.

Falta o segundo parâmetro, o ponteiro para const gchar. Isso nada mais é que uma string, que será o texto da barra de título da janela. Se você lembrar, quando passamos algo como "blá blá blá" (uma string) para uma função, estamos na verdade passando o endereço inicial dessa string. Por exemplo, vamos lembrar a função fopen, que abre um arquivo para leitura e/ou escrita. Seu uso é, por exemplo:
arquivo = fopen ("dados_vendas.txt", "w");
Seu protótipo é:
FILE * fopen ( const char * filename, const char * mode )
... ou seja, a variável filename (ponteiro para caracter) recebe o endereço inicial da string "dados_vendas.txt".

Agora... porque gchar, e não apenas char?

Isso é feito para permitir compatibilidade em plataformas diferentes. Você lembra que, cada tipo de dado em C tem um tamanho em bytes específico, de acordo com a plataforma que está sendo usada. Por exemplo (hipotético), um int numa plataforma pode requerer 4 bytes de espaço, enquanto que em outra plataforma pode requerer 8 bytes. Para evitar esses problemas, e possibilitar uma compilação multiplataforma mais transparente, a biblioteca GLIB fornece tipos de dados padronizados, que serão "iguais" mesmo em plataformas diferentes. Assim, se no seu programa você utiliza os tipos de dados fornecidos pela GLIB, você poderá compilar em diversas plataformas diferentes sem se preocupar com as diferenças entre elas, no que diz respeito aos tipos de dados.

Alguns tipos fornecidos pela Glib:
  • gchar - equivalente ao char
  • gint - equivalente ao int
  • glong - equivalente ao long
  • gboolean - valores TRUE e FALSE (não tem equivalente no C)
Pra resumir:
gtk_window_set_title (GTK_WINDOW (window), "Alou, mundo!!");
... vai colocar a string "Alou mundo!!" na barra de título da janela indicada pela variável window.


Próxima linha:

gtk_widget_show (window);

Você deve estar pensando, "agora sim! beleza! um comando para efetivamente mostrar a janela na tela" !!!

No baby, ainda não...

Este comando apenas DEFINE a visibilidade de um certo objeto (isto é, se ele está visível ou invisível).

Essa questão da visibilidade é mais complexa do que parece num primeiro momento. Vejamos.

Neste nosso primeiro programa, estamos trabalhando apenas com um único objeto (uma janela). Não podemos esquecer que num programa "real", teremos vários objetos ao mesmo tempo, e esses objetos estarão organizados numa hierarquia: uma janela conterá outros objetos. Um desses objetos pode ser uma "frame" (caixa), que pode conter outros objetos (botões, labels, caixas de texto para digitação, etc).

Cada objeto individual possui seu próprio estado de visibilidade ou não. Mas um objeto ser configurado para estar visível não significa que ele vai certamente ser exibido... porque depende da visibilidade do objeto pai (o objeto que está "acima" na hierarquia) !

Vamos a um exemplo mais prático: imagine uma janela, e dentro dela, um único botão. Vamos definir esses 2 objetos como visíveis:

gtk_widget_show (window);
gtk_widget_show (button);

Quando o ambiente gráfico for efetivamente mostrar essas coisas na tela, os 2 objetos estarão visíveis.

Digamos agora que, por algum motivo, queremos esconder o botão do usuário (pra ele não poder clicar):

gtk_widget_hide (button);

Isso define que o botão está agora escondido ("hidden"). Quando o ambiente gráfico atualizar a tela, vai aparecer apenas a janela.

Vamos religar a exibição do botão:

gtk_widget_show (button);

Novamente, janela e botão visíveis na tela.

Agora vamos esconder A JANELA:

gtk_widget_hide (window);

Quando o ambiente gráfico atualizar a tela, não vai mostrar nem a janela, NEM O BOTÃO. Ué, mas o botão não está configurado para ser mostrado? Sim, mas como o botão poderia ser exibido, se ele está dentro da janela, e a própria janela não está visível?

Então, resumindo, definir a exibição ou não de um certo widget, não significa que ele vai ou não vai ser exibido. É preciso levar em consideração toda a estrutura da interface gráfica que você está construindo.

Antes de terminar esse comando, vejamos o protótipo:

void gtk_widget_show (GtkWidget *widget);

A função não devolve nada, e espera receber um ponteiro para GtkWidget. Como nossa variável window é deste tipo, podemos passar ela direto, nenhuma "conversão" necessária.

Último comando, e finalização do programa:

gtk_main();
return 0;

A função gtk_main() passa o controle do gerenciamento da interface gráfica que acabamos de montar e configurar (neste exemplo simples, apenas uma janela vazia) para o ambiente gráfico. Este, então, exibe a interface (a janela) e cuida dos "eventos", ou seja, da interação que o usuário fizer com ela. Note que podemos minimizar, maximizar, mover, redimensionar a janela, mesmo sem termos escrito nenhuma linha de código para tratar dessas questões. Não precisamos, porque quem cuida disso é o próprio ambiente gráfico. gtk_main(), portanto, geralmente é o último comando do nosso main. Quaisquer outros processamentos que nosso programa for realizar (para tratar as ações do usuário) deverá constar de funções, chamadas de "callback functions". Mas isso vai ser papo para outro post.

Note que a janela aparece e tals, mas quando clicamos no 'x' para fechar, ela até fecha, mas o programa continua rodando (o terminal fica "travado"). Isso acontece porque nosso programa não está tratando nenhum "evento" gerado pelo usuário. Para finalizar o programa, mesmo com a janela fechada, deveremos teclar control-C no terminal.

Fico por aqui. Lembrando que sugestões e correções são muito bem-vindas. Até a próxima!

Grande abraço!

Carlão

Um comentário:

Lucas Matheus disse...

Parabéns Professor, Ótimo post...