BotaoDeFecharEmAbasDeGtkNotebook

Botão de Fechar em abas de gtk.Notebook

O widget gtk.Notebook é útil para organizar telas que contenham muitos dados, ou outros widgets que tomam muito espaço útil da tela. Para fazer, digamos, telas de configuração, é bastante simples. O código abaixo, por exemplo, cria um stub de tela de configuração com duas abas.

   1 #!/usr/bin/env python
   2 # -*- coding: utf-8 -*-
   3 import gtk
   4 
   5 window = gtk.Window(gtk.WINDOW_TOPLEVEL)
   6 # Creating notebook
   7 notebook = gtk.Notebook()
   8 
   9 # Creating pages
  10 # First tab label
  11 this_label = gtk.Label("Isso")
  12 # First page content
  13 this_content = gtk.Label('Configura\nIsso')
  14 that_label = gtk.Label("Aquilo")
  15 # First page content
  16 that_content = gtk.Label('Configura\nAquilo')
  17 
  18 # Add to notebook
  19 notebook.append_page(this_content, this_label)
  20 notebook.append_page(that_content, that_label)
  21 
  22 # Finishing
  23 window.add(notebook)
  24 window.show_all()
  25 gtk.main()

O resultado é...

tela-notebook1.png

Às vezes, porém, queremos criar abas dinamicamente, e queremos poder fechá-las após criá-las. Uma maneira de fechar bastante elegante é adicionar um botão de fechar em cada aba. gtk.Notebook não faz isso automaticamente, mas a tarefa é simples. Vamos executá-la passo a passo aqui.

Acrescentando o botão à aba

No código acima, o rótulo da aba é o segundo argumento do método gtk.Notebook.append_page(). Para adicionar um botão ao rótulo, poderíamos trocar o objeto gtk.Label por um gtk.HBox que contivesse tanto o rótulo quanto o botão, como na função abaixo:

   1 def get_tab_label(label_text):
   2     box = gtk.HBox()
   3     # Creating button
   4     button = gtk.Button()
   5     # Retrieving "close" icon
   6     image = gtk.image_new_from_stock(gtk.STOCK_CLOSE, gtk.ICON_SIZE_MENU)
   7     button.add(image)
   8     # Creating label
   9     label = gtk.Label(label_text)
  10     # Putting in the gtk.HBox
  11     box.pack_start(label)
  12     box.pack_start(button)
  13     box.show_all()
  14 
  15     return box

Substituímos os gtk.Labels utilizados como rótulos de abas por uma chamada de get_tab_label:

   1 #!/usr/bin/env python
   2 # -*- coding: utf-8 -*-
   3 import gtk
   4 def get_tab_label(label_text):
   5     box = gtk.HBox()
   6     # ...Doing stuff... see declaration above...
   7     box.show_all()
   8 
   9     return box
  10 
  11 window = gtk.Window(gtk.WINDOW_TOPLEVEL)
  12 notebook = gtk.Notebook()
  13 
  14 # Note the difference: we are not using gtk.Label anymore
  15 this_label = get_tab_label("Isso")
  16 this_content = gtk.Label('Configura\nIsso')
  17 that_label = get_tab_label("Aquilo")
  18 that_content = gtk.Label('Configura\nAquilo')
  19 
  20 # Add to notebook
  21 notebook.append_page(this_content, this_label)
  22 notebook.append_page(that_content, that_label)
  23 
  24 # Finishing
  25 window.add(notebook)
  26 window.show_all()
  27 gtk.main()

E o resultado é

tela-notebook2.png

A classe TabLabel

Nós escrevemos uma função para gerar nosso rótulo, mas vamos aperfeiçoar nosso código. Em vez de uma função, faremos uma classe descendente de gtk.HBox, que será nosso rótulo. Chamaremos essa classe de TabLabel, e sua função de inicialização é, basicamente, o mesmo que a função get_tab_label() acima. Para efeitos de teste, vamos salvá-la no arquivo tablabel.py.

   1 import gtk
   2 
   3 class TabLabel(gtk.HBox):
   4 
   5     def __init__(self, label_text):
   6         gtk.HBox.__init__(self)
   7 
   8         self.button = gtk.Button()
   9         image = gtk.image_new_from_stock(gtk.STOCK_CLOSE,
  10                 gtk.ICON_SIZE_MENU)
  11         self.button.add(image)
  12         self.label = gtk.Label(label_text)
  13 
  14         self.pack_start(self.label)
  15         self.pack_start(self.button)
  16         # Don't forget: you should show all
  17         self.show_all()

Nosso programa ficará assim:

   1 #!/usr/bin/env python
   2 # -*- coding: utf-8 -*-
   3 import gtk
   4 import tablabel
   5 
   6 window = gtk.Window(gtk.WINDOW_TOPLEVEL)
   7 notebook = gtk.Notebook()
   8 
   9 this_label = tablabel.TabLabel("Isso")
  10 this_content = gtk.Label('Configura\nIsso')
  11 that_label = tablabel.TabLabel("Aquilo")
  12 that_content = gtk.Label('Configura\nAquilo')
  13 
  14 notebook.append_page(this_content, this_label)
  15 notebook.append_page(that_content, that_label)
  16 
  17 window.add(notebook)
  18 window.show_all()
  19 gtk.main()

Usarmos uma classe, e não mais só uma função, será muito útil quando formos tornar os botões funcionais.

Removendo páginas

Para remover uma página de um gtk.Notebook, usamos o método gtk.Notebook.remove_page(). Esse método espera como argumento um número inteiro que indicará que aba será removida: 0 para a primeira aba, 1 para a segunda etc., sendo que -1 remove a última, seja qual for.

Temos de usar esse método para remover as abas quando clicarmos nos botões, mas precisamos saber qual é o número da aba. Os métodos de inserção de página (gtk.Notebook.append_page(), prepend_page(), insert_page()) retornam o índice da página criada, mas esse índice pode mudar, por exemplo, se alguém fechar uma página anterior.

Uma maneira de recuperar o índice de uma página é através do método gtk.Notebook.page_num(). Esse método recebe como argumento um widget. Se alguma página possuir esse widget, o índice dessa aba é retornado. Desse modo, na classe TabLabel, podemos criar um método para ser conectado ao evento clicked do botão da aba que, usando gtk.Notebook.remove_page(), encontre a página correspondente e a remova:

   1 def on_button_clicked(self, button):
   2     position = self.notebook.page_num(self.widget)
   3     self.notebook.remove_page(position)

Note que estamos usando campos que não criamos: TabLabel.notebook e TabLabel.widget. A solução é atualizar o método TabLabel.init(), para que receba também, como argumento, o gtk.Notebook cuja aba será usada e o widget que a página armazenará:

   1 import gtk
   2 
   3 class TabLabel(gtk.HBox):
   4 
   5     def __init__(self, label_text, notebook, widget):
   6         gtk.HBox.__init__(self)
   7         # Values
   8         self.notebook = notebook
   9         self.widget = widget
  10         # Widgets
  11         self.button = gtk.Button()
  12         image = gtk.image_new_from_stock(gtk.STOCK_CLOSE,
  13                 gtk.ICON_SIZE_MENU)
  14         self.button.add(image)
  15         self.label = gtk.Label(label_text)
  16         # Packing
  17         self.pack_start(self.label)
  18         self.pack_start(self.button)
  19         # Connecting 'on_button_clicked':
  20         self.button.connect('clicked', self.on_button_clicked)  
  21         # Don't forget: you should show all
  22         self.show_all()
  23 
  24     def on_button_clicked(self, button):
  25         position = self.notebook.page_num(self.widget)
  26         self.notebook.remove_page(position)

Agora, atualizamos o programa...

   1 #!/usr/bin/env python
   2 # -*- coding: utf-8 -*-
   3 import gtk
   4 import tablabel
   5 
   6 window = gtk.Window(gtk.WINDOW_TOPLEVEL)
   7 notebook = gtk.Notebook()
   8 
   9 # Need to create content before label
  10 this_content = gtk.Label('Configura\nIsso')
  11 this_label = tablabel.TabLabel("Isso", notebook, this_content)
  12 that_content = gtk.Label('Configura\nAquilo')
  13 that_label = tablabel.TabLabel("Aquilo", notebook, that_content)
  14 
  15 notebook.append_page(this_content, this_label)
  16 notebook.append_page(that_content, that_label)
  17 
  18 window.add(notebook)
  19 window.show_all()
  20 gtk.main()

...e voila! Nossas abas fecham. Teste executar o programa agora, e clique em um dos botões de fechar para ver as abas se fechando.

A classe EnhancedNotebook

Para simplificar o trabalho de lidar com as novas abas, que tal criar uma classe herdando de gtk.Notebook? Vamos declarar, nessa classe, o método insert_page_with_close_button() análogo ao método gtk.Notebook.insert_page().

   1 import gtk
   2 import tablabel
   3 
   4 class EnhancedNotebook(gtk.Notebook):
   5 
   6     def __init__(self):
   7         gtk.Notebook.__init__(self)
   8 
   9     def insert_page_with_close_button(self, child, tab_text, position=-1):
  10         # Note that we give the text in a string, not a widget
  11         label = tablabel.TabLabel(tab_text, self, child)
  12         self.insert_page(child, label, position)

Salvemos essa classe em enhanced.py. Agora, podemos ter uma nova versão do programa:

   1 import gtk
   2 from enhanced import EnhancedNotebook
   3 
   4 window = gtk.Window(gtk.WINDOW_TOPLEVEL)
   5 notebook = EnhancedNotebook()
   6 
   7 this_content = gtk.Label('Configura\nIsso')
   8 notebook.insert_page_with_close_button(this_content, "Isso")
   9 that_content = gtk.Label('Configura\nAquilo')
  10 notebook.insert_page_with_close_button(that_content, "Aquilo")
  11 
  12 window.add(notebook)
  13 window.show_all()
  14 gtk.main()

Bem mais elegante, não? Podem-se fazer outros métodos, também, correspondentes aos gtk.Notebook.append_page(), gtk.Notebook.prepend_page() etc.

Melhorias estéticas

Nossa nova classe está funcionando a contento. Entretanto, quem quer que já tenha utilizado alguma aplicação em GTK+ com abas notará que os botões são bem feinhos. Veja, por exemplo, a diferença entre nossas abas e as abas do gedit:

diferenca-botoes.png

A primeira coisa que podemos fazer para melhorar a aparência é tirar a borda (ou relief) dos botões. Para isso, basta utilizar o método gtk.Button.set_relief() do botão da aba:

   1 self.button = gtk.Button()
   2 image = gtk.image_new_from_stock(gtk.STOCK_CLOSE, gtk.ICON_SIZE_MENU)
   3 self.button.add(image)
   4 self.button.set_relief(gtk.RELIEF_NONE)

Considerando o quanto a construção do botão de fechar está ficando complicada, vamos declarar um método tablabel.TabLabel.get_close_button() só para criá-lo:

   1 def __init__(self, label_text, notebook, widgeet):
   2     # ...
   3     self.button = self.get_close_button()
   4     # ...
   5 
   6 def get_close_button(self):
   7     button = gtk.Button()
   8     image = gtk.image_new_from_stock(gtk.STOCK_CLOSE,
   9                 gtk.ICON_SIZE_MENU)
  10     button.add(image)
  11     button.set_relief(gtk.RELIEF_NONE)
  12     button.connect('clicked', self.on_button_clicked)
  13     
  14     return button

A melhoria é evidente...

diferenca-botoes2.png

...mas os botões ainda continuam grandes demais. Um bom tamanho seria algo como uns oito pixels maior que o ícone está dentro deles. Entretanto, nós não sabemos, nem temos como recuperar as dimensões de um gtk.Image que contenha um ícone de stock. O que sabemos sobre o tal ícone é que o tamanho dele é definido pela constante gtk.ICON_SIZE_MENU, que não é o tamanho em pixels.

Para recuperar as dimensões de um ícone a partir das constatnes gtk.ICON_SIZE_*, nós utilizamos a função gtk.icon_size_lookup(). Essa função espera como argumento uma constante gtk.ICON_SIZE_* e retorna uma tupla contendo a largura e a altura do ícone. Uma vez que tenhamos obtido esses valores, basta requerir que o botão os adote, através do método gtk.Button.set_size_request(). Assim, podemos fazer um novo método tablabel.TabLabel.get_close_button():

   1 def get_close_button(self):
   2     button = gtk.Button()
   3     # Add icon and remove "visible borders"
   4     image = gtk.image_new_from_stock(gtk.STOCK_CLOSE, gtk.ICON_SIZE_MENU)
   5     button.add(image)
   6     button.set_relief(gtk.RELIEF_NONE)
   7     # Change size
   8     width, height = gtk.icon_size_lookup(image.get_pixel_size())
   9     button.set_size_request(width+4, height+4)
  10     # Connect callback
  11     button.connect('clicked', self.on_button_clicked)
  12 
  13     return button

As nossas abas, agora, estão assim:

diferenca-botoes3.png

É bem verdade que as abas agora são maiores, mas já estão mais bonitas. Infelizmente, reduzir os botões ainda mais acaba truncando os ícones. Se alguém souber como reduzir um pouco mais o botão sem truncar os ícones, me avise :)

Acrescentando setas de rolagem e lidando com alinhamento de rótulos

Se você está procurando por páginas de gtk.Notebook que possuam botões de fechar, é provável que você vá adicionar e remover páginas arbitrariamente ao notebook. Se adicionarmos muitas páginas, as abas podem forçar a janela a crescer demais.

grande-demais-menor.png

A solução é habilitar as barras de rolagem de abas no notebook. Isso é bastante simples: basta usar o método gtk.Notebook.set_scrollable(). Assim, conseguimos o novo método enhanced.EnhancedNotebook.init() abaixo:

   1 class EnhancedNotebook(gtk.Notebook):
   2 
   3     def __init__(self):
   4         gtk.Notebook.__init__(self)
   5         self.set_scrollable(True)

Com essa pequena linha, conseguimos uma melhora considerável:

nao-grande.png

Últimos retoques

Nosso EnhancedNotebook está quase pronto. Para terminar, vamos chamar a atenção para um detalhe chato que pode passar despercebido.

Veja o screenshot abaixo. Nele, fizemos uma combinação de abas e tamanho da janela específica para deixar claro o ponto: a posição do rótulo e do botão de fechar podem variar muito de acordo com o tamanho da aba. Seria melhor, porém, que o rótulo ficasse sempre numa posição à esquerda, e o botão sempre numa posição à direita.

abas-desbalanceadas.png

Pois bem, os deslocamentos ocorrem porque os widgets por padrão tentam dividir o espaço todo que lhes for disponível, expandindo para ocupá-lo ao máximo. Nos widgets gtk.Label, o texto por padrão tenta ficar no centro do rótulo. Nos widgets gtk.Button, o objeto dentro do botão (no caso, uma imagem) faz o mesmo.

Para cada widget, há uma solução. No caso do rótulo, basta usar o método gtk.Label.set_alignment(), que define o alinhamento do texto.

Esse método espera dois argumentos. O primeiro argumento definirá o alinhamento horizontal do texto do rótulo: se o valor desse primeiro argumento for 0, o texto ficará alinhado à esquerda; se o valor for 1, ficará alinhado à direita; se for 0,5 (o valor padrão para os gtk.Label), ficará centralizado1. O segundo argumento define o alinhamento vertical: se seu valor for zero, o texto ficará junto ao topo do gtk.Label; se for 1, ficará colado à base; se for 0,5, o texto ficará exatamente no meio do widget, verticalmente. Nosso interesse é que o texto fique alinhado à esquerda horizontalmente, mas no meio, verticalmente. Faremos isso, então, no método tablabel.TabLabel.init():

   1 class TabLabel(gtk.HBox):
   2 
   3     def __init__(self, label_text, notebook, widget):
   4         gtk.HBox.__init__(self)
   5 
   6         self.notebook = notebook
   7         self.widget = widget
   8         self.button = self.get_close_button()
   9         self.label = gtk.Label(label_text)
  10         self.label.set_alignment(0, 0.5)
  11 
  12         self.pack_start(self.label)
  13         self.pack_start(self.button)
  14         self.show_all()

Quanto ao botão, nos interessa simplesmente que ele não mude de tamanho. O método gtk.Button.set_size_request() não nos serve nesse caso, mas podemos empacotá-lo de modo que ele não se expanda. Podemos aproveitar e já empacotá-lo ao final do gtk.HBox, de modo que fique sempre à direita. Para isso, basta substituir a linha

   1 self.pack_start(self.button)

do tablabel.TabLabel.init() por

   1 self.pack_end(self.button, expand=False)

Enfim, para deixar as abas realmente elegantes, ainda sugerimos que se adicionte um pequeno padding no empacotamento (algo como três pixels).

Conclusão

Você veio aqui atrás de como adicionar botões de fechar em abas de gtk.Notebook, e encontrou um texto gigante falando de detalhes obscuros do processo de criar esses botões... Pois bem, se você leu até aqui, eis o seu prêmio! O código abaixo oferece um notebook cujas páginas podem ser fechadas através do botãozinho de fechar em suas abas. Pode copiar, colar em um arquivo e usar. É o resultado final desse artigo.

   1 #!/usr/bin/env python
   2 # -*- coding: utf-8 -*-
   3 import gtk
   4 
   5 class TabLabel(gtk.HBox):
   6     """ 
   7     Widget used as label for gtk.Notebook page tabs with close buttons. 
   8     """
   9 
  10     def __init__(self, label_text, notebook, widget):
  11         """ 
  12         Creates all widgets needed for labeling and closing the tab.
  13 
  14         label_text
  15             A string containg the label text.
  16 
  17         notebook
  18             The gtk.Notebook instance which will contain the "closable"
  19             pages.
  20 
  21         widget
  22             The widget to be added to the gtk.Notebook page. 
  23         """
  24         gtk.HBox.__init__(self)
  25         # Received
  26         self.notebook = notebook
  27         self.widget = widget
  28         # Creating
  29         self.button = self.get_close_button()
  30         self.label = gtk.Label(label_text)
  31         self.label.set_alignment(0, 0.5)
  32         # Packing
  33         self.pack_start(self.label, padding=3)
  34         self.pack_end(self.button, expand=False, padding=3)
  35         # Showing
  36         self.show_all()
  37 
  38     def get_close_button(self):
  39         """ 
  40         Returns a button configured to be used as a close button.
  41         """
  42         button = gtk.Button()
  43 
  44         # Adding image, removing relief
  45         image = gtk.image_new_from_stock(gtk.STOCK_CLOSE,
  46                 gtk.ICON_SIZE_MENU)
  47         button.add(image)
  48         button.set_relief(gtk.RELIEF_NONE)
  49         # Changing size
  50         width, height = gtk.icon_size_lookup(
  51                 gtk.ICON_SIZE_MENU)
  52         button.set_size_request(width+8, height+8)
  53 
  54         # Connecting callback
  55         button.connect('clicked', self.on_button_clicked)
  56 
  57         return button
  58 
  59     def on_button_clicked(self, button):
  60         """ 
  61         Callback method to be connected to the 'clicked' close 
  62         button signal.
  63 
  64         button
  65             Clicked button. Argument required by signal.
  66         """
  67         position = self.notebook.page_num(self.widget)
  68         self.notebook.remove_page(position)
  69 
  70 class EnhancedNotebook(gtk.Notebook):
  71     """ 
  72     An enhanced gtk.Notebook class, which provides a method for 
  73     adding pages
  74     which can be closed using a close button in its label.
  75     """
  76 
  77     def __init__(self):
  78         gtk.Notebook.__init__(self)
  79         self.set_scrollable(True)
  80 
  81     def insert_page_with_close_button(self, child, tab_text, position=-1):
  82         """ 
  83         Adds a new closable page.
  84 
  85         child
  86             The widget to be added in the page.
  87 
  88         tab_text
  89             A string containing the text to be shown in the tab label.
  90 
  91         position
  92             The position where the page will be inserted: first, 
  93             second etc. By 
  94             default, the tab is inserted at the end of the notebook. 
  95             You can
  96             explicit it giving -1 as this argument.
  97         """
  98         # Note that we give the text in a string, not a widget
  99         label = TabLabel(tab_text, self, child)
 100         self.insert_page(child, label, position)

E, para nos despedirmos, um último screenshot, mostrando o resultado final:

parfait.png

  1. Em verdade, pode-se escolher qualquer valor entre zero e um, que o texto será alinhado nessa posição, relativamente ao tamanho do widget. Sugerimos que estude melhor esse método, para que pegue a exata idéia de seu funcionamento. (1)

BotaoDeFecharEmAbasDeGtkNotebook (editada pela última vez em 2008-09-26 14:06:02 por localhost)