In PyQt 4 I would like to create a QTreeView with possibility to reorganize its structure with drag and drop manipulation.
I have implemented my own model(QAbstractItemModel) for QTreeView so my QTreeView properly displays the data.
Now I would like to add drag and drop support for tree's nodes to be able to move a node inside the tree from one parent to another one, drag-copy and so on, but I cannot find any complete tutorial how to achieve this. I have found few tutorials and hints for QTreeWidget, but not for QTreeView with custom model.
Can someone point me where to look?
Thank you.
You can enable drag and drop support for tree view items by setting QtGui.QAbstractItemView.InternalMove into the dragDropMode property of the treeview control. Also take a look at the documentation here Using drag & drop with item views. Below is a small example of a treeview with internal drag and drop enabled for its items.
import sys
from PyQt4 import QtGui, QtCore
class MainForm(QtGui.QMainWindow):
def __init__(self, parent=None):
super(MainForm, self).__init__(parent)
self.model = QtGui.QStandardItemModel()
for k in range(0, 4):
parentItem = self.model.invisibleRootItem()
for i in range(0, 4):
item = QtGui.QStandardItem(QtCore.QString("item %0 %1").arg(k).arg(i))
parentItem.appendRow(item)
parentItem = item
self.view = QtGui.QTreeView()
self.view.setModel(self.model)
self.view.setDragDropMode(QtGui.QAbstractItemView.InternalMove)
self.setCentralWidget(self.view)
def main():
app = QtGui.QApplication(sys.argv)
form = MainForm()
form.show()
app.exec_()
if __name__ == '__main__':
main()
Edit0: treeview + abstract model with drag and drop support
import sys
from PyQt4 import QtGui, QtCore
class TreeModel(QtCore.QAbstractItemModel):
def __init__(self):
QtCore.QAbstractItemModel.__init__(self)
self.nodes = ['node0', 'node1', 'node2']
def index(self, row, column, parent):
return self.createIndex(row, column, self.nodes[row])
def parent(self, index):
return QtCore.QModelIndex()
def rowCount(self, index):
if index.internalPointer() in self.nodes:
return 0
return len(self.nodes)
def columnCount(self, index):
return 1
def data(self, index, role):
if role == 0:
return index.internalPointer()
else:
return None
def supportedDropActions(self):
return QtCore.Qt.CopyAction | QtCore.Qt.MoveAction
def flags(self, index):
if not index.isValid():
return QtCore.Qt.ItemIsEnabled
return QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable | \
QtCore.Qt.ItemIsDragEnabled | QtCore.Qt.ItemIsDropEnabled
def mimeTypes(self):
return ['text/xml']
def mimeData(self, indexes):
mimedata = QtCore.QMimeData()
mimedata.setData('text/xml', 'mimeData')
return mimedata
def dropMimeData(self, data, action, row, column, parent):
print 'dropMimeData %s %s %s %s' % (data.data('text/xml'), action, row, parent)
return True
class MainForm(QtGui.QMainWindow):
def __init__(self, parent=None):
super(MainForm, self).__init__(parent)
self.treeModel = TreeModel()
self.view = QtGui.QTreeView()
self.view.setModel(self.treeModel)
self.view.setDragDropMode(QtGui.QAbstractItemView.InternalMove)
self.setCentralWidget(self.view)
def main():
app = QtGui.QApplication(sys.argv)
form = MainForm()
form.show()
app.exec_()
if __name__ == '__main__':
main()
hope this helps, regards
Related
I am trying to make everything shown by the current code un-editable.
Previous searches all suggest either modifying the flags() function of the model or using the setEditTriggers of the table. I do both in this code, but neither of them work.
Looking at a widget-by-widget case, I can find readonly modes for LineEdit and others, but not for ComboBox. So I can not even modify the delegate to force the readonly constraint, not that I would necessarily like to do it this way.
EDIT: to clarify, when I say I want the user to not be able to 'edit' I mean that he shouldn't be able to change the state of the widget in any way. E.g. he won't be able to click on a ComboBox (or at least changing the current selected item/index).
from PyQt5 import QtCore, QtWidgets
import sys
class MyWindow(QtWidgets.QWidget):
def __init__(self, *args):
super().__init__(*args)
tableview = TableView()
layout = QtWidgets.QVBoxLayout(self)
layout.addWidget(tableview)
self.setLayout(layout)
class Delegate(QtWidgets.QStyledItemDelegate):
def __init__(self, model):
super().__init__()
self.model = model
def createEditor(self, parent, option, index):
widget = QtWidgets.QComboBox(parent)
widget.addItems(['', 'Cat', 'Dog'])
return widget
def setModelData(self, widget, model, index):
self.model.setData(index, widget.currentIndex())
class Model(QtCore.QAbstractTableModel):
def __init__(self, parent=None):
QtCore.QAbstractTableModel.__init__(self, parent=parent)
self.value = 0
def flags(self, index):
return QtCore.Qt.ItemIsEnabled
def data(self, index, role=QtCore.Qt.DisplayRole):
if not index.isValid() or role != QtCore.Qt.DisplayRole:
return QtCore.QVariant()
return QtCore.QVariant(self.value)
def setData(self, index, value, role=QtCore.Qt.EditRole):
self.value = value
print("data[{}][{}] = {}".format(index.row(), index.column(), value))
return True
def rowCount(self, parent=QtCore.QModelIndex()):
return 1
def columnCount(self, parent=QtCore.QModelIndex()):
return 1
class TableView(QtWidgets.QTableView):
def __init__(self, parent=None):
super().__init__(parent)
self.model = Model(self)
delegate = Delegate(self.model)
self.setItemDelegate(delegate)
self.setModel(self.model)
self.setEditTriggers(QtWidgets.QTableWidget.NoEditTriggers)
for row in range(self.model.rowCount()):
for column in range(self.model.columnCount()):
index = self.model.index(row, column)
self.openPersistentEditor(index)
if __name__ == '__main__':
app = QtWidgets.QApplication(sys.argv)
w = MyWindow()
w.show()
sys.exit(app.exec_())
Explanation:
Some concepts must be clarified:
For Qt to disable editing that a view (QListView, QTableView, QTreeView, etc.) or item of the view implies only that the editor will not open through user events such as clicked, double-clicked, etc.
The user interaction in Qt follows the following path:
The user interacts through the OS with the mouse, keyboard, etc.
the OS notifies Qt of that interaction.
Qt creates QEvents and sends it to the widgets.
The widget analyzes what you should modify regarding the QEvent you receive.
In your case, using openPersistentEditor() shows the widgets, and so the edibility from the Qt point of view is not valid for this case.
Solution:
Considering the above a possible general methodology to make a widget not editable: block some point of the user-widget interaction path. In this case, the simplest thing is to prevent the widget from receiving the QEvents through an event filter.
Considering the above, the solution is:
class DisableEventsManager(QtCore.QObject):
def __init__(self, *, qobject, events=None, apply_childrens=False):
if not isinstance(qobject, QtCore.QObject):
raise TypeError(
f"{qobject} must belong to a class that inherits from QObject"
)
super().__init__(qobject)
self._qobject = qobject
self._events = events or []
self._qobject.installEventFilter(self)
self._children_filter = []
if apply_childrens:
for child in self._qobject.findChildren(QtWidgets.QWidget):
child_filter = DisableEventsManager(
qobject=child, events=events, apply_childrens=apply_childrens
)
self._children_filter.append(child_filter)
#property
def events(self):
return self._events
#events.setter
def events(self, events):
self._events = events
for child_filter in self._children_filter:
child_filter.events = events
def eventFilter(self, obj, event):
if self.events and self._qobject is obj:
if event.type() in self.events:
return True
return super().eventFilter(obj, event)
def createEditor(self, parent, option, index):
combo = QtWidgets.QComboBox(parent)
combo.addItems(["", "Cat", "Dog"])
combo_event_filter = DisableEventsManager(qobject=combo)
combo_event_filter.events = [
QtCore.QEvent.KeyPress,
QtCore.QEvent.FocusIn,
QtCore.QEvent.MouseButtonPress,
QtCore.QEvent.MouseButtonDblClick,
]
return combo
Here is a simple example of a qtableView where every cell have a different item. In the example here i used numbers, but ultimately i wish to display animated gifs for every cells. I wish to keep the MVC design and insert a custom widget proceduraly.
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 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)
tvf = QtGui.QTableView()
model = TableModel(data, 3, self)
tvf.setModel(model)
splitter.addWidget( tvf )
if __name__=="__main__":
import sys
a=QtGui.QApplication(sys.argv)
w=Widget()
w.show()
sys.exit(a.exec_())
After reading a bit more and with the help of #eyllanesc, i understand that there is 2 ways of achieving this: delegates or itemWidget.
It seem that delegates won't work for me since it's not possible to insert widget into cells that way.
ItemWidget seems the way to go but this is not MVC compatible ? i do not wish to loop the function to insert into the table...
is there a third way ?
I need auto completion in a table. So far, I could make it work that I get the same list for the entire table.
However, I need a dynamic list for each cell. How can I get update the list when I move to a new position in the cell?
from PyQt5.QtCore import *
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *
import sys
class mainWindow(QMainWindow):
def __init__(self, parent = None):
super(mainWindow, self).__init__(parent)
self.initUI()
def initUI(self):
self.center_window = centerWindow(parent=self)
self.setCentralWidget(self.center_window)
class centerWindow(QWidget):
def __init__(self, parent=None):
super(centerWindow, self).__init__(parent)
table = QTableWidget()
table.setItemDelegate(TableItemCompleter())
table.setRowCount(5)
table.setColumnCount(1)
vbox = QVBoxLayout(self)
vbox.addWidget(table)
self.setLayout(vbox)
class TableItemCompleter(QStyledItemDelegate):
def __init__(self, parent = None):
super(TableItemCompleter, self).__init__(parent)
def createEditor(self, parent, styleOption, index):
editor = QLineEdit(parent)
completion_ls = ['aaa', 'bbb', 'ccc']
autoComplete = QCompleter(completion_ls)
editor.setCompleter(autoComplete)
return editor
if __name__ == '__main__':
app = QApplication.instance()
if app is None:
app = QApplication(sys.argv)
else:
print('QApplication instance already exists: %s' % str(app))
ex = mainWindow()
ex.show()
sys.exit(app.exec_())
To make it more clear. Instead of having the completion_ls list in the TableItemCompleter, I'd like to add something like this:
table.setItem(row, column) #add my new auto completion list
QCompleter can be established a model that uses as a source for the autocomplete, we could pass the QModelIndex model that provides the method createEditor(self, parent, option, index) through index.model() but the problem is that you can only take a column and is not what is desired.
Then we must do a conversion of the tablemodel to a listmodel, and then we must filter the repeated elements so that the completer shows unique elements, one way to do them is through proxies, classes that inherit from QAbstractProxyModel, the scheme is as follows:
TableModel ----> ReadTable2ListProxyModel ----> DuplicateFilterProxyModel
En la siguiente sección muestros las clases:
class ReadTable2ListProxyModel(QIdentityProxyModel):
def columnCount(self, parent=QModelIndex()):
return 1
def rowCount(self, parent=QModelIndex()):
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 = sourceIndex.model().columnCount() * c + r
return self.index(row, 0)
return 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=QModelIndex()):
return self.createIndex(row, column)
class DuplicateFilterProxyModel(QSortFilterProxyModel):
def setSourceModel(self, model):
model.dataChanged.connect(lambda: self.invalidate())
QSortFilterProxyModel.setSourceModel(self, model)
def filterAcceptsRow(self, row, parent):
value = self.sourceModel().index(row, self.filterKeyColumn())\
.data(self.filterRole())
if value is None:
return False
if row == 0:
return True
for i in reversed(range(0, row)):
val = self.sourceModel().index(i, self.filterKeyColumn())\
.data(self.filterRole())
if val == value:
return False
return True
Then the conversion is established in the delegate:
class TableItemCompleter(QStyledItemDelegate):
def createEditor(self, parent, option, index):
editor = QLineEdit(parent)
completer = QCompleter(parent)
proxy1 = ReadTable2ListProxyModel(parent)
proxy2 = DuplicateFilterProxyModel(parent)
proxy1.setSourceModel(index.model())
proxy2.setSourceModel(proxy1)
completer.setModel(proxy2)
editor.setCompleter(completer)
return editor
In the following link you will find an example, and in the following image the operation is illustrated, in the first widget the table is observed, in the second the conversion to list and in the third the list eliminating duplicate elements.
If you want each item to have a list what can be done is to store the list in each item through a role that is not in use as Qt.UserRole through the setData() method, and in the delegate through the method data() of the QModelIndex:
from PyQt5.QtCore import *
from PyQt5.QtWidgets import *
import random
class TableItemCompleter(QStyledItemDelegate):
def createEditor(self, parent, option, index):
editor = QLineEdit(parent)
completion_ls = index.data(Qt.UserRole) # get list
completer = QCompleter(completion_ls, parent)
editor.setCompleter(completer)
return editor
class Widget(QWidget):
def __init__(self, *args, **kwargs):
QWidget.__init__(self, *args, **kwargs)
lay = QHBoxLayout(self)
tv = QTableWidget(3, 4, self)
lay.addWidget(tv)
l = ["AA", "AB", "AC", "AD", "BA", "BB", "BC"]
for i in range(tv.rowCount()):
for j in range(tv.columnCount()):
it = QTableWidgetItem(f"{i},{j}")
tv.setItem(i, j, it)
it.setData(Qt.UserRole, random.sample(l, 3)) # set list
tv.setItemDelegate(TableItemCompleter(tv))
if __name__ == '__main__':
import sys
app = QApplication(sys.argv)
w = Widget()
w.show()
sys.exit(app.exec_())
When one of the QTableView's QModelIndex is clicked I want to select an entire row of the same-row-indexes.
To accomplish this I connect QTableView's clicked signal to a custom viewClicked() method which receives the clicked QModelIndex automatically:
self.tableview=QTableView()
self.tableview.clicked.connect(self.viewClicked)
Inside of viewClicked(self, clickedIndex) I query clickedIndex's row number, its model and the total number of columns):
row=clickedIndex.row()
model=clickedIndex.model()
columnsTotal=model.columnCount(None)
Finally to select an every index in a row:
for i in range(columnsTotal): self.tableview.selectRow(row)
The problem is it is noticeably slow for Qt to process such an action.
I wonder if there is a faster way to select an entire row of indexes when one of the tableview items is clicked:
from PyQt4.QtCore import *
from PyQt4.QtGui import *
import sys
class Model(QAbstractTableModel):
def __init__(self, parent=None, *args):
QAbstractTableModel.__init__(self, parent, *args)
self.items = ['Row0_Column0','Row0_Column1','Row0_Column2']
def flags(self, index):
return Qt.ItemIsEnabled | Qt.ItemIsSelectable
def rowCount(self, parent):
return 1
def columnCount(self, parent):
return len(self.items)
def data(self, index, role):
if not index.isValid(): return QVariant()
elif role != Qt.DisplayRole:
return QVariant()
column=index.column()
if column<len(self.items):
return QVariant(self.items[column])
else:
return QVariant()
class MyWindow(QWidget):
def __init__(self, *args):
QWidget.__init__(self, *args)
tablemodel=Model(self)
self.tableview=QTableView()
self.tableview.setModel(tablemodel)
self.tableview.clicked.connect(self.viewClicked)
layout = QHBoxLayout(self)
layout.addWidget(self.tableview)
self.setLayout(layout)
def viewClicked(self, clickedIndex):
row=clickedIndex.row()
model=clickedIndex.model()
columnsTotal=model.columnCount(None)
for i in range(columnsTotal):
self.tableview.selectRow(row)
if __name__ == "__main__":
app = QApplication(sys.argv)
w = MyWindow()
w.show()
sys.exit(app.exec_())
EDITED LATER: REVISED WORKING CODE Thanks to Nejat! :
from PyQt4.QtCore import *
from PyQt4.QtGui import *
import sys
class Model(QAbstractTableModel):
def __init__(self, parent=None, *args):
QAbstractTableModel.__init__(self, parent, *args)
self.items = ['Row0_Column0','Row0_Column1','Row0_Column2']
def flags(self, index):
return Qt.ItemIsEnabled | Qt.ItemIsSelectable
def rowCount(self, parent):
return 1
def columnCount(self, parent):
return len(self.items)
def data(self, index, role):
if not index.isValid(): return QVariant()
elif role != Qt.DisplayRole:
return QVariant()
column=index.column()
if column<len(self.items):
return QVariant(self.items[column])
else:
return QVariant()
class MyWindow(QWidget):
def __init__(self, *args):
QWidget.__init__(self, *args)
tablemodel=Model(self)
self.tableview=QTableView()
self.tableview.setModel(tablemodel)
self.tableview.clicked.connect(self.viewClicked)
self.tableview.setSelectionBehavior(QTableView.SelectRows)
layout = QHBoxLayout(self)
layout.addWidget(self.tableview)
self.setLayout(layout)
def viewClicked(self, clickedIndex):
row=clickedIndex.row()
model=clickedIndex.model()
if __name__ == "__main__":
app = QApplication(sys.argv)
w = MyWindow()
w.show()
sys.exit(app.exec_())
You can use setSelectionBehavior function of QTableView to set the selection behavior to QTableView.SelectRows :
self.tableview.setSelectionBehavior(QTableView.SelectRows);
Now when you click on an item, the entire row is selected.
I want to make the first column with checkbox, and get the check status of that, how can I do that? I override the flag(), but it seems do not work, I am sure where is the problem?
And I got this links, but it does work for my code either. And I don't want to use delegate, because it is too complicated.
data = [['00','01','02'],
['10','11','12'],
['20','21','22']]
class MainWindow(QWidget):
def __init__(self, parent=None, *args):
super(MainWindow, self).__init__(parent)
clipTableWidget = QTableWidget()
self.model = TModel(data, self)
clipTableView = QTableView()
clipTableView.setModel(self.model)
layout = QVBoxLayout()
layout.addWidget(clipTableView)
self.setLayout(layout)
class TModel(QAbstractTableModel):
def __init__(self, datain, parent=None):
super(TModel, self).__init__(parent)
self.arraydata = datain
def rowCount(self, parent=QModelIndex()):
return len(self.arraydata)
def columnCount(self, parent=QModelIndex()):
return len(self.arraydata[0])
def data(self, index, role):
if not index.isValid():
return QVariant()
elif role != Qt.DisplayRole:
return QVariant()
return QVariant(self.arraydata[index.row()][index.column()])
def flags(self, index):
if not index.isValid():
return QVariant()
elif index.column() == 1:
return Qt.ItemIsSelectable|Qt.ItemIsEnabled|Qt.ItemIsUserCheckable
return QVariant()
Ok here is the solution to your problem. The reason Why your flags weren't working is because those flags dont work on text , they are meant for QItems .
from PyQt4.QtCore import *
from PyQt4.QtGui import *
import sys
class MainWindow(QWidget):
def __init__(self, parent=None, *args):
super(MainWindow, self).__init__(parent)
data = [['00','01','02'],
['10','11','12'],
['20','21','22']]
clipTableWidget = QTableWidget()
self.model = QStandardItemModel(self)
clipTableView = QTableView()
count1=0
for row in data:
count2 = 0
for column in row:
if count2 == 0:
item = QStandardItem(column)
item.setCheckable(True)
item.setCheckState(False)
item.setFlags(Qt.ItemIsUserCheckable| Qt.ItemIsEnabled)
self.model.setItem(count1,count2,item)
count2+=1
else:
item = QStandardItem(column)
self.model.setItem(count1,count2,item)
count2+=1
count1+=1
clipTableView.setModel(self.model)
layout = QVBoxLayout()
layout.addWidget(clipTableView)
self.setLayout(layout)
def main():
app = QApplication(sys.argv)
w = MainWindow()
w.show()
sys.exit(app.exec_())
if __name__ == "__main__":
main()