Qt Custom Multi-Select QComboBox from SQL Model - python

I want a drop-down category selection box which allows the user to click multiple selections, and which preferably is connected to an SQL query. I'm not attached to the Combobox in particular (disappearing on each click isn't helpful), whatever can get the job done.
At the current moment, I have basically that jerry-rigged together, it allows for multiple selections technically but it's just based on randomly if the mouse is dragged over it or not.
self.catbx=QComboBox()
...
self.catq=QtSql.QSqlQuery(conn)
self.catq.exec("SELECT name FROM categories")
self.catmo=QtSql.QSqlQueryModel()
self.catmo.setQuery(self.catq)
self.catbx.setModel(self.catmo)
...
self.catview=QListView()
self.catview.setModel(self.catmo)
self.catbx.setView(self.catview)
self.catview.setSelectionMode(QAbstractItemView.MultiSelection)
hope that's clear enough and someone can help! :)

Basically it is that the items are not selectable, since that is the event that triggers the closing of the popup so the solution is to eliminate that flag in the model as I did in a previous answer.
On the other hand the option to be checkeable does not come by default in the QSqlQueryModel, so we have to implement it for that we based on another previous answer.
Finally, a class is created that inherits from QComboBox and we overwrite the hidePopup() method to emit a signal that sends the selected items.
Update:
If you want to also be marked when you press any part of the item you must create a delegate and overwrite the editorEvent() method so that it handles the MouseButtonRelease event. But this brings a small problem: to open the popup you have to press the item that is displayed so it will open marked.
from PyQt5 import QtCore, QtGui, QtWidgets, QtSql
class CheckSqlQueryModel(QtSql.QSqlQueryModel):
def __init__(self, *args, **kwargs):
QtSql.QSqlQueryModel.__init__(self, *args, **kwargs)
self.checks = {}
def checkState(self, pindex):
if pindex not in self.checks.keys():
self.checks[pindex] = QtCore.Qt.Unchecked
return self.checks[pindex]
def data(self, index, role=QtCore.Qt.DisplayRole):
if role == QtCore.Qt.CheckStateRole and index.isValid():
return self.checkState(QtCore.QPersistentModelIndex(index))
return QtSql.QSqlQueryModel.data(self, index, role)
def setData(self, index, value, role=QtCore.Qt.EditRole):
if role == QtCore.Qt.CheckStateRole and index.isValid():
self.checks[QtCore.QPersistentModelIndex(index)] = value
return True
return QtSql.QSqlQueryModel(self, index, value, role)
def flags(self, index):
fl = QtSql.QSqlQueryModel.flags(self, index) & ~QtCore.Qt.ItemIsSelectable
fl |= QtCore.Qt.ItemIsEditable | QtCore.Qt.ItemIsUserCheckable
return fl
class CheckComboBox(QtWidgets.QComboBox):
selectedChanged = QtCore.pyqtSignal(list)
def hidePopup(self):
results = []
for i in range(self.count()):
if self.itemData(i, QtCore.Qt.CheckStateRole) == QtCore.Qt.Checked:
results.append(self.itemText(i))
self.selectedChanged.emit(results)
QtWidgets.QComboBox.hidePopup(self)
class CheckDelegate(QtWidgets.QStyledItemDelegate):
def editorEvent(self, event, model, option, index):
if event.type() == QtCore.QEvent.MouseButtonRelease:
val = index.data(QtCore.Qt.CheckStateRole)
new_val = QtCore.Qt.Checked if val == QtCore.Qt.Unchecked else QtCore.Qt.Unchecked
model.setData(index, new_val, QtCore.Qt.CheckStateRole)
return True
return QtWidgets.QStyledItemDelegate.editorEvent(self, event, model, option, index)
class Widget(QtWidgets.QWidget):
def __init__(self, *args, **kwargs):
QtWidgets.QWidget.__init__(self, *args, **kwargs)
lay = QtWidgets.QVBoxLayout(self)
combo = CheckComboBox()
combo.setView(QtWidgets.QListView())
combo.setItemDelegate(CheckDelegate(combo))
model = CheckSqlQueryModel()
model.setQuery("SELECT name FROM categories")
combo.setModel(model)
self.lw = QtWidgets.QListWidget()
combo.selectedChanged.connect(self.on_selectedChanged)
lay.addWidget(combo)
lay.addWidget(self.lw)
def on_selectedChanged(self, items):
self.lw.clear()
self.lw.addItems(items)
def createConnection():
db = QtSql.QSqlDatabase.addDatabase("QSQLITE")
db.setDatabaseName(":memory:")
if not db.open():
QtWidgets.QMessageBox.critical(None, "Cannot open database",
"Unable to establish a database connection.\n"
"This example needs SQLite support. Please read "
"the Qt SQL driver documentation for information how "
"to build it.\n\n"
"Click Cancel to exit.", QMessageBox.Cancel)
return False
query = QtSql.QSqlQuery()
query.exec_("create table categories (id int primary key, name varchar(20))");
for i in range(1, 10):
query.exec_("insert into categories values({i}, 'categories-{i}')".format(i=i));
return True
if __name__ == "__main__":
import sys
app = QtWidgets.QApplication(sys.argv)
if not createConnection():
sys.exit(-1)
w = Widget()
w.show()
sys.exit(app.exec_())

Related

QTableView with QWidget as QStyledItemDelegate

I'm coding a Python CRUD app that shows radiosondes on a map and a QTableView. I'm using QStyledItemDelegate to set an editor and regex validator for each column and it is working great. But for the geometry column I would like to parse the binary data and show it on a custom form (lat, lng, elevation), be able to edit them and if clicked OK encode them back to WKB format and update the data.
When I click OK the filed is not updated, instead it becomes empty. If I try to edit after that any other cell nothing happens and if I try to edit that exact cell the app crashes. The same happens if I click Cancel.
The setData method returns True and the data in the DB gets updated.
I tried with dataChanged.emit() on the QSqlTableModel and also with update() method on the QTableView.
main2.py:
from PyQt5.Qt import QStyledItemDelegate
from PyQt5.QtWebEngineWidgets import QWebEngineView
from PyQt5.QtCore import Qt
from shapely import wkb, wkt
import folium
import io
class Ui_RadioSondes(object):
def setupUi(self, RadioSondes):
self.centerCoord = (44.071800, 17.578125)
RadioSondes.setObjectName("RadioSondes")
.
.
.
self.tableView_2.setItemDelegate(ValidatedItemDelegate())
.
.
.
class ValidatedItemDelegate(QStyledItemDelegate):
def createEditor(self, widget, option, index):
if not index.isValid():
return 0
if index.column() == 0: #only on the cells in the first column
editor = QtWidgets.QLineEdit(widget)
validator = QtGui.QRegExpValidator(QtCore.QRegExp('[\w]{1,10}'), editor)
editor.setValidator(validator)
return editor
if index.column() == 2:
editor = QtWidgets.QSpinBox(widget)
editor.setMaximum(360)
editor.setMinimum(1)
return editor
.
.
.
if index.column() == 9:
self.form = QtWidgets.QWidget()
self.formLayout = QtWidgets.QFormLayout(self.form)
self.formLayout.setVerticalSpacing(12)
self.formLayout.setObjectName("formLayout")
###__________ Latitude__________###
self.latLabel = QtWidgets.QLabel(self.form)
self.latLabel.setObjectName("latLabel")
self.latLabel.setText("Latitude")
self.latLabel.adjustSize()
self.formLayout.setWidget(0, QtWidgets.QFormLayout.LabelRole, self.latLabel)
self.latEdit = QtWidgets.QLineEdit(self.form)
# lineEdit.textChanged.connect(validateFields)
self.latEdit.setObjectName("latEdit")
self.formLayout.setWidget(0, QtWidgets.QFormLayout.FieldRole, self.latEdit)
###__________ Longitude__________###
self.lngLabel = QtWidgets.QLabel(self.form)
self.lngLabel.setObjectName("lngLabel")
self.lngLabel.setText("Longitude")
self.lngLabel.adjustSize()
self.formLayout.setWidget(1, QtWidgets.QFormLayout.LabelRole, self.lngLabel)
self.lngEdit = QtWidgets.QLineEdit(self.form)
# lineEdit.textChanged.connect(validateFields)
self.lngEdit.setObjectName("lngEdit")
self.formLayout.setWidget(1, QtWidgets.QFormLayout.FieldRole, self.lngEdit)
###__________ Elevation__________###
self.elevationLabel = QtWidgets.QLabel(self.form)
self.elevationLabel.setObjectName("elevationLabel")
self.elevationLabel.setText("Elevation")
self.elevationLabel.adjustSize()
self.formLayout.setWidget(2, QtWidgets.QFormLayout.LabelRole, self.elevationLabel)
self.elevationEdit = QtWidgets.QLineEdit(self.form)
# lineEdit.textChanged.connect(validateFields)
self.elevationEdit.setObjectName("elevationEdit")
self.formLayout.setWidget(2, QtWidgets.QFormLayout.FieldRole, self.elevationEdit)
self.buttonBox = QtWidgets.QDialogButtonBox(self.form)
self.buttonBox.setOrientation(QtCore.Qt.Horizontal)
self.buttonBox.setStandardButtons(QtWidgets.QDialogButtonBox.Cancel | QtWidgets.QDialogButtonBox.Ok)
self.buttonBox.setObjectName("buttonBox")
self.formLayout.addWidget(self.buttonBox)
self.form.resize(200, 300)
self.prevData = index.data()
self.index = index
self.widget = widget
self.model = self.widget.parent().parent().parent().parent().parent().parent().parent().objModel
self.t_view = self.widget.parent().parent().parent().parent().parent().parent().parent().tableView_2
data = self.model.data(self.index)
geomWkb = wkb.loads(bytes.fromhex(data))
self.latEdit.setText(str(geomWkb.x))
self.lngEdit.setText(str(geomWkb.y))
self.elevationEdit.setText(str(geomWkb.z))
self.buttonBox.accepted.connect(self.generateGeom)
self.buttonBox.rejected.connect(self.cancelGeomEdit)
return self.form
return super(ValidatedItemDelegate, self).createEditor(widget, option, index)
def generateGeom(self):
print(self.latEdit.text())
print(self.lngEdit.text())
print(self.elevationEdit.text())
geomStr = "POINT Z (" + self.latEdit.text() + " " + self.lngEdit.text() + " " + self.elevationEdit.text() + ")"
geom = wkt.loads(geomStr)
geomWkb = wkb.dumps(geom, hex=True, srid=4326)
try:
self.model.setData(self.index, geomWkb, Qt.EditRole)
self.form.close()
#self.t_view.update()
except AssertionError as error:
print(error)
def cancelGeomEdit(self):
self.form.destroy(destroyWindow=True)
Here is the whole code on GitHub: https://github.com/draugnim/pyCrud
EDIT
I managed to get it working by calling self.model.selet() at the end of generateGeom() and cancelGeomEdit(). But still if I click on the X button and close the form the edited cell becomes blank, also this and all other cells become uneditable.
The item delegate uses the editor's user property, which is considered the main default property of a Qt object. For QLineEdit it's text(), for a QSpinBox it's value(), etc.
If you want to provide a custom, advanced editor, the solution is to create a subclass with a custom user property.
Note that the Qt internal way of dealing with item data is a bit strict on types, and since PyQt doesn't expose the QVariant class, the only option is to use a suitable type. For you case, QVector3D is a perfect choice.
Then, using an external window is a bit tricky, as delegates are normally supposed to be simple field editors that exist inside the view. To work around that, the following must be considered:
the editor must notify the delegate that the inserted data has been accepted, and it must destroy itself whenever it's closed;
key press events must be ignored in the filter (return False), so that the editor can properly handle them;
focus and hide events must ignored too, as the delegate by default tries to update the model when the editor has not been "rejected" but it loses focus or is hidden;
the geometry must be set using the parent's top level window(), and the updateEditorGeometry() must be ignored as the delegate would try to update the geometry whenever the view is shown again after being hidden or it's resized;
Since the given code is not a minimal, reproducible example, I'll provide a generic example of the concept.
from PyQt5 import QtCore, QtGui, QtWidgets
from random import randrange
class CoordinateEditor(QtWidgets.QDialog):
submit = QtCore.pyqtSignal(QtWidgets.QWidget)
def __init__(self, parent):
super().__init__(parent)
self.setWindowModality(QtCore.Qt.WindowModal)
layout = QtWidgets.QFormLayout(self)
self.latitudeSpin = QtWidgets.QDoubleSpinBox(minimum=-90, maximum=90)
layout.addRow('Latitude', self.latitudeSpin)
self.longitudeSpin = QtWidgets.QDoubleSpinBox(minimum=-180, maximum=180)
layout.addRow('Longitude', self.longitudeSpin)
self.elevationSpin = QtWidgets.QDoubleSpinBox(minimum=-100, maximum=100)
layout.addRow('Elevation', self.elevationSpin)
buttonBox = QtWidgets.QDialogButtonBox(
QtWidgets.QDialogButtonBox.Ok|QtWidgets.QDialogButtonBox.Cancel)
layout.addRow(buttonBox)
buttonBox.accepted.connect(self.accept)
buttonBox.rejected.connect(self.reject)
self.finished.connect(self.deleteLater)
def accept(self):
super().accept()
self.submit.emit(self)
#QtCore.pyqtProperty(QtGui.QVector3D, user=True)
def coordinateData(self):
return QtGui.QVector3D(
self.longitudeSpin.value(),
self.latitudeSpin.value(),
self.elevationSpin.value()
)
#coordinateData.setter
def coordinateData(self, data):
self.longitudeSpin.setValue(data.x())
self.latitudeSpin.setValue(data.y())
self.elevationSpin.setValue(data.z())
def showEvent(self, event):
if not event.spontaneous():
geo = self.geometry()
geo.moveCenter(self.parent().window().geometry().center())
self.setGeometry(geo)
QtCore.QTimer.singleShot(0, self.latitudeSpin.setFocus)
class DialogDelegate(QtWidgets.QStyledItemDelegate):
def createEditor(self, parent, option, index):
if index.column() == 1:
editor = CoordinateEditor(parent)
editor.submit.connect(self.commitData)
return editor
else:
return super().createEditor(parent, option, index)
def initStyleOption(self, option, index):
super().initStyleOption(option, index)
if index.column() == 1 and index.data() is not None:
coords = index.data()
option.text = '{:.02f}, {:.02f}, {:.02f}'.format(
coords.y(), coords.x(), coords.z())
def eventFilter(self, source, event):
if isinstance(source, CoordinateEditor):
if event.type() in (event.KeyPress, event.FocusOut, event.Hide):
return False
return super().eventFilter(source, event)
def updateEditorGeometry(self, editor, option, index):
if not isinstance(editor, CoordinateEditor):
super().updateEditorGeometry(editor, option, index)
if __name__ == '__main__':
import sys
app = QtWidgets.QApplication(sys.argv)
test = QtWidgets.QTableView()
test.setItemDelegate(DialogDelegate(test))
model = QtGui.QStandardItemModel(0, 2)
for row in range(10):
coordItem = QtGui.QStandardItem()
coords = QtGui.QVector3D(
randrange(-180, 181),
randrange(-90, 91),
randrange(-100, 101))
coordItem.setData(coords, QtCore.Qt.DisplayRole)
model.appendRow((
QtGui.QStandardItem('Data {}'.format(row + 1)),
coordItem,
))
test.setModel(model)
test.resizeColumnsToContents()
test.show()
sys.exit(app.exec_())

Checkbox with persistent editor

I'm using a table to control the visibility and color of a plot. I would like a checkbox to toggle visibility and a drop-down to select the color. To this end, I have something like the following. It feels as through having a persistent editor prevents use of the checkbox.
The example is a bit contrived (in how the model/view are set up), but illustrates how the checkbox doesn't function while the editor is open.
How can I have a checkbox that can be used alongside a visible combobox? Is it better to use two columns?
import sys
from PyQt5 import QtWidgets, QtCore
class ComboDelegate(QtWidgets.QItemDelegate):
def __init__(self, parent):
super().__init__(parent=parent)
def createEditor(self, parent, option, index):
combo = QtWidgets.QComboBox(parent)
li = []
li.append("Red")
li.append("Green")
li.append("Blue")
li.append("Yellow")
li.append("Purple")
li.append("Orange")
combo.addItems(li)
combo.currentIndexChanged.connect(self.currentIndexChanged)
return combo
def setEditorData(self, editor, index):
editor.blockSignals(True)
data = index.model().data(index)
if data:
idx = int(data)
else:
idx = 0
editor.setCurrentIndex(0)
editor.blockSignals(False)
def setModelData(self, editor, model, index):
model.setData(index, editor.currentIndex())
#QtCore.pyqtSlot()
def currentIndexChanged(self):
self.commitData.emit(self.sender())
class PersistentEditorTableView(QtWidgets.QTableView):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
QtCore.pyqtSlot('QVariant', 'QVariant')
def data_changed(self, top_left, bottom_right):
for row in range(len(self.model().tableData)):
self.openPersistentEditor(self.model().index(row, 0))
class TableModel(QtCore.QAbstractTableModel):
def __init__(self, parent=None):
super(TableModel, self).__init__(parent)
self.tableData = [[1, 2, 3], [1, 2, 3], [1, 2, 3]]
self.checks = {}
def columnCount(self, *args):
return 3
def rowCount(self, *args):
return 3
def checkState(self, index):
if index in self.checks.keys():
return self.checks[index]
else:
return QtCore.Qt.Unchecked
def data(self, index, role=QtCore.Qt.DisplayRole):
row = index.row()
col = index.column()
if role == QtCore.Qt.DisplayRole:
return '{0}'.format(self.tableData[row][col])
elif role == QtCore.Qt.CheckStateRole and col == 0:
return self.checkState(QtCore.QPersistentModelIndex(index))
return None
def setData(self, index, value, role=QtCore.Qt.EditRole):
if not index.isValid():
return False
if role == QtCore.Qt.CheckStateRole:
self.checks[QtCore.QPersistentModelIndex(index)] = value
self.dataChanged.emit(index, index)
return True
return False
def flags(self, index):
fl = QtCore.QAbstractTableModel.flags(self, index)
if index.column() == 0:
fl |= QtCore.Qt.ItemIsEditable | QtCore.Qt.ItemIsUserCheckable
return fl
if __name__ == "__main__":
app = QtWidgets.QApplication(sys.argv)
view = PersistentEditorTableView()
view.setItemDelegateForColumn(0, ComboDelegate(view))
model = TableModel()
view.setModel(model)
model.dataChanged.connect(view.data_changed)
model.layoutChanged.connect(view.data_changed)
index = model.createIndex(0, 0)
persistet_index = QtCore.QPersistentModelIndex(index)
model.checks[persistet_index] = QtCore.Qt.Checked
view.data_changed(index, index)
view.show()
sys.exit(app.exec_())
NOTE: After some rethinking and analysis of the Qt source code, I realized that my original answer, while valid, is a bit imprecise.
The problem relies on the fact that all events received on an index with an active editor are automatically sent to the editor, but since the editor could have a geometry that is smaller than the visual rect of the index, if a mouse event is sent outside the geometry that event is ignored and not processed by the view as it normally would without an editor.
UPDATE: In fact, the view does receive the event; the problem is that if an editor exists the event is automatically ignored furtherward (since it's assumed that the editor handled it) and nothing is sent to the editorEvent method of the delegate.
You can intercept "mouse edit" events as long as you implement the virtual edit(index, trigger, event) function (which is not the same as the edit(index) slot). Note that this means that if you override that function, you cannot call the default edit(index) anymore, unless you create a separate function that calls the default implementation (via super().edit(index)).
Consider that the following code works better if the delegate is actually a QStyledItemDelegate, instead of the simpler QItemDelegate class: the Qt dev team itself suggests to use the styled class instead of the basic one (which is intended for very basic or specific usage), as it's generally considered more consistent.
class PersistentEditorTableView(QtWidgets.QTableView):
# ...
def edit(self, index, trigger, event):
# if the edit involves an index change, there's no event
if (event and index.column() == 0 and
index.flags() & QtCore.Qt.ItemIsUserCheckable and
event.type() in (event.MouseButtonPress, event.MouseButtonDblClick) and
event.button() == QtCore.Qt.LeftButton):
opt = self.viewOptions()
opt.rect = self.visualRect(index)
opt.features |= opt.HasCheckIndicator
checkRect = self.style().subElementRect(
QtWidgets.QStyle.SE_ItemViewItemCheckIndicator,
opt, self)
if event.pos() in checkRect:
if index.data(QtCore.Qt.CheckStateRole):
state = QtCore.Qt.Unchecked
else:
state = QtCore.Qt.Checked
return self.model().setData(
index, state, QtCore.Qt.CheckStateRole)
return super().edit(index, trigger, event)
Obviously, you could do something similar by implementing the mousePressEvent on the view, but that could complicate things if you need some different implementation of the mouse press event also, and you should also consider the double click events. Implementing edit() is conceptually better, since it's more consistent with the purpose: clicking -> toggling.
There's only one last catch: keyboard events.
A persistent editor automatically grabs the keyboard focus, so you can't use any key (usually, the space bar) to toggle the state if the editor handles that event; since a combobox handles some "toggle" events to show its popup, those events won't be received by the view (the focus is on the combo, not on the view!) unless you ignore the event by properly implementing the eventFilter of the delegate for both keyboard events and focus changes.
Original answer
There are various possibilities to work around this:
as you proposed, use a separate column; this is not always possible or suggested, as the model structure could not allow it;
create an editor that also includes a QCheckBox; as long as there will always be an open editor, this is not an issue, but could create some level of inconsistency if editor could actually be open and destroyed;
add the combo to a container that has a fixed margin, so that mouse event not handled by the combo can be captured by the delegate event filter;
The third possibility is a bit more complex, but it ensures that both displaying and interaction are consistent with the normal behavior.
To achieve this, using a QStyledItemDelegate is suggested as it provides access to the style options.
class ComboDelegate(QtWidgets.QStyledItemDelegate):
# ...
def createEditor(self, parent, option, index):
option = QtWidgets.QStyleOptionViewItem(option)
self.initStyleOption(option, index)
style = option.widget.style()
textRect = style.subElementRect(
style.SE_ItemViewItemText, option, option.widget)
editor = QtWidgets.QWidget(parent)
editor.index = QtCore.QPersistentModelIndex(index)
layout = QtWidgets.QHBoxLayout(editor)
layout.setContentsMargins(textRect.left(), 0, 0, 0)
editor.combo = QtWidgets.QComboBox()
layout.addWidget(editor.combo)
editor.combo.addItems(
("Red", "Green", "Blue", "Yellow", "Purple", "Orange"))
editor.combo.currentIndexChanged.connect(self.currentIndexChanged)
return editor
def setEditorData(self, editor, index):
editor.combo.blockSignals(True)
data = index.model().data(index)
if data:
idx = int(data)
else:
idx = 0
editor.combo.setCurrentIndex(0)
editor.combo.blockSignals(False)
def eventFilter(self, editor, event):
if (event.type() in (event.MouseButtonPress, event.MouseButtonDblClick)
and event.button() == QtCore.Qt.LeftButton):
style = editor.style()
size = style.pixelMetric(style.PM_IndicatorWidth)
left = editor.layout().contentsMargins().left()
r = QtCore.QRect(
(left - size) / 2,
(editor.height() - size) / 2,
size, size)
if event.pos() in r:
model = editor.index.model()
index = QtCore.QModelIndex(editor.index)
if model.data(index, QtCore.Qt.CheckStateRole):
value = QtCore.Qt.Unchecked
else:
value = QtCore.Qt.Checked
model.setData(
index, value, QtCore.Qt.CheckStateRole)
return True
return super().eventFilter(editor, event)
def updateEditorGeometry(self, editor, opt, index):
# ensure that the editor fills the whole index rect
editor.setGeometry(opt.rect)
Unrelated, but still important:
Consider that the #pyqtSlot decorator is usually not required in normal situations like these. Also note that you missed the # for the data_changed decorator, and the signature is also invalid, since it's incompatible with the signals you've connected it to. A more correct slot decoration would have been the following:
#QtCore.pyqtSlot(QtCore.QModelIndex, QtCore.QModelIndex, 'QVector<int>')
#QtCore.pyqtSlot('QList<QPersistentModelIndex>', QtCore.QAbstractItemModel.LayoutChangeHint)
def data_changed(self, top_left, bottom_right):
# ...
With the first decoration for the dataChanged signal, and the second for layoutChanged. But, as said before, it's generally unnecessary to use slots as they usually are only required for better threading handling and sometimes provide slightly improved performance (which is not that important to this purpose).
Also note that if you want to ensure that there's always an open editor whenever the model changes its "layout", you should also connect to rowsInserted and columnsInserted signals, since those operations do not send the layout change signal.

Add additional information to items in a QTreeView/QFileSystemModel

I would like to render each item in a QTreeView differently based on a number of attributes stored in a database and based on whether the item is a folder or a file. However, I don't understand how the QTreeView or QFileSystemModel communicate with the delegate. Whenever an item must be drawn, including during initialization, I'd expect to provide the delegate with all the parameters it requires and then use a series of if statements within the delegate to set how the particular item is drawn. I've only found the .setItemDelegate method and don't know when or how the delegate is actually called or how it loops through all the items in the model. Below is an example based on material online. There are two problems:
I placed code in comments that I was unable to get working. Once I understand how the delegate can receive information from the QTreeView (or calling class), I believe I can do the rest.
I was unable to get this subclass of the QTreeView to display the folder and file icons.
Code:
import sys
from PySide.QtCore import *
from PySide.QtGui import *
class fileSystemDelegate(QItemDelegate):
def __init__(self, parent=None):
QItemDelegate.__init__(self, parent) #shouldn't this insure the icons are drawn?
def paint(self, painter, option, index):
painter.save()
# set background
painter.setPen(QPen(Qt.NoPen))
if option.state & QStyle.State_Selected: #DURING DRAW LOOP: idx = self.currentIndex(); if self.fileSystemModel.isDir(idx): PAINT RED
painter.setBrush(QBrush(Qt.red))
else:
painter.setBrush(QBrush(Qt.white)) #ELSE PAINT WHITE
painter.drawRect(option.rect)
# draw item
painter.setPen(QPen(Qt.black))
text = index.data(Qt.DisplayRole)
painter.drawText(option.rect, Qt.AlignLeft, text) #there is no painter.drawIcon?
painter.restore()
class fileSystemBrowser(QTreeView):
def __init__(self, parent=None):
super().__init__(parent)
delegate = fileSystemDelegate()
self.setItemDelegate(delegate) # how to provide delegate with additional info about the item to be drawn ?
self.fileSystemModel = QFileSystemModel()
self.fileSystemModel.setRootPath(QDir.currentPath())
self.setModel(self.fileSystemModel)
if __name__ == '__main__':
app = QApplication(sys.argv)
window = fileSystemBrowser()
window.show()
sys.exit(app.exec_())
EDIT 1:
I've added an example "database" in the form of a dictionary and changed the approach to rely on the data method rather than the delegate. I would expect this code to perform the dictionary lookup whenever information is displayed in the tree and therefore print to the terminal when the user enters C:\Program Files\Internet Explorer\ on a Microsoft Windows computer. However, it just displays the directory without printing anything to the terminal. I'd like to know:
How do I get if statements in the data method to trigger for every item in the display as they are being drawn?
How can I display an icon after the default icon is displayed, on the same row?
Code:
import sys
from PySide.QtCore import *
from PySide.QtGui import *
database = {'C:\Program Files\Internet Explorer\ExtExport.exe':(1,3), 'C:\Program Files\Internet Explorer\iexplore.exe':(0,0)}
class fileSystemBrowser(QTreeView):
def __init__(self, parent=None):
super().__init__(parent)
self.fileSystemModel = QFileSystemModel()
self.fileSystemModel.setRootPath(QDir.currentPath())
self.setModel(self.fileSystemModel)
def data(self, index, role=Qt.DisplayRole):
if index.isValid():
path = self.fileSystemModel.filePath(index)
if self.fileSystemModel.isDir(index):
if database.get(path) != None:
if database[path][0] > 0:
print("Acting on custom data 0.") # add another icon after the regular folder icon
if database[path][1] > 0:
print("Acting on custom data 1.") # add another (different) icon after the regular folder or previous icon
if __name__ == '__main__':
app = QApplication(sys.argv)
window = fileSystemBrowser()
window.show()
sys.exit(app.exec_())
EDIT 2:
Subclassing the model definitely did make a difference. Now the script appears to be calling my new data method on every item. Unfortunately, the data method doesn't work yet so the result is a treeview without icons or text. Sometimes I receive the error: "QFileSystemWatcher: failed to add paths: C:/PerfLogs". Based on examples online, I've commented where I think my errors may be, but I cannot yet get this to work. What am I doing wrong?
import sys
from PySide.QtCore import *
from PySide.QtGui import *
database = {'C:\Program Files\Internet Explorer\ExtExport.exe':(1,3), 'C:\Program Files\Internet Explorer\iexplore.exe':(0,0)}
class newFileModel(QFileSystemModel):
def __init__(self, parent=None):
QFileSystemModel.__init__(self, parent)
#self.elements = [[Do I need this? What should go here?]]
def data(self, index, role=Qt.DisplayRole):
if index.isValid():
path = self.filePath(index)
if self.isDir(index):
if database.get(path) != None:
if database[path][0] > 0:
print("Acting on custom data 0.") # I can add code here for different color text, etc.
if database[path][1] > 0:
print("Acting on custom data 1.") # I'll add code later
#return self.data(index, role) # Do I need this about here?
class fileSystemBrowser(QTreeView):
def __init__(self, parent=None):
super().__init__(parent)
self.fileSystemModel = newFileModel()
self.fileSystemModel.setRootPath(QDir.currentPath())
self.setModel(self.fileSystemModel)
if __name__ == '__main__':
app = QApplication(sys.argv)
window = fileSystemBrowser()
window.show()
sys.exit(app.exec_())
Here is a basic demo that shows how to add an extra column with icons and other formatting. Note that an attempt is made to normalise the file-paths so that comparisons and dictionary look-ups should be more reliable:
import sys
from PySide.QtCore import *
from PySide.QtGui import *
database = {
QFileInfo('C:\Program Files\Internet Explorer\ExtExport.exe').absoluteFilePath(): (1, 3),
QFileInfo('C:\Program Files\Internet Explorer\iexplore.exe').absoluteFilePath(): (0, 0),
}
class FileSystemModel(QFileSystemModel):
def __init__(self, parent=None):
super().__init__(parent)
style = qApp.style()
self.icons = [
style.standardIcon(QStyle.SP_MessageBoxInformation),
style.standardIcon(QStyle.SP_MessageBoxWarning),
]
def columnCount(self, parent=QModelIndex()):
return super().columnCount(parent) + 1
def data(self, index, role=Qt.DisplayRole):
extra = False
if index.isValid():
extra = index.column() == self.columnCount(index.parent()) - 1
info = self.fileInfo(index)
path = info.absoluteFilePath()
if path in database:
major, minor = database[path]
print('found:', (major, minor), path)
if extra:
if role == Qt.DecorationRole:
if major > 0:
return self.icons[0]
else:
return self.icons[1]
elif role == Qt.DisplayRole:
return '%s/%s' % (major, minor)
elif role == Qt.ForegroundRole:
if minor > 2:
return QColor('red')
if not extra:
return super().data(index, role)
def headerData(self, section, orientation, role=Qt.DisplayRole):
if (orientation == Qt.Horizontal and
role == Qt.DisplayRole and
section == self.columnCount() - 1):
return 'Extra'
return super().headerData(section, orientation, role)
class FileSystemBrowser(QTreeView):
def __init__(self, parent=None):
super().__init__(parent)
self.fileSystemModel = FileSystemModel()
self.fileSystemModel.setRootPath(QDir.currentPath())
self.setModel(self.fileSystemModel)
self.header().moveSection(self.fileSystemModel.columnCount() - 1, 1)
if __name__ == '__main__':
app = QApplication(sys.argv)
window = FileSystemBrowser()
window.show()
sys.exit(app.exec_())
EDIT:
The roles used in the data method are all documented under the ItemDataRole enum, and are introduced as follows:
Each item in the model has a set of data elements associated with it,
each with its own role. The roles are used by the view to indicate to
the model which type of data it needs. Custom models should return
data in these types.
For the extra column that has been added, it is necessary to supply everything, because it is a virtual column that is not part of the underlying model. But for the other columns, we can just call the base-class implementation to get the default values (of course, if desired, we could also return custom values for these columns to modify the existing behaviour).

Selecting many rows in Qt table

I am trying to create a QTableView in Qt which is efficient for large tables. I've managed to make the display of data efficient by defining my own abstract table model:
from PyQt4 import QtCore, QtGui
from PyQt4.QtCore import Qt
class DataTableModel(QtCore.QAbstractTableModel):
def columnCount(self, index=None):
return 3
def rowCount(self, index=None):
return 10000
def headerData(self, section, orientation, role):
if role != Qt.DisplayRole:
return None
if orientation == Qt.Horizontal:
return 'c'
elif orientation == Qt.Vertical:
return 'r'
def data(self, index, role):
if not index.isValid():
return None
if role == Qt.DisplayRole:
return "({0},{1})".format(index.row(), index.column())
app = QtGui.QApplication([""])
viewer = QtGui.QTableView()
model = DataTableModel()
viewer.setModel(model)
viewer.show()
This works fine, because the data method is only called for cells that appear in the field of view of the table.
I now want to display an existing selection of some fraction of the rows:
import numpy as np
selected_rows = np.where(np.random.random(10000) > 0.5)[0]
I can tell the table widget about this selection by doing e.g.:
smodel = viewer.selectionModel()
for row in selected_rows:
model_index = model.createIndex(row, 0)
smodel.select(model_index, QtGui.QItemSelectionModel.Select | QtGui.QItemSelectionModel.Rows)
However, this is very inefficient. It typically takes a second to select 1000-2000 rows, when in practice I have tables with millions of rows. There may be ways of speeding up this loop, but I would like to do away with the loop altogether, and instead have Qt only ask me (similarly to the data itself) for information about selections within the visible cells. Is this possible, and if so, what is the best way to achieve this?
You should use the second overloaded version of select, the one that accepts a QItemSelection instead of a single index.
The QItemSelection is able to select ranges of rows by providing the two argument to the constructor:
QItemSelection(start_index, stop_index)
moreover you can merge the items to become a single selection:
selection.merge(other_selection, flags)
This suggest to:
Sort the indices of the rows you want to select
Use itertools.groupby to group together consecutive rows
Use createIndex to get the QModelIndex of all start-end indices of these groups
Create the QItemSelection objects for each group of rows
merge all QItemSelections into a single QItemSelection
Perform the selection over your model.
Note that you want to sort the rows by index, not by their values.
The simplest way would be to reimplement the selection model. The view queries the selection model for the selection status of each index. Alas, the QItemSelectionModel has a major shortcoming: you can't reimplement its isSelected method.
The best you can do is to create a fresh selection model on a model perhaps not attached to any views, then to select the items there, and finally to set the model and selection model on the view.
This is an API shortcoming.
If this is a professional project, you should be compiling your own copy of Qt anyway, under your own git version control, and it's a trivial manner to make the isSelected method virtual.
If you want to display some selected rows only as opposite to
display everything and select some rows, then QSortFilterProxyModel could help:
from PyQt4 import QtCore, QtGui
from PyQt4.QtCore import Qt
import numpy as np
class FilterProxy(QtGui.QSortFilterProxyModel):
afilter = set(np.where(np.random.random(10000) > 0.5)[0])
def updateFilter(self, new_filter):
self.afilter = new_filter
self.invalidateFilter()
def filterAcceptsRow(self, row, parent):
if not self.afilter:
return True
return row in self.afilter
class DataTableModel(QtCore.QAbstractTableModel):
def columnCount(self, index=None):
return 3
def rowCount(self, index=None):
return 10000
def headerData(self, section, orientation, role):
if role != Qt.DisplayRole:
return None
if orientation == Qt.Horizontal:
return 'c'
elif orientation == Qt.Vertical:
return 'r'
def data(self, index, role):
if not index.isValid():
return None
if role == Qt.DisplayRole:
return "({0},{1})".format(index.row(), index.column())
class MyWindow(QtGui.QMainWindow):
def __init__(self):
super(MyWindow, self).__init__()
self.viewer = QtGui.QTableView()
self.setCentralWidget(self.viewer)
self.action = QtGui.QAction("Filter x > 0.5", self)
self.action.triggered.connect(self.updateFilter)
self.addToolBar("Ffilter").addAction(self.action)
self.model = DataTableModel()
self.proxyModel = FilterProxy(self.viewer)
self.proxyModel.setDynamicSortFilter(True)
self.proxyModel.setSourceModel(self.model)
self.viewer.setModel(self.proxyModel)
def updateFilter(self):
new_max = np.random.rand(1)[0]
new_filter = set(np.where(np.random.random(10000) > new_max)[0])
self.action.setText("Filter x > {} N = {}".format(new_max, len(new_filter)))
self.proxyModel.updateFilter(new_filter)
app = QtGui.QApplication([""])
viewer = MyWindow()
viewer.show()
app.exec_()

PyQT list view not responding to datachanged signal

I've been following some tutorials and trying to get a list model set up. My main window has two list views that are accessing the same model. When I update an item in one list, the other list doesn't update itself until it gets focus (I click on it). So it looks like the dataChanged signal isn't being emitted, but I can't work out how my code is different to any of the examples I'm basing it from.
main.py
class Main(QtWidgets.QMainWindow):
def __init__(self, parent=None):
super(Main, self).__init__(parent)
self.ui = uic.loadUi("mainwindow.ui", self)
# Test model and listviews
data = [10,20,30,40,50]
myModel = model.MyListModel(data)
self.ui.listView.setModel(myModel)
self.ui.listView_2.setModel(myModel)
model.py
class MyListModel(QtCore.QAbstractListModel):
def __init__(self, data=[], parent=None):
super(MyListModel, self).__init__(parent)
self.__data = data
def rowCount(self, parent=QtCore.QModelIndex()):
return len(self.__data)
def data(self, index, role=QtCore.Qt.DisplayRole):
row = index.row()
if role in (QtCore.Qt.DisplayRole, QtCore.Qt.EditRole):
return str(self.__data[row])
if role == QtCore.Qt.ToolTipRole:
return 'Item at {0}'.format(row)
def flags(self, index):
return QtCore.Qt.ItemIsEditable | QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable
def setData(self, index, value, role=QtCore.Qt.EditRole):
if role == QtCore.Qt.EditRole:
self.__data[index.row()] = value
self.dataChanged.emit(index, index)
return True
return False
Can anyone see what is wrong here? FYI I'm using PyQT5.2.1 and Python 3.3.
The problem is with the signature of dataChanged signal. In Qt4 it looked like this:
dataChanged(const QModelIndex & topLeft, const QModelIndex & bottomRight)
but in Qt5, it looks like this:
dataChanged(const QModelIndex & topLeft, const QModelIndex & bottomRight,
const QVector<int> & roles = QVector<int>())
when I tried your example code with PyQt-5.1.1, I got an error when attempting to emit the signal without the third argument. Strictly speaking, this was incorrect behaviour, because the third argument has a default value. So this is perhaps why the behaviour has changed.
But it seems that you must now explicitly emit an empty list as the third argument of dataChanged in order for things to work properly in PyQt5:
self.dataChanged.emit(index, index, [])
or, of course, emit a list of the roles that have actually been changed:
self.dataChanged.emit(index, index, [QtCore.Qt.EditRole])
The solution:
self.dataChanged.emit(index, index, ())
do not work for me (python 2.7, PyQt5).
But one of the following will work:
self.dataChanged.emit(index, index, [])
self.dataChanged.emit(index, index, list())

Categories

Resources