I created my own QAbstractItemModel with a tree-like structure and several sub tables like this:
group1
- order1
- item1
- item2
- order2
- item3
group2
- order3
- item4
- item5
- item6
I my goal is it to have a tree and a table view. When I click on an order in the tree view I want to display a table with all items (and their attributes) in a table. So far I got it working.
Now I obviously need headers for the table view, this is where I am somewhat stuck. I tried implementing the solution from this question but it is not working properly. This is what I got so far:
import sys
from PyQt5 import QtCore, QtWidgets
class Node(object):
def __init__(self, parent=None, item_count=0):
self.parent = parent
self.children = []
self.item_count = item_count
if self.parent is not None:
self.parent.add_child(self)
def add_child(self, child):
self.children.append(child)
child.parent = self
#property
def row(self):
if self.parent is None:
return 0
return self.parent.children.index(self)
#property
def child_item_count(self):
if len(self.children):
return self.children[0].item_count
return 1
#property
def child_count(self):
return len(self.children)
def child(self, row):
try:
return self.children[row]
except IndexError:
return None
def header_data(self):
return []
class ItemNode(Node):
def __init__(self, parent=None, name='', description='', price=''):
super(ItemNode, self).__init__(parent=parent, item_count=3)
self.name = name
self.description = description
self.price = price
def data(self, column):
if column == 0: return self.name
if column == 1: return self.description
if column == 2: return self.price
return None
class OrderNode(Node):
def __init__(self, parent=None, order_id='', order_date=''):
super(OrderNode, self).__init__(parent=parent, item_count=2)
self.order_id = order_id
self.order_date = order_date
def data(self, column):
if column == 0: return self.order_id
if column == 1: return self.order_date
return None
def header_data(self):
return ['Name', 'Description', 'Price']
class GroupNode(Node):
def __init__(self, parent=None, name='', description=''):
super(GroupNode, self).__init__(parent=parent, item_count=2)
self.name = name
self.description = description
def data(self, column):
if column == 0: return self.name
if column == 1: return self.description
return None
class ItemModel(QtCore.QAbstractItemModel):
def __init__(self, root=None):
if root is None:
root = Node()
self.root = root
super(ItemModel, self).__init__()
def rowCount(self, parent=None, *args, **kwargs):
node = self.get_node(parent)
return node.child_count
def columnCount(self, parent=None, *args, **kwargs):
node = self.get_node(parent)
return node.child_item_count
def index(self, row, column, parent=None, *args, **kwargs):
node = self.get_node(parent)
return self.createIndex(row, column, node.child(row))
def parent(self, index=None):
node = self.get_node(index)
if node.parent is None:
return QtCore.QModelIndex()
return self.createIndex(node.parent.row, 0, node.parent)
def data(self, index, role=QtCore.Qt.DisplayRole):
node = self.get_node(index)
if role == QtCore.Qt.DisplayRole:
return node.data(index.column())
if role == QtCore.Qt.UserRole + 1: # horizontal header role
return node.header_data()
def get_node(self, index):
if index and index.isValid():
node = index.internalPointer()
if node:
return node
return self.root
def is_order(self, index):
node = self.get_node(index)
return isinstance(node, OrderNode)
class ProxyItemModel(QtCore.QSortFilterProxyModel):
horizontal_header_role = QtCore.Qt.UserRole + 1
def __init__(self):
self.root_index = QtCore.QModelIndex()
super(ProxyItemModel, self).__init__()
def set_root_index(self, index):
self.root_index = self.mapToSource(index)
if self.sourceModel() and self.root_index.internalPointer() is not None:
self.headerDataChanged.emit(
QtCore.Qt.Horizontal, 0, self.sourceModel().columnCount(self.root_index)
)
def headerData(self, section, orientation, role=QtCore.Qt.DisplayRole):
if self.sourceModel() and self.root_index.isValid():
if orientation == QtCore.Qt.Horizontal:
role = self.horizontal_header_role
header_list = self.sourceModel().data(self.root_index, role)
if header_list and 0 <= section < len(header_list):
return header_list[section]
return super(ProxyItemModel, self).headerData(section, orientation, role)
def is_order(self, index):
source_index = self.mapToSource(index)
return self.sourceModel().is_order(source_index)
class MainWindow(QtWidgets.QMainWindow):
def __init__(self, *args, **kwargs):
super(MainWindow, self).__init__(*args, **kwargs)
self.central_widget = QtWidgets.QWidget(self)
self.setCentralWidget(self.central_widget)
central_layout = QtWidgets.QVBoxLayout(self)
self.central_widget.setLayout(central_layout)
self.treeView = QtWidgets.QTreeView(self)
self.tableView = QtWidgets.QTableView(self)
central_layout.addWidget(self.treeView)
central_layout.addWidget(self.tableView)
self.data_model = ItemModel()
self.set_up_data_model()
self.proxy_model = ProxyItemModel()
self.proxy_model.setSourceModel(self.data_model)
self.treeView.setModel(self.proxy_model)
self.treeView.header().hide()
self.tableView.setModel(self.proxy_model)
self.treeView.selectionModel().currentChanged.connect(self.set_root_index)
self.treeView.selectionModel().currentChanged.connect(self.hide_show_table)
self.show()
def set_root_index(self, index):
self.tableView.setRootIndex(index)
self.proxy_model.set_root_index(index)
def hide_show_table(self, index):
if self.proxy_model.is_order(index):
self.tableView.show()
else:
self.tableView.hide()
def set_up_data_model(self):
root = self.data_model.root
group1 = GroupNode(root, name='G1', description='Order Group 1')
group2 = GroupNode(root, name='G2', description='Order Group 2')
order1 = OrderNode(group1, order_id='123', order_date='17.10.2018')
item1 = ItemNode(order1, name='Item 1', price='345')
item2 = ItemNode(order1, name='Item 2', description='item number 2', price='99')
order2 = OrderNode(group1, order_id='987')
if __name__ == '__main__':
app = QtWidgets.QApplication(sys.argv)
ui = MainWindow()
sys.exit(app.exec_())
Now this code results in the horizontal header not being displayed at all. However, when I substitute 0 <= section < len(header_list) in the headerData method with 0 < section < len(header_list), the header is displayed correctly - except for the first column which is just empty. Something must be going on with the code when section = 0 but for the life of me I can't figure out what it is. I had a look with the debugger and saw that for section = 0 both role and header_list have the expected values.
Any help will be greatly appreciated. I tried to delete everything that is not needed to get a minimal working example, it still turned out quite lengthy though, sorry about that.
You have to verify the section is Qt::Horizontal and the role is Qt::DisplayRole, in your case you did not do the second verification so it was overwritten generating that error.
def headerData(self, section, orientation, role=QtCore.Qt.DisplayRole):
if self.sourceModel() and self.root_index.isValid():
if orientation == QtCore.Qt.Horizontal and role == QtCore.Qt.DisplayRole:
header_list = self.sourceModel().data(self.root_index, self.horizontal_header_role)
if header_list and 0 <= section < len(header_list):
return header_list[section]
return super(ProxyItemModel, self).headerData(section, orientation, role)
Related
In the code example below I populate an item model with a set of top level items which contain key-value properties that I can view and edit with a QTreeView.
Looking at the Qt Documentation for QAbstractItemModel::columnCount it says that this should return the number of columns for the children of the given parent, meaning that this should be a hierarchical dependent property.
However, using the code below, if I return the column count as a hierarchical dependent property (in this case root->children have 1 column, root->child->children have 2 columns) then the view will only display 1 column.
Printing node.columnCount() (see the code) will actually show that the Item class nodes do in fact return columnCount = 2 after you expand one of the items.
If I just always return 2 for the model.columnCount function, then the view will properly display both columns.
Is this required to always return the desired number of columns in a view no matter the hierarchy or am I just doing something wrong and if so what? Returning a number of columns for a parent whose children have a different number of columns just to make the view work properly feels like it must be wrong.
import sys
import typing
from PyQt5 import QtCore, QtWidgets
class Node:
def __init__(self, parent=None):
self.parent = parent # type: Node
self.name : str
def children(self) -> list:
return None
def hasChildren(self):
return bool(self.children())
def getData(self, index: QtCore.QModelIndex):
if index.column() == 0:
return self.name
def setData(self, val, index: QtCore.QModelIndex):
if index.column() == 0:
self.name = val
def columnCount(self):
return 1
def rowCount(self):
children = self.children()
return 0 if not children else len(children)
def flags(self, index: QtCore.QModelIndex):
if index.column() == 0:
return (QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEditable)
else:
return QtCore.Qt.NoItemFlags
class Property(Node):
def __init__(self, parent, label, value):
super().__init__(parent)
self.label = label
self.value = value
def getData(self, index: QtCore.QModelIndex):
col = index.column()
if col == 0:
return self.label
elif col == 1:
return self.value
def setData(self, val, index: QtCore.QModelIndex):
if index.column() == 1:
self.value = val
def flags(self, index: QtCore.QModelIndex):
col = index.column()
if col == 0:
return QtCore.Qt.ItemIsEnabled
elif col == 1:
return QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsEditable | QtCore.Qt.ItemIsEditable
class Item(Node):
def __init__(self, parent):
super().__init__(parent)
self.name = 'Item'
self.p1 = Property(self, 'string', 'text')
self.p2 = Property(self, 'float', 1.2)
def children(self):
return [self.p1, self.p2]
def columnCount(self):
return 2
class Root(Node):
def __init__(self):
super().__init__(parent=None)
self._children = list()
def children(self):
return self._children
class Model(QtCore.QAbstractItemModel):
def __init__(self):
super().__init__()
self.root = Root()
def index(self, row: int, column: int, parent: QtCore.QModelIndex = ...) -> QtCore.QModelIndex:
if not self.hasIndex(row, column, parent):
return QtCore.QModelIndex()
node = parent.internalPointer() if parent.isValid() else self.root
if node.children:
return self.createIndex(row, column, node.children()[row])
else:
return QtCore.QModelIndex()
def parent(self, child: QtCore.QModelIndex) -> QtCore.QModelIndex:
if not child.isValid():
return QtCore.QModelIndex()
node = child.internalPointer() # type: Node
if node.parent and node.parent.parent:
row = node.parent.parent.children().index(node.parent)
return self.createIndex(row, 0, node.parent)
else:
return QtCore.QModelIndex()
def rowCount(self, parent: QtCore.QModelIndex = ...) -> int:
node = parent.internalPointer() if parent.isValid() else self.root
children = node.children()
return len(children) if children else 0
def columnCount(self, parent: QtCore.QModelIndex = ...) -> int:
node = parent.internalPointer() if parent.isValid() else self.root
print(f'{node.__class__.__name__} column count: ', node.columnCount()) # shows that column count 2 is returned, when items are expanded
# return 2 # 2nd column only shows up if I just always return 2
return node.columnCount() # view only shows 1 columns
def hasChildren(self, parent: QtCore.QModelIndex = ...) -> bool:
node = parent.internalPointer() if parent.isValid() else self.root
return node.hasChildren()
def data(self, index: QtCore.QModelIndex, role: int = ...):
if index.isValid() and role in (QtCore.Qt.DisplayRole, QtCore.Qt.EditRole):
node = index.internalPointer() # type: Node
return node.getData(index)
else:
return None
def setData(self, index: QtCore.QModelIndex, value: typing.Any, role: int = ...) -> bool:
if role in (QtCore.Qt.EditRole,):
node = index.internalPointer() # type: Node
node.setData(value, index)
self.dataChanged.emit(index, index)
return True
else:
return False
def flags(self, index: QtCore.QModelIndex):
node = index.internalPointer() if index.isValid() else self.root
return node.flags(index)
def appendRow(self, item):
row = len(self.root.children())
self.beginInsertRows(QtCore.QModelIndex(), row, row)
self.root.children().append(item)
self.endInsertRows()
class TreeView(QtWidgets.QTreeView):
def __init__(self, parent=None):
super(TreeView, self).__init__(parent)
self._model = Model()
self.setModel(self._model)
self.setSelectionMode(self.ExtendedSelection)
# self.setDropIndicatorShown(False)
self.setEditTriggers(self.DoubleClicked | self.SelectedClicked | self.EditKeyPressed)
def model(self) -> Model:
return self._model
sys.excepthook = sys.__excepthook__
app = QtWidgets.QApplication(sys.argv)
widget = TreeView()
model = widget.model()
for i in range(2):
model.appendRow(Item(model.root))
widget.show()
widget.setAttribute(QtCore.Qt.WA_DeleteOnClose)
sys.exit(app.exec_())
It seems that the docs is not clear and does not exactly match the implementation, in the implementation the number of columns in the view is dependent on the horizontal QHeaderView, and the horizontal QHeaderView uses the number of columns of the root that is the invisible item, ie the number of columns should be given by Root(), and since Root() does not overwrite the columnCount() it will have the value of 1 by default (although for me the columnCount() of Node must be 0 and children() must return a empty list), so the solution is set to 2 in Root columnCount().
import sys
import typing
from PyQt5 import QtCore, QtWidgets
class Node:
def __init__(self, parent=None):
self.parent = parent # type: Node
self.name : str
def children(self) -> list:
return list()
def hasChildren(self):
return bool(self.children())
def getData(self, index: QtCore.QModelIndex):
if index.column() == 0:
return self.name
def setData(self, val, index: QtCore.QModelIndex):
if index.column() == 0:
self.name = val
def columnCount(self):
return 0
def rowCount(self):
children = self.children()
return len(children)
def flags(self, index: QtCore.QModelIndex):
if index.column() == 0:
return (QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEditable)
else:
return QtCore.Qt.NoItemFlags
class Property(Node):
def __init__(self, parent, label, value):
super().__init__(parent)
self.label = label
self.value = value
def getData(self, index: QtCore.QModelIndex):
col = index.column()
if col == 0:
return self.label
elif col == 1:
return self.value
def setData(self, val, index: QtCore.QModelIndex):
if index.column() == 1:
self.value = val
def flags(self, index: QtCore.QModelIndex):
col = index.column()
if col == 0:
return QtCore.Qt.ItemIsEnabled
elif col == 1:
return QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsEditable | QtCore.Qt.ItemIsEditable
def columnCount(self):
return 1
class Item(Node):
def __init__(self, parent):
super().__init__(parent)
self.name = 'Item'
self.p1 = Property(self, 'string', 'text')
self.p2 = Property(self, 'float', 1.2)
def children(self):
return [self.p1, self.p2]
def columnCount(self):
return 2
class Root(Node):
def __init__(self):
super().__init__(parent=None)
self._children = list()
def children(self):
return self._children
def columnCount(self):
return 2
class Model(QtCore.QAbstractItemModel):
def __init__(self):
super().__init__()
self.root = Root()
def index(self, row: int, column: int, parent: QtCore.QModelIndex = ...) -> QtCore.QModelIndex:
if not self.hasIndex(row, column, parent):
return QtCore.QModelIndex()
node = parent.internalPointer() if parent.isValid() else self.root
if node.children:
return self.createIndex(row, column, node.children()[row])
else:
return QtCore.QModelIndex()
def parent(self, child: QtCore.QModelIndex) -> QtCore.QModelIndex:
if not child.isValid():
return QtCore.QModelIndex()
node = child.internalPointer() # type: Node
if node.parent and node.parent.parent:
row = node.parent.parent.children().index(node.parent)
return self.createIndex(row, 0, node.parent)
else:
return QtCore.QModelIndex()
def rowCount(self, parent: QtCore.QModelIndex = ...) -> int:
node = parent.internalPointer() if parent.isValid() else self.root
children = node.children()
return len(children) if children else 0
def columnCount(self, parent: QtCore.QModelIndex = ...) -> int:
node = parent.internalPointer() if parent.isValid() else self.root
print(f'{node.__class__.__name__} column count: ', node.columnCount()) # shows that column count 2 is returned, when items are expanded
# return 2 # 2nd column only shows up if I just always return 2
return node.columnCount() # view only shows 1 columns
def hasChildren(self, parent: QtCore.QModelIndex = ...) -> bool:
node = parent.internalPointer() if parent.isValid() else self.root
return node.hasChildren()
def data(self, index: QtCore.QModelIndex, role: int = ...):
if index.isValid() and role in (QtCore.Qt.DisplayRole, QtCore.Qt.EditRole):
node = index.internalPointer() # type: Node
return node.getData(index)
else:
return None
def setData(self, index: QtCore.QModelIndex, value: typing.Any, role: int = ...) -> bool:
if role in (QtCore.Qt.EditRole,):
node = index.internalPointer() # type: Node
node.setData(value, index)
self.dataChanged.emit(index, index)
return True
else:
return False
def flags(self, index: QtCore.QModelIndex):
node = index.internalPointer() if index.isValid() else self.root
return node.flags(index)
def appendRow(self, item):
row = len(self.root.children())
self.beginInsertRows(QtCore.QModelIndex(), row, row)
self.root.children().append(item)
self.endInsertRows()
class TreeView(QtWidgets.QTreeView):
def __init__(self, parent=None):
super(TreeView, self).__init__(parent)
self._model = Model()
self.setModel(self._model)
self.setSelectionMode(self.ExtendedSelection)
# self.setDropIndicatorShown(False)
self.setEditTriggers(self.DoubleClicked | self.SelectedClicked | self.EditKeyPressed)
def model(self) -> Model:
return self._model
sys.excepthook = sys.__excepthook__
app = QtWidgets.QApplication(sys.argv)
widget = TreeView()
model = widget.model()
for i in range(2):
model.appendRow(Item(model.root))
widget.show()
widget.setAttribute(QtCore.Qt.WA_DeleteOnClose)
sys.exit(app.exec_())
As #eyllanesc answered, the number of columns in the View is only affected by the root item. However, columnCount() will have an effect on rows that give a number that is less than this number, in that columns less than this number will not be populated.
Taking the above code:
class Root(Node):
...
def columnCount():
return 2
class Property(Node):
...
def columnCount():
return 1
In this case even though two columns are shown, the Property nodes will not show data for the second column because the view is being told by columnCount() that there is only 1 column
This question is related to this previous question:
how to use QSortFilterProxyModel for filter a 2d array?
i have been trying to stack several proxy model to display a 2d array of data into a qtableview.
#eyllanesc provided a really cool solution to my question but it doesn't seem to be compatible with qitemdelegate. When i add it to his example the delegate doesnt display as expected. Without the proxy3 it does show correctly.
import random
import math
from PyQt4 import QtCore, QtGui
class TableModel(QtCore.QAbstractTableModel):
def __init__(self, data, columns, parent=None):
super(TableModel, self).__init__(parent)
self._columns = columns
self._data = data[:]
def rowCount(self, parent=QtCore.QModelIndex()):
if parent.isValid() or self._columns == 0:
return 0
return math.ceil(len(self._data )*1.0/self._columns)
def columnCount(self, parent=QtCore.QModelIndex()):
if parent.isValid():
return 0
return self._columns
def data(self, index, role=QtCore.Qt.DisplayRole):
if not index.isValid():
return
if role == QtCore.Qt.DisplayRole:
try:
value = self._data[ index.row() * self._columns + index.column() ]
return value
except:
pass
class Table2ListProxyModel(QtGui.QIdentityProxyModel):
def columnCount(self, parent=QtCore.QModelIndex()):
return 1
def rowCount(self, parent=QtCore.QModelIndex()):
if parent.isValid():
return 0
return self.sourceModel().rowCount() * self.sourceModel().columnCount()
def mapFromSource(self, sourceIndex):
if sourceIndex.isValid() and sourceIndex.column() == 0 \
and sourceIndex.row() < self.rowCount():
r = sourceIndex.row()
c = sourceIndex.column()
row = c * sourceIndex.model().columnCount() + r
return self.index(row, 0)
return QtCore.QModelIndex()
def mapToSource(self, proxyIndex):
r = proxyIndex.row() / self.sourceModel().columnCount()
c = proxyIndex.row() % self.sourceModel().columnCount()
return self.sourceModel().index(r, c)
def index(self, row, column, parent=QtCore.QModelIndex()):
return self.createIndex(row, column)
class ListFilterProxyModel(QtGui.QSortFilterProxyModel):
def setThreshold(self, value):
setattr(self, "threshold", value)
self.invalidateFilter()
def filterAcceptsRow(self, row, parent):
if hasattr(self, "threshold"):
ix = self.sourceModel().index(row, 0)
val = ix.data()
if val is None:
return False
return int(val.toString()) < getattr(self, "threshold")
return True
class List2TableProxyModel(QtGui.QIdentityProxyModel):
def __init__(self, columns=1, parent=None):
super(List2TableProxyModel, self).__init__(parent)
self._columns = columns
def columnCount(self, parent=QtCore.QModelIndex()):
return self._columns
def rowCount(self, parent=QtCore.QModelIndex()):
if parent.isValid():
return 0
return math.ceil(self.sourceModel().rowCount()/self._columns)
def index(self, row, column, parent=QtCore.QModelIndex()):
return self.createIndex(row, column)
def data(self, index, role=QtCore.Qt.DisplayRole):
r = index.row()
c = index.column()
row = r * self.columnCount() + c
if row < self.sourceModel().rowCount():
return super(List2TableProxyModel, self).data(index, role)
def mapFromSource(self, sourceIndex):
r = math.ceil(sourceIndex.row() / self.columnCount())
c = sourceIndex.row() % self.columnCount()
return self.index(r, c)
def mapToSource(self, proxyIndex):
if proxyIndex.isValid():
r = proxyIndex.row()
c = proxyIndex.column()
row = r * self.columnCount() + c
return self.sourceModel().index(row, 0)
return QtCore.QModelIndex()
class Delegate(QtGui.QItemDelegate):
def __init__(self, parent = None):
QtGui.QItemDelegate.__init__(self, parent)
def paint(self, painter, option, index):
number = str(index.data(QtCore.Qt.DisplayRole).toString())
widget = QtGui.QWidget()
layout = QtGui.QVBoxLayout()
widget.setLayout( layout )
title = QtGui.QLabel("<font color='red'>"+number+"</font>")
layout.addWidget( title )
if not self.parent().tvf.indexWidget(index):
self.parent().tvf.setIndexWidget(
index,
widget
)
QtGui.QItemDelegate.paint(self, painter, option, index)
class Widget(QtGui.QWidget):
def __init__(self, parent=None):
QtGui.QWidget.__init__(self, parent)
data = [random.choice(range(10)) for i in range(20)]
l = QtGui.QHBoxLayout(self)
splitter = QtGui.QSplitter()
l.addWidget(splitter)
tv = QtGui.QTableView()
lv = QtGui.QListView()
lvf = QtGui.QListView()
self.tvf = QtGui.QTableView()
delegate = Delegate(self)
self.tvf.setItemDelegate(delegate)
model = TableModel(data, 3, self)
proxy1 = Table2ListProxyModel(self)
proxy1.setSourceModel(model)
proxy2 = ListFilterProxyModel(self)
proxy2.setSourceModel(proxy1)
proxy2.setThreshold(5)
proxy3 = List2TableProxyModel(3, self)
proxy3.setSourceModel(proxy2)
tv.setModel(model)
lv.setModel(proxy1)
lvf.setModel(proxy2)
self.tvf.setModel(proxy3)
splitter.addWidget(tv)
splitter.addWidget(lv)
splitter.addWidget(lvf)
splitter.addWidget(self.tvf )
if __name__=="__main__":
import sys
a=QtGui.QApplication(sys.argv)
w=Widget()
w.show()
sys.exit(a.exec_())
Here is the expected result I am looking for:
and this is what it look like when i bypass the proxy model (the numbers are red).
replacing :
self.tvf.setModel(proxy3)
to
self.tvf.setModel(model)
You are incorrectly using the delegate that works sometimes if and sometimes not. The concepts of delegate and IndexWidget are 2 alternatives, the first one is a low level painting but also low cost, the second instead a widget is embedded in the item task simple but expensive since a widget is much more than a simple painted , also has a logic.
In this case the solution is to use a QStyledItemDelegate and overwrite the initStyleOption() method by modifying the palette.
class Delegate(QtGui.QStyledItemDelegate):
def initStyleOption(self, option, index):
super(Delegate, self).initStyleOption(option, index)
option.palette.setBrush(QtGui.QPalette.Text, QtGui.QColor("red"))
Note: Note: it is not necessary to write
def __init__(self, parent = None):
QtGui.QItemDelegate.__init__(self, parent)
since it is not modified.
I am working on a simple tree directory explorer based on a qtreeview with a model view/controller/implementation. I need to use some threads that recursively search the sub-folders and feed the model/datas of the qtreeview. All of this works fine.
But my issue is that the view doesnt refresh when the datas change ...
I have tried a few different things, but iam not happy with any of the solutions:
QtGui.QStandardItemModel.dataChanged.emit(QtCore.QModelIndex(), QtCore.QModelIndex())
Emitting data change from setData() of the model should update the view, but it doesnt work for me. Also i havnt found an elegant way of finding the qmodelIndex. Iam just recusively loop all the datas to find the right Index.
from PyQt4 import QtCore, QtGui
from PyQt4.QtGui import *
from PyQt4.QtCore import *
import time
import traceback, sys, os
from glob import glob
from random import randrange
import traceback
DEPTH = 0
threadpool = QThreadPool()
##########################################
###### Example thread function #####
##########################################
def listFolders( parent ):
global DEPTH
time.sleep(2)
if DEPTH>4:
return {'fileList':[], 'parent':parent}
else:
DEPTH+=1
fileList = []
for item in range(randrange(1,5)):
fileList.append('item_'+str(item))
return {'fileList':fileList, 'parent':parent}
##########################################
###### simple threading #####
##########################################
class WorkerSignals(QObject):
finished = pyqtSignal()
error = pyqtSignal(tuple)
result = pyqtSignal(object)
progress = pyqtSignal(int)
class Worker(QRunnable):
def __init__(self, fn, *args, **kwargs):
super(Worker, self).__init__()
# Store constructor arguments (re-used for processing)
self.fn = fn
self.args = args
self.kwargs = kwargs
self.signals = WorkerSignals()
#pyqtSlot()
def run(self):
try:
result = self.fn(*self.args, **self.kwargs)
except:
traceback.print_exc()
exctype, value = sys.exc_info()[:2]
self.signals.error.emit((exctype, value, traceback.format_exc()))
else:
self.signals.result.emit(result) # Return the result of the processing
finally:
self.signals.finished.emit() # Done
##########################################
###### Model for qtreeview #####
##########################################
class SceneGraphModel(QtCore.QAbstractItemModel):
def __init__(self, root ,parent=None):
super(SceneGraphModel, self).__init__(parent)
self._rootNode = root
def rowCount(self, parent):
if not parent.isValid():
parentNode = self._rootNode
else:
parentNode = parent.internalPointer()
return parentNode.childCount()
def columnCount(self, parent):
return 1
def data(self, index, role):
if not index.isValid():
return None
node = index.internalPointer()
if role == QtCore.Qt.DisplayRole or role == QtCore.Qt.EditRole:
if index.column() == 0:
return node.name()
def setData(self, index, value, role=QtCore.Qt.EditRole):
if index.isValid():
if role == QtCore.Qt.EditRole:
node = index.internalPointer()
node.setName(value)
return True
return False
def headerData(self, section, orientation, role):
if role == QtCore.Qt.DisplayRole:
if section == 0:
return "Scenegraph"
def flags(self, index):
return QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEditable
def parent(self, index):
node = self.getNode(index)
parentNode = node.parent()
if parentNode == self._rootNode:
return QtCore.QModelIndex()
if parentNode == None:
row = 0
else:
row = parentNode.row()
return self.createIndex(row, 0, parentNode)
def index(self, row, column, parent):
parentNode = self.getNode(parent)
childItem = parentNode.child(row)
if childItem:
return self.createIndex(row, column, childItem)
else:
return QtCore.QModelIndex()
def getNode(self, index):
if index.isValid():
node = index.internalPointer()
if node:
return node
return self._rootNode
##########################################
###### Node class that contain the qtreeview datas #####
##########################################
class Node(object):
def __init__(self, name, parent=None):
self._name = name
self._children = []
self._parent = parent
if parent is not None:
parent.addChild(self)
def typeInfo(self):
return "folder"
def addChild(self, child):
self._children.append(child)
def name(self):
return self._name
def child(self, row):
return self._children[row]
def childCount(self):
return len(self._children)
def parent(self):
return self._parent
def row(self):
if self._parent is not None:
return self._parent._children.index(self)
def __repr__(self):
return 'NODE_'+self.name()
##########################################
###### qtreeview containing the threading #####
##########################################
class DirectoryTree(QTreeView):
def __init__(self):
super(DirectoryTree, self).__init__()
#create root node
self.rootNode = Node('root')
#add model to treeview
self._model = SceneGraphModel(self.rootNode)
self.setModel(self._model)
#recurive loop with thread to add more datas
self.loop( self.rootNode )
def thread(self, path):
return listFolders(path)
def threadResult(self, result ):
for item in result['fileList']:
newNode = Node(item,result['parent'])
self.loop(newNode)
def loop(self, parent ):
worker = Worker( self.thread, parent )
worker.signals.result.connect( self.threadResult )
threadpool.start(worker)
##########################################
###### window with countdown #####
##########################################
class MainWindow(QMainWindow):
def __init__(self, *args, **kwargs):
super(MainWindow, self).__init__(*args, **kwargs)
self.counter = 0
self.layout = QVBoxLayout()
self.l = QLabel("Start")
self.layout.addWidget(self.l)
w = QWidget()
w.setLayout(self.layout)
self.setCentralWidget(w)
self.treeView = DirectoryTree()
self.layout.addWidget(self.treeView)
self.show()
self.timer = QTimer()
self.timer.setInterval(1000)
self.timer.timeout.connect(self.recurring_timer)
self.timer.start()
self.setGeometry(0, 0, 650, 550)
self.setWindowTitle("shot tree")
self.centerOnScreen()
def centerOnScreen (self):
resolution = QtGui.QDesktopWidget().screenGeometry()
self.move((resolution.width() / 2) - (self.frameSize().width() / 2),
(resolution.height() / 2) - (self.frameSize().height() / 2))
def recurring_timer(self):
self.counter +=1
self.l.setText("Counter: %d" % self.counter)
##### This is a hack to refresh the view
##### i want to remove this line
##### and properly emit the changes from the node class to refresh the qtreeview
self.treeView.expandAll()
app = QApplication([])
window = MainWindow()
app.exec_()
This is my code example. there is a count down in the main window that would execute : self.treeView.expandAll()
every second to force the view to update, i want to find a better solution ...
Related topics i found:
Refresh view when model data has not changed (Qt/PySide/PyQt)?
PyQt and MVC-pattern
The problem has nothing to do with threads. For the view to be notified the model must emit the signal layoutAboutToBeChanged before the change and layoutChanged after the change, but for this the node must access the model, so the model must be made as a Node attribute. With that change you no longer need a QTimer to update the view.
class SceneGraphModel(QtCore.QAbstractItemModel):
def __init__(self, root, parent=None):
super(SceneGraphModel, self).__init__(parent)
self._rootNode = root
self._rootNode._model = self
def rowCount(self, parent=QtCore.QModelIndex()):
if not parent.isValid():
parentNode = self._rootNode
else:
parentNode = parent.internalPointer()
return parentNode.childCount()
def columnCount(self, parent=QtCore.QModelIndex()):
return 1
def data(self, index, role=QtCore.Qt.DisplayRole):
if not index.isValid():
return None
node = index.internalPointer()
if role == QtCore.Qt.DisplayRole or role == QtCore.Qt.EditRole:
if index.column() == 0:
return node.name()
def setData(self, index, value, role=QtCore.Qt.EditRole):
if index.isValid():
if role == QtCore.Qt.EditRole:
node = index.internalPointer()
node.setName(value)
return True
return False
def headerData(self, section, orientation, role=QtCore.Qt.DisplayRole):
if role == QtCore.Qt.DisplayRole:
if section == 0:
return "Scenegraph"
def flags(self, index):
return QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEditable
def parent(self, index):
node = self.getNode(index)
parentNode = node.parent()
if parentNode == self._rootNode:
return QtCore.QModelIndex()
if parentNode is None:
row = 0
else:
row = parentNode.row()
return self.createIndex(row, 0, parentNode)
def index(self, row, column, parent=QtCore.QModelIndex()):
parentNode = self.getNode(parent)
childItem = parentNode.child(row)
if childItem:
return self.createIndex(row, column, childItem)
else:
return QtCore.QModelIndex()
def getNode(self, index):
if index.isValid():
node = index.internalPointer()
if node:
return node
print("node", node)
return self._rootNode
class Node(object):
def __init__(self, name, parent=None):
self._name = name
self._children = []
self._parent = parent
self._model = None
if parent is not None:
parent.addChild(self)
def typeInfo(self):
return "folder"
def addChild(self, child):
self._model.layoutAboutToBeChanged.emit()
self._children.append(child)
child._model = self._model
self._model.layoutChanged.emit()
def name(self):
return self._name
def setName(self, name):
self._name = name
def child(self, row):
return self._children[row] if row < len(self._children) else None
def childCount(self):
return len(self._children)
def parent(self):
return self._parent
def row(self):
return 0 if self.parent() is None else self._parent._children.index(self)
def __repr__(self):
return 'NODE_' + self.name()
I have problems implementing Qt tree model with lazy loading using canFetchMore() and fetchMore(). I have this code:
from PySide.QtCore import Qt, QAbstractItemModel, QModelIndex
from PySide.QtWidgets import QTreeView, QApplication
BATCH_SIZE = 100
class ObjectItem(object):
def __init__(self, parent, name, data, row):
self.parent = parent
self.name = name
self.data = data
self.row = row
self.hasChildren = False
self.canFetchMore = False
# ints have no children
if isinstance(data, int):
return
self.childCount = 0
self.childItems = []
if len(data) > 0:
self.canFetchMore = True
self.hasChildren = True
self.childCount = len(self.childItems)
if self.childCount > 0:
self.hasChildren = True
def fetchMore(self):
loadedCount = len(self.childItems)
totalCount = len(self.data)
remainder = totalCount - loadedCount
fetchCount = min(BATCH_SIZE, remainder)
for _ in range(fetchCount):
name = "[{}]".format(loadedCount)
value = self.data[loadedCount]
self.childItems.append(ObjectItem(self, name, value, loadedCount))
loadedCount += 1
self.canFetchMore = loadedCount < totalCount
return fetchCount
class MyTreeModel(QAbstractItemModel):
def __init__(self, data, parent=None):
super(MyTreeModel, self).__init__(parent)
self.rootItem = ObjectItem(None, "root", data, 0)
self.headerLabels = ["Name", "Type", "Value"]
def hasChildren(self, parent=QModelIndex()):
return parent.internalPointer().hasChildren if parent.isValid() else True
def rowCount(self, parent=QModelIndex()):
return parent.internalPointer().childCount if parent.isValid() else 1
def columnCount(self, parent=QModelIndex()):
return 3
def index(self, row, column, parent=QModelIndex()):
if not parent.isValid():
return self.createIndex(row, column, self.rootItem)
parentItem = parent.internalPointer()
return self.createIndex(row, column, parentItem.childItems[row])
def parent(self, index):
item = index.internalPointer()
if item == self.rootItem:
return QModelIndex()
parentItem = item.parent
return self.createIndex(parentItem.row, 0, parentItem)
def data(self, index, role):
item = index.internalPointer()
if role == Qt.DisplayRole:
if index.column() == 0:
return item.name
elif index.column() == 1:
return item.data.__class__.__name__
elif index.column() == 2:
return repr(item.data)
return None
def headerData(self, index, orientation, role):
if orientation == Qt.Horizontal and role == Qt.DisplayRole:
return self.headerLabels[index]
return None
def canFetchMore(self, parent=QModelIndex()):
if parent.isValid():
return parent.internalPointer().canFetchMore
else:
return False
def fetchMore(self, parent=QModelIndex()):
parentItem = parent.internalPointer()
firstIndex = parentItem.childCount
fetchedCount = parentItem.fetchMore()
lastIndex = firstIndex + fetchedCount - 1
self.beginInsertRows(parent, firstIndex, lastIndex)
parentItem.childCount = len(parentItem.childItems)
self.endInsertRows()
# test data
data = [list(range(1000)), list(range(1000))]
app = QApplication([])
view = QTreeView()
model = MyTreeModel(data)
view.setModel(model)
view.show()
app.exec_()
It is probably the shortest version (simplified, unoptimized, not general enough etc.) of the code which can replicate the problem. I want to display a tree of lists of ints. Note that in the example above I have two lists of ints, each containing 1000 ints. But for some unknown reason, only he first 300 ints are displayed when the node is expanded and after I scroll to the very bottom. I would expect that the items should be added as I am scrolling down. However they are only added when I collapse and expand again the node.
In other words, the nodes are being added only as a reaction to collapsing and expanding of the parent nodes, not as a reaction to scrolling to the bottom.
What is the problem? Have I overlooked anything?
UPDATE: I simplified the code even more, compared to the first version.
UPDATE2: To compare it with a model without hierarchy, which works as expected (fetching more data as user scrolls down), look at this code:
from PySide.QtCore import Qt, QAbstractItemModel, QModelIndex
from PySide.QtGui import QApplication, QTreeView
BATCH_SIZE = 100
class LazyModel(QAbstractItemModel):
def __init__(self, totalCount, parent=None):
super(LazyModel, self).__init__(parent)
self.items = []
self.totalCount = totalCount
self.loadedCount = 0
def hasChildren(self, parent=QModelIndex()):
if not parent.isValid():
return True
else:
return False
def rowCount(self, parent=QModelIndex()):
return self.loadedCount
def columnCount(self, parent=QModelIndex()):
return 2
def index(self, row, column, parent=QModelIndex()):
return self.createIndex(row, column, None)
def parent(self, index):
return QModelIndex()
def data(self, index, role):
if role == Qt.DisplayRole:
if index.column() == 0:
return str(index.row())
else:
return str(self.items[index.row()])
return None
def canFetchMore(self, parent=QModelIndex()):
return self.loadedCount < self.totalCount
def fetchMore(self, parent=QModelIndex()):
remainder = self.totalCount - self.loadedCount
fetchCount = min(BATCH_SIZE, remainder)
for i in range(fetchCount):
self.items.append(len(self.items))
self.beginInsertRows(parent, self.loadedCount,
self.loadedCount + fetchCount - 1)
self.loadedCount += fetchCount
self.endInsertRows()
app = QApplication([])
view = QTreeView()
model = LazyModel(10000)
view.setModel(model)
view.show()
app.exec_()
Does it mean I got something wrong with hierarchy?
I'm using QDateTimeEdit as a delegate on my QTableview to show start date and end date.
when I try to populate data I receive from database, QDateTimeEdit delegate does not display it.
Here is my code:
Class DateDelegate:
class DateDelegate(QtGui.QItemDelegate):
def __init__(self, parent):
QtGui.QItemDelegate.__init__(self, parent)
def createEditor(self, parent, option, index):
self.dateEdit = QtGui.QDateTimeEdit(parent)
self.dateEdit.setCalendarPopup(True)
self.dateEdit.setMinimumDate(QtCore.QDate(2014, 03, 01))
self.dateEdit.setDisplayFormat(_translate("Form", "dd/mm/yyyy", None))
return self.dateEdit
def setModelData(self, editor, model, index):
value = self.dateEdit.dateTime().toPyDateTime()
strDate = value.strftime('%d/%m/%Y')
model.setData(index, strDate, QtCore.Qt.EditRole)
Class AssetTableModel:
class AssetTableModel(QtCore.QAbstractTableModel):
def __init__(self, assets = [], headers = [], parent = None):
QtCore.QAbstractTableModel.__init__(self, parent)
self.__assets = assets
self.__headers = headers
def rowCount(self, parent):
return len(self.__assets)
def columnCount(self, parent):
return len(self.__assets[0])
def flags(self, index):
return QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsEditable
def data(self, index, role):
row = index.row()
column = index.column()
if role == QtCore.Qt.EditRole:
return self.__assets[row][column]
if role == QtCore.Qt.DisplayRole:
print self.__assets[row][column]
return self.__assets[row][column]
def setData(self, index, value, role = QtCore.Qt.EditRole):
if role == QtCore.Qt.EditRole:
row = index.row()
column = index.column()
self.__assets[row][column] = value
self.dataChanged.emit(index, index)
return True
return False
def headerData(self, section, orientation, role):
if role == QtCore.Qt.DisplayRole:
if orientation == QtCore.Qt.Horizontal:
if section < len(self.__headers):
return self.__headers[section]
else:
return "not implimented"
else:
return "verticle not implimented"
def insertRows(self, position, rows, parent = QtCore.QModelIndex()):
self.beginInsertRows( parent, position, position + rows - 1 )
for i in range(rows):
defaultValues = [ "" for c in range( self.columnCount( None ) ) ]
self.__assets.insert( position, defaultValues )
self.endInsertRows()
return True
Class AssetWidget:
class AssetWidget(QtGui.QDialog):
def __init__(self, parent = None):
super(AssetWidget, self).__init__(parent)
uic.loadUi(uipath+'/AssetTable.ui', self)
# DB call here
self.loadAssetData()
# db call ends here
self.model = None
self.fillCombo(self.assetType)
self.cellDelegate = CellDelegate(self)
for i in range(10):
self.assetTV.setItemDelegateForColumn(i, self.cellDelegate)
self.sDateDelegate = DateDelegate(self)
self.assetTV.setItemDelegateForColumn(10, self.sDateDelegate )
self.assetTV.setItemDelegateForColumn(11, self.sDateDelegate)
self.connect(self.assettypeCB, QtCore.SIGNAL("currentIndexChanged(int)"), self.loadAssets )
self.connect(self.closeBTN , QtCore.SIGNAL("clicked()"), self.close )
self.connect(self.addRowBTN, QtCore.SIGNAL("clicked()"), self.addRow )
self.connect(self.assetTV, QtCore.SIGNAL("doubleClicked(QModelIndex)"), self.tableEdited )
self.show()
I think you are missing the setEditorData() method in your ItemDelegate.
From your attached sourcecode, I assume you are storing the date as a string? In my opinion, it is better to use a QDateTime object to store your date/time. If you do this, you do not need a ItemDelegate to provide an appropriate editor, because Qt knows which editor it needs to provide for this kind of datatype. (see Qt Documentation - Standard Editing Widgets.
However, if you still want to store your date as a string, see this sample program below on how to use delegtes.
from PyQt4 import QtCore
from PyQt4 import QtGui
import sys
class myModel(QtCore.QAbstractTableModel):
def __init__(self, parent):
QtCore.QAbstractTableModel.__init__(self, parent)
self.lst = []
#populate with a few dummy dates
#store dates as str values
dateTime = QtCore.QDateTime.currentDateTime()
for i in range(10):
strDate = dateTime.toString("dd/mm/yyyy")
self.lst.append([strDate])
dateTime = dateTime.addDays(1)
def rowCount(self, parent = QtCore.QModelIndex()):
return len(self.lst)
def columnCount(self, parent = QtCore.QModelIndex()):
return 1
def data(self, index, role = QtCore.Qt.DisplayRole):
row = index.row()
col = index.column()
if role == QtCore.Qt.DisplayRole:
return self.lst[row][col]
if role == QtCore.Qt.EditRole:
return self.lst[row][col]
def setData(self, index, value, role = QtCore.Qt.EditRole):
row = index.row()
col = index.column()
self.lst[row][col] = value
def flags(self, index):
return (QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEditable)
class DateDelegate(QtGui.QItemDelegate):
def __init__(self, parent):
QtGui.QItemDelegate.__init__(self, parent)
def createEditor(self, parent, option, index):
dateTimeEdit = QtGui.QDateTimeEdit(parent) #create new editor
#set properties of editor
dateTimeEdit.setDisplayFormat("dd/mm/yyyy")
dateTimeEdit.setCalendarPopup(True)
return dateTimeEdit
def setModelData(self, editor, model, index):
value = editor.dateTime().toString("dd/mm/yyyy")
model.setData(index, value)
def setEditorData(self, editor, index):
value = index.model().data(index, QtCore.Qt.EditRole)
qdate = QtCore.QDateTime().fromString(value, "dd/mm/yyyy")
editor.setDateTime(qdate)
if __name__ == "__main__":
app = QtGui.QApplication(sys.argv)
table = QtGui.QTableView()
data = myModel(table)
table.setModel(data)
d = DateDelegate(table)
table.setItemDelegateForColumn(0, d)
table.resize(800, 600)
table.show()
sys.exit(app.exec_())