How to save selected Items to Qsettings from QListWidget, QTableWidget - python

I am Using PyQt5 and Py3.7, I am trying to loop through all my Qlistwidgets and save their string data, but also save all selected items on that widget. I slightly modified the loop from here, but am having some trouble getting the selected items to save and restore using the listwidget array loop. I checked the docs but can't seem to understand how to add additional options into the array (such as save all selected items), from the Qt docs for SetArrayIndex, here.
My listWidgets have selectionMode set to MultiSelection.
I am currently saving using this:
def save_list_data(self):
self.settings = QSettings("data.ini", QSettings.IniFormat)
for name, obj in inspect.getmembers(self):
if isinstance(obj, QListWidget):
name = obj.objectName()
self.settings.beginWriteArray(name)
for i in range(obj.count()):
self.settings.setArrayIndex(i)
self.settings.setValue(name, obj.item(i).text())
self.settings.endArray()
And then restoring the listWidget data using:
def open_list_data(self):
self.settings = QSettings("data.ini", QSettings.IniFormat)
for name, obj in inspect.getmembers(self):
if isinstance(obj, QListWidget):
name = obj.objectName()
size = self.settings.beginReadArray(name)
for i in range(size):
self.settings.setArrayIndex(i)
value = self.settings.value(name)
if value != None:
obj.addItem(value)
self.settings.endArray()
This works fine for the data, but how can I get the selectedItems from the ListWidgets to save and restore as well?

For my solution take into account the following:
Using the inspect module may be beneficial for other libraries, but for Qt the widget is not necessarily a member of the class so it is best to use the Qt introspection itself using findChildren.
In the example you use, you are only saving the text but a QListWidgetItem can have more information associated with the roles such as background color, foreground color, etc. So instead I will use the QDataStream operator since this take and save takes the item information.
I will use the "objectname/property" format to save the information since the same widget can have several properties that you want to save.
To save the information of the selected items, it is only necessary to save the row.
Considering the above, the solution is:
from PyQt5 import QtCore, QtGui, QtWidgets
class Widget(QtWidgets.QWidget):
def __init__(self, parent=None):
super().__init__(parent)
self.listwidget_1 = QtWidgets.QListWidget(
objectName="listwidget_1",
selectionMode=QtWidgets.QAbstractItemView.MultiSelection
)
listwidget_2 = QtWidgets.QListWidget(
objectName="listwidget_2",
selectionMode=QtWidgets.QAbstractItemView.MultiSelection
)
lay = QtWidgets.QVBoxLayout(self)
lay.addWidget(self.listwidget_1)
lay.addWidget(listwidget_2)
self.read_settings()
def closeEvent(self, event):
self.write_settings()
super().closeEvent(event)
def read_settings(self):
settings = QtCore.QSettings("data.ini", QtCore.QSettings.IniFormat)
childrens = self.findChildren(QtWidgets.QWidget)
for children in childrens:
if isinstance(children, QtWidgets.QListWidget) and children.objectName():
settings.beginGroup(children.objectName())
items = settings.value("items")
selecteditems = settings.value("selecteditems")
selectionMode = settings.value("selectionMode", type=QtWidgets.QAbstractItemView.SelectionMode)
children.setSelectionMode(selectionMode)
# In the first reading the initial values must be established
if items is None:
if children.objectName() == "listwidget_1":
for i in range(10):
children.addItem(QtWidgets.QListWidgetItem(str(i)))
elif children.objectName() == "listwidget_2":
for i in "abcdefghijklmnopqrstuvwxyz":
children.addItem(QtWidgets.QListWidgetItem(i))
else:
stream = QtCore.QDataStream(items, QtCore.QIODevice.ReadOnly)
while not stream.atEnd():
it = QtWidgets.QListWidgetItem()
stream >> it
children.addItem(it)
stream = QtCore.QDataStream(selecteditems, QtCore.QIODevice.ReadOnly)
while not stream.atEnd():
row = stream.readInt()
it = children.item(row)
it.setSelected(True)
settings.endGroup()
def write_settings(self):
settings = QtCore.QSettings("data.ini", QtCore.QSettings.IniFormat)
childrens = self.findChildren(QtWidgets.QWidget)
for children in childrens:
if isinstance(children, QtWidgets.QListWidget) and children.objectName():
settings.beginGroup(children.objectName())
items = QtCore.QByteArray()
stream = QtCore.QDataStream(items, QtCore.QIODevice.WriteOnly)
for i in range(children.count()):
stream << children.item(i)
selecteditems = QtCore.QByteArray()
stream = QtCore.QDataStream(selecteditems, QtCore.QIODevice.WriteOnly)
for it in children.selectedItems():
stream.writeInt(children.row(it))
settings.setValue("items", items)
settings.setValue("selecteditems", selecteditems)
settings.setValue("selectionMode", children.selectionMode())
settings.endGroup()
if __name__ == "__main__":
import sys
app = QtWidgets.QApplication(sys.argv)
w = Widget()
w.show()
sys.exit(app.exec_())
Plus:
import contextlib
from PyQt5 import QtCore, QtGui, QtWidgets
class SettingsManager:
def __init__(self, filename):
self.m_settings = QtCore.QSettings(filename, QtCore.QSettings.IniFormat)
#property
def settings(self):
return self.m_settings
def read(self, widget):
self.settings.beginGroup(widget.objectName())
if isinstance(widget, QtWidgets.QAbstractItemView):
selectionMode = self.settings.value(
"selectionMode", type=QtWidgets.QAbstractItemView.SelectionMode
)
widget.setSelectionMode(selectionMode)
if isinstance(widget, QtWidgets.QListWidget):
items = self.settings.value("items")
selecteditems = self.settings.value("selecteditems")
# In the first reading the initial values must be established
if items is None:
self.read_defaults(widget)
else:
stream = QtCore.QDataStream(items, QtCore.QIODevice.ReadOnly)
while not stream.atEnd():
it = QtWidgets.QListWidgetItem()
stream >> it
widget.addItem(it)
stream = QtCore.QDataStream(
selecteditems, QtCore.QIODevice.ReadOnly
)
while not stream.atEnd():
row = stream.readInt()
it = widget.item(row)
if it is not None:
it.setSelected(True)
if isinstance(widget, QtWidgets.QTableWidget):
rowCount = self.settings.value("rowCount", type=int)
columnCount = self.settings.value("columnCount", type=int)
widget.setRowCount(rowCount)
widget.setColumnCount(columnCount)
items = self.settings.value("items")
if items is None:
self.read_defaults(widget)
else:
stream = QtCore.QDataStream(items, QtCore.QIODevice.ReadOnly)
while not stream.atEnd():
it = QtWidgets.QTableWidgetItem()
i = stream.readInt()
j = stream.readInt()
stream >> it
widget.setItem(i, j, it)
selecteditems = self.settings.value("selecteditems")
stream = QtCore.QDataStream(
selecteditems, QtCore.QIODevice.ReadOnly
)
while not stream.atEnd():
i = stream.readInt()
j = stream.readInt()
it = widget.item(i, j)
if it is not None:
it.setSelected(True)
self.settings.endGroup()
def write(self, widget):
self.settings.beginGroup(widget.objectName())
if isinstance(widget, QtWidgets.QAbstractItemView):
self.settings.setValue("selectionMode", widget.selectionMode())
if isinstance(widget, QtWidgets.QListWidget):
items = QtCore.QByteArray()
stream = QtCore.QDataStream(items, QtCore.QIODevice.WriteOnly)
for i in range(widget.count()):
stream << widget.item(i)
self.settings.setValue("items", items)
selecteditems = QtCore.QByteArray()
stream = QtCore.QDataStream(
selecteditems, QtCore.QIODevice.WriteOnly
)
for it in widget.selectedItems():
stream.writeInt(widget.row(it))
self.settings.setValue("selecteditems", selecteditems)
if isinstance(widget, QtWidgets.QTableWidget):
self.settings.setValue("rowCount", widget.rowCount())
self.settings.setValue("columnCount", widget.columnCount())
items = QtCore.QByteArray()
stream = QtCore.QDataStream(items, QtCore.QIODevice.WriteOnly)
for i in range(widget.rowCount()):
for j in range(widget.columnCount()):
it = widget.item(i, j)
if it is not None:
stream.writeInt(i)
stream.writeInt(j)
stream << it
self.settings.setValue("items", items)
selecteditems = QtCore.QByteArray()
stream = QtCore.QDataStream(
selecteditems, QtCore.QIODevice.WriteOnly
)
for it in widget.selectedItems():
# print(it.row(), it.column())
stream.writeInt(it.row())
stream.writeInt(it.column())
self.settings.setValue("selecteditems", selecteditems)
self.settings.endGroup()
def release(self):
self.m_settings.sync()
def read_defaults(self, widget):
if widget.objectName() == "listwidget_1":
widget.setSelectionMode(QtWidgets.QAbstractItemView.MultiSelection)
for i in range(10):
widget.addItem(QtWidgets.QListWidgetItem(str(i)))
elif widget.objectName() == "listwidget_2":
widget.setSelectionMode(QtWidgets.QAbstractItemView.MultiSelection)
for i in "abcdefghijklmnopqrstuvwxyz":
widget.addItem(QtWidgets.QListWidgetItem(i))
elif widget.objectName() == "tablewidget":
widget.setSelectionMode(QtWidgets.QAbstractItemView.MultiSelection)
widget.setRowCount(10)
widget.setColumnCount(10)
for i in range(widget.rowCount()):
for j in range(widget.columnCount()):
it = QtWidgets.QTableWidgetItem("{}-{}".format(i, j))
widget.setItem(i, j, it)
#contextlib.contextmanager
def settingsContext(filename):
manager = SettingsManager(filename)
try:
yield manager
finally:
manager.release()
class Widget(QtWidgets.QWidget):
def __init__(self, parent=None):
super().__init__(parent)
self.listwidget_1 = QtWidgets.QListWidget(objectName="listwidget_1")
listwidget_2 = QtWidgets.QListWidget(objectName="listwidget_2")
tablewidget = QtWidgets.QTableWidget(objectName="tablewidget")
lay = QtWidgets.QVBoxLayout(self)
lay.addWidget(self.listwidget_1)
lay.addWidget(listwidget_2)
lay.addWidget(tablewidget)
self.read_settings()
def closeEvent(self, event):
self.write_settings()
super().closeEvent(event)
def read_settings(self):
with settingsContext("data.ini") as m:
for children in self.findChildren(QtWidgets.QWidget):
if children.objectName():
m.read(children)
def write_settings(self):
with settingsContext("data.ini") as m:
for children in self.findChildren(QtWidgets.QWidget):
if children.objectName():
m.write(children)
if __name__ == "__main__":
import sys
app = QtWidgets.QApplication(sys.argv)
w = Widget()
w.resize(640, 480)
w.show()
sys.exit(app.exec_())

Related

How to have multiple columns in a QComboBox with a QAbstractTableModel

I've seen questions similar to this one but they are aimed at QTableView. This is not using that,, this is just for a dropdown (QComboBox) with a custom QAbstractTableModel, which needs to have 2 columns.
BIG UPDATE
(Note: Legacy code has been deleted as this is a better approach on the same question, and legacy code was confusing as hell).
Okay, so trying to catch up with what #eyllanesc explained, I changed this from a QAbstractListModel to a QAbstractTableModel. The result is:
class ModelForComboboxesWithID(QAbstractTableModel):
"""Create our basic model"""
def __init__(self, program, records):
super(ModelForComboboxesWithID, self).__init__()
self._data = records
self.program = program
self.path_images = program.PATH_IMAGES
def rowCount(self, index: int = 0) -> int:
"""The length of the outer list. Structure: [row, row, row]"""
if not self._data:
return 0 # Doubt: Do we need to return this if self._data is empty?
return len(self._data)
def columnCount(self, index: int = 0) -> int:
"""The length of the sub-list inside the outer list. Meaning that Columns are inside rows
Structure: [row [column], row [column], row [column]]"""
if not self._data:
return 0 # Doubt: Do we need to return this if self._data is empty?
return len(self._data[0])
def data(self, index, role=None):
"""return the data on this index as data[row][column]"""
# 1 - Display data based on its content (this edits the text that you visually see)
if role == Qt.DisplayRole:
value = self._data[index.row()][index.column()]
return value
# 2 - Tooltip displayed when hovering on it
elif role == Qt.ToolTipRole:
return f"ID: {self._data[index.row()][1]}"
Which I set this way:
def eventFilter(self, target, event: QEvent):
if event.type() == QEvent.MouseButtonPress:
if target == self.Buscadorcombo_cliente:
records = ... # my query to the database
set_combo_records_with_ids(self.program, target, records)
target.currentIndexChanged.connect(self.test)
def set_combo_records_with_ids(program, combobox: QComboBox, records):
"""Clear combobox, set model/data and sort it"""
combobox.clear()
model = ModelForComboboxesWithID(program, records)
combobox.setModel(model)
combobox.model().sort(0, Qt.AscendingOrder)
combobox.setModelColumn(0)
The result of this works almost perfect:
On the dropdown(Combobox) it displays the name.
If you hover on an item, it displays the ID.
Now I am able to get any data of it this way.
def test(self, index):
data_id = self.Buscadorcombo_cliente.model().index(index, 1).data()
data_name = self.Buscadorcombo_cliente.model().index(index, 0).data()
print(data_id)
print(data_name)
You have to set a QTableView as a view:
from PySide2 import QtGui, QtWidgets
def main():
import sys
app = QtWidgets.QApplication(sys.argv)
w = QtWidgets.QWidget()
combo = QtWidgets.QComboBox()
model = QtGui.QStandardItemModel(0, 2)
for i in range(10):
items = []
for j in range(model.columnCount()):
it = QtGui.QStandardItem(f"it-{i}{j}")
items.append(it)
model.appendRow(items)
combo.setModel(model)
view = QtWidgets.QTableView(
combo, selectionBehavior=QtWidgets.QAbstractItemView.SelectRows
)
combo.setView(view)
view.verticalHeader().hide()
view.horizontalHeader().hide()
header = view.horizontalHeader()
for i in range(header.count()):
header.setSectionResizeMode(i, QtWidgets.QHeaderView.Stretch)
lay = QtWidgets.QVBoxLayout(w)
lay.addWidget(combo)
lay.addStretch()
w.resize(640, 480)
w.show()
sys.exit(app.exec_())
if __name__ == "__main__":
main()

Iterate QTreeView Rows

How would i modify the function below to properly iterate through just the rows in my QTreeview and not each row and each column?
While iterating through each row, I want to get the UserRole and DisplayRole data of the second column.
I'm close on the iteration part, but im not sure how to access UserRole and Display Role of the second column.
import sys, random
from PySide import QtGui, QtCore
class Browser(QtGui.QDialog):
def __init__(self, parent=None):
super(Browser, self).__init__(parent)
self.resize(300, 400)
self.uiItems = QtGui.QTreeView()
self.uiItems.setAlternatingRowColors(True)
self.uiItems.setSortingEnabled(True)
self.uiItems.sortByColumn(0, QtCore.Qt.AscendingOrder)
self.uiItems.setEditTriggers(QtGui.QAbstractItemView.NoEditTriggers)
self.proxyModel = QtGui.QSortFilterProxyModel()
self.proxyModel.setSourceModel(QtGui.QStandardItemModel())
self.proxyModel.setDynamicSortFilter(True)
self.uiItems.setModel(self.proxyModel)
self.button = QtGui.QPushButton('Print')
grid = QtGui.QVBoxLayout(self)
grid.addWidget(self.uiItems)
grid.addWidget(self.button)
self.populate()
self.button.clicked.connect(self.printData)
def populate(self):
items = ['Cookie','Hummus','Spaghetti','Candy','Chocolate']
subitems = ['Sweet','Caramel','Sucker','Apple']
model = self.proxyModel.sourceModel()
model.clear()
model.setHorizontalHeaderLabels(['Name', 'Count'])
for item in items:
count = random.randint(0,3)
col1 = QtGui.QStandardItem(item)
# col2 = QtGui.QStandardItem(str(count))
# subitems:
for i in range(count):
child1 = QtGui.QStandardItem(subitems[random.randint(0,3)])
child2 = QtGui.QStandardItem(str(random.randint(0,20)))
col1.appendRow([child1, child2])
model.appendRow([col1])
self.uiItems.expandAll()
self.uiItems.header().setResizeMode(QtGui.QHeaderView.ResizeToContents)
def iterItems(self, root):
if root is not None:
stack = [root]
while stack:
parent = stack.pop(0)
for row in range(parent.rowCount()):
for column in range(parent.columnCount()):
child = parent.child(row, column)
yield child
if child and child.hasChildren():
stack.append(child)
def printData(self):
for x in self.iterItems(self.proxyModel.sourceModel().invisibleRootItem()):
print x
def main():
app = QtGui.QApplication(sys.argv)
ex = Browser()
ex.show()
sys.exit(app.exec_())
if __name__ == '__main__':
main()
def iterItems(self, root):
if root is not None:
for row in range(root.rowCount()):
row_item = root.child(row, 0)
if row_item.hasChildren():
for childIndex in range(row_item.rowCount()):
# Take second column from "child"-row
child = row_item.child(childIndex, 1)
yield child
def printData(self):
for x in self.iterItems(self.proxyModel.sourceModel().invisibleRootItem()):
print x.data(role=QtCore.Qt.DisplayRole), x.data(role=QtCore.Qt.UserRole)
If it works for you, a QTreeWidgetItemIterator might be much easier:
def iterTreeItems(tree)
iterator = QtWidgets.QTreeWidgetItemIterator(tree)
item: QtWidgets.QTreeWidgetItem = iterator.value()
while item is not None:
yield item
iterator += 1
item = iterator.value()

Single column QTreeview search filter

I have two questions:
I was wondering if this is the proper way to do a search/filter on a single column treeview. I feel like a lot of my copying/pasting could contain unnecessary stuff. Is all the code in the subclass of QSortFilterProxyModel and the code in the search_text_changed method needed? I don't feel like the regex is needed, since I set the filter-proxy to ignore case-sensitivity.
How can I make it so that when a user double-clicks a treeview item a signal emits a string list containing the string of the item clicked and all of its ancestors recursively? For example, if I double-clicked "Birds", it would return ['Birds','Animals']; and if I double-clicked "Animals", it would just return ['Animals'].
import os, sys
from PySide import QtCore, QtGui
tags = {
"Animals": [
"Birds",
"Various"
],
"Brick": [
"Blocks",
"Special"
],
"Manmade": [
"Air Conditioners",
"Audio Equipment"
],
"Food": [
"Fruit",
"Grains and Seeds"
]
}
class SearchProxyModel(QtGui.QSortFilterProxyModel):
def __init__(self, parent=None):
super(SearchProxyModel, self).__init__(parent)
self.text = ''
# Recursive search
def _accept_index(self, idx):
if idx.isValid():
text = idx.data(role=QtCore.Qt.DisplayRole).lower()
condition = text.find(self.text) >= 0
if condition:
return True
for childnum in range(idx.model().rowCount(parent=idx)):
if self._accept_index(idx.model().index(childnum, 0, parent=idx)):
return True
return False
def filterAcceptsRow(self, sourceRow, sourceParent):
# Only first column in model for search
idx = self.sourceModel().index(sourceRow, 0, sourceParent)
return self._accept_index(idx)
def lessThan(self, left, right):
leftData = self.sourceModel().data(left)
rightData = self.sourceModel().data(right)
return leftData < rightData
class TagsBrowserWidget(QtGui.QWidget):
clickedTag = QtCore.Signal(list)
def __init__(self, parent=None):
super(TagsBrowserWidget, self).__init__(parent)
self.resize(300,500)
# controls
self.ui_search = QtGui.QLineEdit()
self.ui_search.setPlaceholderText('Search...')
self.tags_model = SearchProxyModel()
self.tags_model.setSourceModel(QtGui.QStandardItemModel())
self.tags_model.setDynamicSortFilter(True)
self.tags_model.setFilterCaseSensitivity(QtCore.Qt.CaseInsensitive)
self.ui_tags = QtGui.QTreeView()
self.ui_tags.setSortingEnabled(True)
self.ui_tags.sortByColumn(0, QtCore.Qt.AscendingOrder)
self.ui_tags.setEditTriggers(QtGui.QAbstractItemView.NoEditTriggers)
self.ui_tags.setHeaderHidden(True)
self.ui_tags.setRootIsDecorated(True)
self.ui_tags.setUniformRowHeights(True)
self.ui_tags.setModel(self.tags_model)
# layout
main_layout = QtGui.QVBoxLayout()
main_layout.addWidget(self.ui_search)
main_layout.addWidget(self.ui_tags)
self.setLayout(main_layout)
# signals
self.ui_tags.doubleClicked.connect(self.tag_double_clicked)
self.ui_search.textChanged.connect(self.search_text_changed)
# init
self.create_model()
def create_model(self):
model = self.ui_tags.model().sourceModel()
self.populate_tree(tags, model.invisibleRootItem())
self.ui_tags.sortByColumn(0, QtCore.Qt.AscendingOrder)
def populate_tree(self, children, parent):
for child in sorted(children):
node = QtGui.QStandardItem(child)
parent.appendRow(node)
if isinstance(children, dict):
self.populate_tree(children[child], node)
def tag_double_clicked(self, item):
text = item.data(role=QtCore.Qt.DisplayRole)
print [text]
self.clickedTag.emit([text])
def search_text_changed(self, text=None):
regExp = QtCore.QRegExp(self.ui_search.text(), QtCore.Qt.CaseInsensitive, QtCore.QRegExp.FixedString)
self.tags_model.text = self.ui_search.text().lower()
self.tags_model.setFilterRegExp(regExp)
if len(self.ui_search.text()) >= 1 and self.tags_model.rowCount() > 0:
self.ui_tags.expandAll()
else:
self.ui_tags.collapseAll()
def main():
app = QtGui.QApplication(sys.argv)
ex = TagsBrowserWidget()
ex.show()
sys.exit(app.exec_())
if __name__ == '__main__':
main()
There's no point in setting the case-sensivity of the filter-proxy at all, because you are by-passing the built-in filtering by overriding filterAcceptsRow. And even if you weren't doing that, setFilterRegExp ignores the current case sensitiviy settings anyway.
I would simplify the filter-proxy to this:
class SearchProxyModel(QtGui.QSortFilterProxyModel):
def setFilterRegExp(self, pattern):
if isinstance(pattern, str):
pattern = QtCore.QRegExp(
pattern, QtCore.Qt.CaseInsensitive,
QtCore.QRegExp.FixedString)
super(SearchProxyModel, self).setFilterRegExp(pattern)
def _accept_index(self, idx):
if idx.isValid():
text = idx.data(QtCore.Qt.DisplayRole)
if self.filterRegExp().indexIn(text) >= 0:
return True
for row in range(idx.model().rowCount(idx)):
if self._accept_index(idx.model().index(row, 0, idx)):
return True
return False
def filterAcceptsRow(self, sourceRow, sourceParent):
idx = self.sourceModel().index(sourceRow, 0, sourceParent)
return self._accept_index(idx)
and change the search method to this:
def search_text_changed(self, text=None):
self.tags_model.setFilterRegExp(self.ui_search.text())
if len(self.ui_search.text()) >= 1 and self.tags_model.rowCount() > 0:
self.ui_tags.expandAll()
else:
self.ui_tags.collapseAll()
So now the SearchProxyModel has sole responsibilty for deciding how searches are performed via its setFilterRegExp method. The case-sensitivity is handled transparently, so there is no need to pre-process the input.
The method for getting a list of descendants, can be written like this:
def tag_double_clicked(self, idx):
text = []
while idx.isValid():
text.append(idx.data(QtCore.Qt.DisplayRole))
idx = idx.parent()
text.reverse()
self.clickedTag.emit(text)

How to avoid drop on node

In my QtreeViewI use a 'QStandardItemModel` for displaying several items with individual properties. I want to avoid that the item will not be mixed. e.g. Bananas should be moveable to vegetables (same child level) but not below Asia (higher level), moving to Asia - Fruits is ok (same child level)
Sample
I've worked with .itemChanged but it appears to late. I need a signal before it will be dropped and the item where it will be dropped. I tried eventFilterand get
event.type() == QtCore.QEvent.DragMove:
but how do I get the index of the item where the item will be dropped to decide it its in the same child level?
To solve this problem I have created a custom mimetype that sends the information of the index and the level of depth that it has, and it will only move those indexes that have the same level as the children of the destination.
class TreeView(QTreeView):
customMimeType = "application/x-customqstandarditemmodeldatalist"
def __init__(self, *args, **kwargs):
QTreeView.__init__(self, *args, **kwargs)
self.setSelectionMode(QAbstractItemView.SingleSelection)
self.setDragEnabled(True)
self.viewport().setAcceptDrops(True)
self.setDropIndicatorShown(True)
self.setDragDropMode(QTreeView.InternalMove)
def itemsToPixmap(self, indexes):
rect = self.viewport().visibleRegion().boundingRect()
pixmap = QPixmap(rect.size())
pixmap.fill(Qt.transparent)
painter = QPainter(pixmap)
for index in indexes:
painter.drawPixmap(self.visualRect(index), self.viewport().grab(self.visualRect(index)))
return pixmap
def mimeTypes(self):
mimetypes = QTreeView.mimeTypes(self)
mimetypes.append(TreeView.customMimeType)
return mimetypes
def startDrag(self, supportedActions):
drag = QDrag(self)
mimedata = self.model().mimeData(self.selectedIndexes())
encoded = QByteArray()
stream = QDataStream(encoded, QIODevice.WriteOnly)
self.encodeData(self.selectedIndexes(), stream)
mimedata.setData(TreeView.customMimeType, encoded)
drag.setMimeData(mimedata)
px = self.itemsToPixmap(self.selectedIndexes())
drag.setPixmap(px)
drag.setHotSpot(self.viewport().mapFromGlobal(QCursor.pos()) - QPoint(self.horizontalOffset(),
self.verticalOffset()))
drag.exec_(supportedActions)
def encodeData(self, items, stream):
stream.writeInt32(len(items))
for item in items:
p = item
rows = []
while p.isValid():
rows.append(p.row())
p = p.parent()
stream.writeInt32(len(rows))
for row in reversed(rows):
stream.writeInt32(row)
def dropEvent(self, event):
if event.source() == self:
if event.mimeData().hasFormat(TreeView.customMimeType):
encoded = event.mimeData().data(TreeView.customMimeType)
items = self.decodeData(encoded, event.source())
ix = self.indexAt(event.pos())
current = self.model().itemFromIndex(ix)
p = current
level = 1
while p:
p = p.parent()
level += 1
for item, ilevel in items:
if level == ilevel:
item.parent().takeRow(item.row())
current.appendRow(item)
self.clearSelection()
event.acceptProposedAction()
else:
event.ignore()
def decodeData(self, encoded, tree):
items = []
rows = []
stream = QDataStream(encoded, QIODevice.ReadOnly)
while not stream.atEnd():
nItems = stream.readInt32()
for i in range(nItems):
path = stream.readInt32()
row = []
for j in range(path):
row.append(stream.readInt32())
rows.append(row)
for row in rows:
it = self.model().item(row[0])
for r in row[1:]:
it = it.child(r)
items.append((it, len(row)))
return items
A complete example can be found in the following link
The following changes allow to move an item inside a row of childs, but not outside. Still working for changing the cursor if target is not the same level.
def dropEvent(self, event):
if event.source() == self:
if event.mimeData().hasFormat(TreeView.customMimeType):
encoded = event.mimeData().data(TreeView.customMimeType)
items = self.decodeData(encoded, event.source())
ix = self.indexAt(event.pos())
current = self.model().itemFromIndex(ix)
p = current
level = 0
while p:
p = p.parent()
level += 1
for item, ilevel in items:
if level == ilevel:
item.parent().takeRow(item.row())
current.parent().insertRow(current.row(),item)
self.clearSelection()
event.acceptProposedAction()
else:
event.ignore()

How to format the list items of QCompleter's popup list properly?

I want to investigate how to make a small user interface in which a user can type some letters and gets some suggestions based on a given data source (list here) which makes searches easier. For this purpose i am using Qt's QCompleter class.
In the matching elements the typed letters shall be highlighted with HTML like the example in the code below: Au<b>st</b>ria.
Finally i merged some SO answers (see How to make item view render rich (html) text in Qt) and tutorials to a small standalone module:
from PySide import QtCore, QtGui
class HTMLDelegate(QtGui.QStyledItemDelegate):
""" From: https://stackoverflow.com/a/5443112/1504082 """
def paint(self, painter, option, index):
options = QtGui.QStyleOptionViewItemV4(option)
self.initStyleOption(options, index)
if options.widget is None:
style = QtGui.QApplication.style()
else:
style = options.widget.style()
doc = QtGui.QTextDocument()
doc.setHtml(options.text)
doc.setTextWidth(option.rect.width())
options.text = ""
style.drawControl(QtGui.QStyle.CE_ItemViewItem, options, painter)
ctx = QtGui.QAbstractTextDocumentLayout.PaintContext()
# Highlighting text if item is selected
# if options.state & QtGui.QStyle.State_Selected:
# ctx.palette.setColor(QtGui.QPalette.Text,
# options.palette.color(QtGui.QPalette.Active,
# QtGui.QPalette.HighlightedText))
textRect = style.subElementRect(QtGui.QStyle.SE_ItemViewItemText,
options)
painter.save()
painter.translate(textRect.topLeft())
painter.setClipRect(textRect.translated(-textRect.topLeft()))
doc.documentLayout().draw(painter, ctx)
painter.restore()
def sizeHint(self, option, index):
options = QtGui.QStyleOptionViewItemV4(option)
self.initStyleOption(options, index)
doc = QtGui.QTextDocument()
doc.setHtml(options.text)
doc.setTextWidth(options.rect.width())
return QtCore.QSize(doc.size().width(), doc.size().height())
class CustomQCompleter(QtGui.QCompleter):
""" Implement "contains" filter mode as the filter mode "contains" is not
available in Qt < 5.2
From: https://stackoverflow.com/a/7767999/1504082 """
def __init__(self, parent=None):
super(CustomQCompleter, self).__init__(parent)
self.local_completion_prefix = ""
self.source_model = None
self.delegate = HTMLDelegate()
def setModel(self, model):
self.source_model = model
super(CustomQCompleter, self).setModel(self.source_model)
def updateModel(self):
local_completion_prefix = self.local_completion_prefix
# see: http://doc.qt.io/qt-4.8/model-view-programming.html#proxy-models
class InnerProxyModel(QtGui.QSortFilterProxyModel):
def filterAcceptsRow(self, sourceRow, sourceParent):
# model index mapping by row, 1d model => column is always 0
index = self.sourceModel().index(sourceRow, 0, sourceParent)
source_data = self.sourceModel().data(index, QtCore.Qt.DisplayRole)
# performs case insensitive matching
# return True if item shall stay in th returned filtered data
# return False to reject an item
return local_completion_prefix.lower() in source_data.lower()
proxy_model = InnerProxyModel()
proxy_model.setSourceModel(self.source_model)
super(CustomQCompleter, self).setModel(proxy_model)
# #todo: Why to be set here again?
self.popup().setItemDelegate(self.delegate)
def splitPath(self, path):
self.local_completion_prefix = path
self.updateModel()
return ""
class AutoCompleteEdit(QtGui.QLineEdit):
""" Basically from:
http://doc.qt.io/qt-5/qtwidgets-tools-customcompleter-example.html
"""
def __init__(self, list_data, separator=' ', addSpaceAfterCompleting=True):
super(AutoCompleteEdit, self).__init__()
# settings
self._separator = separator
self._addSpaceAfterCompleting = addSpaceAfterCompleting
# completer
self._completer = CustomQCompleter(self)
self._completer.setCaseSensitivity(QtCore.Qt.CaseInsensitive)
self._completer.setCompletionMode(QtGui.QCompleter.PopupCompletion)
self.model = QtGui.QStringListModel(list_data)
self._completer.setModel(self.model)
# connect the completer to the line edit
self._completer.setWidget(self)
# trigger insertion of the selected completion when its activated
self.connect(self._completer,
QtCore.SIGNAL('activated(QString)'),
self._insertCompletion)
self._ignored_keys = [QtCore.Qt.Key_Enter,
QtCore.Qt.Key_Return,
QtCore.Qt.Key_Escape,
QtCore.Qt.Key_Tab]
def _insertCompletion(self, completion):
"""
This is the event handler for the QCompleter.activated(QString) signal,
it is called when the user selects an item in the completer popup.
It will remove the already typed string with the one of the completion.
"""
stripped_text = self.text()[:-len(self._completer.completionPrefix())]
extra_text = completion # [-extra:]
if self._addSpaceAfterCompleting:
extra_text += ' '
self.setText(stripped_text + extra_text)
def textUnderCursor(self):
text = self.text()
textUnderCursor = ''
i = self.cursorPosition() - 1
while i >= 0 and text[i] != self._separator:
textUnderCursor = text[i] + textUnderCursor
i -= 1
return textUnderCursor
def keyPressEvent(self, event):
if self._completer.popup().isVisible():
if event.key() in self._ignored_keys:
event.ignore()
return
super(AutoCompleteEdit, self).keyPressEvent(event)
completionPrefix = self.textUnderCursor()
if completionPrefix != self._completer.completionPrefix():
self._updateCompleterPopupItems(completionPrefix)
if len(event.text()) > 0 and len(completionPrefix) > 0:
self._completer.complete()
if len(completionPrefix) == 0:
self._completer.popup().hide()
def _updateCompleterPopupItems(self, completionPrefix):
"""
Filters the completer's popup items to only show items
with the given prefix.
"""
self._completer.setCompletionPrefix(completionPrefix)
# self._completer.popup().setCurrentIndex(
# self._completer.completionModel().index(0, 0))
if __name__ == '__main__':
def demo():
import sys
app = QtGui.QApplication(sys.argv)
values = ['Germany',
'Au<b>st</b>ria',
'Switzerland',
'Hungary',
'The United Kingdom of Great Britain and Northern Ireland']
editor = AutoCompleteEdit(values)
window = QtGui.QWidget()
hbox = QtGui.QHBoxLayout()
hbox.addWidget(editor)
window.setLayout(hbox)
window.show()
sys.exit(app.exec_())
demo()
My problem is the suggestion of user Timo in the answer https://stackoverflow.com/a/5443112/1504082:
After line: 'doc.setHtml(options.text)', you need to set also doc.setTextWidth(option.rect.width()), otherwise the delegate wont render longer content correctly in respect to target drawing area. For example does not wrap words in QListView.
So i did this to avoid cropping of long text in the completer's popup. But i get the following output:
Where does this additional vertical margin come from?
I investigated this a bit and i see that the sizeHint method of HTMLDelegate is sometimes called with an options parameter which contains a rectangle with attributes (0, 0, 0, 0). And the display behaviour finally changes after the call of doc.setTextWidth(options.rect.width()). But i couldnt finally find out who calls it with this parameter and how i could properly fix this.
Can somebody explain where this comes from and how i can fix this porperly?
Finally i found another way to realize it using the idea of https://stackoverflow.com/a/8036666/1504082. Its much more forward for me without all this custom drawing things which i dont understand yet :)
from PySide import QtCore, QtGui
class TaskDelegate(QtGui.QItemDelegate):
# based on https://stackoverflow.com/a/8036666/1504082
# https://doc.qt.io/archives/qt-4.7/qitemdelegate.html#drawDisplay
# https://doc.qt.io/archives/qt-4.7/qwidget.html#render
margin_x = 5
margin_y = 3
def drawDisplay(self, painter, option, rect, text):
label = self.make_label(option, text)
# calculate render anchor point
point = rect.topLeft()
point.setX(point.x() + self.margin_x)
point.setY(point.y() + self.margin_y)
label.render(painter, point, renderFlags=QtGui.QWidget.DrawChildren)
def sizeHint(self, option, index):
# get text using model and index
text = index.model().data(index)
label = self.make_label(option, text)
return QtCore.QSize(label.width(), label.height() + self.margin_y)
def make_label(self, option, text):
label = QtGui.QLabel(text)
if option.state & QtGui.QStyle.State_Selected:
p = option.palette
p.setColor(QtGui.QPalette.WindowText,
p.color(QtGui.QPalette.Active,
QtGui.QPalette.HighlightedText)
)
label.setPalette(p)
label.setStyleSheet("border: 1px dotted black")
# adjust width according to widget's target width
label.setMinimumWidth(self.target_width - (2 * self.margin_x))
label.setMaximumWidth(self.target_width - self.margin_x)
label.setWordWrap(True)
label.adjustSize()
return label
class CustomQCompleter(QtGui.QCompleter):
""" Implement "contains" filter mode as the filter mode "contains" is not
available in Qt < 5.2
From: https://stackoverflow.com/a/7767999/1504082 """
def __init__(self, parent=None):
super(CustomQCompleter, self).__init__(parent)
self.local_completion_prefix = ""
self.source_model = None
self.delegate = TaskDelegate()
# widget not set yet
# self.delegate.target_width = self.widget().width()
def setModel(self, model):
self.source_model = model
super(CustomQCompleter, self).setModel(self.source_model)
def updateModel(self):
local_completion_prefix = self.local_completion_prefix
# see: http://doc.qt.io/qt-4.8/model-view-programming.html#proxy-models
class InnerProxyModel(QtGui.QSortFilterProxyModel):
def filterAcceptsRow(self, sourceRow, sourceParent):
# model index mapping by row, 1d model => column is always 0
index = self.sourceModel().index(sourceRow, 0, sourceParent)
source_data = self.sourceModel().data(index, QtCore.Qt.DisplayRole)
# performs case insensitive matching
# return True if item shall stay in th returned filtered data
# return False to reject an item
return local_completion_prefix.lower() in source_data.lower()
proxy_model = InnerProxyModel()
proxy_model.setSourceModel(self.source_model)
super(CustomQCompleter, self).setModel(proxy_model)
# #todo: Why to be set here again?
# -> rescale popup list items to widget width
self.delegate.target_width = self.widget().width()
self.popup().setItemDelegate(self.delegate)
def splitPath(self, path):
self.local_completion_prefix = path
self.updateModel()
return ""
class AutoCompleteEdit(QtGui.QLineEdit):
""" Basically from:
http://doc.qt.io/qt-5/qtwidgets-tools-customcompleter-example.html
"""
def __init__(self, list_data, separator=' ', addSpaceAfterCompleting=True):
super(AutoCompleteEdit, self).__init__()
# settings
self._separator = separator
self._addSpaceAfterCompleting = addSpaceAfterCompleting
# completer
self._completer = CustomQCompleter(self)
self._completer.setCaseSensitivity(QtCore.Qt.CaseInsensitive)
self._completer.setCompletionMode(QtGui.QCompleter.PopupCompletion)
self.model = QtGui.QStringListModel(list_data)
self._completer.setModel(self.model)
# connect the completer to the line edit
self._completer.setWidget(self)
# trigger insertion of the selected completion when its activated
self.connect(self._completer,
QtCore.SIGNAL('activated(QString)'),
self._insertCompletion)
self._ignored_keys = [QtCore.Qt.Key_Enter,
QtCore.Qt.Key_Return,
QtCore.Qt.Key_Escape,
QtCore.Qt.Key_Tab]
def _insertCompletion(self, completion):
"""
This is the event handler for the QCompleter.activated(QString) signal,
it is called when the user selects an item in the completer popup.
It will remove the already typed string with the one of the completion.
"""
stripped_text = self.text()[:-len(self._completer.completionPrefix())]
extra_text = completion # [-extra:]
if self._addSpaceAfterCompleting:
extra_text += ' '
self.setText(stripped_text + extra_text)
def textUnderCursor(self):
text = self.text()
textUnderCursor = ''
i = self.cursorPosition() - 1
while i >= 0 and text[i] != self._separator:
textUnderCursor = text[i] + textUnderCursor
i -= 1
return textUnderCursor
def keyPressEvent(self, event):
if self._completer.popup().isVisible():
if event.key() in self._ignored_keys:
event.ignore()
return
super(AutoCompleteEdit, self).keyPressEvent(event)
completionPrefix = self.textUnderCursor()
if completionPrefix != self._completer.completionPrefix():
self._updateCompleterPopupItems(completionPrefix)
if len(event.text()) > 0 and len(completionPrefix) > 0:
self._completer.complete()
if len(completionPrefix) == 0:
self._completer.popup().hide()
def _updateCompleterPopupItems(self, completionPrefix):
"""
Filters the completer's popup items to only show items
with the given prefix.
"""
self._completer.setCompletionPrefix(completionPrefix)
# self._completer.popup().setCurrentIndex(
# self._completer.completionModel().index(0, 0))
if __name__ == '__main__':
def demo():
import sys
app = QtGui.QApplication(sys.argv)
values = ['Germany',
'Au<b>st</b>ria',
'Switzerland',
'Hungary',
'The United Kingdom of Great Britain and Northern Ireland',
'USA']
editor = AutoCompleteEdit(values)
window = QtGui.QWidget()
hbox = QtGui.QHBoxLayout()
hbox.addWidget(editor)
window.setLayout(hbox)
window.show()
sys.exit(app.exec_())
demo()

Categories

Resources