PyQt4 : Drag and Drop in QTreeView - python

I make a UI with PyQt4. It has a treeView and I want to deal with it.
The treeView is made up with model-base. I create a data in .py file and import it.
So, I can see the data tree in my treeView.
But I can't drag and drop it, so can't change the order.
I referred some articles so add it in my script, but they couldn't work.
I plant some "print", so I chased my problem.
I found that when drag a item, it transferred to MIME data.
But when it is dropped, I can't find any outputs.
It seems that the script doesn't call "dropMimeData" method.
How can I fix my script?
from PyQt4 import QtCore, QtGui
from setting import *
from copy import deepcopy
from cPickle import dumps, load, loads
from cStringIO import StringIO
class PyMimeData(QtCore.QMimeData):
MIME_TYPE = QtCore.QString('text/plain')
def __init__(self, data=None):
QtCore.QMimeData.__init__(self)
self._local_instance = data
if data is not None:
try:
pdata = dumps(data)
except:
return
self.setData(self.MIME_TYPE, dumps(data.__class__) + pdata)
#classmethod
def coerce(cls, md):
if isinstance(md, cls):
return md
if not md.hasFormat(cls.MIME_TYPE):
return None
nmd = cls()
nmd.setData(cls.MIME_TYPE, md.data())
return nmd
def instance(self):
if self._local_instance is not None:
return self._local_instance
io = StringIO(str(self.data(self.MIME_TYPE)))
try:
load(io)
return load(io)
except:
pass
return None
def instanceType(self):
if self._local_instance is not None:
return self._local_instance.__class__
try:
return loads(str(self.data(self.MIME_TYPE)))
except:
pass
return None
class treeItem(QtGui.QStandardItem):
def __init__(self, data, parent=None):
super(treeItem, self).__init__(data)
self.parentItem = parent
self.itemData = data
self.childItems = []
def appendChild(self, item):
self.childItems.append(item)
def parent(self):
return self.parentItem
def childAtRow(self, row):
return self.childItems[row]
def rowOfChild(self, child):
for i, item in enumerate(self.childItems):
if item == child:
return i
return -1
class treeModel(QtGui.QStandardItemModel):
def __init__(self, name, parent=None):
super(treeModel, self).__init__(parent)
self.headerName = name
self.childItems = []
def appendChild(self, item):
self.childItems.append(item)
def removeRowAll(self):
pass
def addItemList(self, parent, elements):
for text, children in elements:
item = treeItem(text, parent)
self.addItems(parent, item)
if children:
self.addItemList(item, children)
def addItems(self, parent, inputItem):
parent.appendRow(inputItem)
parent.appendChild(inputItem)
def headerData(self, section, orientation, role):
if orientation == QtCore.Qt.Horizontal and role == QtCore.Qt.DisplayRole:
return self.headerName
def supportedDropActions(self):
return QtCore.Qt.MoveAction | QtCore.Qt.CopyAction
def flags(self, index):
defaultFlags = QtCore.QAbstractItemModel.flags(self, index)
if index.isValid():
return QtCore.Qt.ItemIsEditable | QtCore.Qt.ItemIsDragEnabled | QtCore.Qt.ItemIsDropEnabled | defaultFlags
else:
return QtCore.Qt.ItemIsDropEnabled | defaultFlags
def mimeTypes(self):
types = QtCore.QStringList()
types.append('text/plain')
return types
def mimeData(self, index):
node = self.nodeFromIndex(index[0])
mimeData = PyMimeData(node)
return mimeData
def dropMimeData(self, mimedata, action, row, column, parentIndex):
print mimedata, action, row, column, parentIndex
if action == QtCore.Qt.IgnoreAction:
return True
dragNode = mimedata.instance()
print dragNode
parentNode = self.nodeFromIndex(parentIndex)
# copy of node being moved
newNode = deepcopy(dragNode)
print newNode
newNode.setParent(parentNode)
self.insertRow(len(parentNode)-1, parentIndex)
self.emit(QtCore.SIGNAL("dataChanged(QtCore.QModelIndex,QtCore.QModelIndex)"), parentIndex, parentIndex)
return True
def nodeFromIndex(self, index):
##return index.internalPointer() if index.isValid() else self.root
return index.model() if index.isValid() else self.parent()
def insertRow(self, row, parent):
return self.insertRows(row, 1, parent)
def insertRows(self, row, count, parent):
self.beginInsertRows(parent, row, (row + (count - 1)))
self.endInsertRows()
return True
def removeRow(self, row, parentIndex):
return self.removeRows(row, 1, parentIndex)
def removeRows(self, row, count, parentIndex):
self.beginRemoveRows(parentIndex, row, row)
node = self.nodeFromIndex(parentIndex)
node.removeChild(row)
self.endRemoveRows()
return True
added script
here is ui creation (the script above is imported in this script)
class RigControlWindow(QtGui.QMainWindow, Ui_MainWindow):
def __init__(self, parent = getMayaWindow()):
super(RigControlWindow, self).__init__(parent)
self.setupUi(self)
self.bodyrig_treelist.setDragEnabled(1)
self.bodyrig_treelist.setAcceptDrops(1)
self.bodyrig_treelist.setDropIndicatorShown(1)
self.bodyrig_treelist.setDragDropMode(QtGui.QAbstractItemView.InternalMove)
QtCore.QObject.connect(self.finalize_button, QtCore.SIGNAL("clicked()"), self.AddData_treeList)
def AddData_treeList(self):
self.localtreeModel = treeModel("objects")
self.bodyrig_treelist.setModel(self.localtreeModel)
self.localtreeModel.addItemList(self.localtreeModel, data)
and data is
data = [("root",[("upper",[("hand",[]),
("head",[])
]),
("lower",[("leg",[]),
("foot",[])
])
])
]

The QTreeView.dragMoveEvent and QTreeView.dragEnterEvent methods both check the object returned by event.mimeData() to see if it can return data for any of the formats supported by the model (i.e. those returned by model.mimeTypes()).
But your PyMimeData subclass doesn't support any formats, because it never successfully sets the data passed to its constructor.
The problem is located in PyMimeData.__init__:
...
try:
pdata = dumps(data)
except:
return
self.setData(self.MIME_TYPE, dumps(data.__class__) + pdata)
The data is passed in from the treeModel.mimeData method:
def mimeData(self, index):
node = self.nodeFromIndex(index[0])
mimeData = PyMimeData(node)
return mimeData
But if you check the type of data/node you'll see that it is a treeModel instance, and so dumps(data) will fail because data can't be pickled. As a result of this, the PyMimeData object is not initialized properly, and so it is ignored by drag events.

Related

How to alter dropEvent action in treeview without loosing basic drag-n-drop functionality in PyQt5?

I'm using my custom item model (subclassed from QAbstractItemModel) with custom QTreeView. I want to allow internal drag-n-drop movement (MoveAction) and, when modifier key or right mouse button is pressed, pass CopyAction to my model (to dropMimeData) to copy items. However, default implementation of dropEvent() in QTreeView seems (from C code) only capable of passing MoveAction but when I try to reimplement dropEvent() in my QTreeView subclass like this:
def dropEvent(self, e):
index = self.indexAt(e.pos())
parent = index.parent()
self.model().dropMimeData(e.mimeData(), e.dropAction(), index.row(), index.column(), parent)
e.accept()
... it works, but works horribly in terms of user interaction because there are tons of comlex code determining right index to drop item on in default implementation.
When i'm trying to modify action and call to superclass: super(Tree, self).dropEvent(e) dropAction() data is also lost.
What can I do in order to modify dropAction without loosing all fancy things that default dropEvent is doing for me?
Horrible mess of my current WIP code (i hope it's somewhere near minimal example)
from copy import deepcopy
import pickle
import config_editor
from PyQt5 import QtCore, QtGui, QtWidgets
from PyQt5.QtCore import Qt as Qt
from PyQt5.QtGui import QCursor, QStandardItemModel
from PyQt5.QtWidgets import QAbstractItemView, QTreeView, QMenu
class ConfigModelItem:
def __init__(self, label, value="", is_section=False, state='default', parent=None):
self.itemData = [label, value]
self.is_section = is_section
self.state = state
self.childItems = []
self.parentItem = parent
if self.parentItem is not None:
self.parentItem.appendChild(self)
def appendChild(self, item):
self.childItems.append(item)
item.parentItem = self
def addChildren(self, items, row):
if row == -1:
row = 0
self.childItems[row:row] = items
for item in items:
item.parentItem = self
def child(self, row):
return self.childItems[row]
def childCount(self):
return len(self.childItems)
def columnCount(self):
return 2
def data(self, column):
try:
return self.itemData[column]
except IndexError:
return None
def set_data(self, data, column):
try:
self.itemData[column] = data
except IndexError:
return False
return True
def parent(self):
return self.parentItem
def row(self):
if self.parentItem is not None:
return self.parentItem.childItems.index(self)
return 0
def removeChild(self, position):
if position < 0 or position > len(self.childItems):
return False
child = self.childItems.pop(position)
child.parentItem = None
return True
def __repr__(self):
return str(self.itemData)
class ConfigModel(QtCore.QAbstractItemModel):
def __init__(self, data, parent=None):
super(ConfigModel, self).__init__(parent)
self.rootItem = ConfigModelItem("Option", "Value")
self.setup(data)
def headerData(self, section, orientation, role):
if role == Qt.DisplayRole and orientation == Qt.Horizontal:
return self.rootItem.data(section)
def columnCount(self, parent):
return 2
def rowCount(self, parent):
if parent.column() > 0:
return 0
if not parent.isValid():
parentItem = self.rootItem
else:
parentItem = parent.internalPointer()
return parentItem.childCount()
def index(self, row, column, parent):
if not self.hasIndex(row, column, parent):
return QtCore.QModelIndex()
parentItem = self.nodeFromIndex(parent)
childItem = parentItem.child(row)
if childItem:
return self.createIndex(row, column, childItem)
else:
return QtCore.QModelIndex()
def parent(self, index):
if not index.isValid():
return QtCore.QModelIndex()
childItem = index.internalPointer()
parentItem = childItem.parent()
if parentItem == self.rootItem or parentItem is None:
return QtCore.QModelIndex()
return self.createIndex(parentItem.row(), 0, parentItem)
def nodeFromIndex(self, index):
if index.isValid():
return index.internalPointer()
return self.rootItem
def data(self, index, role):
if not index.isValid():
return None
item = index.internalPointer()
if role == Qt.DisplayRole or role == Qt.EditRole:
return item.data(index.column())
return None
def setData(self, index, value, role=Qt.EditRole):
if not index.isValid():
return False
item = index.internalPointer()
if role == Qt.EditRole:
item.set_data(value, index.column())
self.dataChanged.emit(index, index, (role,))
return True
def flags(self, index):
if not index.isValid():
return QtCore.Qt.ItemIsDragEnabled | QtCore.Qt.ItemIsDropEnabled # Qt.NoItemFlags
item = index.internalPointer()
flags = Qt.ItemIsEnabled | Qt.ItemIsSelectable
if index.column() == 0:
flags |= int(QtCore.Qt.ItemIsDragEnabled)
if item.is_section:
flags |= int(QtCore.Qt.ItemIsDropEnabled)
if index.column() == 1 and not item.is_section:
flags |= Qt.ItemIsEditable
return flags
def supportedDropActions(self):
return QtCore.Qt.CopyAction | QtCore.Qt.MoveAction
def mimeTypes(self):
return ['app/configitem', 'text/xml']
def mimeData(self, indexes):
mimedata = QtCore.QMimeData()
index = indexes[0]
mimedata.setData('app/configitem', pickle.dumps(self.nodeFromIndex(index)))
return mimedata
def dropMimeData(self, mimedata, action, row, column, parentIndex):
print('action', action)
if action == Qt.IgnoreAction:
return True
droppedNode = deepcopy(pickle.loads(mimedata.data('app/configitem')))
print('copy', action & Qt.CopyAction)
print(droppedNode.itemData, 'node')
self.insertItems(row, [droppedNode], parentIndex)
self.dataChanged.emit(parentIndex, parentIndex)
if action & Qt.CopyAction:
return False # to not delete original item
return True
def removeRows(self, row, count, parent):
print('rem', row, count)
self.beginRemoveRows(parent, row, row+count-1)
parentItem = self.nodeFromIndex(parent)
for x in range(count):
parentItem.removeChild(row)
self.endRemoveRows()
print('removed')
return True
#QtCore.pyqtSlot()
def removeRow(self, index):
parent = index.parent()
self.beginRemoveRows(parent, index.row(), index.row())
parentItem = self.nodeFromIndex(parent)
parentItem.removeChild(index.row())
self.endRemoveRows()
return True
def insertItems(self, row, items, parentIndex):
print('ins', row)
parent = self.nodeFromIndex(parentIndex)
self.beginInsertRows(parentIndex, row, row+len(items)-1)
parent.addChildren(items, row)
print(parent.childItems)
self.endInsertRows()
self.dataChanged.emit(parentIndex, parentIndex)
return True
def setup(self, data: dict, parent=None):
if parent is None:
parent = self.rootItem
for key, value in data.items():
if isinstance(value, dict):
item = ConfigModelItem(key, parent=parent, is_section=True)
self.setup(value, parent=item)
else:
parent.appendChild(ConfigModelItem(key, value))
def to_dict(self, parent=None) -> dict:
if parent is None:
parent = self.rootItem
data = {}
for item in parent.childItems:
item_name, item_data = item.itemData
if item.childItems:
data[item_name] = self.to_dict(item)
else:
data[item_name] = item_data
return data
#property
def dict(self):
return self.to_dict()
class ConfigDialog(config_editor.Ui_config_dialog):
def __init__(self, data):
super(ConfigDialog, self).__init__()
self.model = ConfigModel(data)
def setupUi(self, config_dialog):
super(ConfigDialog, self).setupUi(config_dialog)
self.config_view = Tree()
self.config_view.setObjectName("config_view")
self.config_view.setModel(self.model)
self.gridLayout.addWidget(self.config_view, 0, 0, 1, 1)
self.config_view.expandAll()
#self.config_view.setDragDropMode(True)
#self.setDragDropMode(QAbstractItemView.InternalMove)
#self.setDragEnabled(True)
#self.setAcceptDrops(True)
#self.setDropIndicatorShown(True)
self.delete_button.pressed.connect(self.remove_selected)
def remove_selected(self):
index = self.config_view.selectedIndexes()[0]
self.model.removeRow(index)\
class Tree(QTreeView):
def __init__(self):
QTreeView.__init__(self)
self.setContextMenuPolicy(Qt.CustomContextMenu)
self.customContextMenuRequested.connect(self.open_menu)
self.setSelectionMode(self.SingleSelection)
self.setDragDropMode(QAbstractItemView.InternalMove)
self.setDragEnabled(True)
self.setAcceptDrops(True)
self.setDropIndicatorShown(True)
self.setAnimated(True)
def dropEvent(self, e):
print(e.dropAction(), 'baseact', QtCore.Qt.CopyAction)
# if e.keyboardModifiers() & QtCore.Qt.AltModifier:
# #e.setDropAction(QtCore.Qt.CopyAction)
# print('copy')
# else:
# #e.setDropAction(QtCore.Qt.MoveAction)
# print("drop")
print(e.dropAction())
#super(Tree, self).dropEvent(e)
index = self.indexAt(e.pos())
parent = index.parent()
print('in', index.row())
self.model().dropMimeData(e.mimeData(), e.dropAction(), index.row(), index.column(), parent)
e.accept()
def open_menu(self):
menu = QMenu()
menu.addAction("Create new item")
menu.exec_(QCursor.pos())
if __name__ == '__main__':
import sys
def except_hook(cls, exception, traceback):
sys.__excepthook__(cls, exception, traceback)
sys.excepthook = except_hook
app = QtWidgets.QApplication(sys.argv)
Dialog = QtWidgets.QDialog()
data = {"section 1": {"opt1": "str", "opt2": 123, "opt3": 1.23, "opt4": False, "...": {'subopt': 'bal'}},
"section 2": {"opt1": "str", "opt2": [1.1, 2.3, 34], "opt3": 1.23, "opt4": False, "...": ""}}
ui = ConfigDialog(data)
ui.setupUi(Dialog)
print(Qt.DisplayRole)
Dialog.show()
print(app.exec_())
print(Dialog.result())
print(ui.model.to_dict())
sys.exit()
setDragDropMode(QAbstractItemView.InternalMove) only allows move operations (as the name would suggest, although the docs do leave some uncertainty in the way this is stated). You probably want to set it to QAbstractItemView.DragDrop mode. You can set the default action with setDefaultDropAction(). Other than that, it's up to the model to return the right item flags and supportedDropActions()/canDropMimeData(), which it looks like yours does. There's also a dragDropOverwriteMode property which may be interesting.
One thing that has surprised me before is that in the model's dropMimeData() method if you return True from a Qt.MoveAction, the QAbstractItemView will remove the dragged item from the model automatically (with a removeRows()/removeColumns() call to your model). This can cause some puzzling results if your model has already actually moved that row (and deleted the old one). I never quite understood that behavior. OTOH if you return False it doesn't matter to the item view, as long as the data is actually moved/updated properly.

wxpython DataViewCtrl - correctly identify drop target

I would like to use a DataViewCtrl with a PyDataViewModel as tree-like control with drag-and-drop support. I am almost there but I cannot figure out how to distinguish between dropping an item between the lines or on top of the parent item. The two screenshots illustrate the issue. In both cases the item identified as the drop target is the parent item.
This is the code:
import wx
import wx.dataview as dv
print(wx.__version__)
class Container(list):
def __init__(self, name):
self.name = name
def __str__(self):
return 'Container {}'.format(self.name)
class Element(object):
def __init__(self, name):
self.name = name
def __str__(self):
return 'Element {}'.format(self.name)
#property
def len(self):
return str(len(self.name))
class MyTreeListModel(dv.PyDataViewModel):
def __init__(self, data):
dv.PyDataViewModel.__init__(self)
self.data = data
def GetColumnCount(self):
return 2
def GetColumnType(self, col):
mapper = { 0 : 'string',
1 : 'string'
}
return mapper[col]
def GetChildren(self, parent, children):
if not parent:
for cont in self.data:
children.append(self.ObjectToItem(cont))
return len(self.data)
node = self.ItemToObject(parent)
if isinstance(node, Container):
for ds in node:
children.append(self.ObjectToItem(ds))
return len(node)
return 0
def IsContainer(self, item):
if not item:
return True
node = self.ItemToObject(item)
if isinstance(node, Container):
return True
return False
def GetParent(self, item):
if not item:
return dv.NullDataViewItem
node = self.ItemToObject(item)
if isinstance(node, Container):
return dv.NullDataViewItem
elif isinstance(node, Element):
for g in self.data:
try:
g.index(node)
except ValueError:
continue
else:
return self.ObjectToItem(g)
def GetValue(self, item, col):
node = self.ItemToObject(item)
if isinstance(node, Container):
mapper = { 0 : node.name,
1 : '',
}
return mapper[col]
elif isinstance(node, Element):
mapper = { 0 : node.name,
1 : node.len
}
return mapper[col]
else:
raise RuntimeError("unknown node type")
class MyFrame(wx.Frame):
def __init__(self, parent):
wx.Frame.__init__(self, parent, -1, "Table")
panel = wx.Panel(self)
dvcTree = dv.DataViewCtrl(panel,style=wx.BORDER_THEME|dv.DV_MULTIPLE)
self.model = MyTreeListModel(data)
dvcTree.AssociateModel(self.model)
dvcTree.AppendTextColumn("Container", 0, width=80)
dvcTree.AppendTextColumn("Element", 1, width=80)
dvcTree.Bind(dv.EVT_DATAVIEW_ITEM_BEGIN_DRAG, self._onDrag)
dvcTree.Bind(dv.EVT_DATAVIEW_ITEM_DROP, self._onEndDrag)
dvcTree.Bind(dv.EVT_DATAVIEW_ITEM_DROP_POSSIBLE, self._onDropPossible)
self.dvcTree = dvcTree
dvcTree.EnableDragSource(wx.DataFormat(wx.DF_UNICODETEXT))
dvcTree.EnableDropTarget(wx.DataFormat(wx.DF_UNICODETEXT))
box = wx.BoxSizer(wx.VERTICAL)
box.Add(dvcTree, 1, wx.EXPAND)
panel.SetSizer(box)
self.Layout()
def _onDropPossible(self, evt):
item = evt.GetItem()
mod = evt.GetModel()
if not evt.GetItem().IsOk():
return
def _onEndDrag(self, evt):
if not evt.GetItem().IsOk():
evt.Veto()
return
mod = evt.GetModel()
print('dropped at', mod.ItemToObject(evt.GetItem()))
try:
print('parent:',mod.ItemToObject(mod.GetParent(evt.GetItem())))
except TypeError:
print('parent: None')
def _onDrag(self, evt):
evt.Allow()
mod = evt.GetModel()
print('from', mod.GetValue(evt.GetItem(),0))
evt.SetDataObject(wx.TextDataObject('don\'t know how to retrieve that information in the drop handler'))
evt.SetDragFlags(wx.Drag_AllowMove)
data = [Container('eins'),Container('zwei'),Container('drei')]
for d in data:
d[:] = [Element('element {}'.format('X'*q)) for q in range(5)]
if __name__ == "__main__":
app = wx.App()
f = MyFrame(None)
f.Show()
app.MainLoop()
EDIT:
This is mostly a problem on OSX since the MSW implementation does not offer to drop between lines. I was not able to test it on linux though.

How can I record that QTreeView items have been double-clicked?

I have a QTreeView and I listen for the doubleClicked signal. Then if I double-click the item, I will open a text file in a QTabwidget. Is there any good way to record if I have already opened the text from the item?
I think maybe I should create something like a DataFrame to record it, but I'm not sure if this is a good solution. Any suggestions?
class PrjTreeModel(QStandardItemModel):
def __init__(self, parent,data):
super(PrjTreeModel, self).__init__(parent)
self.items = datapd.DataFrame([['HHH','BBB','RRR']],columns=['UserId','ProjectId','Status'])
self.refreshItems()
def refreshItems(self):
prjId = self.items['ProjectId']
child = ['A','B','C']
for i,row in prjId.iteritems():
parent = QtGui.QStandardItem(row)
for j in child:
parent.appendRow(QtGui.QStandardItem(j))
self.appendRow(parent)
def flags(self, index):
if not index.isValid():
return QtCore.Qt.ItemIsEnabled
return QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable
def headerData(self, section, orientation, role=QtCore.Qt.DisplayRole):
if (role == QtCore.Qt.DisplayRole) and (orientation == QtCore.Qt.Horizontal) and (self.items is not None):
return self.items['UserId'][0]
else:
return QStandardItemModel.headerData(self, section, orientation, role)
class MainWindow(QMainWindow):
[...]
qTreeView.setModel(PrjTreeModel())
qTreeView.doubleClicked.connect(self.dClicked)
self dClicked(self):
[...]
## open txtfile to add in QTabWidget
## when doubled clicked, I don't want to open same item again
## how should I check the state?
You can set a flag on the double-clicked item using a custom role:
class MainWindow(QMainWindow):
def __init__(self):
...
qTreeView.setModel(PrjTreeModel())
qTreeView.doubleClicked.connect(self.dClicked)
def dblClicked(self, index):
item = index.model().itemFromIndex(index)
if not item.data(Qt.UserRole + 1):
item.setData(True, Qt.UserRole + 1)
print('open txt file:', item.text())
else:
print('already double-clicked')

PyQt5 - Unexpected behaviour of QTreeView when a QSortFilterProxyModel is set as model

I am developing an application with PyQt5 5.7.1 on Python 3.5. I use a QTreeView to display some data with a hierachy. I would like to be able to filter this data, so I used a QSortFilterProxy model.
self.model = CustomTreeModel(data)
self.proxy = QSortFilterProxyModel(self.model)
self.ui.treeView.setModel(self.proxy)
This gives a strange behaviour:
By default, the treeView is collapsed. If I expand any item which is not the first one, and then click on any of its children, the first item of the treeView gets expanded (it shouldn't). Actually, whenever I click on any item which is not first level (a child of the root item), the first child of the root item gets expanded.
I thought there was an issue with my model, but if I don't use QSortFilterProxy model, like this :
self.model = CustomTreeModel(data)
self.ui.treeView.setModel(self.model)
The treeView behaves as expected, the first element doesn't expand when it shouldn't.
It seems to me that this is a bug in PyQt5, but I didn't find any information about it on the Internet. On the contrary, there are some examples of using QSortFilterProxyModel with a QTreeView and nobody seems to report such issue. So is it really a bug in PyQt or am I missing something ?
Here is a demo code:
from PyQt5.QtCore import QAbstractItemModel, QModelIndex, Qt
class CustomTreeModel(QAbstractItemModel):
# Based on http://trevorius.com/scrapbook/uncategorized/pyqt-custom-abstractitemmodel/
def __init__(self, headerData, nodes):
QAbstractItemModel.__init__(self)
self._headerData = headerData
self._root = CustomNode(None)
for node in nodes:
self._root.addChild(node)
def addChild(self, in_node, in_parent=QModelIndex()):
if not in_parent or not in_parent.isValid():
parent = self._root
else:
parent = in_parent.internalPointer()
parent.addChild(in_node)
def index(self, in_row, in_column, in_parent=QModelIndex()):
if not in_parent.isValid():
parent = self._root
else:
parent = in_parent.internalPointer()
if not QAbstractItemModel.hasIndex(self, in_row, in_column, in_parent):
return QModelIndex()
child = parent.child(in_row)
if child:
return QAbstractItemModel.createIndex(self, in_row, in_column, child)
else:
return QModelIndex()
def rowCount(self, in_index=QModelIndex()):
if in_index.isValid():
return in_index.internalPointer().childCount()
return self._root.childCount()
def child(self, row, index):
if index.isValid():
c = index.internalPointer().child(row)
return QAbstractItemModel.createIndex(self, row, 0, c)
return QModelIndex()
def parent(self, in_index):
if in_index.isValid():
p = in_index.internalPointer().parent()
if p:
return QAbstractItemModel.createIndex(self, p.row(),0,p)
return QModelIndex()
def columnCount(self, in_index):
if in_index.isValid():
return in_index.internalPointer().columnCount()
return self._root.columnCount()
def data(self, in_index, role):
if not in_index.isValid():
return None
node = in_index.internalPointer()
if role == Qt.DisplayRole:
return node.data(in_index.column())
return None
def headerData(self, section, orientation, role=Qt.DisplayRole):
if (section < 0) or (section > self.columnCount(QModelIndex())):
return None
if (orientation == Qt.Horizontal) and (role == Qt.DisplayRole):
return self._headerData[section]
return None
class CustomNode(object):
def __init__(self, in_data):
self._data = in_data
if type(in_data) == tuple:
self._data = list(in_data)
if type(in_data) == str or not hasattr(in_data, '__getitem__'):
self._data = [in_data]
self._columncount = len(self._data)
self._children = []
self._parent = None
self._row = 0
def data(self, in_column):
if in_column >= 0 and in_column < len(self._data):
return self._data[in_column]
return None
def columnCount(self):
return self._columncount
def childCount(self):
return len(self._children)
def child(self, in_row):
if in_row >= 0 and in_row < self.childCount():
return self._children[in_row]
return None
def parent(self):
return self._parent
def row(self):
return self._row
def addChild(self, in_child):
in_child._parent = self
in_child._row = len(self._children)
self._children.append(in_child)
self._columncount = max(in_child.columnCount(), self._columncount)
if __name__ == '__main__':
import sys
from PyQt5.QtCore import QSortFilterProxyModel
from PyQt5.QtWidgets import QApplication, QTreeView
app = QApplication(sys.argv)
header = ['Food']
fruits = CustomNode(['Fruit'])
fruits.addChild(CustomNode(['Apple']))
fruits.addChild(CustomNode(['Banana']))
fruits.addChild(CustomNode(['Orange']))
vegetables = CustomNode(['Vegetables'])
vegetables.addChild(CustomNode(['Carott']))
vegetables.addChild(CustomNode(['Potato']))
meat = CustomNode(['Meat'])
meat.addChild(CustomNode(['Beef']))
meat.addChild(CustomNode(['Chicken']))
meat.addChild(CustomNode(['Pork']))
nodes = [fruits, vegetables, meat]
v = QTreeView()
model = CustomTreeModel(header, nodes)
proxy = QSortFilterProxyModel()
proxy.setSourceModel(model)
v.setModel(proxy)
v.show()
sys.exit(app.exec_())

How to lazy load changing data into a QColumnView (with PyQt)?

I have a hierarchical data source for a QColumnView I want to fill. The data source loads the data from a server using a REST interface.
Lets say the hierarchy looks like this:
Car_Manufacturer -> Car_Type -> Specific_Model -> Motor_Type
I have to use a QColumnView to display this (since it is a customer requirement). The behavior is supposed to be like this:
When the program starts, it loads the Car_Manufacturer from the server. When one of the Car_Manufacturer items is clicked, the Car_Type items for the selected Car_Manufacturer is loaded from the server and displayed in a new column. When the Car_Manufacturer is clicked again, the data has to be fetched again from the server and the column has to be updated. When Car_Type is clicked, the Specific_Model items for this Car_Manufacturer and Car_type have to be queried from the server and loaded into a new column... and so on.
The datasource has this api:
datasource.get_manufacturers(hierarchy) # hierarchy = []
datasource.get_car_type(hierarchy) # hierarchy = [manufacturer, ]
datasource.get_specific_model(hierarchy) # hierarchy = [manufacturer, car_type]
datasource.get_motor_type(hierarchy) # hierarchy = [manufacturer, car_type, specific_model ]
Where each element in the hierarchy is a string key representation of the item. When an item is clicked it has to inform a controller about this with the hierarchy of the curernt item.
How can I get the QColumnView to update the children of one item when the item is clicked using the datasource? How can this stay flexible when a new hierarchy layer is added or removed?
Here is an example which implements a custom DirModel.
The method _create_children is called lazily and should return a list of instances which implement AbstractTreeItem.
import sys
import os
import abc
from PyQt4.QtCore import QAbstractItemModel, QModelIndex, Qt, QVariant
from PyQt4.QtGui import QColumnView, QApplication
class TreeModel(QAbstractItemModel):
def __init__(self, root, parent=None):
super(TreeModel, self).__init__(parent)
self._root_item = root
self._header = self._root_item.header()
def columnCount(self, parent=None):
if parent and parent.isValid():
return parent.internalPointer().column_count()
else:
return len(self._header)
def data(self, index, role):
if not index.isValid():
return QVariant()
item = index.internalPointer()
if role == Qt.DisplayRole:
return item.data(index.column())
if role == Qt.UserRole:
if item:
return item.person
return QVariant()
def headerData(self, column, orientation, role):
if orientation == Qt.Horizontal and role == Qt.DisplayRole:
try:
return QVariant(self._header[column])
except IndexError:
pass
return QVariant()
def index(self, row, column, parent):
if not self.hasIndex(row, column, parent):
return QModelIndex()
if not parent.isValid():
parent_item = self._root_item
else:
parent_item = parent.internalPointer()
child_item = parent_item.child_at(row)
if child_item:
return self.createIndex(row, column, child_item)
else:
return QModelIndex()
def parent(self, index):
if not index.isValid():
return QModelIndex()
child_item = index.internalPointer()
if not child_item:
return QModelIndex()
parent_item = child_item.parent()
if parent_item == self._root_item:
return QModelIndex()
return self.createIndex(parent_item.row(), 0, parent_item)
def rowCount(self, parent=QModelIndex()):
if parent.column() > 0:
return 0
if not parent.isValid():
parent_item = self._root_item
else:
parent_item = parent.internalPointer()
return parent_item.child_count()
class AbstractTreeItem(object):
__metaclass__ = abc.ABCMeta
def __init__(self, parent=None):
self._children = None
self._parent = parent
#abc.abstractmethod
def header(self):
#return ["name"]
raise NotImplementedError(self.header)
#abc.abstractmethod
def column_count(self):
#return 1
raise NotImplementedError(self.column_count)
def parent(self):
return self._parent
#abc.abstractmethod
def _create_children(self):
# subclass this method
return []
def row(self):
if self._parent:
return self._parent._children.index(self)
return 0
#property
def children(self):
if self._children is None:
self._children = self._create_children()
return self._children
def child_at(self, row):
return self.children[row]
#abc.abstractmethod
def data(self, column):
#return ""
raise NotImplementedError(self.data)
def child_count(self):
count = len(self.children)
return count
class DirPathModel(AbstractTreeItem):
def __init__(self, root="/", parent=None):
super(DirPathModel, self).__init__(parent)
self._root = root
def _create_children(self):
print "walking into", self._root
children = []
try:
entries = os.listdir(self._root)
except OSError:
# no permission etc
entries = []
for name in entries:
fn = os.path.join(self._root, name)
if os.path.isdir(fn):
children.append(self.__class__(fn, self))
return children
def data(self, column):
#assert column == 0
return os.path.basename(self._root)
def header(self):
return ["name"]
def column_count(self):
return 1
def main():
app = QApplication(sys.argv)
view = QColumnView()
view.setWindowTitle("Dynamic Column view test")
view.resize(1024, 768)
root = DirPathModel("/")
model = TreeModel(root)
view.setModel(model)
view.show()
return app.exec_()
if __name__ == "__main__":
sys.exit(main() or 0)
Asuming you can't bring all data at once and filter it out, you'll have to modify the item model (adding and removing rows) on the go based on whatever the user has selected from the QColumnView.
There's more than one way to remove the items:
You can use the index of the selected column and remove all items "on the left" of this column.
You can remove items based whose parent (or grandparent) matches the selection being made
Any option you take, you'll have to mirror the relationship between items in some way. That or implement from QAbstractItemModel, which I think would be an overkill.

Categories

Resources