13. Widgets de árvore e lista

A Gtk.TreeView e seus widgets associados são uma maneira extremamente poderosa de exibir dados. Eles são usados em conjunto com um Gtk.ListStore ou Gtk.TreeStore e fornecem uma maneira de exibir e manipular dados de várias maneiras, incluindo:

  • Atualizações automáticas quando os dados são adicionados, removidos ou editados

  • Suporte a arrastar e soltar

  • Ordenação de dados

  • Incorporação de widgets, como caixas de seleção, barras de progresso, etc.

  • Colunas reordenáveis e redimensionáveis

  • Filtragem de dados

Com o poder e a flexibilidade de um Gtk.TreeView vem a complexidade. Geralmente, é difícil para os desenvolvedores iniciantes serem capazes de utilizá-lo corretamente devido ao número de métodos necessários.

13.1. O modelo

Cada Gtk.TreeView possui um Gtk.TreeModel, o qual contém os dados exibidos pelo TreeView. Cada Gtk.TreeModel pode ser usado por mais de um Gtk.TreeView. Por exemplo, isso permite que os mesmos dados subjacentes sejam exibidos e editados de duas maneiras diferentes ao mesmo tempo. Ou os 2 modos de exibição podem exibir colunas diferentes dos mesmos dados do modelo, da mesma forma que duas consultas SQL (ou “views”) podem mostrar campos diferentes da mesma tabela de banco de dados.

Embora você possa teoricamente implementar seu próprio Model, você normalmente usará as classes de modelo Gtk.ListStore ou Gtk.TreeStore. Gtk.ListStore contém linhas simples de dados, e cada linha não tem filhos, enquanto Gtk.TreeStore contém linhas de dados, e cada linha pode ter linhas filhas.

Ao construir um modelo, você deve especificar os tipos de dados para cada coluna que o modelo contém.

store = Gtk.ListStore(str, str, float)

Isso cria um armazenamento de lista com três colunas, duas colunas de string e uma coluna flutuante.

A adição de dados ao modelo é feita usando Gtk.ListStore.append() ou Gtk.TreeStore.append(), dependendo de qual tipo de modelo foi criado.

Para um Gtk.ListStore:

treeiter = store.append(["The Art of Computer Programming",
                         "Donald E. Knuth", 25.46])

Para um Gtk.TreeStore você deve especificar uma linha existente para anexar a nova linha, usando um Gtk.TreeIter, ou None para o nível superior da árvore:

treeiter = store.append(None, ["The Art of Computer Programming",
                               "Donald E. Knuth", 25.46])

Ambos os métodos retornam uma instância Gtk.TreeIter, que aponta para a localização da linha recém-inserida. Você pode recuperar um Gtk.TreeIter chamando Gtk.TreeModel.get_iter().

Depois que os dados foram inseridos, você pode recuperar ou modificar dados usando o iterador de árvore e o índice de coluna.

print(store[treeiter][2]) # Prints value of third column
store[treeiter][2] = 42.15

Assim como no objeto interno list do Python, você pode usar len() para obter o número de linhas e usar fatias para recuperar ou definir valores.

# Print number of rows
print(len(store))
# Print all but first column
print(store[treeiter][1:])
# Print last column
print(store[treeiter][-1])
# Set last two columns
store[treeiter][1:] = ["Donald Ervin Knuth", 41.99]

Iterar sobre todas as linhas de um modelo de árvore é muito simples também.

for row in store:
    # Print values of all columns
    print(row[:])

Tenha em mente que, se você usar Gtk.TreeStore, o código acima irá apenas iterar sobre as linhas do nível superior, mas não os filhos dos nós. Para iterar sobre todas as linhas, use Gtk.TreeModel.foreach().

def print_row(store, treepath, treeiter):
    print("\t" * (treepath.get_depth() - 1), store[treeiter][:], sep="")

store.foreach(print_row)

Além de acessar valores armazenados em um Gtk.TreeModel com o método list-like mencionado acima, também é possível usar as instâncias Gtk.TreeIter ou Gtk.TreePath. Ambos fazem referência a uma linha específica em um modelo de árvore. Pode-se converter um caminho para um iterador chamando Gtk.TreeModel.get_iter(). Como Gtk.ListStore contém apenas um nível, ou seja, nós não têm nenhum nó filho, um caminho é essencialmente o índice da linha que você deseja acessar.

# Get path pointing to 6th row in list store
path = Gtk.TreePath(5)
treeiter = liststore.get_iter(path)
# Get value at 2nd column
value = liststore.get_value(treeiter, 1)

No caso de Gtk.TreeStore, um caminho é uma lista de índices ou uma string. O formulário de string é uma lista de números separados por dois pontos. Cada número refere-se ao deslocamento nesse nível. Assim, o caminho “0” refere-se ao nó raiz e o caminho “2:4” refere-se ao quinto filho do terceiro nó.

# Get path pointing to 5th child of 3rd row in tree store
path = Gtk.TreePath([2, 4])
treeiter = treestore.get_iter(path)
# Get value at 2nd column
value = treestore.get_value(treeiter, 1)

Instâncias de Gtk.TreePath podem ser acessadas como listas, len(treepath) retorna a profundidade do item treepath está apontando para, e treepath[i] retorna o índice do filho no nível i.

13.2. A visão

Embora existam vários modelos diferentes para escolher, há apenas um widget de visualização para lidar. Funciona com a lista ou com o armazenamento em árvore. Configurar um Gtk.TreeView não é uma tarefa difícil. Ele precisa de um Gtk.TreeModel para saber de onde recuperar seus dados, seja passando-o para o construtor Gtk.TreeView, ou chamando Gtk.TreeView.set_model().

tree = Gtk.TreeView(model=store)

Uma vez que o widget Gtk.TreeView possua um modelo, ele precisará saber como exibir o modelo. Ele faz isso com colunas e renderizadores de célula. headers_visible controla se exibe cabeçalhos de coluna.

Os renderizadores de célula são usados para desenhar os dados no modelo de árvore de uma maneira específica. Existem vários renderizadores de célula que vêm com o GTK+, por exemplo Gtk.CellRendererText, Gtk.CellRendererPixbuf e Gtk.CellRendererToggle. Além disso, é relativamente fácil escrever um renderizador personalizado criando uma subclasse de Gtk.CellRenderer e adicionando propriedades com GObject.Property().

Um Gtk.TreeViewColumn é o objeto que usa Gtk.TreeView para organizar as colunas verticais na visualização em árvore e conter um ou mais renderizadores de células. Cada coluna pode ter um title que ficará visível se o Gtk.TreeView estiver mostrando os cabeçalhos das colunas. O modelo é mapeado para a coluna usando argumentos nomeados com propriedades do renderizador como identificadores e índices das colunas do modelo como argumentos.

renderer = Gtk.CellRendererPixbuf()
column = Gtk.TreeViewColumn(cell_renderer=renderer, icon_name=3)
tree.append_column(column)

Argumentos posicionais podem ser usados para o título da coluna e o renderizador.

renderer = Gtk.CellRendererText()
column = Gtk.TreeViewColumn("Title", renderer, text=0, weight=1)
tree.append_column(column)

Para renderizar mais de uma coluna de modelo em uma coluna de visão, você precisa criar uma instância Gtk.TreeViewColumn e usar Gtk.TreeViewColumn.pack_start() para adicionar as colunas de modelo a ela.

column = Gtk.TreeViewColumn("Title and Author")

title = Gtk.CellRendererText()
author = Gtk.CellRendererText()

column.pack_start(title, True)
column.pack_start(author, True)

column.add_attribute(title, "text", 0)
column.add_attribute(author, "text", 1)

tree.append_column(column)

13.3. A seleção

A maioria dos aplicativos precisará não apenas lidar com a exibição de dados, mas também receber eventos de entrada dos usuários. Para fazer isso, basta obter uma referência a um objeto de seleção e conectar-se ao sinal “changed”.

select = tree.get_selection()
select.connect("changed", on_tree_selection_changed)

Em seguida, para recuperar dados para a linha selecionada:

def on_tree_selection_changed(selection):
    model, treeiter = selection.get_selected()
    if treeiter is not None:
        print("You selected", model[treeiter][0])

Você pode controlar quais seleções são permitidas chamando Gtk.TreeSelection.set_mode(). Gtk.TreeSelection.get_selected() não funciona se o modo de seleção estiver definido como Gtk.SelectionMode.MULTIPLE, use Gtk.TreeSelection.get_selected_rows().

13.4. Classificação

A classificação é um recurso importante para as visualizações em árvore e é suportada pelos modelos de árvore padrão (Gtk.TreeStore e Gtk.ListStore), que implementam a interface Gtk.TreeSortable.

13.4.1. Classificando clicando em colunas

Uma coluna de um Gtk.TreeView pode ser facilmente ordenada com uma chamada para Gtk.TreeViewColumn.set_sort_column_id(). Depois, a coluna pode ser ordenada clicando no cabeçalho.

Primeiro precisamos de um simples Gtk.TreeView e um Gtk.ListStore como modelo.

model = Gtk.ListStore(str)
model.append(["Benjamin"])
model.append(["Charles"])
model.append(["alfred"])
model.append(["Alfred"])
model.append(["David"])
model.append(["charles"])
model.append(["david"])
model.append(["benjamin"])

treeView = Gtk.TreeView(model=model)

cellRenderer = Gtk.CellRendererText()
column = Gtk.TreeViewColumn("Title", renderer, text=0)

O próximo passo é ativar a classificação. Note que o column_id (0 no exemplo) refere-se à coluna do modelo e não à coluna do TreeView.

column.set_sort_column_id(0)

13.4.2. Definindo uma função de classificação personalizada

Também é possível definir uma função de comparação personalizada para alterar o comportamento de classificação. Como exemplo, criaremos uma função de comparação que classifica maiúsculas e minúsculas. No exemplo acima, a lista classificada parecia com:

alfred
Alfred
benjamin
Benjamin
charles
Charles
david
David

A lista classificada com distinção entre maiúsculas e minúsculas será semelhante a:

Alfred
Benjamin
Charles
David
alfred
benjamin
charles
david

Em primeiro lugar, é necessária uma função de comparação. Esta função obtém duas linhas e tem que retornar um inteiro negativo se o primeiro deve vir antes do segundo, zero se eles forem iguais e um inteiro positivo se o segundo vier antes do primeiro.

def compare(model, row1, row2, user_data):
    sort_column, _ = model.get_sort_column_id()
    value1 = model.get_value(row1, sort_column)
    value2 = model.get_value(row2, sort_column)
    if value1 < value2:
        return -1
    elif value1 == value2:
        return 0
    else:
        return 1

Então a função sort deve ser definida por Gtk.TreeSortable.set_sort_func().

model.set_sort_func(0, compare, None)

13.5. Filtragem

Ao contrário da classificação, a filtragem não é tratada pelos dois modelos que vimos anteriormente, mas pela classe Gtk.TreeModelFilter. Esta classe, como Gtk.TreeStore e Gtk.ListStore, é uma Gtk.TreeModel. Ele age como uma camada entre o modelo “real” (a Gtk.TreeStore ou a Gtk.ListStore), ocultando alguns elementos para a view. Na prática, ele fornece o Gtk.TreeView com um subconjunto do modelo subjacente. Instâncias de Gtk.TreeModelFilter podem ser empilhadas umas sobre as outras, para usar múltiplos filtros no mesmo modelo (da mesma forma que você usaria cláusulas “AND” em uma requisição SQL). Eles também podem ser encadeados com instâncias Gtk.TreeModelSort.

Você pode criar uma nova instância de Gtk.TreeModelFilter e dar a ela um modelo para filtrar, mas a maneira mais fácil é gerá-lo diretamente do modelo filtrado, usando o método Gtk.TreeModel.filter_new() método.

filter = model.filter_new()

Da mesma forma que funciona a função de classificação, o Gtk.TreeModelFilter usa de uma função “visibility”, que, dada uma linha do modelo subjacente, retornará um booleano indicando se essa linha deve ser filtrada ou não. É definido por Gtk.TreeModelFilter.set_visible_func():

filter.set_visible_func(filter_func, data=None)

A alternativa para uma função de “visibilidade” é usar uma coluna booleana no modelo para especificar quais linhas filtrar. Escolha qual coluna com Gtk.TreeModelFilter.set_visible_column().

Vejamos um exemplo completo que usa a pilha inteira Gtk.ListStoreGtk.TreeModelFilterGtk.TreeModelFilterGtk.TreeView.

_images/treeview_filter_example.png
 1import gi
 2
 3gi.require_version("Gtk", "3.0")
 4from gi.repository import Gtk
 5
 6# list of tuples for each software, containing the software name, initial release, and main programming languages used
 7software_list = [
 8    ("Firefox", 2002, "C++"),
 9    ("Eclipse", 2004, "Java"),
10    ("Pitivi", 2004, "Python"),
11    ("Netbeans", 1996, "Java"),
12    ("Chrome", 2008, "C++"),
13    ("Filezilla", 2001, "C++"),
14    ("Bazaar", 2005, "Python"),
15    ("Git", 2005, "C"),
16    ("Linux Kernel", 1991, "C"),
17    ("GCC", 1987, "C"),
18    ("Frostwire", 2004, "Java"),
19]
20
21
22class TreeViewFilterWindow(Gtk.Window):
23    def __init__(self):
24        super().__init__(title="Treeview Filter Demo")
25        self.set_border_width(10)
26
27        # Setting up the self.grid in which the elements are to be positioned
28        self.grid = Gtk.Grid()
29        self.grid.set_column_homogeneous(True)
30        self.grid.set_row_homogeneous(True)
31        self.add(self.grid)
32
33        # Creating the ListStore model
34        self.software_liststore = Gtk.ListStore(str, int, str)
35        for software_ref in software_list:
36            self.software_liststore.append(list(software_ref))
37        self.current_filter_language = None
38
39        # Creating the filter, feeding it with the liststore model
40        self.language_filter = self.software_liststore.filter_new()
41        # setting the filter function, note that we're not using the
42        self.language_filter.set_visible_func(self.language_filter_func)
43
44        # creating the treeview, making it use the filter as a model, and adding the columns
45        self.treeview = Gtk.TreeView(model=self.language_filter)
46        for i, column_title in enumerate(
47            ["Software", "Release Year", "Programming Language"]
48        ):
49            renderer = Gtk.CellRendererText()
50            column = Gtk.TreeViewColumn(column_title, renderer, text=i)
51            self.treeview.append_column(column)
52
53        # creating buttons to filter by programming language, and setting up their events
54        self.buttons = list()
55        for prog_language in ["Java", "C", "C++", "Python", "None"]:
56            button = Gtk.Button(label=prog_language)
57            self.buttons.append(button)
58            button.connect("clicked", self.on_selection_button_clicked)
59
60        # setting up the layout, putting the treeview in a scrollwindow, and the buttons in a row
61        self.scrollable_treelist = Gtk.ScrolledWindow()
62        self.scrollable_treelist.set_vexpand(True)
63        self.grid.attach(self.scrollable_treelist, 0, 0, 8, 10)
64        self.grid.attach_next_to(
65            self.buttons[0], self.scrollable_treelist, Gtk.PositionType.BOTTOM, 1, 1
66        )
67        for i, button in enumerate(self.buttons[1:]):
68            self.grid.attach_next_to(
69                button, self.buttons[i], Gtk.PositionType.RIGHT, 1, 1
70            )
71        self.scrollable_treelist.add(self.treeview)
72
73        self.show_all()
74
75    def language_filter_func(self, model, iter, data):
76        """Tests if the language in the row is the one in the filter"""
77        if (
78            self.current_filter_language is None
79            or self.current_filter_language == "None"
80        ):
81            return True
82        else:
83            return model[iter][2] == self.current_filter_language
84
85    def on_selection_button_clicked(self, widget):
86        """Called on any of the button clicks"""
87        # we set the current language filter to the button's label
88        self.current_filter_language = widget.get_label()
89        print("%s language selected!" % self.current_filter_language)
90        # we update the filter, which updates in turn the view
91        self.language_filter.refilter()
92
93
94win = TreeViewFilterWindow()
95win.connect("destroy", Gtk.main_quit)
96win.show_all()
97Gtk.main()