Interfaces Gráficas: Interações Complexas 21 21.5 O contexto gráfico Os componentes de uma interface gráfica são desenhados na tela do computador a partir de primitivas que permitem a construção de linhas, ovais e polı́gonos, o preenchimento de áreas, a escrita de caracteres, além da definição da cor e do tipo de fonte utilizados. Estas primitivas gráficas fazem parte do contexto gráfico dos componentes e são implementadas através de objetos da classe Graphics. Um componente é desenhado quando aparece pela primeira vez na tela e, após isso, sempre que for redimensionado ou em resposta a qualquer operação que torne visı́vel partes que antes estavam ocultas. 21.5.1 A classe Graphics Quando um componente precisa ser desenhado (ou redesenhado, caso já esteja vı́sivel) o ambiente de execução usa um objeto da classe Graphics, adaptando-o para refletir as propriedades do componente, e executa as operações de desenho através deste objeto. Alguns dos métodos da classe Graphics são: abstract void drawLine(int x1, int y1, int x2, int y2). Desenha, usando a cor corrente, uma linha que vai da coordenada (x1,y1) à coordenada (x2,y2). void drawRect(int x, int y, int comp, int alt). Desenha, usando a cor corrente, o contorno de um retângulo com vértice superior esquerdo na coordenada (x,y), largura comp e altura alt. 2 Interfaces Gráficas abstract void fillRect(int x, int y, int comp, int alt). Preenche, usando a cor corrente, a área de um retângulo com vértice superior esquerdo na coordenada (x,y), largura comp e altura alt. abstract void drawOval(int x, int y, int comp, int alt). Desenha, usando a cor corrente, o contorno de uma oval inscrita no retângulo que tem vértice superior esquerdo na coordenada (x,y), largura comp e altura alt. abstract void drawString(String str, int x, int y). Desenha a cadeia de caracteres str, usando a fonte corrente, a partir da coordenada (x,y). abstract Color getColor(). Obtém a cor corrente. abstract void setColor(Color c). Define c como a cor corrente. abstract Font getFont(). Obtém a fonte corrente. abstract void setFont(Font f). Define f como a fonte corrente. Cada objeto da classe Graphics possui uma área de desenho que inicialmente é determinada pelo componente ao qual o contexto gráfico está associado. Se um componente possui uma largura de 100 e uma altura de 60 pixels, então o contexto gráfico que o sistema utiliza para desenhá-lo é adaptado para ter sua área de desenho com essas dimensões. A área de desenho de um contexto gráfico pode ser obtida e modificada com os métodos: abstract Rectangle getClipBounds(). Obtém como um objeto da classe Rectangle a área de desenho usada pelo contexto gráfico. Todo objeto da classe Rectangle possui os atributos x e y, que definem as coordenadas do seu vértice superior esquerdo, e width e height, que definem sua largura e altura. abstract void setClip(int x, int y, int comp, int alt). Define a área de desenho como sendo o retângulo com vétice superior esquerdo na coordenada (x,y), largura comp e altura alt. A classe Graphics possui outros métodos que permitem a realização de outras operações de desenho e o controle do contexto gráfico no qual estas operações ocorrem. 21.5.2 Desenhando componentes Todo componente possui os métodos paint e getGraphics, herdados de Component: void paint(Graphics g). Método usado para desenhar o componente através das primitivas gráficas implementadas pelo objeto g. 21.5 O contexto gráfico 3 Figura 21.1. Componente desenhado no Exemplo 21.1 Graphics getGraphics(). Cria um (objeto que implementa o) contexto gráfico para este componente. Quando um componente precisa ser desenhado, o ambiente de execução usa um contexto gráfico adaptado para refletir as caracterı́sticas do componente (cor, fonte, área de desenho) e invoca o método paint do componente fornecendo este contexto gráfico como argumento. Se o componente é um contêiner e possui outros componentes como membros, então o método paint de cada membro é invocado em cascata. 21.5.3 Desenhando novos componentes Podemos desenhar nossos próprios componentes estendendo um componente já existente e sobrescrevendo o seu método paint. A classe Canvas serve como base para o desenho de novos componentes porque apenas define uma região vazia da tela, sem nenhuma decoração. Exemplo 21.1. O programa a seguir cria um componente no qual são desenhados uma oval amarela e um segmento de reta azul. A classe Tela que especifica o novo componente é declarada como subclasse de Canvas nas linhas 2 a 13. O construtor dessa classe (linhas 3 a 5) apenas define a dimensão inicial de um objeto Tela. O método paint nas linhas 6 a 12 sobrescreve o método paint (que para a classe Canvas não possui nenhuma funcionalidade). 1 2 3 4 5 6 7 8 9 10 11 12 import java.awt.*; class Tela extends Canvas { public Tela () { setSize(100,100); } public void paint(Graphics g) { Dimension r = getSize(); g.setColor(Color.yellow); g.fillOval(r.width/4, r.height/4, r.width/2, r.height/2); g.setColor(Color.blue); g.drawLine(r.width/4, r.height/4, 3*r.width/4, 3*r.height/4); } 4 13 14 15 16 17 18 19 20 21 Interfaces Gráficas } public class C21ExemploCG1 { public static void main(String[] args) { Frame janela = new Frame(); janela.add(new Tela()); janela.pack(); janela.setVisible(true); } } Um objeto da classe Tela é criado e adicionado à janela do programa, na linha 17. Quando a janela torna-se visı́vel (linha 19) o seu método paint é invocado pelo ambiente de execução e, como trata-se de um contêiner, também o método paint de todos os seus membros. Ao ser executado, o método paint de um objeto Tela obtém a dimensão do objeto que o executa (linha 7), define o amarelo como a cor corrente (linha 8), preenche uma oval ocupando metade da área do componente (linha 9), define o azul como a cor corrente (linha 10) e desenha uma linha cortando a oval já desenhada (linha 11). A janela resultante é mostrada na Figura 21.1. 21.5.4 Realizando o desenho de componentes Não é aconselhável que um programa invoque o método paint diretamente, já que não se pode controlar as demais chamadas que podem ser feitas a este método. Durante a execução de um programa o método paint pode ser invocado várias vezes, sob o controle do gerente de janelas, que determina os componentes que precisam ser desenhados. Se um programa precisa executar o método paint de um determinado componente, o ideal é que ele registre essa necessidade junto ao gerente de janelas e deixe que o ambiente de execução determine o momento apropriado para a execução do método. A necessidade de desenho deve ser registrada através do método repaint, que envia a solicitação ao gerente de janelas que, por sua vez, invoca o método update. Quando executado, o método update limpa o fundo do componente e chama o método paint. A chamada ao método repaint é assı́ncrona; isto é, o programa que a faz prossegue o processamento, sem esperar pelo término de sua execução. É comum que uma série de chamadas ao método repaint sejam transformadas em uma única chamada ao método update do componente. Exemplo 21.2. O programa abaixo modifica o programa do Exemplo 21.1 para que as cores da oval e da linha mudem caso o ponteiro do mouse entre na área do componente. Agora o método paint (linhas 20 a 26) usa os atributos corOval e corLinha, declarados na linha 4, para definir as cores com que serão desenhadas a oval e a linha. Para responder aos eventos de entrada e saı́da do mouse, um monitor de eventos da classe MouseEvent é criado através da classe MouseAdapter e registrado junto ao componente na linha 7. 21.5 O contexto gráfico 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 5 import java.awt.*; import java.awt.event.*; class Tela extends Canvas { Color corOval = Color.yellow, corLinha = Color.blue; public Tela () { setSize(100,100); addMouseListener(new MouseAdapter() { public void mouseEntered(MouseEvent e) { corOval = Color.yellow; corLinha = Color.blue; Tela t = (Tela)e.getSource(); t.repaint(); } public void mouseExited(MouseEvent e) { corOval = Color.red; corLinha = Color.white; Tela t = (Tela)e.getSource(); t.repaint(); } }); } public void paint(Graphics g) { Dimension r = getSize(); g.setColor(corOval); g.fillOval(r.width/4, r.height/4, r.width/2, r.height/2); g.setColor(corLinha); g.drawLine(r.width/4, r.height/4, 3*r.width/4, 3*r.height/4); } } public class C21ExemploCG2 { public static void main(String[] args) { Frame janela = new Frame(); janela.add(new Tela()); janela.pack(); janela.setVisible(true); } } Sempre que o cursor do mouse entrar na área do componente o método mouseEntered (linhas 8 a 12) será executado. O componente que gerou o evento é determinado na linha 10 e, através dele, o método repaint é invocado na linha 11. A próxima execução do método paint usará então as novas cores determinadas na linha 9. Um comportamento semelhante ocorre, pela execução do método mouseExited, quando o cursor do mouse sair da área do componente. Este programa também ilustra, nas linhas 7 a 18, a criação de um objeto de uma classe cuja especificação é usada uma única vez (para criá-lo). 6 Interfaces Gráficas 21.5.5 Sobrescrevendo outros componentes Embora o uso mais freqüente das primitivas gráficas seja para a criação de componentes desenhados como uma extensão da classe Canvas, podemos adicionar elementos gráficos a componentes que já possuam desenhos, bastando para isso estender a classe do componente e sobrescrever o seu método paint. Exemplo 21.3. O programa a seguir estende a classe Button, adicionando duas linhas vermelhas (no topo e na base) ao desenho de um botão convencional. Os objetos da classe Botao são criados herdando a funcionalidade e as caracterı́sticas da classe Button. O método paint da nova classe é definido nas linhas 7 a 12. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 import java.awt.*; import java.awt.event.*; class Botao extends Button { Botao (String rotulo) { super(rotulo); } public void paint(Graphics g) { Dimension t = getSize(); g.setColor(Color.red); g.fillRect(1,1, t.width - 2, 2); g.fillRect(1, t.height - 3, t.width - 2, 2); } } public class C21ExemploCG3 { public static void main(String[] args) { Frame janela = new Frame(); janela.add(new Botao("Novo botao")); janela.pack(); janela.setVisible(true); } } Este programa ilustra o cuidado que se deve ter ao interferir no comportamento especificado para as classes já existentes. Quando a janela principal é tornada visı́vel o novo botão aparece com as linhas vermelhas nele desenhadas. As novas linhas vermelhas, entretanto, desaparecem caso o botão seja clicado. Isto ocorre porque quando o botão é clicado ou liberado os métodos que respondem a estes eventos invocam o método paint da classe Button. Para que as novas linhas vermelhas continuem aparecendo deve-se registrar um tratador de eventos do mouse para os componentes da classe Botao e através dele chamar o método repaint, de modo semelhante ao realizado no Exemplo 21.2. 21.5 O contexto gráfico 21.5.6 7 Propagando as ações de desenho Os componentes de uma interface gráfica são chamados de componentes pesados se possuem acesso direto aos recursos controlados pelo gerente de janelas, ou de componentes leves se têm acesso a esses recursos através de outros componentes. Os contêineres, por exemplo, são componentes pesados. Quando um componente pesado executa seu método paint o ambiente de execução invoca o método paint para todos os membros deste componente. Assim, quando se estende um contêiner e sobrescreve-se o seu método paint, deve-se codificar uma chamada ao método paint da classe ancestral: super.paint(g) Essa chamada ao método ancestral fará com que os membros do objeto que estende o contêiner também sejam desenhados. 21.5.7 O contexto gráfico com componentes swing O pacote swing possui componentes cuja estrutura é mais complexa que a dos componentes do pacote awt, permitindo o desenho de bordas e a especificação de transparência, por exemplo. Esta complexidade adicional faz com que o desenho destes componentes se torne mais complexo. Uma das diferenças é que o método paint de um componente swing realiza o desenho do componente em etapas, invocando os seguintes métodos: protected void paintComponent(Graphics g). Usado para desenhar o componente. protected void paintBorder(Graphics g). Usado para desenhar as bordas do componente. protected void paintChildren(Graphics g). Usado para desenhar os componentes que dependem do componente sendo desenhado. Para sobrescrever as operações de desenho de uma classe do pacote swing devemos sobrescrever o método paintComponent (e não o método paint). Normalmente as ações relacionadas ao desenho das bordas e à propagação do desenho para outros componentes não precisam ser modificadas. A solicitação de desenho pelos programas aplicativos deve continuar sendo feita através do método repaint1 . Os componentes swing também permitem o controle e a uniformização de sua aparência através de objetos controladores da interface do usuário (UI delegates). Esses objetos realizam muito do desenho do componente e o ambiente de execução cuida para que seus métodos de desenho sejam chamados sempre que necessário. 1 Os componentes swing podem realizar solicitações de desenho sı́ncronas, através do método paintImmediately, que não são discutidas aqui. 8 Interfaces Gráficas Quando se estende componentes swing que implementam o controle da aparência através de objetos controladores (como é o caso de JPanel), o método paintComponent da classe ancestral deve ser invocado: super.paintComponent(g) Esta chamada garante que a aparência da interface continuará conforme definida no ambiente de execução.