Python Qt6 QTableView - Iterate Column and Value - python

I have a QTableView object that is successfully displaying data as expected. When the user clicks on a cell it automatically selects the entire row without any problems. What I would like to do is display the column name and cell value for the selected row (e.g. iterate the row), which will feed into a new QTableView (basically want to transpose the selected row for easier viewing).
I've read through simpler examples and gone through the QT documentation but really struggling to get this working. I can retrieve the cell value but not the column name.
self.tblView = QTableView()
self.tblModel = TableModel(data)
self.tblModel.layoutChanged.emit()
self.tblView.setModel(self.tblModel)
self.tblView.selectRow(0)
self.tblView.clicked.connect(self.qTableViewSingleRowSelection)
def qTableViewSingleRowSelection(self, clickedIndex):
row = clickedIndex.row()
column = clickedIndex.column()
print("Selected Row: {0}".format(row))
print("Selected Column: {0}".format(column))
self.tblView.selectRow(row)
I haven't included the code that didn't work but all I want is to return a dictionary object that contains {'Column 1' : 'Value 1'} etc. for only the selected row. Seems so simple yet really struggling to get this working. Really appreciate any assistance.

You just need to add this
class YourClass():
...
self.dictionary = {}
...
def qTableViewSingleRowSelection(self, clickedIndex):
...
model = self.tblView.model()
value = model.data(model.index(row,column), Qt.DisplayRole)
header = model.headerData(column, Qt.Horizontal, Qt.DisplayRole)
self.dictionary[header] = value
Anyway, everytime you select an item of a column where you already selected an item, you will subscribe that item.
exemple:
Table:
| 1 | 2 | 3 |
| a | b | c |
| d | e | f |
If you select a and then you select e, your dictonary will be {1:'a',2:'e'}, but whe you select d, your dictonary will be {1:'d',2:'e'}

I'm not sure if this is the best approach but this is how I overcome the problem in the end.
class TableModel(QtCore.QAbstractTableModel):
def __init__(self, data = None):
super(TableModel, self).__init__()
self._data = data
...
def rowRecord(self, index):
column = str(self._data.columns[index.column()])
value = self._data.iloc[index.row(), index.column()]
return column, value
class Metadata(QWidget):
def __init__(self):
super().__init__()
...
def qTableViewSingleRowSelection(self, clickedIndex):
row = clickedIndex.row()
self.tblView.selectRow(row)
columns = self.tblModel.columnCount(None)
row_dictionary = {}
for columnIndex in range(columns):
column, value = self.tblModel.rowRecord(self.tblModel.index(row, columnIndex))
if(str(value).strip() != ""):
row_dictionary[column] = (value)
self.dataframe_record = pd.DataFrame.from_dict(row_dictionary,orient='index')

Related

New pandas column based on whether functions are called

I have a class which calls functions depending on whether initial keywords are true or false. The intention is to be able to control how I create a new column in dataframe df. An abridged version of the class and functions is as follows:
class DFSetter:
def __init__(self, justify=True, caps=True, table=True):
if justify:
self.set_justify()
if caps:
self.set_all_caps()
if self.table:
self.set_table()
def set_justify(self):
self.justify = (self.df['jc'] != self.df['jc'].shift())
def set_all_caps(self):
self.all_caps = ((self.df['caps']==True) & (self.df['cap_diffs']>5))
def set_table(self):
self.table = ((self.df['table'] == True) & (self.df['table'].shift() == False))
Suppose I want to make a new column, row_break in this dataframe which will set to True if any of the conditions are met. How could I create this new column if the call to one of the functions is switched off by initialising it as False?
This is currently how I'm doing it with everything set to True:
self.df['row_break'] = (self.justify | self.all_caps | self.table | pStyle)
* UPDATE WITH ANSWER *
Initialise with additional self.switches={} and add self.switches.update({'item':self.item}) to each function.
Create a new dataframe from the self.switches dictionary: self.switches_df=(self.switches)
Set 'row_break' column on main self.df dataframe by seeing if any of the columns are True: self.df['row_break'] = (self.switches_df.any(axis='columns')
i.e
class DFSetter:
def __init__(self, justify=True, caps=True, table=True):
self.switches={}
if justify:
self.set_justify()
if caps:
self.set_all_caps()
if self.table:
self.set_table()
def set_justify(self):
self.justify = (self.df['jc'] != self.df['jc'].shift())
self.switches.update({'justify':self.justify})
def set_all_caps(self):
self.all_caps = ((self.df['caps']==True) & (self.df['cap_diffs']>5))
self.switches.update({'caps':self.all_caps})
def set_table(self):
self.table = ((self.df['table'] == True) & (self.df['table'].shift() == False))
self.switches.update({'table':self.table})
def set_row_break(self):
switches_df = pd.DataFrame(self.switches)
self.df['row_break'] = switches_df.any(axis='columns')
You could initialize the new column to false and then update it for each of the conditions that are set to true.
I'm assuming here that the dataframe is one of the inputs to the class (since you use self.df).
class DFSetter:
def __init__(self, df, justify=True, caps=True, table=True):
self.df = df
self.df['row_break'] = False
if justify:
self.set_justify()
if caps:
self.set_all_caps()
if self.table:
self.set_table()
def set_justify(self):
self.justify = (self.df['jc'] != self.df['jc'].shift())
self.df['row_break'] = self.df['row_break'] | self.justify
def set_all_caps(self):
self.all_caps = ((self.df['caps']==True) & (self.df['cap_diffs']>5))
self.df['row_break'] = self.df['row_break'] | self.all_caps
def set_table(self):
self.table = ((self.df['table'] == True) & (self.df['table'].shift() == False))
self.df['row_break'] = self.df['row_break'] | self.table
Alternativly, keeping to the same idea, the self.df['row_break'] assignments could be done inside each if statement. In this way, the set_-methods would be kept cleaner.

iterate over pyspark dataframe columns

I have the following pyspark.dataframe:
age state name income
21 DC john 30-50K
NaN VA gerry 20-30K
I'm trying to achieve the equivalent of df.isnull().sum() (from pandas) which produces:
age 1
state 0
name 0
income 0
At first I tried something along the lines of:
null_counter = [df[c].isNotNull().count() for c in df.columns]
but this produces the following error:
TypeError: Column is not iterable
Similarly, this is how I'm currently iterating over columns to get the minimum value:
class BaseAnalyzer:
def __init__(self, report, struct):
self.report = report
self._struct = struct
self.name = struct.name
self.data_type = struct.dataType
self.min = None
self.max = None
def __repr__(self):
return '<Column: %s>' % self.name
class BaseReport:
def __init__(self, df):
self.df = df
self.columns_list = df.columns
self.columns = {f.name: BaseAnalyzer(self, f) for f in df.schema.fields}
def calculate_stats(self):
find_min = self.df.select([fn.min(self.df[c]).alias(c) for c in self.df.columns]).collect()
min_row = find_min[0]
for column, min_value in min_row.asDict().items():
self[column].min = min_value
def __getitem__(self, name):
return self.columns[name]
def __repr__(self):
return '<Report>'
report = BaseReport(df)
calc = report.calculate_stats()
for column in report1.columns.values():
if hasattr(column, 'min'):
print("{}:{}".format(column, column.min))
which allows me to 'iterate over the columns'
<Column: age>:1
<Column: name>: Alan
<Column: state>:ALASKA
<Column: income>:0-1k
I think this method has become way to complicated, how can I properly iterate over ALL columns to provide vaiour summary statistcs (min, max, isnull, notnull, etc..) The distinction between pyspark.sql.Row and pyspark.sql.Column seems strange coming from pandas.
Have you tried something like this:
names = df.schema.names
for name in names:
print(name + ': ' + df.where(df[name].isNull()).count())
You can see how this could be modified to put the information into a dictionary or some other more useful format.
you can try this one :
nullDf= df.select([count(when(col(c).isNull(), c)).alias(c) for c in df.columns])
nullDf.show()
it will give you a list of columns with the number of null its null values.

In qListView, items deleted are not getting updated in view

I have qListView populated with items which are actually filenames that I am reading from a folder.
Now, using context menu action "remove", I am deleting the corresponding file in the background.
The issue is the qListView, not getting updated ie. it is still showing the item that I have already remove.
My query is, how to refresh the view dynamically? I am new to MVC programming, was wondering if it is possible do it in model? Or, do I have to using recursive function to update views. BTW m using qAbstract list model and even tried currentItemChanged and dataChanged but nothing seems to work.
TestStepInstViewHdlr is the instance of QListView class:
TestStepInstViewHdlr.setSelectionMode(QAbstractItemView.MultiSelection)
TestStepInstViewHdlr.show()
TestStepViewHdlr.stepSelected.connect(getTestStepName)
TestStepInstViewHdlr.itemSelectionChanged.connect(TestStepInstViewHdlr.getInstanceName)
TestStepInstViewHdlr.customContextMenuRequested.connect(TestStepInstViewHdlr.onContext)
def getInstanceName(self):
index = self.selectedIndexes()
val = ""
valArray = []
for i in index:
val = i.data()
valArray.append(val)
print(valArray)
return valArray
def onContext(self, position):
instArray = []
constHdlr = const.Constant()
# Create a menu
menu = QtGui.QMenu()
rmvAction = menu.addAction("Remove")
canAction = menu.addAction("Cancel")
action = menu.exec_(self.mapToGlobal(position))
if action == rmvAction:
instArray = self.getInstanceName()
path = constHdlr.TEST_STEP_INSTANCE_PATH + StepName+"\\"
for inst in instArray:
path = path + inst
if os.path.isfile(path):
os.remove(path)
if action == canAction:
pass
My model is:
class TestStepInstListModel(QtCore.QAbstractListModel):
def __init__(self, TestSteps = [], parent = None):
QtCore.QAbstractListModel.__init__(self, parent)
self.__TestSteps = TestSteps
def rowCount(self, parent = None):
return len(self.__TestSteps)
def data(self, index, role):
if role == QtCore.Qt.DisplayRole:
row = index.row()
return self.__TestSteps[row]
def flags(self, index):
return QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled
def removeRows(self, position, rows, parent = QtCore.QModelIndex()):
self.beginRemoveRows(parent, position, position + rows - 1)
for i in range(rows):
value = self.__TestSteps[position]
self.__TestSteps.remove(value)
self.endRemoveRows()
return True
Thanks for your time :)
QStandardItemModel
Chirag if you are writing your own model, it will consume a lot of time. Instead check out QStandardItemModel, as it provides us with lots of things that are already implemented and we need to just use them in our code as per our requirements.
I am using this QStandardItemModel and have my own contextmenu.
self.model = QtGui.QStandardItemModel()
If I choose delete option in my code, this piece of code will help us in deleting the item selected in our listview(i.e. to delete that particular row).
item_to_be_deleted = self.listView.selectionModel().currentIndex().data().toString()
model = self.model
for item in model.findItems(item_to_be_deleted):
model.removeRow(item.row())

add different length columns to gtk TreeStore(Treeview)

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!

qt pyside - qsql*model, qabstractitemmodel and qtreeview interaction

I want to produce a simple enough application which uses a QTreeView widget to show hierarchical data from a SQLite3 (flat) table, use QDataWidgetMapper to populate some lineedit fields, allow user to edit, which in turn updates the table. Simple & basic (for most!).
I have been working on the basis that the following process would be the best way of doing this:
Connect to Dbase
Query data
Create and populate custom QAbstractItemModel from the data (manipulating it through a dict to create nodes, parents and children dynamically - for each dict entry a 'node' is generated with an associated parent)
Use QDatawidgetmapper to populate other widgets
User edits data
QAbstractItemModel (QAIM) is updated
Then have to run an UPDATE, INSERT or whatever query using new values in the QAIM model.
Refresh the QAIM and associated widgets.
I realise if I were just using a QTableView or QListView I would not need the custom model and could just write straight back into the database. The process I have outlined above seems to mean having to keep two sets of data going - i.e. the SQLite table and the custom QAIM and ensure that they are both kept up to date. This seems a bit cumbersome to me and I'm sure there must be a better way of doing it where the QTreeView is taking its data straight from the SQLite table - with the obvious need for some manipulation to convert the flat data into hierarchical data.
I am wondering, of course, whether I have completely misunderstood the relationship between QAbstractItemModel and the QSQL*Models and I am overcomplicating it through ignorance?
Thanks
What you want is a proxy model that acts as a bridge between QSql*Model and the view. For that, you need to subclass QAbstractProxyModel. You have to have a consistent way of finding parent-child relationships in proxy model and mapping them to the source model, so that might require keeping some tally in the proxy model.
When you are sub-classing QAbstractProxyModel, you need to re-define, at minimum, these methods:
rowCount
columnCount
parent
index
data
mapToSource
mapFromSource
Also, keep in mind that QAbstractProxyModel does not auto-propagate signals through. So, in order to have the view be aware of changes in source model (like insert, delete, update), you need to pass them in the proxy model (while of course, updating your mappings in the proxy model).
It will require some work, but in the end you'll have a more flexible structure. And it will eliminate all the stuff that you need to do for synchronizing database and custom QAbstractItemModel.
Edit
A custom proxy model that groups items from a flat model according to a given column:
import sys
from collections import namedtuple
import random
from PyQt4 import QtCore, QtGui
groupItem = namedtuple("groupItem",["name","children","index"])
rowItem = namedtuple("rowItem",["groupIndex","random"])
class GrouperProxyModel(QtGui.QAbstractProxyModel):
def __init__(self, parent=None):
super(GrouperProxyModel, self).__init__(parent)
self._rootItem = QtCore.QModelIndex()
self._groups = [] # list of groupItems
self._groupMap = {} # map of group names to group indexes
self._groupIndexes = [] # list of groupIndexes for locating group row
self._sourceRows = [] # map of source rows to group index
self._groupColumn = 0 # grouping column.
def setSourceModel(self, source, groupColumn=0):
super(GrouperProxyModel, self).setSourceModel(source)
# connect signals
self.sourceModel().columnsAboutToBeInserted.connect(self.columnsAboutToBeInserted.emit)
self.sourceModel().columnsInserted.connect(self.columnsInserted.emit)
self.sourceModel().columnsAboutToBeRemoved.connect(self.columnsAboutToBeRemoved.emit)
self.sourceModel().columnsRemoved.connect(self.columnsRemoved.emit)
self.sourceModel().rowsInserted.connect(self._rowsInserted)
self.sourceModel().rowsRemoved.connect(self._rowsRemoved)
self.sourceModel().dataChanged.connect(self._dataChanged)
# set grouping
self.groupBy(groupColumn)
def rowCount(self, parent):
if parent == self._rootItem:
# root level
return len(self._groups)
elif parent.internalPointer() == self._rootItem:
# children level
return len(self._groups[parent.row()].children)
else:
return 0
def columnCount(self, parent):
if self.sourceModel():
return self.sourceModel().columnCount(QtCore.QModelIndex())
else:
return 0
def index(self, row, column, parent):
if parent == self._rootItem:
# this is a group
return self.createIndex(row,column,self._rootItem)
elif parent.internalPointer() == self._rootItem:
return self.createIndex(row,column,self._groups[parent.row()].index)
else:
return QtCore.QModelIndex()
def parent(self, index):
parent = index.internalPointer()
if parent == self._rootItem:
return self._rootItem
else:
parentRow = self._getGroupRow(parent)
return self.createIndex(parentRow,0,self._rootItem)
def data(self, index, role):
if role == QtCore.Qt.DisplayRole:
parent = index.internalPointer()
if parent == self._rootItem:
return self._groups[index.row()].name
else:
parentRow = self._getGroupRow(parent)
sourceRow = self._sourceRows.index(self._groups[parentRow].children[index.row()])
sourceIndex = self.createIndex(sourceRow, index.column(), 0)
return self.sourceModel().data(sourceIndex, role)
return None
def flags(self, index):
return QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable
def headerData(self, section, orientation, role):
return self.sourceModel().headerData(section, orientation, role)
def mapToSource(self, index):
if not index.isValid():
return QtCore.QModelIndex()
parent = index.internalPointer()
if not parent.isValid():
return QtCore.QModelIndex()
elif parent == self._rootItem:
return QtCore.QModelIndex()
else:
rowItem_ = self._groups[parent.row()].children[index.row()]
sourceRow = self._sourceRows.index(rowItem_)
return self.createIndex(sourceRow, index.column(), QtCore.QModelIndex())
def mapFromSource(self, index):
rowItem_ = self._sourceRows[index.row()]
groupRow = self._getGroupRow(rowItem_.groupIndex)
itemRow = self._groups[groupRow].children.index(rowItem_)
return self.createIndex(itemRow,index.column(),self._groupIndexes[groupRow])
def _clearGroups(self):
self._groupMap = {}
self._groups = []
self._sourceRows = []
def groupBy(self,column=0):
self.beginResetModel()
self._clearGroups()
self._groupColumn = column
sourceModel = self.sourceModel()
for row in range(sourceModel.rowCount(QtCore.QModelIndex())):
groupName = sourceModel.data(self.createIndex(row,column,0),
QtCore.Qt.DisplayRole)
groupIndex = self._getGroupIndex(groupName)
rowItem_ = rowItem(groupIndex,random.random())
self._groups[groupIndex.row()].children.append(rowItem_)
self._sourceRows.append(rowItem_)
self.endResetModel()
def _getGroupIndex(self, groupName):
""" return the index for a group denoted with name.
if there is no group with given name, create and then return"""
if groupName in self._groupMap:
return self._groupMap[groupName]
else:
groupRow = len(self._groupMap)
groupIndex = self.createIndex(groupRow,0,self._rootItem)
self._groupMap[groupName] = groupIndex
self._groups.append(groupItem(groupName,[],groupIndex))
self._groupIndexes.append(groupIndex)
self.layoutChanged.emit()
return groupIndex
def _getGroupRow(self, groupIndex):
for i,x in enumerate(self._groupIndexes):
if id(groupIndex)==id(x):
return i
return 0
def _rowsInserted(self, parent, start, end):
for row in range(start, end+1):
groupName = self.sourceModel().data(self.createIndex(row,self._groupColumn,0),
QtCore.Qt.DisplayRole)
groupIndex = self._getGroupIndex(groupName)
self._getGroupRow(groupIndex)
groupItem_ = self._groups[self._getGroupRow(groupIndex)]
rowItem_ = rowItem(groupIndex,random.random())
groupItem_.children.append(rowItem_)
self._sourceRows.insert(row, rowItem_)
self.layoutChanged.emit()
def _rowsRemoved(self, parent, start, end):
for row in range(start, end+1):
rowItem_ = self._sourceRows[start]
groupIndex = rowItem_.groupIndex
groupItem_ = self._groups[self._getGroupRow(groupIndex)]
childrenRow = groupItem_.children.index(rowItem_)
groupItem_.children.pop(childrenRow)
self._sourceRows.pop(start)
if not len(groupItem_.children):
# remove the group
groupRow = self._getGroupRow(groupIndex)
groupName = self._groups[groupRow].name
self._groups.pop(groupRow)
self._groupIndexes.pop(groupRow)
del self._groupMap[groupName]
self.layoutChanged.emit()
def _dataChanged(self, topLeft, bottomRight):
topRow = topLeft.row()
bottomRow = bottomRight.row()
sourceModel = self.sourceModel()
# loop through all the changed data
for row in range(topRow,bottomRow+1):
oldGroupIndex = self._sourceRows[row].groupIndex
oldGroupItem = self._groups[self._getGroupRow(oldGroupIndex)]
newGroupName = sourceModel.data(self.createIndex(row,self._groupColumn,0),QtCore.Qt.DisplayRole)
if newGroupName != oldGroupItem.name:
# move to new group...
newGroupIndex = self._getGroupIndex(newGroupName)
newGroupItem = self._groups[self._getGroupRow(newGroupIndex)]
rowItem_ = self._sourceRows[row]
newGroupItem.children.append(rowItem_)
# delete from old group
oldGroupItem.children.remove(rowItem_)
if not len(oldGroupItem.children):
# remove the group
groupRow = self._getGroupRow(oldGroupItem.index)
groupName = oldGroupItem.name
self._groups.pop(groupRow)
self._groupIndexes.pop(groupRow)
del self._groupMap[groupName]
self.layoutChanged.emit()

Categories

Resources