13. Träd- och listkomponenter

En Gtk.TreeView och dess associerade komponenter är ett extremt kraftfullt sätt att visa data. De används tillsammans med en Gtk.ListStore eller Gtk.TreeStore och tillhandahåller ett sätt att visa och manipulera data på många sätt, inklusive:

  • Automatiska uppdateringar då data läggs till, tas bort eller redigeras

  • Stöd för dra-och-släpp

  • Sortering av data

  • Inbäddning av komponenter så som kryssrutor, förloppsindikatorer o.s.v.

  • Kolumner som går att ordna om och ändra storlek på

  • Filtrering av data

Med kraften och flexibiliteten hos en Gtk.TreeView kommer komplexitet. Det är ofta svårt för nybörjarutvecklare att kunna använda den korrekt på grund av de antal metoder som krävs.

13.1. Modellen

Varje Gtk.TreeView har en associerad Gtk.TreeModel som innehåller de data som visas av denna TreeView. Varje Gtk.TreeModel kan användas av mer än en Gtk.TreeView. Exempelvis låter detta samma underliggande data visas och redigeras på två olika sätt på samma gång. Eller så kan de två vyerna visa olika kolumner från samma Model-data, på samma sätt som två SQL-frågor (eller ”vyer”) kan visa olika fält från samma databastabell.

Även om du teoretiskt kan implementera din egen Model, så kommer du vanligen använda antingen modellklasserna Gtk.ListStore eller Gtk.TreeStore. Gtk.ListStore innehåller enkla rader med data, och varje rad har inget barn, medan Gtk.TreeStore innehåller rader av data, och varje rad kan ha barnrader.

Då du konstruerar en modell måste du ange datatyperna för varje kolumn som modellen innehåller.

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

Detta skapar en listlagring med tre kolumner, två strängkolumner och en flyttalskolumn.

Att lägga till data till modellen görs med Gtk.ListStore.append() eller Gtk.TreeStore.append(), beroende på vilken sorts modell som skapades.

För en Gtk.ListStore:

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

För en Gtk.TreeStore måste du ange en befintlig rad att lägga till den nya raden till, med en Gtk.TreeIter, eller None för toppnivån på trädet:

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

Båda metoderna returnerar en Gtk.TreeIter-instans, vilken pekar på platsen för den nyligen infogade raden. Du kan erhålla en Gtk.TreeIter genom att anropa Gtk.TreeModel.get_iter().

När data har infogats kan du erhålla eller ändra data med träditeratorn och kolumnindexet.

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

Som med Pythons inbyggda list-objekt kan du använda len() för att få antalet rader och använda slicing för att få eller ställa in värden.

# 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]

Att iterera över alla rader i en trädmodell är också väldigt enkelt.

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

Tänk på att om du använder Gtk.TreeStore kommer koden ovan endast iterera över raderna på toppnivån, inte nodernas barn. För att iterera över alla rader, använd Gtk.TreeModel.foreach().

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

store.foreach(print_row)

Förutom att komma åt värden lagrade i en Gtk.TreeModel med den listliknande metoden nämnd ovan, så är det också möjligt att använda instanser av antingen Gtk.TreeIter eller Gtk.TreePath. Båda hänvisar till en specifik rad i en trädmodell. Man kan konvertera en stig till en iterator genom att anropa Gtk.TreeModel.get_iter(). Då Gtk.ListStore innehåller endast en nivå, d.v.s. att noder inte har några barnnoder, så är en stig helt enkelt indexet för raden som du vill komma åt.

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

I fallet för Gtk.TreeStore är en stig en lista över index eller en sträng. Strängformen är en lista över tal som skiljs åt av ett kolon. Varje tal hänvisar till positionen på den nivån. Därmed hänvisar stigen ”0” till rotnoden och stigen ”2:4” till det femte barnet till den tredje noden.

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

Instanser av Gtk.TreePath kan kommas åt som listor, d.v.s. len(treepath) returnerar djupet på objektet som treepath pekar på, och treepath[i] returnerar barnets index på nivå i.

13.2. Vyn

Medan det finns flera olika modeller att välja på, så finns det endast en vykomponent att hantera, Den fungerar med antingen listan eller trädlagringen. Att konfigurera en Gtk.TreeView är inte svårt. Den behöver en Gtk.TreeModel för att veta var den ska erhålla sina data från, antingen genom att skicka det till Gtk.TreeView-konstruktorn, eller genom att anropa Gtk.TreeView.set_model().

tree = Gtk.TreeView(model=store)

Då komponenten Gtk.TreeView har en modell kommer den behöva veta hur den ska visa modellen. Den gör detta med kolumner och cellrenderare. headers_visible styr huruvida den visar kolumnrubriker.

Cellrenderare används för att rita data i trädmodellen på ett specifikt sätt. Det finns ett antal cellrenderare som kommer med GTK+, exempelvis Gtk.CellRendererText, Gtk.CellRendererPixbuf och Gtk.CellRendererToggle. Vidare är det relativt enkelt att själv skriva en anpassad renderare genom att skapa en underklass till Gtk.CellRenderer och lägga till egenskaper med GObject.Property().

En Gtk.TreeViewColumn är objektet som Gtk.TreeView använder för att organisera de vertikala kolumnerna i trädvyn och hålla en eller flera cellrenderare. Varje kolumn kan ha en title som kommer vara synlig om Gtk.TreeView visar kolumnrubriker. Modellen mappas till kolumnen genom att använda nyckelordsargument med egenskaper för renderaren som identifierare och index för modellkolumnerna som argument.

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

Positionsargument kan användas för kolumntiteln och renderaren.

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

För att rendera mer än en modellkolumn i en vykolumn behöver du skapa en Gtk.TreeViewColumn-instans och använda Gtk.TreeViewColumn.pack_start() för att lägga till modellkolumnerna till den.

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. Valet

De flesta program kommer behöva inte bara arbeta med att visa data, utan också ta emot inmatningshändelser från användare. För att göra detta, ta helt enkelt en referens till ett valobjekt och anslut till ”changed”-signalen.

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

För att sedan erhålla data för den valda raden:

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

Du kan styra vilka val som tillåts genom att anropa Gtk.TreeSelection.set_mode(). Gtk.TreeSelection.get_selected() fungerar inte om valläget är inställt till Gtk.SelectionMode.MULTIPLE, använd Gtk.TreeSelection.get_selected_rows() istället.

13.4. Sortering

Sortering är en viktig funktion för trädvyer och stöds av standardträdmodellerna (Gtk.TreeStore och Gtk.ListStore), vilka implementerar gränssnittet Gtk.TreeSortable.

13.4.1. Sortering genom att klicka på kolumner

En kolumn för en Gtk.TreeView kan lätt göras sorterbar med ett anrop till Gtk.TreeViewColumn.set_sort_column_id(). Efter det kan kolumnen sorteras genom att klicka på dess rubrik.

Först behöver vi en enkel Gtk.TreeView och en Gtk.ListStore som en modell.

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)

Nästa steg är att aktivera sortering. Observera att column_id (0 i exemplet) hänvisar till modellens kolumn och inte till kolumnen för vår TreeView.

column.set_sort_column_id(0)

13.4.2. Ställa in en anpassad sorteringsfunktion

Det är också möjligt att ställa in en anpassad jämförelsefunktion för att ändra sorteringsbeteendet. Som ett exempel kommer vi skapa en jämförelsefunktion som sorterar skiftlägeskänsligt. I exemplet ovan såg listan ut som:

alfred
Alfred
benjamin
Benjamin
charles
Charles
david
David

Den skiftlägeskänsliga sorterade listan kommer se ut som:

Alfred
Benjamin
Charles
David
alfred
benjamin
charles
david

Först av allt behövs en jämförelsefunktion. Denna funktion får två rader och ska returnera ett negativt heltal om den första skulle komma före den andra, noll om de är lika, och ett positivt heltal om den andra skulle komma före den första.

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

Sedan måste sorteringsfunktionen ställas in med Gtk.TreeSortable.set_sort_func().

model.set_sort_func(0, compare, None)

13.5. Filtrering

Till skillnad från sortering hanteras filtrering inte av de två modeller vi tidigare såg, utan av klassen Gtk.TreeModelFilter. Denna klass är liksom Gtk.TreeStore och Gtk.ListStore en Gtk.TreeModel. Den agerar som ett lager mellan den ”riktiga” modellen (en Gtk.TreeStore eller en Gtk.ListStore) och döljer några element för vyn. I praktiken tillhandahåller den en delmängd av den underliggande modellen åt Gtk.TreeView. Instanser av Gtk.TreeModelFilter kan staplas på varandra, för att använda flera filter på samma modell (på samma sätt som du skulle använda ”AND”-klausuler i en SQL-förfrågan). De kan också sättas i en kedja med instanser av Gtk.TreeModelSort.

Du kan skapa en ny instans av ett Gtk.TreeModelFilter och ge det en modell att filtrera, men det lättaste sättet är att starta det direkt från den filtrerade modellen, med metoden Gtk.TreeModel.filter_new().

filter = model.filter_new()

På samma sätt som sorteringsfunktionen fungerar, så använder Gtk.TreeModelFilter en ”synlighets”-funktion, som givet en rad från den underliggande modellen, kommer returnera ett booleskt värde som indikerar om denna rad ska filtreras ut eller inte. Den ställs in av Gtk.TreeModelFilter.set_visible_func():

filter.set_visible_func(filter_func, data=None)

Alternativet till en ”synlighets”-funktion är att använda en boolesk kolumn i modellen för att ange vilka rader som ska filtreras. Välj vilken kolumn med Gtk.TreeModelFilter.set_visible_column().

Låt oss se ett fullständigt exempel som använder hela stacken Gtk.ListStore - Gtk.TreeModelFilter - Gtk.TreeModelFilter - Gtk.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()