There are different ways to change row order in QTableWidget:
by internal move via drag & drop
by separate buttons which shift a selected row up or down by one position
It turned out that these two approaches are not very practical for longer lists and my special purpose.
So, I tried to implement the following approach by assigning the new position by changing cell values:
the first column holds current position number
by editing these numbers I want to assign the new position to this row
I want to allow editing only on the first column
if an invalid position number is entered (within the range of number of rows) nothing should change
if a valid position number is entered the other position numbers in the first column are modified accordingly.
then I can get the rearranged rows in new order by clicking on the column header for sorting by the first column.
Example: position numbers 1,2,3,4,5.
If I change the value in row3,column1 from 3 to 1, the position numbers in the first column should change as follows:
1 --> 2
2 --> 3
3 --> 1
4 --> 4
5 --> 5
However, it seems I get problems with setEditTriggers(QAbstractItemView.NoEditTriggers) and setEditTriggers(QAbstractItemView.DoubleClicked).
Depending on some different code variations I tried, it looks like I still get an EditTrigger although I think I have disabled EditTriggers via self.setEditTriggers(QAbstractItemView.NoEditTriggers).
Or I get RecursionError: maximum recursion depth exceeded while calling a Python object.
Or TypeError: '>' not supported between instances of 'NoneType' and 'int'.
I hope I could make the problem clear enough. What am I doing wrong here?
Code: (minimized non-working example. Should be copy & paste & run)
import sys
from PyQt5.QtWidgets import QMainWindow, QApplication, QWidget, QAction, QTableWidget, QTableWidgetItem, QVBoxLayout, QPushButton, QAbstractItemView
from PyQt5.QtCore import pyqtSlot, Qt
import random
class MyTableWidget(QTableWidget):
def __init__(self):
super().__init__()
self.setColumnCount(3)
self.setRowCount(7)
self.setSortingEnabled(False)
header = self.horizontalHeader()
header.setSortIndicatorShown(True)
header.sortIndicatorChanged.connect(self.sortItems)
self.setSelectionBehavior(QAbstractItemView.SelectRows)
self.setEditTriggers(QAbstractItemView.NoEditTriggers)
self.col_pos = 0
self.oldPosValue = None
self.manualChange = False
self.cellDoubleClicked.connect(self.cell_doubleClicked)
self.cellChanged.connect(self.cell_changed)
def cell_doubleClicked(self):
self.setEditTriggers(QAbstractItemView.NoEditTriggers)
if self.currentColumn() != self.col_pos: # editing allowed only for this column
return
self.setEditTriggers(QAbstractItemView.DoubleClicked)
try:
self.oldPosValue = int(self.currentItem().text())
except:
pass
self.manualChange = True
def cell_changed(self):
if not self.manualChange:
return
self.setEditTriggers(QAbstractItemView.NoEditTriggers)
try:
newPosValue = int(self.currentItem().text())
except:
newPosValue = None
rowChanged = self.currentRow()
print("Value: {} --> {}".format(self.oldPosValue, newPosValue))
if newPosValue>0 and newPosValue<=self.rowCount():
for row in range(self.rowCount()):
if row != rowChanged:
try:
value = int(self.item(row,self.col_pos).text())
if value<newPosValue:
self.item(row,self.col_pos).setData(Qt.EditRole,value+1)
except:
print("Error")
pass
else:
self.item(rowChanged,self.col_pos).setData(Qt.EditRole,self.oldPosValue)
print("New value outside range")
self.manualChange = True
class App(QWidget):
def __init__(self):
super().__init__()
self.title = 'PyQt5 table'
self.initUI()
def initUI(self):
self.setWindowTitle(self.title)
self.setGeometry(0,0,400,300)
self.layout = QVBoxLayout()
self.tw = MyTableWidget()
self.layout.addWidget(self.tw)
self.pb_refill = QPushButton("Refill")
self.pb_refill.clicked.connect(self.on_click_pb_refill)
self.layout.addWidget(self.pb_refill)
self.setLayout(self.layout)
self.show()
#pyqtSlot()
def on_click_pb_refill(self):
self.tw.setEditTriggers(QAbstractItemView.NoEditTriggers)
for row in range(self.tw.rowCount()):
for col in range(self.tw.columnCount()):
if col==0:
number = row+1
else:
number = random.randint(1000,9999)
twi = QTableWidgetItem()
self.tw.setItem(row, col, twi)
self.tw.item(row, col).setData(Qt.EditRole,number)
if __name__ == '__main__':
app = QApplication(sys.argv)
ex = App()
sys.exit(app.exec_())
Result:
The main problem is that you're trying to disable editing in the wrong way: toggling the edit triggers won't give you a valid result due to the way the view reacts to events.
The recursion error is due to the fact that you are changing data in the signal that reacts to data changes, which clearly is not a good thing to do.
The other problem is related to the current item, which could become None in certain situations.
First of all, the correct way to disable editing of items is by setting the item's flags. This solves another problem you didn't probably found yet: pressing Tab while in editing mode, allows to change data in the other columns.
Then, in order to correctly use the first column to set the order, you should ensure that all other rows get correctly "renumbered". Since doing that also requires setting data in other items, you must temporarily disconnect from the changed signal.
class MyTableWidget(QTableWidget):
def __init__(self):
super().__init__()
self.setColumnCount(3)
self.setRowCount(7)
self.setSortingEnabled(False)
header = self.horizontalHeader()
header.setSortIndicatorShown(True)
header.sortIndicatorChanged.connect(self.sortItems)
self.setSelectionBehavior(QAbstractItemView.SelectRows)
self.setEditTriggers(QAbstractItemView.DoubleClicked)
self.itemChanged.connect(self.cell_changed)
def cell_changed(self, item):
if item.column():
return
newRow = item.data(Qt.DisplayRole)
self.itemChanged.disconnect(self.cell_changed)
if not 1 <= newRow <= self.rowCount():
if newRow < 1:
newRow = 1
item.setData(Qt.DisplayRole, 1)
elif newRow > self.rowCount():
newRow = self.rowCount()
item.setData(Qt.DisplayRole, self.rowCount())
otherItems = []
for row in range(self.rowCount()):
otherItem = self.item(row, 0)
if otherItem == item:
continue
otherItems.append(otherItem)
otherItems.sort(key=lambda i: i.data(Qt.DisplayRole))
for r, item in enumerate(otherItems, 1):
if r >= newRow:
r += 1
item.setData(Qt.DisplayRole, r)
self.itemChanged.connect(self.cell_changed)
def setItem(self, row, column, item):
# override that automatically disables editing if the item is not on the
# first column of the table
self.itemChanged.disconnect(self.cell_changed)
super().setItem(row, column, item)
if column:
item.setFlags(item.flags() & ~Qt.ItemIsEditable)
self.itemChanged.connect(self.cell_changed)
Note that you must also change the function that creates the items and use item.setData before adding the item to the table:
def on_click_pb_refill(self):
for row in range(self.tw.rowCount()):
for col in range(self.tw.columnCount()):
if col==0:
number = row+1
else:
number = random.randint(1000,9999)
twi = QTableWidgetItem()
twi.setData(Qt.EditRole, number)
self.tw.setItem(row, col, twi)
You can use slightly modified QStandardItemModel and QSortFilterProxyModel for that
from PyQt5 import QtWidgets, QtCore, QtGui
from PyQt5.QtCore import Qt, pyqtSignal
import random
from contextlib import suppress
def shiftRows(old, new, count):
items = list(range(1, count + 1))
item = items.pop(old - 1)
items.insert(new - 1, item)
return {item: i + 1 for i, item in enumerate(items)}
class Model(QtGui.QStandardItemModel):
orderChanged = pyqtSignal()
def __init__(self, rows, columns, parent = None):
super().__init__(rows, columns, parent)
self._moving = True
for row in range(self.rowCount()):
self.setData(self.index(row, 0), int(row + 1))
self.setData(self.index(row, 1), random.randint(1000,9999))
self.setData(self.index(row, 2), random.randint(1000,9999))
self._moving = False
def swapRows(self, old, new):
self._moving = True
d = shiftRows(old, new, self.rowCount())
for row in range(self.rowCount()):
index = self.index(row, 0)
v = index.data()
if d[v] != v:
self.setData(index, d[v])
self.orderChanged.emit()
self._moving = False
def flags(self, index):
if index.column() == 0:
return Qt.ItemIsEditable | Qt.ItemIsSelectable | Qt.ItemIsEnabled
return Qt.ItemIsSelectable | Qt.ItemIsEnabled
def headerData(self, section, orientation, role):
if orientation == Qt.Vertical and role == Qt.DisplayRole:
return self.index(section, 0).data()
return super().headerData(section, orientation, role)
def setData(self, index, value, role = Qt.DisplayRole):
if role == Qt.EditRole and index.column() == 0:
if self._moving:
return super().setData(self, index, value, role)
with suppress(ValueError):
value = int(value)
if value < 1 or value > self.rowCount():
return False
prev = index.data()
self.swapRows(prev, value)
return True
return super().setData(index, value, role)
if __name__ == "__main__":
app = QtWidgets.QApplication([])
model = Model(5, 3)
sortModel = QtCore.QSortFilterProxyModel()
sortModel.setSourceModel(model)
model.orderChanged.connect(lambda: sortModel.sort(0))
view = QtWidgets.QTableView()
view.setModel(sortModel)
view.show()
app.exec_()
I want to display two levels of hierarchical data using gtk Treeview(with model gtk Treestore)
The data is in the following format:
**First(parent)** level
col_a, col_b, col_c, col_d, col_e
val_a, val_b, val_c, val_d, val_e
**Second(child)** level
col_x, col_y, col_z
val_x, val_y, val_z
And the hierarchy of data is as follows:
> val_a1, val_b1, val_c1, val_d1, val_e1
val_x1, val_y1, val_z1
val_x2, val_y2, val_z2
> val_a2, val_b2, val_c2, val_s2, val_e2
val_x3, val_y3, val_z3
> val_a3, val_b3, val_c3, val_d3, val_e3
> val_a4, val_b4, val_c4, val_d4, val_e4
val_x4, val_y4, val_z4
val_x5, val_y5, val_z5
The following pygtk code is what I have tried(Modified the code from gtk tutorial)
import pygtk
pygtk.require('2.0')
import gtk
data = [
[('val_a1', 'val_b1', 'val_c1', 'val_d1', 'val_e1'), ('val_x1', 'val_y1', 'val_z1'), ('val_x2', 'val_y2', 'val_z2')],
[('val_a2', 'val_b2', 'val_c2', 'val_d2', 'val_e2'), ('val_x3', 'val_y3', 'val_z3')],
[('val_a3', 'val_b3', 'val_c3', 'val_d3', 'val_e3')],
[('val_a4', 'val_b4', 'val_c4', 'val_d4', 'val_e4'), ('val_x4', 'val_y4', 'val_z4'), ('val_x5', 'val_y5', 'val_z5')],
]
class BasicTreeViewExample:
def delete_event(self, widget, event, data=None):
gtk.main_quit()
return False
def __init__(self):
self.window = gtk.Window(gtk.WINDOW_TOPLEVEL)
self.window.set_title("Basic TreeView Example")
self.window.set_size_request(200, 200)
self.window.connect("delete_event", self.delete_event)
self.treestore = gtk.TreeStore(str, str, str, str, str)
for detail in data:
for index, elem in enumerate(detail):
if index == 0:
piter = self.treestore.append(None, elem)
else:
self.treestore.append(piter, elem)
self.treeview = gtk.TreeView(self.treestore)
for i in range(5):
tvcolumn = gtk.TreeViewColumn('Column %s' % (i))
self.treeview.append_column(tvcolumn)
cell = gtk.CellRendererText()
tvcolumn.pack_start(cell, True)
tvcolumn.add_attribute(cell, 'text', i)
self.window.add(self.treeview)
self.window.show_all()
def main():
gtk.main()
if __name__ == "__main__":
tvexample = BasicTreeViewExample()
main()
But, I'm getting the following error when I try running the above code:
Traceback (most recent call last):
File "test.py", line 55, in <module>
tvexample = BasicTreeViewExample()
File "test.py", line 33, in __init__
self.treestore.append(piter, detail[index])
ValueError: row sequence has wrong length
So my questions are:
How can I add data to gtk TreeStore with different number of columns in the different levels of hierarchy
Also, Is it possible to display column names for each row in the gtk treestore
i.e In the Treeview I want to see the output as follows:
col_a, col_b, col_c, col_d, col_e
> val_a1, val_b1, val_c1, val_d1, val_e1
col_x, col_y, col_z
val_x1, val_y1, val_z1
col_x, col_y, col_z
val_x2, val_y2, val_z2
col_a, col_b, col_c, col_d, col_e
> val_a2, val_b2, val_c2, val_s2, val_e2
col_x, col_y, col_z
val_x3, val_y3, val_z3
col_a, col_b, col_c, col_d, col_e
> val_a3, val_b3, val_c3, val_d3, val_e3
col_a, col_b, col_c, col_d, col_e
> val_a4, val_b4, val_c4, val_d4, val_e4
col_x, col_y, col_z
val_x4, val_y4, val_z4
col_x, col_y, col_z
val_x5, val_y5, val_z5
If this is not possible using the treeview, is there any alternative/workarounds using which I can achieve the above?
Short answers and introduction
How can I add data to gtk.TreeStore with different number of columns in the different levels of hierarchy?
Simple: you can't. GtkListStore as well as GtkTreeStore are designed to hold
data as a table. Columns are defined in a fixed way with an index and a data
type. The only difference between a ListStore and a TreeStore is that in a
TreeStore, rows have a hierarchy. Even worse, the GtkTreeView widget also
expects data to be stored as a table, as each row will unconditionally fetch
the cells using their column index, and expects to find something there.
Unless you write your own widget, but you probably don't want to
(God, this file is 16570 lines long...).
However, if you can't write your own widget, you still could write your own
model. And this will give you some flexibility.
Also, is it possible to display column names for each row in the gtk.TreeStore ?
Displaying data in the TreeView involve two components: the GtkTreeView itself,
that fetches data in the TreeStore and display them. The TreeView widget doesn't
have the feature of displaying headers for each row. But there are some tricks
to process data between the model and the view, which could end to the desired
effect, though not that nice probably.
Basics
So, the TreeView expects to work on a table of data, and we can't change that.
OK. But we still can trick it into thinking the data is a table, when actually
it isn't... Let's start with the view. We need at least five columns to display
the data of the parents. The children can then use only three columns out of
these five, so this is fine.
Note that columns of the model do not always map to a column in the tree view.
They actually map to some properties of cell renderers. For example, you can
have a column in the model that defines the background color of the row,
or a column that defines an icon to display. Columns in the view are just a way
to align groups of cell renderers, possibly under a header. But here, let's
assume all values are text that should go into a single CellRendererText in its
own column.
Parents will use all five columns while children will use only the columns 2, 3
and 4. We'll then trick the model to return an empty text when data isn't
available for the target cell.
Creating a new TreeModel
Some explainations about implementing a custom GtkTreeModel in PyGTK are
available in this tutorial.
This is a sample implementation of it:
import pygtk
pygtk.require('2.0')
import gtk
data = [
[('val_a1', 'val_b1', 'val_c1', 'val_d1', 'val_e1'), ('val_x1', 'val_y1', 'val_z1'), ('val_x2', 'val_y2', 'val_z2')],
[('val_a2', 'val_b2', 'val_c2', 'val_d2', 'val_e2'), ('val_x3', 'val_y3', 'val_z3')],
[('val_a3', 'val_b3', 'val_c3', 'val_d3', 'val_e3')],
[('val_a4', 'val_b4', 'val_c4', 'val_d4', 'val_e4'), ('val_x4', 'val_y4', 'val_z4'), ('val_x5', 'val_y5', 'val_z5')],
]
class MyTreeModel(gtk.GenericTreeModel):
# The columns exposed by the model to the view
column_types = (str, str, str, str, str)
def __init__(self, data):
gtk.GenericTreeModel.__init__(self)
self.data = data
def on_get_flags(self):
"""
Get Model capabilities
"""
return gtk.TREE_MODEL_ITERS_PERSIST
def on_get_n_columns(self):
"""
Get number of columns in the model
"""
return len(self.column_types)
def on_get_column_type(self, n):
"""
Get data type of a specified column in the model
"""
return self.column_types[n]
def on_get_iter(self, path):
"""
Obtain a reference to the row at path. For us, this is a tuple that
contain the position of the row in the double list of data.
"""
if len(path) > 2:
return None # Invalid path
parent_idx = path[0]
if parent_idx >= len(self.data):
return None # Invalid path
first_level_list = self.data[parent_idx]
if len(path) == 1:
# Access the parent at index 0 in the first level list
return (parent_idx, 0)
else:
# Access a child, at index path[1] + 1 (0 is the parent)
child_idx = path[1] + 1
if child_idx >= len(first_level_list):
return None # Invalid path
else:
return (parent_idx, child_idx)
def on_get_path(self, iter_):
"""
Get a path from a rowref (this is the inverse of on_get_iter)
"""
parent_idx, child_idx = iter_
if child_idx == 0:
return (parent_idx, )
else:
(parent_idx, child_idx-1)
def on_get_value(self, iter_, column):
"""
This is where the view asks for values. This is thus where we
start mapping our data model to a fake table to present to the view
"""
parent_idx, child_idx = iter_
item = self.data[parent_idx][child_idx]
# For parents, map columns 1:1 to data
if child_idx == 0:
return item[column]
# For children, we have to fake some columns
else:
if column == 0 or column == 4:
return "" # Fake empty text
else:
return item[column-1] # map 1, 2, 3 to 0, 1, 2.
def on_iter_next(self, iter_):
"""
Get the next sibling of the item pointed by iter_
"""
parent_idx, child_idx = iter_
# For parents, point to the next parent
if child_idx == 0:
next_parent_idx = parent_idx + 1
if next_parent_idx < len(self.data):
return (next_parent_idx, 0)
else:
return None
# For children, get next tuple in the list
else:
next_child_idx = child_idx + 1
if next_child_idx < len(self.data[parent_idx]):
return (parent_idx, next_child_idx)
else:
return None
def on_iter_has_child(self, iter_):
"""
Tells if the row referenced by iter_ has children
"""
parent_idx, child_idx = iter_
if child_idx == 0 and len(self.data[parent_idx]) > 1:
return True
else:
return False
def on_iter_children(self, iter_):
"""
Return a row reference to the first child row of the row specified
by iter_. If iter_ is None, a reference to the first top level row
is returned. If there is no child row None is returned.
"""
if iter_ is None:
return (0, 0)
parent_idx, child_idx = iter_
if self.on_iter_has_child(iter_):
return (parent_idx, 1)
else:
return None
def on_iter_n_children(self, iter_):
"""
Return the number of child rows that the row specified by iter_
has. If iter_ is None, the number of top level rows is returned.
"""
if iter_ is None:
return len(self.data)
else:
parent_idx, child_idx = iter_
if child_idx == 0:
return len(self.data[parent_idx]) - 1
else:
return 0
def on_iter_nth_child(self, iter_, n):
"""
Return a row reference to the nth child row of the row specified by
iter_. If iter_ is None, a reference to the nth top level row is
returned.
"""
if iter_ is None:
if n < len(self.data):
return (n, 0)
else:
return None
else:
parent_idx, child_idx = iter_
if child_idx == 0:
if n+1 < len(self.data[parent_idx]):
return (parent_idx, n+1)
else:
return None
else:
return None
def on_iter_parent(self, iter_):
"""
Get a reference to the parent
"""
parent_idx, child_idx = iter_
if child_idx == 0:
return None
else:
return (parent_idx, 0)
class BasicTreeViewExample:
def delete_event(self, widget, event, data=None):
gtk.main_quit()
return False
def __init__(self):
self.window = gtk.Window(gtk.WINDOW_TOPLEVEL)
self.window.set_title("Basic TreeView Example")
self.window.set_size_request(200, 200)
self.window.connect("delete_event", self.delete_event)
# Create the model with data in it
self.model = MyTreeModel(data)
self.treeview = gtk.TreeView(self.model)
for i in range(5):
tvcolumn = gtk.TreeViewColumn('Column %s' % (i))
self.treeview.append_column(tvcolumn)
cell = gtk.CellRendererText()
tvcolumn.pack_start(cell, True)
tvcolumn.add_attribute(cell, 'text', i)
self.window.add(self.treeview)
self.window.show_all()
def main():
gtk.main()
if __name__ == "__main__":
tvexample = BasicTreeViewExample()
main()
And the result:
Faking headers in cells
Let's now add some kind of title in each cell using the model to generate
the desired data. Full code is here.
class MyTreeModel(gtk.GenericTreeModel):
# The columns exposed by the model to the view
column_types = (str, str, str, str, str)
# Column headers
parent_headers = ("P.Col 1", "P.Col 2", "P.Col 3", "P.Col 4", "P.Col 5")
child_headers = ("C.Col 1", "C.Col 2", "C.Col 3")
...
def on_get_value(self, iter_, column):
"""
This is where the view asks for values. This is thus where we
start mapping our data model to a fake table to present to the view
"""
parent_idx, child_idx = iter_
item = self.data[parent_idx][child_idx]
# For parents, map columns 1:1 to data
if child_idx == 0:
return self.markup(item[column], column, False)
# For children, we have to fake some columns
else:
if column == 0 or column == 4:
return "" # Fake empty text
else:
# map 1, 2, 3 to 0, 1, 2.
return self.markup(item[column-1], column-1, True)
def markup(self, text, column, is_child):
"""
Produce a markup for a cell with a title and a text
"""
headers = self.child_headers if is_child else self.parent_headers
title = headers[column]
return "<b>%s</b>\n%s"%(title, text)
...
class BasicTreeViewExample:
def __init__(self):
...
self.treeview = gtk.TreeView(self.model)
self.treeview.set_headers_visible(False)
for i in range(5):
...
tvcolumn.pack_start(cell, True)
tvcolumn.add_attribute(cell, 'markup', i)
...
And the result:
Using set_cell_data_func or TreeModelFilter
Provided that you manage to fit your data into a ListStore or TreeStore, that
is to say you find a trick so that parents and children share the same amount
and type of columns, you can then manipulate data using either a
GtkTreeCellDataFunc
or a GtkTreeModelFilter.
PyGTK documentation provides examplefor Cell Data Functions
and Tree Model Filters.
Adding column headers using these concepts for example maybe easier than
creating a full custom model.
Here is the code using TreeCellDataFunc.
Note how the data input has been formatted so that children and parents have the
same amount of data. This is a condition to be able to use GtkTreeStore.
import pygtk
pygtk.require('2.0')
import gtk
data = [
[('val_a1', 'val_b1', 'val_c1', 'val_d1', 'val_e1'), ('', 'val_x1', 'val_y1', 'val_z1', ''), ('', 'val_x2', 'val_y2', 'val_z2', '')],
[('val_a2', 'val_b2', 'val_c2', 'val_d2', 'val_e2'), ('', 'val_x3', 'val_y3', 'val_z3', '')],
[('val_a3', 'val_b3', 'val_c3', 'val_d3', 'val_e3')],
[('val_a4', 'val_b4', 'val_c4', 'val_d4', 'val_e4'), ('', 'val_x4', 'val_y4', 'val_z4', ''), ('', 'val_x5', 'val_y5', 'val_z5', '')],
]
class BasicTreeViewExample:
parent_headers = ("P.Col 1", "P.Col 2", "P.Col 3", "P.Col 4", "P.Col 5")
child_headers = ("C.Col 1", "C.Col 2", "C.Col 3")
def delete_event(self, widget, event, data=None):
gtk.main_quit()
return False
def __init__(self):
self.window = gtk.Window(gtk.WINDOW_TOPLEVEL)
self.window.set_title("Basic TreeView Example")
self.window.set_size_request(200, 200)
self.window.connect("delete_event", self.delete_event)
self.treestore = gtk.TreeStore(str, str, str, str, str)
for detail in data:
for index, elem in enumerate(detail):
if index == 0:
piter = self.treestore.append(None, elem)
else:
self.treestore.append(piter, elem)
self.treeview = gtk.TreeView(self.treestore)
for i in range(5):
tvcolumn = gtk.TreeViewColumn('Column %s' % (i))
self.treeview.append_column(tvcolumn)
cell = gtk.CellRendererText()
tvcolumn.pack_start(cell, True)
# Delegate data fetching to callback
tvcolumn.set_cell_data_func(cell, self.cell_add_header, i)
self.window.add(self.treeview)
self.window.show_all()
def cell_add_header(self, treeviewcolumn, cell, model, iter_, column):
text = model.get_value(iter_, column)
if model.iter_parent(iter_) is None:
# This is a parent
title = self.parent_headers[column]
markup = "<b>%s</b>\n%s"%(title, text)
else:
# We have a child
if column == 0 or column == 4:
# Cell is not used by child, leave it empty
markup = ""
else:
title = self.child_headers[column-1]
markup = "<b>%s</b>\n%s"%(title, text)
cell.set_property('markup', markup)
def main():
gtk.main()
if __name__ == "__main__":
tvexample = BasicTreeViewExample()
main()
GtkTreeModelFilter leads to pretty much the same thing. The result is the same
than in Faking headers in cells (except that I forgot to set the headers invisible):
I hope this helped you, and the others that will have the same kind of question!