I know how to drag rows
QTableView.verticalHeader().setSectionsMovable(True)
QTableView.verticalHeader().setDragEnabled(True)
QTableView.verticalHeader().setDragDropMode(qtw.QAbstractItemView.InternalMove)
but I want to be able to drag and drop just a single (or a pair of) cell.
Can anyone point me in the right direction?
PS: my current idea would be to intercept the clicked() -> dragEnter() -> dragLeave() or fork to dragDrop() -> dragIndicatorPosition()
events, but it sounds kinda convoluted
I did read this but I am confused on how to implement it, especially the section "Enabling drag and drop for items" seems to address exactly what I need. I'll see if I can post a "slim" example.
EDIT:
here an example with some other stuff. in MyStandardItemModel I try to do the trick:
from PyQt5 import QtCore, QtWidgets, QtSql
from PyQt5.QtCore import QModelIndex
from PyQt5.QtGui import QStandardItemModel
from PyQt5.QtWidgets import QApplication, QTableView, QTableWidget
class MyStandardItemModel(QStandardItemModel):
def __init__(self, parent=None, *arg, **kwargs):
super().__init__(parent, *arg, **kwargs)
self.__readonly_cols = []
def flags(self, index: QtCore.QModelIndex) -> QtCore.Qt.ItemFlags:
try:
default_Flags = QStandardItemModel.flags(self,index)
if (index.column() in self.__readonly_cols):
return QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable
else:
if (index.isValid()):
return default_Flags | QtCore.Qt.ItemIsDragEnabled | QtCore.Qt.ItemIsDropEnabled
else:
return default_Flags
except Exception as ex:
print(ex)
def setReadOnly(self, columns: [int]):
for i in columns:
if i <= (self.columnCount() - 1) and i not in self.__readonly_cols:
self.__readonly_cols.append(i)
def resetReadOnly(self):
self.__readonly_cols = []
class MyTableView(QtWidgets.QTableView):
def __init__(self, parent=None, *args, **kwargs):
super().__init__(parent, *args, **kwargs)
class CheckBoxDelegate(QtWidgets.QItemDelegate):
"""
A delegate that places a fully functioning QCheckBox cell of the column to which it's applied.
"""
# signal to inform clicking
user_click = QtCore.pyqtSignal(int, int, bool)
def __init__(self, parent):
QtWidgets.QItemDelegate.__init__(self, parent)
def createEditor(self, parent, option, index):
"""
Important, otherwise an editor is created if the user clicks in this cell.
"""
return None
def paint(self, painter, option, index):
"""
Paint a checkbox without the label.
"""
self.drawCheck(painter, option, option.rect, QtCore.Qt.Unchecked if int(index.data()) == 0 else QtCore.Qt.Checked)
def editorEvent(self, event, model, option, index):
'''
Change the data in the model and the state of the checkbox
if the user presses the left mousebutton and this cell is editable. Otherwise do nothing.
'''
if not int(index.flags() & QtCore.Qt.ItemIsEditable) > 0:
return False
if event.type() == QtCore.QEvent.MouseButtonRelease and event.button() == QtCore.Qt.LeftButton:
# Change the checkbox-state
self.setModelData(None, model, index)
return True
# if event.type() == QtCore.QEvent.MouseButtonPress or event.type() == QtCore.QEvent.MouseMove:
# return False
return False
def setModelData (self, editor, model, index):
'''
The user wanted to change the old state in the opposite.
'''
try:
if int(index.data()) == 0:
model.setData(index, 1, QtCore.Qt.EditRole)
ret = True
else:
model.setData(index, 0, QtCore.Qt.EditRole)
ret = False
# emit signal with row, col, and the status
self.user_click.emit(index.row(), index.column(), ret)
except Exception as ex:
print(ex)
if __name__ == '__main__':
import sys
app = QApplication(sys.argv)
model = MyStandardItemModel(5, 5)
header_labels = ['', 'Signal', 'Type', 'Routing', 'Input']
model.setHorizontalHeaderLabels(header_labels)
tableView = MyTableView()
tableView.setModel(model)
delegate = CheckBoxDelegate(None)
tableView.setItemDelegateForColumn(0, delegate)
for row in range(5):
for column in range(4):
index = model.index(row, column, QModelIndex())
model.setData(index, 0)
model.setReadOnly([1,2])
tableView.setWindowTitle("CheckBox, readonly and drag&drop example")
tableView.show()
sys.exit(app.exec_())
Related
I'm trying to make a QListView where each rows are represented has a complex widget.
I want to have a QLabel and a QTableView representing some data.
All good so far.
The problem is that when i click on the QTableView (which i disabled the focus policy), it doesn't select the row of the QListView.
But it's working when i click on the QLabel (which is also disabled focus policy)
Is there anything i do wrong ? My change_line method does get called when clicking on the "This is line" label but not on the table bellow it :(
I've tried to play with the FocusPolicy and the setFocusProxy in the editor, but so far can't make it work.
Thanks for the help
import sys
from typing import Any, Dict
from PyQt5 import QtWidgets, QtCore
from PyQt5.QtCore import QAbstractListModel, QModelIndex, Qt, QAbstractTableModel, QSize
from PyQt5.QtMultimedia import QMediaMetaData
from PyQt5.QtWidgets import QApplication, QMainWindow, QVBoxLayout, QListView, QWidget, QStyledItemDelegate, \
QTableView, QHeaderView, QAbstractScrollArea, QLabel
class TableModel(QAbstractTableModel):
def __init__(self):
super().__init__()
self._data_list = []
#property
def data_list(self):
return self._data_list
#data_list.setter
def data_list(self, data_list):
self.beginResetModel()
self._data_list = data_list
self.endResetModel()
def headerData(self, section: int, orientation: QMediaMetaData.Orientation, role: int = ...) -> Any:
if role == Qt.DisplayRole:
if section == 0:
return "header"
def data(self, index: QModelIndex, role: int = ...) -> Any:
if index.column() == 0:
if role == Qt.DisplayRole:
return self.data_list[index.row()]
def rowCount(self, parent: QModelIndex = ..., *args, **kwargs) -> int:
return len(self.data_list)
def columnCount(self, parent: QModelIndex = ..., *args, **kwargs) -> int:
return 1
class ListModel(QAbstractListModel):
def __init__(self):
super().__init__()
def rowCount(self, parent=QModelIndex(), *args, **kwargs) -> int:
return 2
def data(self, index: QModelIndex, role: int = Qt.DisplayRole) -> Any:
if index.row() == 0:
if role == Qt.DisplayRole:
return ["Hello", "World!"]
elif index.row() == 1:
if role == Qt.DisplayRole:
return ["One", "Two", "Three!"]
class ItemEditor(QWidget):
def __init__(self, parent=None):
super().__init__(parent)
self.verticalLayout = QVBoxLayout(self)
self.line_label = QLabel("This is a line", self)
self.verticalLayout.addWidget(self.line_label)
self.table_view = QTableView(self)
self.table_view.setFocusPolicy(QtCore.Qt.NoFocus)
self.table_view.setSizeAdjustPolicy(QtWidgets.QAbstractScrollArea.AdjustToContents)
self.table_view.setSelectionMode(QtWidgets.QAbstractItemView.NoSelection)
self.table_view.horizontalHeader().setVisible(False)
self.table_view.verticalHeader().setVisible(False)
self.verticalLayout.addWidget(self.table_view)
class StyledItemDelegate(QStyledItemDelegate):
def __init__(self, parent):
super(StyledItemDelegate, self).__init__(parent)
self.editors: Dict[int, QTableView] = {}
def sizeHint(self, option: 'QStyleOptionViewItem', index: QModelIndex) -> QSize:
if index.row() in self.editors.keys():
return QSize(min(self.parent().width(), self.editors[index.row()].sizeHint().width()),
self.editors[index.row()].sizeHint().height())
else:
return super(StyledItemDelegate, self).sizeHint(option, index)
def createEditor(self, parent, option, index):
editor = ItemEditor(parent)
editor.setFocusProxy(parent)
editor.table_view.setModel(TableModel())
editor.table_view.setSizeAdjustPolicy(QAbstractScrollArea.AdjustToContents)
header = editor.table_view.horizontalHeader()
header.setSectionResizeMode(0, QHeaderView.Stretch)
return editor
def setEditorData(self, editor: QTableView, index):
editor.table_view.model().data_list = index.data()
self.editors[index.row()] = editor
def change_line(index: QModelIndex):
# FIXME Only working when clicking on the "editor" widget or the QLabel, not on the Table
print(f"Changing to line {index.row()}")
if __name__ == "__main__":
app = QApplication([])
main_window = QMainWindow()
centralwidget = QWidget(main_window)
verticalLayout = QVBoxLayout(centralwidget)
listView = QListView(centralwidget)
listView.clicked.connect(change_line)
verticalLayout.addWidget(listView)
main_window.setCentralWidget(centralwidget)
listModel = ListModel()
listView.setModel(listModel)
delegate = StyledItemDelegate(listView)
listView.setItemDelegate(delegate)
for i in range(listModel.rowCount()):
index = listModel.index(i, 0)
listView.openPersistentEditor(index)
main_window.resize(600,400)
main_window.show()
sys.exit(app.exec_())
Since no mouse interaction is required, a possibility is to make the table "transparent" to mouse events:
self.table_view.setAttribute(QtCore.Qt.WA_TransparentForMouseEvents)
Note that by default the editor doesn't change the size hint of the cell but the other way around, so you should emit sizeHintChanged when creating the editor, and you should add the editor to the dictionary in createEditor, not in setEditorData.
self.editors[index.row()] = editor
QtCore.QTimer.singleShot(0, lambda: self.sizeHintChanged.emit(index))
return editor
I'm using a subclassed QAbstractTableModel with dataclasses as items. Each dataclass contains a field "field1" with a list, which I'd like to display in a listview and have it automatically change whenever I edit or add an item in the listview.
To do that I set a custom delegate to the QDataWidgetMapper which will retrieve and set the values from that dataclass. This works the way I want it to.
My problem is that I want to add additional items to that listview with the press of a button and have the QDataWidgetMapper add them automatically to the model.
This is what I have so far:
import sys
import dataclasses
from typing import List, Any
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
#dataclasses.dataclass()
class StorageItem:
field1: List[str] = dataclasses.field(default_factory=list)
class StorageModel(QAbstractTableModel):
def __init__(self, parent=None):
super().__init__(parent)
test = StorageItem()
test.field1 = ['Item °1', 'Item °2']
self._data: List[StorageItem] = [test]
def data(self, index: QModelIndex, role: int = ...) -> Any:
if not index.isValid():
return
item = self._data[index.row()]
col = index.column()
if role in {Qt.DisplayRole, Qt.EditRole}:
if col == 0:
return item.field1
else:
return None
def setData(self, index: QModelIndex, value, role: int = ...) -> bool:
if not index.isValid() or role != Qt.EditRole:
return False
item = self._data[index.row()]
col = index.column()
if col == 0:
item.field1 = value
self.dataChanged.emit(index, index)
print(self._data)
return True
def flags(self, index: QModelIndex) -> Qt.ItemFlags:
return Qt.ItemFlags(
Qt.ItemIsEnabled | Qt.ItemIsSelectable | Qt.ItemIsEditable
)
def rowCount(self, parent=None) -> int:
return len(self._data)
def columnCount(self, parent=None) -> int:
return len(dataclasses.fields(StorageItem))
class TestDelegate(QStyledItemDelegate):
def __init__(self, parent=None):
super().__init__(parent)
def setEditorData(self, editor: QWidget, index: QModelIndex) -> None:
if isinstance(editor, QListView):
data = index.model().data(index, Qt.DisplayRole)
editor.model().setStringList(data)
else:
super().setEditorData(editor, index)
def setModelData(
self, editor: QWidget,
model: QAbstractItemModel,
index: QModelIndex
) -> None:
if isinstance(editor, QListView):
data = editor.model().stringList()
model.setData(index, data, Qt.EditRole)
else:
super().setModelData(editor, model, index)
class CustomListView(QListView):
item_added = pyqtSignal(name='itemAdded')
def __init__(self, parent=None):
super().__init__(parent)
self.setModel(QStringListModel())
def add_item(self, item: str):
str_list = self.model().stringList()
str_list.append(item)
self.model().setStringList(str_list)
self.item_added.emit()
class MainWindow(QMainWindow):
def __init__(self, parent=None):
super().__init__(parent)
cent_widget = QWidget()
self.setCentralWidget(cent_widget)
# Vertical Layout
v_layout = QVBoxLayout()
v_layout.setContentsMargins(10, 10, 10, 10)
# Listview
self.listview = CustomListView()
v_layout.addWidget(self.listview)
# Button
self.btn = QPushButton('Add')
self.btn.clicked.connect(lambda: self.listview.add_item('New Item'))
v_layout.addWidget(self.btn)
cent_widget.setLayout(v_layout)
# Set Mapping
self.mapper = QDataWidgetMapper()
self.mapper.setItemDelegate(TestDelegate())
self.mapper.setSubmitPolicy(QDataWidgetMapper.AutoSubmit)
self.mapper.setModel(StorageModel())
self.mapper.addMapping(self.listview, 0)
self.mapper.toFirst()
self.listview.itemAdded.connect(self.mapper.submit)
def main():
app = QApplication(sys.argv)
window = MainWindow()
window.show()
app.exec()
if __name__ == '__main__':
main()
Currently, I'm using the signal itemAdded from inside the custom ListView to manually submit the QDataWidgetMapper.
Is there a way to do this within CustomListView, without using a custom signal?
Somehow the delegate knows when data in the listview has been edited. How can I trigger that same mechanism when new items are added?
TL; DR; It can not.
The submitPolicy QDataWidgetMapper::AutoSubmit indicates that the model will be updated when focus is lost. The model is also updated when the commitData or closeEditor signal of the delegate is invoked, which happens by default when some specific keys are pressed.
A better implementation would be to create a signal that is emitted every time a change is made in the QListView model and connect it to submit, not just the method of adding elements. Also it is better to use a custom qproperty.
class CustomListView(QListView):
items_changed = pyqtSignal(name="itemsChanged")
def __init__(self, parent=None):
super().__init__(parent)
self.setModel(QStringListModel())
self.model().rowsInserted.connect(self.items_changed)
self.model().rowsRemoved.connect(self.items_changed)
self.model().dataChanged.connect(self.items_changed)
self.model().layoutChanged.connect(self.items_changed)
def add_item(self, item: str):
self.items += [item]
#pyqtProperty(list, notify=items_changed)
def items(self):
return self.model().stringList()
#items.setter
def items(self, data):
if len(data) == len(self.items) and all(
x == y for x, y in zip(data, self.items)
):
return
self.model().setStringList(data)
self.items_changed.emit()
# Set Mapping
self.mapper = QDataWidgetMapper()
self.mapper.setModel(StorageModel())
self.mapper.addMapping(self.listview, 0, b"items")
self.mapper.toFirst()
self.listview.items_changed.connect(self.mapper.submit)
I'm trying to reorder this QSqlTableModel from a QListView but it seems impossible i tried every thing i found on the internet ( offical documentation, examples forums blogs ) but Nothing happend,i activated the moving action and override flags method mimeData method, honnestly i don't know if i did it correctly. the drag action is working but the problem is on dragging i think.
Here is my advanced programm
import sys
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
from PyQt5.QtGui import *
from PyQt5.QtSql import *
import sys, os
def create_connection():
db = QSqlDatabase.addDatabase("QSQLITE")
db.setDatabaseName("medias.sqlite")
if not db.open():
print(db.lastError().text())
return False
q = QSqlQuery()
if not q.exec(
"""
CREATE TABLE IF NOT EXISTS fichiers (
id INTEGER PRIMARY KEY AUTOINCREMENT UNIQUE NOT NULL,
path VARCHAR(300) NOT NULL
)
"""
):
print(q.lastError().text())
return False
print(db.tables())
return True
class listModel(QSqlTableModel):
def __init__(self,*args, **kwargs):
super(listModel, self).__init__(*args, **kwargs)
self.setEditStrategy(QSqlTableModel.OnFieldChange)
print('mimetype', self.mimeTypes())
self.setTable("fichiers")
self.select()
def flags(self, index):
return Qt.ItemIsEditable | Qt.ItemIsDragEnabled | Qt.ItemIsSelectable | Qt.ItemIsEnabled | Qt.ItemIsDropEnabled
def ajouter(self, fichier):
rec = self.record()
rec.setValue("path", fichier)
self.insertRecord(-1, rec)
self.select()
def mimeData(self, indexes):
types = self.mimeTypes()
mimeData = QMimeData()
print('mimedata', mimeData)
encodedData = QByteArray()
stream = QDataStream(encodedData, QIODevice.WriteOnly)
for index in indexes:
if not index.isValid():
continue
if index.isValid():
node = index.internalPointer()
text = self.data(index, Qt.DisplayRole)
mimeData.setData(types[0], encodedData)
return mimeData
def supportedDragActions(self):
return Qt.MoveAction
def supportedDropActions(self):
return Qt.MoveAction
def dropMimeData(self, data, action, row, column, parent):
if action == Qt.MoveAction:
print ("Moving")
self.insertRows(row, 1, QModelIndex())
idx = self.index(row, 0, QModelIndex())
self.setData(idx, self.mimeTypes())
return True
if row != -1:
beginRow = row
elif parent.isValid():
beginRow = parent.row()
else:
beginRow = rowCount(QModelIndex())
class StyledItemDelegate(QStyledItemDelegate):
def initStyleOption(self, option, index):
super().initStyleOption(option, index)
data = str(index.data())
text = data.split('/')[-1]
option.text = f"{index.row() + 1} - {text}"
class MainWindow(QMainWindow):
def __init__(self, *args, **kwargs):
super(MainWindow, self).__init__(*args, **kwargs)
self.setGeometry(900, 180, 800, 600)
self.setWindowTitle("Media Display")
self.setWindowIcon(QIcon('favicon.png'))
self.model = listModel()
self.listview = QListView()
delegate = StyledItemDelegate(self.listview)
self.listview.setItemDelegate(delegate)
self.listview.setModel(self.model)
self.listview.setModelColumn(1)
self.listview.setAcceptDrops(True)
self.listview.setDragEnabled(True)
self.listview.setDragDropOverwriteMode(True)
self.listview.viewport().setAcceptDrops(True)
# self.listview.setSelectionMode(QAbstractItemView.ExtendedSelection)
self.listview.setDropIndicatorShown(True)
self.listview.setDragDropMode(QAbstractItemView.InternalMove)
self.init_ui()
def addImage(self):
fichier_base, _ = QFileDialog.getOpenFileName(
self, "select video", QDir.homePath(), "Images (*.png *.xpm *.jpg *.jpeg)"
)
if fichier_base:
self.model.ajouter(fichier_base)
def clearDb(self):
query = QSqlQuery()
query.exec("DELETE FROM fichiers")
self.model.select()
def init_ui(self):
self.add_img_btn = QPushButton("Add image ")
self.add_img_btn.setFixedWidth(100)
self.add_img_btn.clicked.connect(self.addImage)
self.clear_db_btn = QPushButton("clear DB")
self.clear_db_btn.setFixedWidth(100)
self.clear_db_btn.clicked.connect(self.clearDb)
group_btns = QHBoxLayout()
main_app = QVBoxLayout()
main_app.addWidget(self.listview)
main_app.addLayout(group_btns)
group_btns.addWidget(self.add_img_btn)
group_btns.addWidget(self.clear_db_btn)
widget = QWidget()
vboxlay = QHBoxLayout(widget)
vboxlay.addLayout(main_app)
self.setCentralWidget(widget)
if __name__ == "__main__":
app = QApplication(sys.argv)
if not create_connection():
sys.exit(-1)
window = MainWindow()
window.setStyleSheet("background-color:#fff;")
window.show()
sys.exit(app.exec_())
I hate to be "that guy" but... what do you actually expect to be able to do, here?
A QSQLTableModel is a representation of an SQL table, which will have its specific ordering. You are able to sort the records in different ways (with .sort() or, if you need anything more complex, by piping it through a QSortFilterProxyModel). Implementing an arbitrary ordering where you can drag and drop elements in different places, though, is not something I would expect to be able to do with an SQL table. If you really need something like that, you would probably have to implement your own custom model, on which you "load" the sql-fetched data but which you then manage on your own. There you can implement your own ordering scheme, which you can manipulate from the view.
I'd like to modify QTreeWidget to make the selected cell editable when the enter key is hit, but keep the selection to full rows.
I've done a hacky implementation of figuring out where the last click was and saving the value, then sending those values to my edit_item function on the key press (also used for the itemDoubleClicked signal). It's not great though and I'm wondering if there's a much easier way to do it.
For the record, clicking on an item still selects the whole row. It's probably hidden behaviour by default, but in Maya there's a visible selection thing of the last cell that was moved over while the mouse button was held. If I could somehow get access to that, I could also add in behaviour to control it with the arrow keys.
This is an example of the selected cell:
This is my code so far:
class QTreeWidget(QtWidgets.QTreeWidget):
returnPressed = QtCore.Signal(QTreeWidget, int)
def __init__(self, *args, **kwargs):
QtWidgets.QTreeWidget.__init__(self, *args, **kwargs)
def keyPressEvent(self, event):
if event.key() == QtCore.Qt.Key_Return:
self.returnPressed.emit(self._selected_item, self._selected_column)
else:
QtWidgets.QTreeWidget.keyPressEvent(self, event)
def _mouse_pos_calculate(self, x_pos):
"""Find the currently selected column."""
try:
item = self.selectedItems()[0]
except IndexError:
item = None
header = self.header()
total_width = 0
for i in range(self.columnCount()):
total_width += header.sectionSize(i)
if total_width > x_pos:
return (item, i)
def mousePressEvent(self, event):
QtWidgets.QTreeWidget.mousePressEvent(self, event)
self._selected_item, self._selected_column = self._mouse_pos_calculate(event.pos().x())
def mouseReleaseEvent(self, event):
QtWidgets.QTreeWidget.mouseReleaseEvent(self, event)
self._selected_item, self._selected_column = self._mouse_pos_calculate(event.pos().x())
Edit: Improved function thanks to eyllanesc
class QTreeWidget(QtWidgets.QTreeWidget):
"""Add ability to edit cells when pressing return."""
itemEdit = QtCore.Signal(QtWidgets.QTreeWidgetItem, int)
def __init__(self, *args, **kwargs):
QtWidgets.QTreeWidget.__init__(self, *args, **kwargs)
self._last_item = None
self._last_column = 0
self.itemDoubleClicked.connect(self._edit_item_intercept)
def _edit_item_intercept(self, item=None, column=None):
if item is None:
item = self._last_item
if column is None:
column = self._last_column
self.itemEdit.emit(item, column)
def _store_last_cell(self, pos):
selected_item = self.itemAt(pos)
if selected_item is None:
return
self._last_item = selected_item
self._last_column = self.header().logicalIndexAt(pos.x())
def keyPressEvent(self, event):
if event.key() == QtCore.Qt.Key_Return:
return self._edit_item_intercept()
QtWidgets.QTreeWidget.keyPressEvent(self, event)
def mouseMoveEvent(self, event):
QtWidgets.QTreeWidget.mouseMoveEvent(self, event)
self._store_last_cell(event.pos())
You are doing a lot of calculation unnecessarily, in the next part I show a cleaner solution:
from PySide2 import QtCore, QtGui, QtWidgets
class QTreeWidget(QtWidgets.QTreeWidget):
def __init__(self, *args, **kwargs):
super(TreeWidget, self).__init__(*args, **kwargs)
self.special_item = None
self.special_col = 0
def keyPressEvent(self, event):
if event.key() == QtCore.Qt.Key_Return:
self.editItem(self.special_item, self.special_col)
QtWidgets.QTreeWidget.keyPressEvent(self, event)
def editEnable(self, pos):
press_item = self.itemAt(pos)
if press_item is None:
return
if press_item is self.selectedItems()[0]:
col = self.header().logicalIndexAt(pos.x())
self.special_item = press_item
self.special_col = col
def mousePressEvent(self, event):
QtWidgets.QTreeWidget.mousePressEvent(self, event)
self.editEnable(event.pos())
I am dynamically creating a QTableView from a Pandas dataframe. I have example code here.
I can create the table, with the checkboxes but I cannot get the checkboxes to reflect the model data, or even to change at all to being unchecked.
I am following example code from this previous question and taking #raorao answer as a guide. This will display the boxes in the table, but non of the functionality is working.
Can anyone suggest any changes, or what is wrong with this code. Why is it not reflecting the model, and why can it not change?
Do check out my full example code here.
Edit one : Update after comment from Frodon :
corrected string cast to bool with a comparison xxx == 'True'
class CheckBoxDelegate(QtGui.QStyledItemDelegate):
"""
A delegate that places a fully functioning QCheckBox in every
cell of the column to which it's applied
"""
def __init__(self, parent):
QtGui.QItemDelegate.__init__(self, parent)
def createEditor(self, parent, option, index):
'''
Important, otherwise an editor is created if the user clicks in this cell.
** Need to hook up a signal to the model
'''
return None
def paint(self, painter, option, index):
'''
Paint a checkbox without the label.
'''
checked = index.model().data(index, QtCore.Qt.DisplayRole) == 'True'
check_box_style_option = QtGui.QStyleOptionButton()
if (index.flags() & QtCore.Qt.ItemIsEditable) > 0:
check_box_style_option.state |= QtGui.QStyle.State_Enabled
else:
check_box_style_option.state |= QtGui.QStyle.State_ReadOnly
if checked:
check_box_style_option.state |= QtGui.QStyle.State_On
else:
check_box_style_option.state |= QtGui.QStyle.State_Off
check_box_style_option.rect = self.getCheckBoxRect(option)
# this will not run - hasFlag does not exist
#if not index.model().hasFlag(index, QtCore.Qt.ItemIsEditable):
#check_box_style_option.state |= QtGui.QStyle.State_ReadOnly
check_box_style_option.state |= QtGui.QStyle.State_Enabled
QtGui.QApplication.style().drawControl(QtGui.QStyle.CE_CheckBox, check_box_style_option, painter)
def editorEvent(self, event, model, option, index):
'''
Change the data in the model and the state of the checkbox
if the user presses the left mousebutton or presses
Key_Space or Key_Select and this cell is editable. Otherwise do nothing.
'''
print 'Check Box editor Event detected : '
if not (index.flags() & QtCore.Qt.ItemIsEditable) > 0:
return False
print 'Check Box edior Event detected : passed first check'
# Do not change the checkbox-state
if event.type() == QtCore.QEvent.MouseButtonRelease or event.type() == QtCore.QEvent.MouseButtonDblClick:
if event.button() != QtCore.Qt.LeftButton or not self.getCheckBoxRect(option).contains(event.pos()):
return False
if event.type() == QtCore.QEvent.MouseButtonDblClick:
return True
elif event.type() == QtCore.QEvent.KeyPress:
if event.key() != QtCore.Qt.Key_Space and event.key() != QtCore.Qt.Key_Select:
return False
else:
return False
# Change the checkbox-state
self.setModelData(None, model, index)
return True
Here's a port of the same code above for PyQt5.
Posting here as there doesn't appear to be another example of a working CheckBox Delegate in QT5, and i was tearing my hair out trying to get it working. Hopefully it's useful to someone.
from PyQt5 import QtCore, QtWidgets
from PyQt5.QtCore import QModelIndex
from PyQt5.QtGui import QStandardItemModel
from PyQt5.QtWidgets import QApplication, QTableView
class CheckBoxDelegate(QtWidgets.QItemDelegate):
"""
A delegate that places a fully functioning QCheckBox cell of the column to which it's applied.
"""
def __init__(self, parent):
QtWidgets.QItemDelegate.__init__(self, parent)
def createEditor(self, parent, option, index):
"""
Important, otherwise an editor is created if the user clicks in this cell.
"""
return None
def paint(self, painter, option, index):
"""
Paint a checkbox without the label.
"""
self.drawCheck(painter, option, option.rect, QtCore.Qt.Unchecked if int(index.data()) == 0 else QtCore.Qt.Checked)
def editorEvent(self, event, model, option, index):
'''
Change the data in the model and the state of the checkbox
if the user presses the left mousebutton and this cell is editable. Otherwise do nothing.
'''
if not int(index.flags() & QtCore.Qt.ItemIsEditable) > 0:
return False
if event.type() == QtCore.QEvent.MouseButtonRelease and event.button() == QtCore.Qt.LeftButton:
# Change the checkbox-state
self.setModelData(None, model, index)
return True
return False
def setModelData (self, editor, model, index):
'''
The user wanted to change the old state in the opposite.
'''
model.setData(index, 1 if int(index.data()) == 0 else 0, QtCore.Qt.EditRole)
if __name__ == '__main__':
import sys
app = QApplication(sys.argv)
model = QStandardItemModel(4, 3)
tableView = QTableView()
tableView.setModel(model)
delegate = CheckBoxDelegate(None)
tableView.setItemDelegateForColumn(1, delegate)
for row in range(4):
for column in range(3):
index = model.index(row, column, QModelIndex())
model.setData(index, 1)
tableView.setWindowTitle("Check Box Delegate")
tableView.show()
sys.exit(app.exec_())
I've found a solution for you. The trick was:
to write the setData method of the model
to always return a QVariant in the data method
Here it is. (I had to create a class called Dataframe, to make the example work without pandas. Please replace all the if has_pandas statements by yours):
from PyQt4 import QtCore, QtGui
has_pandas = False
try:
import pandas as pd
has_pandas = True
except:
pass
class TableModel(QtCore.QAbstractTableModel):
def __init__(self, parent=None, *args):
super(TableModel, self).__init__()
self.datatable = None
self.headerdata = None
def update(self, dataIn):
print 'Updating Model'
self.datatable = dataIn
print 'Datatable : {0}'.format(self.datatable)
if has_pandas:
headers = dataIn.columns.values
else:
headers = dataIn.columns
header_items = [
str(field)
for field in headers
]
self.headerdata = header_items
print 'Headers'
print self.headerdata
def rowCount(self, parent=QtCore.QModelIndex()):
return len(self.datatable.index)
def columnCount(self, parent=QtCore.QModelIndex()):
if has_pandas:
return len(self.datatable.columns.values)
else:
return len(self.datatable.columns)
def data(self, index, role=QtCore.Qt.DisplayRole):
if role == QtCore.Qt.DisplayRole:
i = index.row()
j = index.column()
return QtCore.QVariant('{0}'.format(self.datatable.iget_value(i, j)))
else:
return QtCore.QVariant()
def setData(self, index, value, role=QtCore.Qt.DisplayRole):
if index.column() == 4:
self.datatable.iset_value(index.row(), 4, value)
return value
return value
def headerData(self, col, orientation, role):
if orientation == QtCore.Qt.Horizontal and role == QtCore.Qt.DisplayRole:
return '{0}'.format(self.headerdata[col])
def flags(self, index):
if index.column() == 4:
return QtCore.Qt.ItemIsEditable | QtCore.Qt.ItemIsEnabled
else:
return QtCore.Qt.ItemIsEnabled
class TableView(QtGui.QTableView):
"""
A simple table to demonstrate the QComboBox delegate.
"""
def __init__(self, *args, **kwargs):
QtGui.QTableView.__init__(self, *args, **kwargs)
self.setItemDelegateForColumn(4, CheckBoxDelegate(self))
class CheckBoxDelegate(QtGui.QStyledItemDelegate):
"""
A delegate that places a fully functioning QCheckBox in every
cell of the column to which it's applied
"""
def __init__(self, parent):
QtGui.QItemDelegate.__init__(self, parent)
def createEditor(self, parent, option, index):
'''
Important, otherwise an editor is created if the user clicks in this cell.
** Need to hook up a signal to the model
'''
return None
def paint(self, painter, option, index):
'''
Paint a checkbox without the label.
'''
checked = index.data().toBool()
check_box_style_option = QtGui.QStyleOptionButton()
if (index.flags() & QtCore.Qt.ItemIsEditable) > 0:
check_box_style_option.state |= QtGui.QStyle.State_Enabled
else:
check_box_style_option.state |= QtGui.QStyle.State_ReadOnly
if checked:
check_box_style_option.state |= QtGui.QStyle.State_On
else:
check_box_style_option.state |= QtGui.QStyle.State_Off
check_box_style_option.rect = self.getCheckBoxRect(option)
# this will not run - hasFlag does not exist
#if not index.model().hasFlag(index, QtCore.Qt.ItemIsEditable):
#check_box_style_option.state |= QtGui.QStyle.State_ReadOnly
check_box_style_option.state |= QtGui.QStyle.State_Enabled
QtGui.QApplication.style().drawControl(QtGui.QStyle.CE_CheckBox, check_box_style_option, painter)
def editorEvent(self, event, model, option, index):
'''
Change the data in the model and the state of the checkbox
if the user presses the left mousebutton or presses
Key_Space or Key_Select and this cell is editable. Otherwise do nothing.
'''
print 'Check Box editor Event detected : '
print event.type()
if not (index.flags() & QtCore.Qt.ItemIsEditable) > 0:
return False
print 'Check Box editor Event detected : passed first check'
# Do not change the checkbox-state
if event.type() == QtCore.QEvent.MouseButtonPress:
return False
if event.type() == QtCore.QEvent.MouseButtonRelease or event.type() == QtCore.QEvent.MouseButtonDblClick:
if event.button() != QtCore.Qt.LeftButton or not self.getCheckBoxRect(option).contains(event.pos()):
return False
if event.type() == QtCore.QEvent.MouseButtonDblClick:
return True
elif event.type() == QtCore.QEvent.KeyPress:
if event.key() != QtCore.Qt.Key_Space and event.key() != QtCore.Qt.Key_Select:
return False
else:
return False
# Change the checkbox-state
self.setModelData(None, model, index)
return True
def setModelData (self, editor, model, index):
'''
The user wanted to change the old state in the opposite.
'''
print 'SetModelData'
newValue = not index.data().toBool()
print 'New Value : {0}'.format(newValue)
model.setData(index, newValue, QtCore.Qt.EditRole)
def getCheckBoxRect(self, option):
check_box_style_option = QtGui.QStyleOptionButton()
check_box_rect = QtGui.QApplication.style().subElementRect(QtGui.QStyle.SE_CheckBoxIndicator, check_box_style_option, None)
check_box_point = QtCore.QPoint (option.rect.x() +
option.rect.width() / 2 -
check_box_rect.width() / 2,
option.rect.y() +
option.rect.height() / 2 -
check_box_rect.height() / 2)
return QtCore.QRect(check_box_point, check_box_rect.size())
###############################################################################################################################
class Dataframe(dict):
def __init__(self, columns, values):
if len(values) != len(columns):
raise Exception("Bad values")
self.columns = columns
self.values = values
self.index = values[0]
super(Dataframe, self).__init__(dict(zip(columns, values)))
pass
def iget_value(self, i, j):
return(self.values[j][i])
def iset_value(self, i, j, value):
self.values[j][i] = value
if __name__=="__main__":
from sys import argv, exit
class Widget(QtGui.QWidget):
"""
A simple test widget to contain and own the model and table.
"""
def __init__(self, parent=None):
QtGui.QWidget.__init__(self, parent)
l=QtGui.QVBoxLayout(self)
cdf = self.get_data_frame()
self._tm=TableModel(self)
self._tm.update(cdf)
self._tv=TableView(self)
self._tv.setModel(self._tm)
for row in range(0, self._tm.rowCount()):
self._tv.openPersistentEditor(self._tm.index(row, 4))
self.setGeometry(300, 300, 550, 200)
l.addWidget(self._tv)
def get_data_frame(self):
if has_pandas:
df = pd.DataFrame({'Name':['a','b','c','d'],
'First':[2.3,5.4,3.1,7.7], 'Last':[23.4,11.2,65.3,88.8], 'Class':[1,1,2,1], 'Valid':[True, False, True, False]})
else:
columns = ['Name', 'First', 'Last', 'Class', 'Valid']
values = [['a','b','c','d'], [2.3,5.4,3.1,7.7], [23.4,11.2,65.3,88.8], [1,1,2,1], [True, False, True, False]]
df = Dataframe(columns, values)
return df
a=QtGui.QApplication(argv)
w=Widget()
w.show()
w.raise_()
exit(a.exec_())
I added
if event.type() == QEvent.MouseButtonPress or event.type() == QEvent.MouseMove:
return False
to prevent checkbox from flickering when moving the mouse