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.

For a Gtk.ListStore:

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

For a Gtk.TreeStore you must specify an existing row to append the new row to, using a Gtk.TreeIter, or None for the top level of the tree:

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[:])

Keep in mind, that if you use Gtk.TreeStore, the above code will only iterate over the rows of the top level, but not the children of the nodes. To iterate over all rows, 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)

Once the Gtk.TreeView widget has a model, it will need to know how to display the model. It does this with columns and cell renderers. headers_visible controls whether it displays column headers.

Cell renderers are used to draw the data in the tree model in a specific way. There are a number of cell renderers that come with GTK+, for instance Gtk.CellRendererText, Gtk.CellRendererPixbuf and Gtk.CellRendererToggle. In addition, it is relatively easy to write a custom renderer yourself by subclassing a Gtk.CellRenderer, and adding properties with GObject.Property().

A Gtk.TreeViewColumn is the object that Gtk.TreeView uses to organize the vertical columns in the tree view and hold one or more cell renderers. Each column may have a title that will be visible if the Gtk.TreeView is showing column headers. The model is mapped to the column by using keyword arguments with properties of the renderer as the identifiers and indexes of the model columns as the arguments.

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

Positional arguments can be used for the column title and renderer.

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

First of all a comparison function is needed. This function gets two rows and has to return a negative integer if the first one should come before the second one, zero if they are equal and a positive integer if the second one should come before the first one.

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()

In the same way the sorting function works, the Gtk.TreeModelFilter uses a “visibility” function, which, given a row from the underlying model, will return a boolean indicating if this row should be filtered out or not. It’s set by Gtk.TreeModelFilter.set_visible_func():

filter.set_visible_func(filter_func, data=None)

The alternative to a “visibility” function is to use a boolean column in the model to specify which rows to filter. Choose which column with Gtk.TreeModelFilter.set_visible_column().

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

_images/treeview_filter_example.png
 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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
import gi

gi.require_version("Gtk", "3.0")
from gi.repository import Gtk

# list of tuples for each software, containing the software name, initial release, and main programming languages used
software_list = [
    ("Firefox", 2002, "C++"),
    ("Eclipse", 2004, "Java"),
    ("Pitivi", 2004, "Python"),
    ("Netbeans", 1996, "Java"),
    ("Chrome", 2008, "C++"),
    ("Filezilla", 2001, "C++"),
    ("Bazaar", 2005, "Python"),
    ("Git", 2005, "C"),
    ("Linux Kernel", 1991, "C"),
    ("GCC", 1987, "C"),
    ("Frostwire", 2004, "Java"),
]


class TreeViewFilterWindow(Gtk.Window):
    def __init__(self):
        super().__init__(title="Treeview Filter Demo")
        self.set_border_width(10)

        # Setting up the self.grid in which the elements are to be positioned
        self.grid = Gtk.Grid()
        self.grid.set_column_homogeneous(True)
        self.grid.set_row_homogeneous(True)
        self.add(self.grid)

        # Creating the ListStore model
        self.software_liststore = Gtk.ListStore(str, int, str)
        for software_ref in software_list:
            self.software_liststore.append(list(software_ref))
        self.current_filter_language = None

        # Creating the filter, feeding it with the liststore model
        self.language_filter = self.software_liststore.filter_new()
        # setting the filter function, note that we're not using the
        self.language_filter.set_visible_func(self.language_filter_func)

        # creating the treeview, making it use the filter as a model, and adding the columns
        self.treeview = Gtk.TreeView(model=self.language_filter)
        for i, column_title in enumerate(
            ["Software", "Release Year", "Programming Language"]
        ):
            renderer = Gtk.CellRendererText()
            column = Gtk.TreeViewColumn(column_title, renderer, text=i)
            self.treeview.append_column(column)

        # creating buttons to filter by programming language, and setting up their events
        self.buttons = list()
        for prog_language in ["Java", "C", "C++", "Python", "None"]:
            button = Gtk.Button(label=prog_language)
            self.buttons.append(button)
            button.connect("clicked", self.on_selection_button_clicked)

        # setting up the layout, putting the treeview in a scrollwindow, and the buttons in a row
        self.scrollable_treelist = Gtk.ScrolledWindow()
        self.scrollable_treelist.set_vexpand(True)
        self.grid.attach(self.scrollable_treelist, 0, 0, 8, 10)
        self.grid.attach_next_to(
            self.buttons[0], self.scrollable_treelist, Gtk.PositionType.BOTTOM, 1, 1
        )
        for i, button in enumerate(self.buttons[1:]):
            self.grid.attach_next_to(
                button, self.buttons[i], Gtk.PositionType.RIGHT, 1, 1
            )
        self.scrollable_treelist.add(self.treeview)

        self.show_all()

    def language_filter_func(self, model, iter, data):
        """Tests if the language in the row is the one in the filter"""
        if (
            self.current_filter_language is None
            or self.current_filter_language == "None"
        ):
            return True
        else:
            return model[iter][2] == self.current_filter_language

    def on_selection_button_clicked(self, widget):
        """Called on any of the button clicks"""
        # we set the current language filter to the button's label
        self.current_filter_language = widget.get_label()
        print("%s language selected!" % self.current_filter_language)
        # we update the filter, which updates in turn the view
        self.language_filter.refilter()


win = TreeViewFilterWindow()
win.connect("destroy", Gtk.main_quit)
win.show_all()
Gtk.main()