segunda-feira, 31 de maio de 2010

Programação de GUIs utilizando GTK - Parte 1

.

Começo a partir de hoje uma série de posts para compartilhar minha experiência no aprendizado da biblioteca GTK, utilizada para desenvolvimento de software com interface gráfica para o usuário (GUI, Graphical User Interface).

Quero ressaltar que não sou um grande conhecedor da biblioteca, pelo contrário, estou ainda em processo de aprendizado. Tenho apenas um software completo desenvolvido com interface em GTK, que provavelmente serve muito mais como exemplo de como NÃO programar do que qualquer outra coisa ... :-) (apesar da ajuda inestimável do amigo Bruno Boaventura!).

O objetivo aqui é apenas compartilhar o conhecimento, mas mais que isso, motivar outras pessoas a ingressarem no mundo GTK, GNOME, e quem sabe um dia nos tornarmos desenvolvedores/colaboradores dessa que é a melhor interface gráfica do mundo!!! :-) (usuários KDE, por favor, levem na brincadeira... :-) )

O livro que estou utilizando para esse estudo é o excelente "Foundations of GTK+ Development", de Andrew Krause (Apress). Recomendo fortemente sua aquisição para quem deseja programar com GTK seriamente. O livro é completo e objetivo. Para cada elemento da biblioteca, o autor apresenta um ou mais códigos-fonte de exemplo, mostrando suas características fundamentais. Para aqueles com poucos recursos, a Apress comercializa uma versão PDF (ebook), a um preço um pouco mais acessível do que a versão em papel, principalmente porque não é preciso pagar o frete para o envio da versão em papel, que as vezes custa mais que o próprio livro... Um arquivo com todos os códigos-fonte dos exemplos do livro pode ser baixado gratuitamente no site oficial do livro, neste link.

Conto com a ajuda e a colaboração dos amigos que estiverem lendo isso, caso percebam algum erro no conteúdo apresentado, ou mesmo para proporem alguma adição que explique melhor algum ponto.

Ah, mais uma coisinha: o requisito básico para se acompanhar estes posts (e, de certa forma, pra estudar GTK) é um forte conhecimento da linguagem C básica, incluindo PONTEIROS.


***************

Pra começar: GTK+ é uma biblioteca escrita na linguagem C, sob o paradigma da orientação a objetos.

Ué, (dirá você), mas a linguagem C NÃO é orientada a objetos!! Como pode?

Esse é um erro comum de muita gente boa. Uma coisa é um paradigma de programação, ou seja, uma forma de organizar as idéias para criar um programa de computador. Outra coisa são as características e os recursos de uma linguagem de programação específica. Uma coisa não está completamente amarrada a outra. O que acontece é que existem linguagens de programação MAIS ADEQUADAS para utilização com um certo paradigma. Por exemplo, as linguagens C++ e Java foram criadas com recursos que tornam a programação no paradigma orientado a objetos mais confortável. Mas isso não significa que não se possa programar "orientado a objetos" com a linguagem C. O que vai acontecer é que teremos um "trabalhinho adicional", já que C não vem preparada para orientação a objetos. Por sorte, este trabalhinho já está pronto, na figura da biblioteca GObject, utilizada pela biblioteca GTK.

Vamos neste post, portanto, ver algumas características bem básicas do paradigma de orientação a objetos, fundamentais para que possamos entender como programar utilizando GTK.

Mas atenção! O que vai ser apresentado aqui é um resumo básico, introdutório, e principalmente, incompleto! Portanto, se você é aluno de computação, envolvido com as questões de orientação a objetos, não deixe de ler alguma referência oficial sobre o assunto. E depois volte aqui pra me ajudar a complementar o texto... :-)


DIFERENÇAS FUNDAMENTAIS ENTRE OS PARADIGMAS "PROCEDURAL" E "ORIENTADO A OBJETOS"


No paradigma procedural, temos duas características fundamentais:

  • código e dados bem separados;
  • o código atua sobre os dados;

Por exemplo, um programa que faça o gerencimento de uma conta bancária, no paradigma procedural, poderia ser representado pela seguinte figura:


Para, por exemplo, verificar o saldo da conta de "beltrano", o código (função) "saldo" atua sobre os dados da conta de beltrano:


No paradigma orientado a objetos, código e dados são reunidos em entidades chamadas "objetos". Um objeto possui características (dados, atributos) e também um comportamento (o objeto "faz" alguma coisa, ele responde a solicitações, é o seu código).


Repetindo o exemplo acima, para verificar o saldo da conta de "beltrano", agora nós enviamos uma "mensagem" para o objeto "conta de beltrano", para que esse objeto nos diga qual é o saldo da conta. Ou seja, este objeto tem um dado (o valor do saldo), e um comportamento (o objeto sabe responder quando lhe perguntamos seu saldo). Uma analogia: o objeto "ser humano" tem um dado (seu nome), e um comportamento (sabe responder quando lhe perguntam o nome). Se eu mandar, para um objeto "ser humano" a mensagem "qual é o seu nome?", o objeto ser humano devolve o valor armazenado na variável NOME... :-)


Resumindo: o objeto é responsável, ele mesmo, por verificar (e exibir) o saldo da conta.

CLASSE: MODELO GENÉRICO DE UM OBJETO

Os objetos são criados a partir de um "modelo", que diz como o objeto será (quais são seus atributos, dados, e qual é seu comportamento, código). Por exemplo, podemos definir a classe "ser humano", e a partir desse modelo, criar efetivamente os objetos "Ana", "Bianca", "Claudia", etc...

Eu comparo, mal-e-porcamente, uma classe a uma struct em C. Com a struct, temos dois momentos: o de definição da struct, e o de efetivamente criar uma variável a partir da struct. Assim, por exemplo,

struct AGENDA
{
. char NOME[30];
. char EMAIL[20];
}

... na verdade cria apenas um modelo de como será a struct AGENDA. Mas não há efetivamente nada criado na memória para guardar NOME e/ou EMAIL. Com a seguinte instrução,

struct AGENDA MinhaAgenda;

... aí sim, criamos na memória a variável "MinhaAgenda", e dentro dela temos NOME e EMAIL.

Uma classe é, portanto, uma "struct metida a besta", que além de dados, tem também código. :-)

Vamos ver um exemplo do que poderia ser uma classe "conta bancária", e alguns objetos gerados a partir dela:


Na figura, "instanciar" é simplesmente o processo de gerar um objeto, a partir de uma classe.

Uma classe, portanto, define todas as características comuns a um tipo de objeto.

Outro conceito pertencente ao paradigma de orientação a objetos é o de ENCAPSULAMENTO. Como o nome diz, traz a idéia de "cápsula", ou seja, a idéia de que não podemos ver o que tem dentro da cápsula. Outra analogia (bem simplória), numa cápsula de remédio, não sabemos o que tem dentro da cápsula (as várias substâncias e suas respectivas quantidades). Também não podemos (ou pelo menos, não devemos... :-) ) abrir a cápsula e manipular diretamente algum ingrediente específico.

Da mesma forma, um objeto não pode (ou não deve...) manipular diretamente os dados internos de outro objeto. Assim, no exemplo visto, só quem pode manipular o saldo (dado) que existe no objeto "conta de fulano" é o código que também existe dentro do mesmo objeto... não faria sentido o código que está dentro do objeto "conta de beltrano" manipular a variável SALDO existente dentro do objeto "conta de fulano"!!

ALGUNS TERMOS DA O.O.

Atributos: dados armazenados dentro dos objetos

Métodos: as funções (ou seja, o código) internas aos objetos, que manipulam os atributos

Envio de mensagem: dizemos que um objeto "envia uma mensagem" a outro objeto, para que este último faça alguma coisa. Por exemplo, podemos ter o objeto "caixa eletrônico da rua 5" enviando a mensagem "mostre o saldo" para o objeto "conta de fulano". O objeto "conta de fulano" então realiza esta ação, e devolve o valor para o objeto "caixa eletrônico", que vai mostrar na tela, imprimir, ou o que quer que seja.

Na prática, é uma chamada de função, ou melhor, a execução de um método presente em um objeto.

Em um código-fonte fictício, isso seria:

objeto "caixa eletrônico"
{
. ...
. conta_de_fulano.verificar_saldo();
. ...
}

Ou seja, estamos chamando a função (método) "verificar_saldo()", que existe dentro do objeto "conta_de_fulano".

Interface de um objeto: o conjunto de métodos (e seus parâmetros) que existem em um objeto.


HERANÇA

Essa é uma das características fundamentais da orientação a objetos, cujo conhecimento é muito importante para programação utilizando a biblioteca GTK.

A herança é um mecanismo que permite basear uma nova classe na definição de uma classe previamente existente. Ou seja, não precisamos ficar "reinventando a roda" a todo momento. Pega-se algo que já está pronto, e adicionamos outros recursos. Usando herança, sua nova classe herda todos os atributos e métodos presentes na classe previamente existente.

Como exemplo, vamos pensar em duas classes: "homem" e "mulher". Serão classes diferentes, já que apresentarão diferenças de comportamento entre si (por exemplo, os objetos "homem" terão o método "entender a lei do impedimento"... os objetos "mulher" terão o método "reconhecer a cor fúcsia"... :-) ). Só que, além de diferenças, terão também características em comum, como por exemplo, ambos os objetos terão um "nome" e uma "idade", e ambos os objetos saberão "dizer seu nome" e "dizer sua idade". Desta forma, podemos criar uma classe "ser humano", com essas características básicas, e A PARTIR DELA, as duas classes "homem" e "mulher", com as características específicas de cada uma.


Como os objetos "filhos" herdam todas as características do objeto "pai", independentemente do tipo de objeto (ser humano, homem, mulher), a todos eles poderemos mandar a mensagem "dizer nome", e todos responderão corretamente.

Isso nos leva a outra característica bastante interessante da orientação a objetos:


POLIMORFISMO

Significa "muitas formas" (dããã...). Permite que um único nome de classe ou nome de método represente um código diferente, selecionado por um mecanismo automático. Trocando em miúdos: podemos ter códigos diferentes (que fazem coisas diferentes), mas com o mesmo nome de método, em objetos (classes) diferentes. Na hora da execução, o código correto é selecionado pra execução, a partir do tipo do objeto para o qual se está enviando a mensagem.

Vamos tentar um exemplo "prático" pra deixar isso mais claro.

Imagine o seguinte "código-fonte", fictício, numa linguagem "parecida" com C. Neste código, vamos "definir" a classe "Pessoa", e classes filhas dela, "Pessimista", "Otimista", "Timido" e "Extrovertido". Em todas as classes, vamos definir o mesmo método, "fale()", só que com código diferente em cada uma delas.

classe Pessoa
{
. void fale()
. {
. printf("Eu sou uma pessoa comum\n");
. }
}

classe Pessimista, filha de Pessoa
{
. void fale()
. {
. printf("O copo está meio vazio...\n");
. }
}

classe Otimista, filha de Pessoa
{
. void fale()
. {
. printf("O copo está meio cheio!\n");
. }
}

classe Timido, filha de Pessoa
{
. void fale()
. {
. printf("oi...\n");
. }
}

classe Extrovertido, filha de Pessoa
{
. void fale()
. {
. printf("Olá! blabla..! voce sabia que ... bla bla!\n");
. }
}


Como dissemos anteriormente, uma classe é apenas um modelo. Vamos agora criar objetos, a partir das classes definidas acima (obs: qualquer semelhança com a forma de criar variáveis em C, "tipo nome_variável;" não terá sido mera coincidência... :-) )

Pessimista   Paulo;    /* Paulo é um objeto do tipo "Pessimista" */
Otimista Otavio;
Timido Tiago;
Extrovertido Eduardo;

Vamos agora criar um vetor, para armazenar objetos do tipo "Pessoa":
Pessoa Galera[4];

... ou seja ...

Galera[0] vai armazenar um objeto do tipo Pessoa;
Galera[1] vai armazenar um objeto do tipo Pessoa;
Galera[2] idem...
Galera[3] idem...

E para fechar o exemplo, vamos fazer:

Galera[0] = Paulo;
Galera[1] = Otavio;
Galera[2] = Tiago;
Galera[3] = Eduardo;

Oras... dirá você... mas se você criou um vetor para armazenar "Pessoas", como pode armazenar outra coisa que não "Pessoas" ? Por exemplo, Galera[0] foi criado para armazenar um objeto do tipo "Pessoa", mas estamos armazenando um objeto do tipo "Pessimista" (Paulo)!!

Isto é possível porque a classe "Pessimista" é derivada da classe "Pessoa", ou seja, de certa forma, um "Pessimista" É uma "Pessoa"! O mesmo acontece com as variáveis armazenadas nos outros elementos do vetor.

GTK faz uso extensivo deste recurso, por exemplo, sejam duas classes:

  • GtkWidget: objeto gráfico básico, genérico;
  • GtkWindow: uma janela (com título, botões de max, min, fechar...)
A "widget hierarchy" de GtkWindow é a seguinte:


Independente das "classes intermediárias", podemos dizer que um GtkWindow É um GtkWidget.

Completando o exemplo... como fazemos para enviar a mensagem "fale()" para cada objeto, já que são objetos diferentes, e cada um deveria falar uma coisa diferente?

Bem, sem o recurso da herança, teriamos que ter um monte de IF's aninhados, testando o tipo de cada objeto, e aí chamando a função correta: "se for pessimista, fala isso... senão, se for otimista, fala aquilo... senão... ".

Com o recurso da herança, e do polimorfismo, essa tarefa fica MUITO mais fácil:

 for ( CONT=0 ; CONT<4 ; CONT++ )
{
Galera[CONT].fale();
}

Passo-a-passo:

  • inicializa CONT com 0;
  • CONT é menor que 4? SIM! entra no laço.
  • chama o método "fale()" na variável Galera[0]
  • agora é que está o "pulo do gato": Galera[0] é do tipo Pessoa, mas nela está armazenado um objeto do tipo "Pessimista", logo, AUTOMATICAMENTE, vai ser chamado o método "fale()" da classe Pessimista, e não o da classe Pessoa!!!
  • fim do laço: CONT fica igual a 1, ainda menor que 4, entra no laço novamente
  • chama o método "fale()" na variável Galera[1]
  • o objeto armazenado em Galera[1] é "Otavio", do tipo "Otimista", então automaticamente vai ser chamada a função "fale()" da classe "Otimista"
  • fim do laço: CONT fica igual a 2, entra no laço novamente
  • e por aí vai...

Assim, a saída que teremos, será:

O copo está meio vazio
O copo está meio cheio
oi...
Olá !! bla bla bla !! ...


Mais um exemplinho... seja uma função qualquer, que receba como PARÂMETRO um objeto do tipo "Pessoa", e retorne um inteiro com a idade dessa pessoa. A função seria escrita assim (na nossa "pseudo" linguagem C... ):

 int Diz_Idade ( Pessoa FulanoDeTal );
{
...
...
}

A seguinte chamada dessa função é válida:

...
Diz_Idade( Paulo );
...

Deixando claro: a função espera receber um parâmetro do tipo "Pessoa". Estamos passando o objeto "Paulo", que é do tipo "Pessimista". Nenhum problema aí, já que "Pessimista" É uma "Pessoa" (pela definição de herança!). As chamadas abaixo também são válidas:

Diz_Idade( Otavio );
Diz_Idade( Tiago );
Diz_Idade( Eduardo );


Pra fechar esse post-monstro, um exemplinho com GTK, pra ir tomando o gosto...
MAS ATENÇÃO!! O exemplo está INCOMPLETO e SIMPLIFICADO!!

1:  int main( ... )
2: {
3: GtkWidget *window;
4:
5: ...
6:
7: window = gtk_window_new ( ... );
8:
9: gtk_window_set_title ( window, "Hello World!!" );
10:
11: ...
12: }


Vamos ver passo a passo?

linha 3: estamos criando uma variável de nome "window", que é do tipo "ponteiro" para o tipo de dados GtkWidget (como já vimos, o tipo de dado básico de todos os objetos em GTK);

linha 7: utilizamos a função "gtk_window_new" que cria toda a definição necessária para uma janela gráfica na memória do computador. Esta função retorna o endereço inicial desta definição (analogia: comando malloc, cria uma área na memória, e devolve o endereço inicial da área). Armazenamos este endereço na variável "window".

linha 9: utilizamos a função "gtk_window_set_title" para definir o texto que será exibido como título da janela. Passamos como parâmetros, obviamente, a variável que tem o endereço da janela cujo título queremos definir (neste exemplo, variável "window"), e o texto que queremos como título (a string "Hello World!").

Aqui podemos ver a herança em ação: a função gtk_window_set_title espera receber de parâmetro um objeto do tipo GtkWindow. Mas, o que passamos foi a variável "window", que é um GtkWidget. Ora, como já vimos no exemplo anterior, e também na hierarquia da classe GtkWindow, uma GtkWindow É um GtkWidget. Logo podemos passar a variável "window" para esta função sem problemas.

Repetindo: este exemplo está SIMPLIFICADO e INCOMPLETO. Foi só pra ilustrar o que vimos até aqui.

Chega por hoje!

Abraços a todas e a todos!

Carlão

2 comentários:

Flávio disse...

Ótimo post! Apesar de não estar estudando Gtk (ainda) eu realmente me interessei pelo mini resumo sobre OO. Era justamente desses exemplos mais práticos que eu estava precisando pra entender melhor esses conceitos, principalmente agora que estou começando (ou pelo menos tentando) estudar Java. Claro que vou procurar me aprofundar mais nessa parte, mas o conteúdo do post foi um ótimo pontapé inicial ^^.

Carlos José Pereira disse...

Valeu Flávio!
O "curso" andou meio parado, mas estou retomando com a corda toda. Fique ligado para os novos posts.
E desculpe pela demora em moderar seu comentário... me perdi aqui... :-)
Abraços!
Carlão