I have a QTableView with a boolean value in a column, which is interpreted as a checkbox.
In PySide2 when I click on the checkbox, it correctly updates the UI and the stored value. But in PySide6 the same code sample will not work.
Here is the sample code that runs fine on PySide2 but breaks on PySide6:
from PySide6 import QtCore, QtWidgets
class TableModel(QtCore.QAbstractTableModel):
def __init__(self, parent) -> None:
super().__init__(parent)
self.model = False
def rowCount(self, _):
return 1
def columnCount(self, _):
return 1
def headerData(self, section, orientation, role):
if role != QtCore.Qt.ItemDataRole.DisplayRole:
return None
return "Test"
def flags(self, index):
return QtCore.Qt.ItemFlag.ItemIsEnabled | QtCore.Qt.ItemFlag.ItemIsUserCheckable
def data(self, index, role):
if role == QtCore.Qt.ItemDataRole.CheckStateRole:
print("Get data : ",self.model)
return QtCore.Qt.CheckState.Checked if self.model else QtCore.Qt.CheckState.Unchecked
def setData(self, index, value, role):
if role == QtCore.Qt.ItemDataRole.CheckStateRole:
self.model = bool(value)
print("Set data : ", self.model)
self.dataChanged.emit(index, index)
return True
return False
class MainWindow(QtWidgets.QMainWindow):
def __init__(self) -> None:
super().__init__()
self.table = QtWidgets.QTableView()
self.table_model = TableModel(self)
self.setCentralWidget(self.table)
self.table.setModel(self.table_model)
self.show()
if __name__ == "__main__":
app = QtWidgets.QApplication([])
conv = MainWindow()
app.exec_()
After clicking once on the checkbox, self.model is updated, but the UI does not change and keeps sending True when calling setData method.
I looked at the changes between PySide2 and PySide6 (Porting from PySide2 to PySide6) and there's nothing that could trigger this change of behaviour.
Edit :
This is a known bug (PYSIDE-1930) that should be fixed in version 6.3.2. Thanks to musicamente for pointing it out.
Related
I would like to add a column of checkbox to my qtableview. I need the checkboxes to be in the center of column (i.e. aligned center). I have this example which works fine, BUT the checkboxes are aligned left.
import sys
import pandas as pd
from PyQt5 import QtCore, Qt
from PyQt5.QtCore import pyqtSlot
from PyQt5.QtWidgets import QCheckBox, QHBoxLayout, QItemDelegate, QTableView, QWidget, QApplication, QMainWindow
class MyCheckboxDelegate(QItemDelegate):
def __init__(self, parent):
QItemDelegate.__init__(self, parent)
def createEditor(self, parent, option, index):
check = QCheckBox(parent)
check.clicked.connect(self.currentIndexChanged)
return check
def setModelData(self, editor, model, index):
model.setData(index, editor.checkState())
#pyqtSlot()
def stateChanged(self):
self.commitData.emit(self.sender())
class TableModel(QtCore.QAbstractTableModel):
def __init__(self, data):
super().__init__()
print(data)
self._data = data
def rowCount(self, index=None):
return self._data.shape[0]
def columnCount(self, parnet=None):
return self._data.shape[1]
def data(self, index, role=QtCore.Qt.DisplayRole):
if index.isValid():
if role == QtCore.Qt.DisplayRole or role == QtCore.Qt.EditRole:
if self._data.columns[index.column()]=='Delete':
return ''
value = self._data.iloc[index.row(), index.column()]
return str(value)
class MyWindow(QMainWindow):
def __init__(self, *args):
QWidget.__init__(self, *args)
table_model = TableModel(pd.DataFrame([['', ''], ['','']]))
self.table_view = QTableView()
self.table_view.setModel(table_model)
self.table_view.setItemDelegateForColumn(0, MyCheckboxDelegate(self))
for row in range(0, table_model.rowCount()):
self.table_view.openPersistentEditor(table_model.index(row, 0))
self.setCentralWidget(self.table_view)
app = QApplication(sys.argv)
window = MyWindow()
window.show()
sys.exit(app.exec_())
To force them to come at center, I create a widget, add a layout and add the check box to the layout. In other words, I change the createEditor function of MyCheckboxDelegate as follows:
def createEditor(self, parent, option, index):
w = QWidget(parent)
layout = QHBoxLayout(w)
check = QCheckBox(parent)
check.clicked.connect(self.currentIndexChanged)
check.setStyleSheet("color: red;")
layout.addWidget(check)
layout.setAlignment(Qt.AlignCenter)
return w
The problem is that now, the setModelData will not be called anymore. I need to access `model' after a checkbox is clicked.
Has anybody an idea how to fix it?
Item delegates are able to set model data as long as the editor has a user property, and a basic QWidget doesn't.
The solution is to create a QWidget subclass that implements that property, and connect the checkbox to a signal that will actually do the same as before:
class CenterCheckBox(QWidget):
toggled = pyqtSignal(bool)
def __init__(self, parent):
super().__init__(parent)
layout = QHBoxLayout(self)
layout.setContentsMargins(0, 0, 0, 0)
self.check = QCheckBox()
layout.addWidget(self.check, alignment=Qt.AlignCenter)
self.check.setFocusProxy(self)
self.check.toggled.connect(self.toggled)
# set a 0 spacing to avoid an empty margin due to the missing text
self.check.setStyleSheet('color: red; spacing: 0px;')
#pyqtProperty(bool, user=True) # note the user property parameter
def checkState(self):
return self.check.isChecked()
#checkState.setter
def checkState(self, state):
self.check.setChecked(state)
class MyCheckboxDelegate(QStyledItemDelegate):
def __init__(self, parent):
super().__init__(parent)
def createEditor(self, parent, option, index):
check = CenterCheckBox(parent)
check.toggled.connect(lambda: self.commitData.emit(check))
return check
def setModelData(self, editor, model, index):
model.setData(index, editor.checkState)
Note that:
it's usually better to use QStyledItemDelegate, which is more consistent with the overall application appearance;
you should always check the column (or row) before returning the editor and do the same (or at least check the editor) in setModelData(), or, alternatively, use setItemDelegateForColumn();
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
Hello i started with PyQt5 recently and tried to implement a csv parser:
But i struggled with the descendant of the QAbstractTableModel.
No data in the view!
The tutorial from qt5 (https://doc.qt.io/qt-5/qabstracttablemodel.html) says i have to implement some methods in the TableModel - class.
I think i did that but - no data in the view.
from PyQt5.QtWidgets import \
QApplication, QWidget, QVBoxLayout, QTableView
from PyQt5.QtCore import \
QAbstractTableModel, QVariant, Qt
class TableModel(QAbstractTableModel):
def __init__(self):
super().__init__()
self._data = []
def set(self, data):
self._data = data
def rowCount(self, index):
return len(self._data)
def columnCount(self, index):
return len(self._data[0])
def data(self, index, role=Qt.DisplayRole):
if not index.isValid():
return QVariant()
elif role == Qt.DisplayRole:
row = index.row()
return self._data[row]
else:
return QVariant()
def headerData(self, p_int, Qt_Orientation, role=None):
return self._data[0]
class View(QWidget):
def __init__(self):
super().__init__()
model = TableModel()
model.set(data)
self.tbl = QTableView(self)
self.tbl.setModel(model)
def setup(self):
self.setMinimumHeight(600)
self.setMinimumWidth(800)
self.vlayout = QVBoxLayout(self)
self.vlayout.addWidget(self.tbl)
def main():
import sys
app = QApplication(sys.argv)
window = View()
window.setup()
window.show()
sys.exit(app.exec_())
main()
Some Mock data from here:
data = [f.strip().split(",") for f in """\
id,first_name,last_name,email,gender,ip_address
1,Hillard,Tasseler,htasseler0#google.com.br,Male,104.153.237.243
2,Tyrus,Oley,toley1#ft.com,Male,163.73.24.45
3,Kora,Covil,kcovil2#privacy.gov.au,Female,158.44.166.87
4,Phineas,McEntee,pmcentee3#rambler.ru,Male,71.82.246.45
5,Dottie,Spraging,dspraging4#berkeley.edu,Female,226.138.241.22
6,Andria,Ivatts,aivatts5#about.com,Female,57.5.76.78
7,Missy,Featherstone,mfeatherstone6#unblog.fr,Female,9.56.215.203
8,Anstice,Sargant,asargant7#about.me,Female,36.115.185.109
9,Teresita,Trounce,ttrounce8#myspace.com,Female,240.228.133.166
10,Sib,Thomke,sthomke9#ibm.com,Female,129.191.2.7
11,Amery,Dallander,adallandera#elpais.com,Male,4.115.194.100
12,Rourke,Rowswell,rrowswellb#bloomberg.com,Male,48.111.190.66
13,Cloe,Benns,cbennsc#slideshare.net,Female,142.48.24.44
14,Enos,Fery,eferyd#pen.io,Male,59.19.200.235
15,Russell,Capelen,rcapelene#fc2.com,Male,38.205.20.141""".split()]
The data method must return a value as a string, not a list, in your case self._data[row] is a list so the solution is to use the column to obtain a single element from that list:
def data(self, index, role=Qt.DisplayRole):
if not index.isValid():
return QVariant()
elif role == Qt.DisplayRole:
row = index.row()
col = index.column()
return self._data[row][col]
else:
return QVariant()
The code below creates QTableView with a single column. How to make the header column stretch along the entire width of the QTableView view?
from PyQt4 import QtCore, QtGui
app=QtGui.QApplication(sys.argv)
class TableModel(QtCore.QAbstractTableModel):
def __init__(self):
QtCore.QAbstractTableModel.__init__(self)
def rowCount(self, parent=QtCore.QModelIndex()):
return 0
def columnCount(self, index=QtCore.QModelIndex()):
return 1
def headerData(self, column, orientation, role=QtCore.Qt.DisplayRole):
if role!=QtCore.Qt.DisplayRole: return QtCore.QVariant()
if orientation==QtCore.Qt.Horizontal: return QtCore.QVariant('Column Name')
class TableView(QtGui.QTableView):
def __init__(self):
super(TableView, self).__init__()
model=TableModel()
self.setModel(model)
self.show()
view=TableView()
sys.exit(app.exec_())
What you're looking for is the QHeaderView::setResizeMode function. I would recommend checking out the docs, but here's the code
self.horizontalHeader().setResizeMode(QtGui.QHeaderView.Stretch)
or, if you want to only stretch the least header item:
self.horizontalHeader().setStretchLastSection(True)
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