QTableWidget loads very slow after clearing it with QLineEdits inside - python

I have a QTableWidget with QLineEdits as CellWidgets inside of it.
If I clear the table and refill it with the same function it needs much longer to finish.
In this example the difference is between 0.1 and 3.9 seconds, but in my real code the difference is between 0.1 seconds and 10 minutes.
So here is the example code:
class Test_Layout(QWidget):
def __init__(self, parent=None):
super(Test_Layout, self).__init__(parent=None)
self.left = 0
self.top = 0
self.width = 0
self.height = 0
self.initUI()
self.isMaximized()
def initUI(self):
self.setGeometry(self.left, self.top, self.width, self.height)
self.createTable()
start_time = time.time()
self.fillTable()
print(time.time() - start_time)
self.combo = QComboBox(self)
self.combo.addItem("Reset")
for i in range(0, 5):
self.combo.addItem(str(i))
self.combo.currentTextChanged.connect(self.on_combo_changed)
self.vbox = QVBoxLayout()
self.vbox.addWidget(self.combo)
self.vbox.addWidget(self.table)
self.setLayout(self.vbox)
def fill_row(self, row):
self.table.insertRow(row)
placeholder = QLineEdit()
self.table.setCellWidget(row, 0, placeholder)
placeholder = QLineEdit()
self.table.setCellWidget(row, 1, placeholder)
placeholder = QLineEdit()
self.table.setCellWidget(row, 2, placeholder)
def on_combo_changed(self, currentText):
self.table.setRowCount(0)
if currentText == "Reset":
start_time = time.time()
self.fillTable()
print(time.time() - start_time)
else:
for row in range(0, int(currentText)):
self.fill_row(row)
def createTable(self):
self.table = QTableWidget()
self.table.setColumnCount(3)
self.table.setHorizontalHeaderLabels([
"LineEdit0",
"LineEdit1",
"LineEdit2",
])
header = self.table.horizontalHeader()
for i in range(0, 3):
header.setSectionResizeMode(i, QHeaderView.ResizeToContents)
def fillTable(self):
for row in range(0, 1000):
self.fill_row(row)
Output:
0.14005303382873535
And after using the QCombobox and setting it back to "Reset":
3.9842889308929443
And before somebody asks, I fill the QTableWidget with QLineEdits, because I want to use placeholders.

The difference is not only due to the fact that you're using cell widgets for each cell (and, let me say that 3000 widgets are a lot), but because you're calling setRowCount() each time.
You can also see that the problem happens not after "clearing", but just when creating the new cells: just remove the first call to fillTable in the __init__ and the same delay occurs.
Each time the model layout changes (by adding/removing rows or columns), lots of things happen not only for the model, but for the view that shows its contents, and since you are adding rows individually, this results in longer time required for the view to process its contents, even if you cannot see it instantly (and that's because the repainting is queued and only happens as soon as the event queue is cleared).
To improve performance, in your case, you should call setRowCount() only once with the final number of rows that are going to be shown:
def fill_row(self, row):
# comment or remove the following line
# self.table.insertRow(row)
placeholder = QLineEdit()
self.table.setCellWidget(row, 0, placeholder)
placeholder = QLineEdit()
self.table.setCellWidget(row, 1, placeholder)
placeholder = QLineEdit()
self.table.setCellWidget(row, 2, placeholder)
def on_combo_changed(self, currentText):
self.table.setRowCount(0)
if currentText == "Reset":
start_time = time.time()
self.fillTable()
print(time.time() - start_time)
else:
count = int(currentText)
self.table.setRowCount(count)
for row in range(0, int(currentText)):
self.fill_row(row)
def fillTable(self):
self.table.setRowCount(1000)
for row in range(0, 1000):
self.fill_row(row)
Finally, if you're really going to show that many rows, I strongly suggest to find an alternative, as the documentation explains for setIndexWidget() (which is internally called by setCellWidget()):
This function should only be used to display static content within the visible area corresponding to an item of data. If you want to display custom dynamic content or implement a custom editor widget, subclass QStyledItemDelegate instead.
This is because large amounts of widgets will cause drastic performances issues (exactly like yours).
If what you need is a placeholder, using a QLineEdit for each cell is a bad choice, not only for performance reasons, but also because in that way you don't have direct access to the model data, but you would always need to find the cell widget before that.
A more elegant and preferable solution is to use a custom delegate, which will show the placeholder text when there is no data for the cell, and add CurrentChanged to the table edit triggers:
self.table.setEditTriggers(self.table.editTriggers() | self.table.CurrentChanged)
A simple delegate implementation could be like this:
class PlaceholderDelegate(QStyledItemDelegate):
def __init__(self, placeholder='', parent=None):
super().__init__(parent)
self.placeholder = placeholder
def createEditor(self, parent, option, index):
editor = super().createEditor(parent, option, index)
if isinstance(editor, QLineEdit):
editor.setPlaceholderText(self.placeholder)
return editor
def paint(self, painter, option, index):
super().paint(painter, option, index)
if not index.data():
try:
# placeholderText palette role was introduced on Qt 5.12
color = option.palette.placeholderText().color()
except:
color = option.palette.text().color()
color.setAlpha(128)
painter.setPen(color)
style = option.widget.style()
margin = style.pixelMetric(style.PM_FocusFrameHMargin, option, option.widget)
rect = option.rect.adjusted(margin, 0, -margin, 0)
text = option.fontMetrics.elidedText(self.placeholder, option.textElideMode, rect.width())
painter.drawText(rect, option.displayAlignment, text)
class Test_Layout(QWidget):
# ...
def createTable(self):
# ...
for i in range(0, 3):
header.setSectionResizeMode(i, QHeaderView.ResizeToContents)
self.table.setItemDelegateForColumn(
i, PlaceholderDelegate('empty {}'.format(i + 1), self.table))
Note: you should not use setGeometry() with 0 width and height, and always provide a default position can be very annoying for users with large screens or more than one screen; also, width and height are default properties for all QWidget subclasses, and should never be overwritten with custom attributes.

Related

Drag and Drop a Widget in QTreeWidget

I have a QTreeWidget where I want to move around the items. This works fine with the cities (see example) but not with the buttons.
First Question: What do I need to do to make the buttons moveable like the cities?
Second Question: If I move the cities I get a copy, but I want to move the city only (delete from original place)
class Example(QTreeWidget):
def __init__(self):
super().__init__()
self.setDragEnabled(True)
self.setAcceptDrops(True)
self.setWindowTitle('Drag and Drop Button')
self.setGeometry(300, 300, 550, 450)
self.cities = QTreeWidgetItem(self)
self.cities.setText(0,"Cities")
self.setDragDropMode(self.InternalMove)
osloItem = QTreeWidgetItem(self.cities)
osloItem.setText(0,"Oslo")
bergenItem = QTreeWidgetItem(self.cities)
bergenItem.setText(0,"Bergen")
stavangerItem = QTreeWidgetItem(self.cities)
stavangerItem.setText(0,"Stavanger")
button1 = QPushButton('Button1',self)
button2 = QPushButton("Button2",self)
label = QLabel("dragHandle")
container = QWidget()
containerLayout = QHBoxLayout()
container.setLayout(containerLayout)
containerLayout.addWidget(label)
containerLayout.addWidget(button1)
containerLayout.addWidget(button2)
b1 = QTreeWidgetItem(self.cities)
self.setItemWidget(b1,0,container)
def main():
app = QApplication(sys.argv)
ex = Example()
ex.show()
app.exec_()
if __name__ == '__main__':
main()
An important aspect about index widgets (including persistent editors) is that the view takes complete ownership on the widget, and whenever index is removed for any reason, the widget associated to that widget gets automatically destroyed. This also happens when calling again setIndexWidget() with another widget for the same index.
There is absolutely no way to prevent that, the destruction is done internally (by calling deleteLater()), and reparenting the widget won't change anything.
The only way to "preserve" the widget is to set a "fake" container as the index widget, create a layout for it, and add the actual widget to it.
Then, the problem comes when using drag&drop, because item views always use serialization of items, even when the InternalMove flag is set.
This means that when an item is moved, the original index gets removed (and the widget along with it, including widgets for any child item).
The solution, then, is to "capture" the drop operation before it's performed, reparent the contents of the container with a similar copy, proceed with the base implementation and then restore the widgets for the new target indexes.
Since we are dealing with tree models, this automatically calls for recursive functions, both for reparenting and restoration.
In the following code I've created an implementation that should be compatible for all standard views (QTreeView, QTableView, QListView) and their higher level widgets. I didn't consider QColumnView, as it's a quite peculiar view and rarely has such requirement.
from PyQt5.QtCore import *
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *
class Handle(QWidget):
'''
A custom widget that shows a handle for item dragging whenever
the editor doesn't support it
'''
def __init__(self):
super().__init__()
self.setFixedWidth(self.style().pixelMetric(
QStyle.PM_ToolBarHandleExtent))
def paintEvent(self, event):
qp = QPainter(self)
opt = QStyleOption()
opt.initFrom(self)
style = self.style()
opt.state |= style.State_Horizontal
style.drawPrimitive(style.PE_IndicatorToolBarHandle, opt, qp)
class Example(QTreeWidget):
def __init__(self):
super().__init__()
self.setColumnCount(2)
self.setDragEnabled(True)
self.setAcceptDrops(True)
self.setWindowTitle('Drag and Drop Button')
self.cities = QTreeWidgetItem(self)
self.cities.setText(0, 'Cities')
self.setDragDropMode(self.InternalMove)
osloItem = QTreeWidgetItem(self.cities)
osloItem.setText(0,'Oslo')
bergenItem = QTreeWidgetItem(self.cities)
bergenItem.setText(0,'Bergen')
stavangerItem = QTreeWidgetItem(self.cities)
stavangerItem.setText(0, 'Stavanger')
button1 = QPushButton('Button1', self)
button2 = QPushButton('Button2', self)
label = QLabel('dragHandle')
container = QWidget()
containerLayout = QGridLayout()
container.setLayout(containerLayout)
containerLayout.addWidget(label)
containerLayout.addWidget(button1, 0, 1)
containerLayout.addWidget(button2, 0, 2)
b1 = QTreeWidgetItem(self.cities)
self.setItemWidget(b1, 0, container)
anotherItem = QTreeWidgetItem(self, ['Whatever'])
anotherChild = QTreeWidgetItem(anotherItem, ['Another child'])
grandChild = QTreeWidgetItem(anotherChild, ['a', 'b'])
self.setItemWidget(grandChild, 1, QPushButton('Whatever'))
self.expandAll()
height = self.header().sizeHint().height() + self.frameWidth() * 2
index = self.model().index(0, 0)
while index.isValid():
height += self.rowHeight(index)
index = self.indexBelow(index)
self.resize(500, height + 100)
def setItemWidget(self, item, column, widget, addHandle=False):
if widget and addHandle or widget.layout() is None:
container = QWidget()
layout = QHBoxLayout(container)
layout.setContentsMargins(0, 0, 0, 0)
layout.setSpacing(0)
layout.addWidget(Handle())
layout.addWidget(widget)
widget = container
super().setItemWidget(item, column, widget)
def createNewContainer(self, index):
'''
create a copy of the container and its layout, then reparent all
child widgets by adding them to the new layout
'''
oldWidget = self.indexWidget(index)
if oldWidget is None:
return
oldLayout = oldWidget.layout()
if oldLayout is None:
return
newContainer = oldWidget.__class__()
newLayout = oldLayout.__class__(newContainer)
newLayout.setContentsMargins(oldLayout.contentsMargins())
newLayout.setSpacing(oldLayout.spacing())
if isinstance(oldLayout, QGridLayout):
newLayout.setHorizontalSpacing(
oldLayout.horizontalSpacing())
newLayout.setVerticalSpacing(
oldLayout.verticalSpacing())
for r in range(oldLayout.rowCount()):
newLayout.setRowStretch(r,
oldLayout.rowStretch(r))
newLayout.setRowMinimumHeight(r,
oldLayout.rowMinimumHeight(r))
for c in range(oldLayout.columnCount()):
newLayout.setColumnStretch(c,
oldLayout.columnStretch(c))
newLayout.setColumnMinimumWidth(c,
oldLayout.columnMinimumWidth(c))
items = []
for i in range(oldLayout.count()):
layoutItem = oldLayout.itemAt(i)
if not layoutItem:
continue
if layoutItem.widget():
item = layoutItem.widget()
elif layoutItem.layout():
item = layoutItem.layout()
elif layoutItem.spacerItem():
item = layoutItem.spacerItem()
if isinstance(oldLayout, QBoxLayout):
items.append((item, oldLayout.stretch(i), layoutItem.alignment()))
else:
items.append((item, ) + oldLayout.getItemPosition(i))
for item, *args in items:
if isinstance(item, QWidget):
newLayout.addWidget(item, *args)
elif isinstance(item, QLayout):
newLayout.addLayout(item, *args)
else:
if isinstance(newLayout, QBoxLayout):
newLayout.addSpacerItem(item)
else:
newLayout.addItem(item, *args)
return newContainer
def getNewIndexWidgets(self, parent, row):
'''
A recursive function that returns a nested list of widgets and those of
child indexes, by creating new parent containers in the meantime to
avoid their destruction
'''
model = self.model()
rowItems = []
for column in range(model.columnCount()):
index = model.index(row, column, parent)
childItems = []
if column == 0:
for childRow in range(model.rowCount(index)):
childItems.append(self.getNewIndexWidgets(index, childRow))
rowItems.append((
self.createNewContainer(index),
childItems
))
return rowItems
def restoreIndexWidgets(self, containers, parent, startRow=0, startCol=0):
'''
Restore index widgets based on the previously created nested list of
widgets, based on the new parent and drop row (and column for tables)
'''
model = self.model()
for row, rowItems in enumerate(containers, startRow):
for column, (widget, childItems) in enumerate(rowItems, startCol):
index = model.index(row, column, parent)
if widget:
self.setIndexWidget(index, widget)
self.restoreIndexWidgets(childItems, index)
def dropEvent(self, event):
'''
Assume that the selected index is the source of the drag and drop
operation, then create a nested list of possible index widget that
are reparented *before* the drop is applied to avoid their destruction
and then restores them based on the drop index
'''
containers = []
if event.source() == self:
dropTarget = QPersistentModelIndex(self.indexAt(event.pos()))
dropPos = self.dropIndicatorPosition()
selection = self.selectedIndexes()
if selection and len(set(i.row() for i in selection)) == 1:
index = selection[0]
containers.append(
self.getNewIndexWidgets(index.parent(), index.row()))
super().dropEvent(event)
if containers:
model = self.model()
startCol = 0
if dropPos == self.OnViewport:
parent = QModelIndex()
dropRow = model.rowCount() - 1
else:
if dropPos == self.OnItem:
if isinstance(self, QTreeView):
# tree views move items as *children* of the drop
# target when the action is *on* an item
parent = model.index(
dropTarget.row(), dropTarget.column(),
dropTarget.parent())
dropRow = model.rowCount(parent) - 1
else:
# QTableView and QListView use the drop index as target
# for the operation, so the parent index is actually
# the root index of the view
parent = self.rootIndex()
dropRow = model.rowCount(dropTarget.parent()) - 1
dropRow = dropTarget.row()
startCol = dropTarget.column() - index.column()
else:
if isinstance(self, QTreeView):
parent = dropTarget.parent()
else:
parent = self.rootIndex()
if dropPos == self.AboveItem:
dropRow = dropTarget.row() - 1
else:
dropRow = dropTarget.row() + 1
# try to restore the widgets based on the above
self.restoreIndexWidgets(containers, parent, dropRow, startCol)
# ensure that all geometries are updated after the drop operation
self.updateGeometries()
if __name__ == '__main__':
import sys
app = QApplication(sys.argv)
ex = Example()
ex.show()
app.exec_()
Notes:
while the above is done for QTreeWidget, it can be easily ported for generic usage of other views; the only difference is the optional override of setItemWidget (which would be setIndexWidget() for basic views or setCellWidget() for QTableWidget, and with the relative argument differences;
item views (and "item widget views") and models might have different behaviors on drag&drop operations, and depending on their settings (starting with dragDropMode); always consider the default behavior of those classes, and also the defaultDropAction;
please study all the code above with extreme care: it's a generic example, drag and drop operations are quite complex, and you might need custom behavior; I strongly suggest to patiently and carefully read that code to understand its aspects whenever you need more advanced control over d&d, especially when dealing with tree structures;
remember that index widgets, while useful, are often problematic; most of the times, using a custom delegate might be a better choice, even for interactive widgets such as buttons or complex widgets; remember that the base source of item views is the model data, including eventual "widgets" (or even editors and their contents);

How to word wrap the header contents of QTableWidget in PyQt5 Python

I am working on PyQt5 where I have a QTableWidget. It has a header column which I want to word wrap. Below is how the table looks like:
As we can see that the header label like Maximum Variation Coefficient has 3 words, thus its taking too much column width. How can wrap the words in the header.
Below is the code:
import sys
from PyQt5 import QtWidgets
from PyQt5.QtWidgets import *
# Main Window
class App(QWidget):
def __init__(self):
super().__init__()
self.title = 'PyQt5 - QTableWidget'
self.left = 0
self.top = 0
self.width = 300
self.height = 200
self.setWindowTitle(self.title)
self.setGeometry(self.left, self.top, self.width, self.height)
self.createTable()
self.layout = QVBoxLayout()
self.layout.addWidget(self.tableWidget)
self.setLayout(self.layout)
# Show window
self.show()
# Create table
def createTable(self):
self.tableWidget = QTableWidget()
# Row count
self.tableWidget.setRowCount(3)
# Column count
self.tableWidget.setColumnCount(2)
self.tableWidget.setHorizontalHeaderLabels(["Maximum Variation Coefficient", "Maximum Variation Coefficient"])
self.tableWidget.setSizeAdjustPolicy(QtWidgets.QAbstractScrollArea.AdjustToContents)
self.tableWidget.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeToContents)
self.tableWidget.setItem(0, 0, QTableWidgetItem("3.44"))
self.tableWidget.setItem(0, 1, QTableWidgetItem("5.3"))
self.tableWidget.setItem(1, 0, QTableWidgetItem("4.6"))
self.tableWidget.setItem(1, 1, QTableWidgetItem("1.2"))
self.tableWidget.setItem(2, 0, QTableWidgetItem("2.2"))
self.tableWidget.setItem(2, 1, QTableWidgetItem("4.4"))
# Table will fit the screen horizontally
self.tableWidget.horizontalHeader().setStretchLastSection(True)
self.tableWidget.horizontalHeader().setSectionResizeMode(
QHeaderView.Stretch)
if __name__ == '__main__':
app = QApplication(sys.argv)
ex = App()
sys.exit(app.exec_())
I tried adding this self.tableWidget.setWordWrap(True) but this doesnt make any change. Can anyone give some good solution. Please help. Thanks
EDIT:
Also tried this :
self.tableWidget.horizontalHeader().setDefaultAlignment(QtCore.Qt.AlignHCenter | Qt.Alignment(QtCore.Qt.TextWordWrap))
But it didnt worked
In order to achieve what you need, you must set your own header and proceed with the following two assumptions:
the header must provide the correct size hint height according to the section contents in case the width of the column is not sufficient;
the text alignment must include the QtCore.Qt.TextWordWrap flag, so that the painter knows that it can wrap text;
Do note that, while the second aspect might be enough in some situations (as headers are normally tall enough to fit at least two lines), the first point is mandatory as the text might require more vertical space, otherwise some text would be cut out.
The first point requires to subclass QHeaderView and reimplement sectionSizeFromContents():
class WrapHeader(QtWidgets.QHeaderView):
def sectionSizeFromContents(self, logicalIndex):
# get the size returned by the default implementation
size = super().sectionSizeFromContents(logicalIndex)
if self.model():
if size.width() > self.sectionSize(logicalIndex):
text = self.model().headerData(logicalIndex,
self.orientation(), QtCore.Qt.DisplayRole)
if not text:
return size
# in case the display role is numeric (for example, when header
# labels are not defined yet), convert it to a string;
text = str(text)
option = QtWidgets.QStyleOptionHeader()
self.initStyleOption(option)
alignment = self.model().headerData(logicalIndex,
self.orientation(), QtCore.Qt.TextAlignmentRole)
if alignment is None:
alignment = option.textAlignment
# get the default style margin for header text and create a
# possible rectangle using the current section size, then use
# QFontMetrics to get the required rectangle for the wrapped text
margin = self.style().pixelMetric(
QtWidgets.QStyle.PM_HeaderMargin, option, self)
maxWidth = self.sectionSize(logicalIndex) - margin * 2
rect = option.fontMetrics.boundingRect(
QtCore.QRect(0, 0, maxWidth, 10000),
alignment | QtCore.Qt.TextWordWrap,
text)
# add vertical margins to the resulting height
height = rect.height() + margin * 2
if height >= size.height():
# if the height is bigger than the one provided by the base
# implementation, return a new size based on the text rect
return QtCore.QSize(rect.width(), height)
return size
class App(QWidget):
# ...
def createTable(self):
self.tableWidget = QTableWidget()
self.tableWidget.setHorizontalHeader(
WrapHeader(QtCore.Qt.Horizontal, self.tableWidget))
# ...
Then, to set the word wrap flag, there are two options:
set the alignment flag on the underlying model with setHeaderData() for each existing column:
# ...
model = self.tableWidget.model()
default = self.tableWidget.horizontalHeader().defaultAlignment()
default |= QtCore.Qt.TextWordWrap
for col in range(self.tableWidget.columnCount()):
alignment = model.headerData(
col, QtCore.Qt.Horizontal, QtCore.Qt.TextAlignmentRole)
if alignment:
alignment |= QtCore.Qt.TextWordWrap
else:
alignment = default
model.setHeaderData(
col, QtCore.Qt.Horizontal, alignment, QtCore.Qt.TextAlignmentRole)
Use a QProxyStyle to override the painting of the header, by applying the flag on the option:
# ...
class ProxyStyle(QtWidgets.QProxyStyle):
def drawControl(self, control, option, painter, widget=None):
if control in (self.CE_Header, self.CE_HeaderLabel):
option.textAlignment |= QtCore.Qt.TextWordWrap
super().drawControl(control, option, painter, widget)
if __name__ == '__main__':
app = QApplication(sys.argv)
app.setStyle(ProxyStyle())
ex = App()
sys.exit(app.exec_())
Finally, consider that:
using setSectionResizeMode with ResizeToContents or Stretch, along with setStretchLastSection, will always cause the table trying to use as much space as required by the headers upon showing the first time;
by default, QHeaderView sections are not clickable (which is a mandatory requirement for sorting) and the highlightSections property is also False; both QTableView and QTableWidget create their headers with those values as True, so when a new header is set you must explicitly change those aspects if sorting and highlighting are required:
self.tableWidget.setHorizontalHeader(
WrapHeader(QtCore.Qt.Horizontal, self.tableWidget))
self.tableWidget.horizontalHeader().setSectionsClickable(True)
self.tableWidget.horizontalHeader().setHighlightSections(True)
both sorting and section highlighting can create some issues, as the sort indicator requires further horizontal space and highlighted sections are normally shown with a bold font (but are shown normally while the mouse is pressed); all this might create some flickering and odd behavior; unfortunately, there's no obvious solution for these problems, but when using the QProxyStyle it's possible to avoid some flickering by overriding the font style:
def drawControl(self, control, option, painter, widget=None):
if control in (self.CE_Header, self.CE_HeaderLabel):
option.textAlignment |= QtCore.Qt.TextWordWrap
if option.state & self.State_Sunken:
option.state |= self.State_On
super().drawControl(control, option, painter, widget)

Delegate with radio buttons

I have the following code:
import datetime
from PyQt5 import QtCore
from PyQt5.QtWidgets import *
from PyQt5.QtGui import *
class RadioDelegate(QStyledItemDelegate):
def __init__(self, owner, chs):
super().__init__(owner)
self.items = chs
def paint(self, painter, option, index):
if isinstance(self.parent(), QAbstractItemView):
self.parent().openPersistentEditor(index)
super(RadioDelegate, self).paint(painter, option, index)
def createEditor(self, parent, option, index):
editor = QGroupBox(parent)
# editor.setMinimumHeight(38)
editor.setContentsMargins(0, 0, 0, 0)
layout = QHBoxLayout()
style = """
padding: 0px;
margin-top: 0px;
outline: none;
border: none
"""
for k in self.items:
rb = QRadioButton(k)
rb.setStyleSheet(style)
layout.addWidget(rb)
editor.setStyleSheet(style)
editor.setLayout(layout)
return editor
def setEditorData(self, editor, index):
value = index.data(QtCore.Qt.DisplayRole)
print("setEditorData-" + str(datetime.datetime.now()) + " " + str(value))
def setModelData(self, editor, model, index):
print("setModelData-" + str(datetime.datetime.now()))
def updateEditorGeometry(self, editor, option, index):
editor.setGeometry(option.rect)
class Window(QMainWindow):
def __init__(self):
super().__init__()
self.setGeometry(40, 80, 550, 250)
self.table = QTableView(self)
self.model = QStandardItemModel(self)
self.table.setModel(self.model)
self.table.setItemDelegateForColumn(3, RadioDelegate(self, ["abc", "xyz"]))
self.populate()
self.table.resizeRowsToContents()
self.table.clicked.connect(self.on_click)
self.setCentralWidget(self.table)
def on_click(self, event):
row = event.row()
column = event.column()
cell_dict = self.model.itemData(event)
cell_value = cell_dict.get(0)
print("Row {}, column {} clicked: {}".format(row, column, cell_value))
def populate(self):
values = [["1st", "2nd", "3rd", "radio"], ["111", "222", "333", ""], ["abc", "xyz", "123", ""]]
for value in values:
row = []
for item in value:
cell = QStandardItem(str(item))
row.append(cell)
self.model.appendRow(row)
if __name__ == '__main__':
import sys
app = QApplication(sys.argv)
ex = Window()
ex.show()
sys.exit(app.exec_())
And I get the look of the edited cell (4th column), which is not acceptable:
cell with radio buttons
How can I get rid of these margins or this padding? Or make the group box extend to cover the whole cell?
I have tried to manipulate style sheets, as you can see, but with no effect.
Of course, I can use "editor.setMinimumHeight(38)", but the top margin or whatever it is, still remains, and I can select the whole cell (not only the radio buttons).
By the way! Do you know how the paint method should look like to display radio buttons also in display mode?
You have to set the margins of the layout too:
def createEditor(self, parent, option, index):
editor = QGroupBox(parent)
# editor.setMinimumHeight(38)
editor.setContentsMargins(0, 0, 0, 0)
layout = QHBoxLayout()
layout.setContentsMargins(0, 0, 0, 0)
When a layout is created it has no margins, but they will change as soon as it is applied to a widget:
From the getContentsMargins() documentation:
By default, QLayout uses the values provided by the style.
These values are provided by the following QStyle's pixelmetrics: PM_LayoutLeftMargin, PM_LayoutTopMargin, PM_LayoutRightMargin, PM_LayoutBottomMargin. They all depend on the style internal parameters, the level of the widget (if it's a top-level widget - a "window" - or a child) and the dpi of the screen.
While newly created layouts usually return (0, 0, 0, 0) from getContentsMargins(), internally their value is actually -1. When the layout is applied to a widget, Qt will use the style's fallback values only if they've not been set to a value >= 0.
So, the margin has to be explicitly set even for 0 values to ensure that no margin actually exists; conversely, the style's default values can be restored by setting each margin to -1.
A couple of suggestions, even if they're not related to the actual question.
First of all, don't try to open the persistent editor within the paintEvent. Despite the fact that the method you used wouldn't work anyway, paintEvents happen very often, and creating a new widget inside a paintEvent would require another call to the paintEvent itself, potentially creating an infinite recursion (luckily, Qt would ignore the openPersistentEditor if it already exists, but that's not the point).
Whenever you're using a complex widget, it's better to set the background of the editor, otherwise the item's text might be visible under the editor in most styles. Also, there's no need for a QGroupBox for this situation, a simple QWidget will suffice.
With CSS you can just do this (there's no need to apply the style to the children):
editor.setStyleSheet("QWidget { background: palette(base); }")
If you don't need specific customization, you can avoid stylesheets at all and just set the autoFillBackground property:
editor.setAutoFillBackground(True)
Finally, the biggest issue of this kind of implementation is that if the column is not wide enough, some radio buttons might become invisible as using setGeometry will "clip" the contents to the item rectangle.
To avoid that, you can adjust the geometry rectangle to accomodate all its contents, based on its minimum size hint:
def updateEditorGeometry(self, editor, option, index):
rect = QtCore.QRect(option.rect)
minWidth = editor.minimumSizeHint().width()
if rect.width() < minWidth:
rect.setWidth(minWidth)
editor.setGeometry(rect)
Unfortunately, this will not be a very good thing if you want to show the editor even in "display mode", as you requested.
To do that in a good fashion while keeping a good user experience, things will be a bit more complex: the editor should be always be clipped when the item is not the current index, and automatically "resized" when some interaction is needed; also, if a hidden radio is selected, the user might click on an unchecked one, which will make that radio checked, without knowing the previous status.
To do so I'm using an event filter that will "clip" the widget using setMask() if it has no focus, and show all of it whenever it gets it. Since radio buttons can steal focus, I'm setting their focusPolicy to NoFocus. This also allows me to check if the widget has received a click to toggle radio buttons or just to start the editing; in that case, the click event will be ignored.
class RadioDelegate(QStyledItemDelegate):
def __init__(self, owner, chs):
super().__init__(owner)
self.items = chs
def createEditor(self, parent, option, index):
editor = QWidget(parent)
editor.setContentsMargins(0, 0, 0, 0)
editor.setAutoFillBackground(True)
# create a button group to keep track of the checked radio
editor.buttonGroup = QButtonGroup()
# adding the widget as an argument to the layout constructor automatically
# applies it to the widget
layout = QHBoxLayout(editor)
layout.setContentsMargins(0, 0, 0, 0)
for i, k in enumerate(self.items):
rb = QRadioButton(k)
layout.addWidget(rb)
# prevent the radio to get focus from keyboard or mouse
rb.setFocusPolicy(QtCore.Qt.NoFocus)
rb.installEventFilter(self)
editor.buttonGroup.addButton(rb, i)
# add a stretch to always align contents to the left
layout.addStretch(1)
# set a property that will be used for the mask
editor.setProperty('offMask', QRegion(editor.rect()))
editor.installEventFilter(self)
return editor
def eventFilter(self, source, event):
if event.type() == QtCore.QEvent.MouseButtonPress:
if isinstance(source, QRadioButton):
if not source.parent().hasFocus():
# the parent has no focus, set it and ignore the click
source.parent().setFocus()
return True
elif not source.hasFocus():
# the container has been clicked, check
source.setFocus()
elif event.type() == QtCore.QEvent.FocusIn:
# event received as a consequence of setFocus
# clear the mask to show it completely
source.clearMask()
elif event.type() == QtCore.QEvent.FocusOut:
# another widget has requested focus, set the mask
source.setMask(source.property('offMask'))
# update the table viewport to get rid of possible
# grid lines left after masking
source.parent().update()
return super().eventFilter(source, event)
def updateEditorGeometry(self, editor, option, index):
rect = QtCore.QRect(option.rect)
minWidth = editor.minimumSizeHint().width()
if rect.width() < minWidth:
rect.setWidth(minWidth)
editor.setGeometry(rect)
# create a new mask based on the option rectangle, then apply it
mask = QRegion(0, 0, option.rect.width(), option.rect.height())
editor.setProperty('offMask', mask)
editor.setMask(mask)
def setEditorData(self, editor, index):
value = index.data(QtCore.Qt.DisplayRole)
if value in self.items:
editor.buttonGroup.button(self.items.index(value)).setChecked(True)
def setModelData(self, editor, model, index):
button = editor.buttonGroup.checkedId()
if button >= 0:
model.setData(index, self.items[button], QtCore.Qt.DisplayRole)
class Window(QMainWindow):
def __init__(self):
# ...
# I just added an option for the purpose of this example
self.table.setItemDelegateForColumn(3, RadioDelegate(self, ["abc", "xyz", 'www']))
# ...
# open a persistent editor for all rows in column 4
for row in range(self.model.rowCount()):
self.table.openPersistentEditor(self.model.index(row, 3))

QTreeWidget - re-calculate row height per row content?

It's a quick one. I put a QWidget with layout containing 10 buttons in each of my QTreeWidgetItem that i can expand-contract.
If i contract the QWidget(hide), the row then remains the same height. It should get resized to be a lot smaller to contain only unhide button. The space occupied by the row remains empty. Only when I add a new widget. The row's height gets re-calculated. Same goes for expanding. If I expand widget, it still is 17 pixels in height. Only when I add a new widget the row's height gets recalculated.
Is there a built-in function in QTreeWidget that I can call to update its row height - content, or do I need to loop over each item and adjust its height manually?
Also is it a good idea to put a QWidget containing layout and lots of widgets inside QTreeWidgetItem? If I want to have lets say 500 QtreeWidgetItems, each having QTreeWidget with 20 Buttons/etc items in it?
Regards
Dariusz
Edit1.
I spend quite a bit of time on it. Tried different paths but so far no luck. I have attached my rough code to show the issue. Just click on hide 1-2 times to see what happens. I'm struggling to understand how the "sizeHint" thini work...
Attached my test below.
import sys
from PyQt5.QtGui import *
from PyQt5.QtCore import *
from PyQt5.QtWidgets import *
sys._excepthook = sys.excepthook
def my_exception_hook(exctype, value, traceback):
# print(exctype, value, traceback)
sys._excepthook(exctype, value, traceback)
sys.exit(1)
sys.excepthook = my_exception_hook
class myWidget(QWidget):
def __init__(self, parent, something):
super(QWidget, self).__init__()
self.MW = parent
self.MWa = something
self.lay_main = QGridLayout()
self.setLayout(self.lay_main)
self.le_lineA = QLineEdit()
self.btn_a = QPushButton("Hey")
self.btn_a.clicked.connect(self.hideunhide)
self.subWidget = QWidget()
self.subLay = QGridLayout()
self.subWidget.setLayout(self.subLay)
self.btn_b = QPushButton("Boo")
self.lay_main.addWidget(self.btn_a)
self.lay_main.addWidget(self.subWidget)
self.subLay.addWidget(self.le_lineA)
self.subLay.addWidget(self.btn_b)
print("Im created :- )")
self.size = False
def hideunhide(self):
if self.subWidget.isHidden():
self.subWidget.show()
self.size = True
self.MW.sizeHint(1)
self.MW.setSizeHint(0, QSize(0, 0))
else:
self.subWidget.hide()
self.size = False
self.MW.sizeHint(1)
self.MW.setSizeHint(0, QSize(0, 0))
class widgetItemDictUpdate(QTreeWidgetItem):
def __init__(self, parent, MW, title, dicId):
super(QTreeWidgetItem, self).__init__(parent)
self.MW = MW
self.dicId = dicId
class myDelegate(QItemDelegate):
def __init__(self, mw):
self.MW = mw
super(QItemDelegate, self).__init__()
def sizeHint(self, option, index):
print("myDelegate", option, index)
if self.MW.itemFromIndex(index).layer.size:
return QSize(100, 100)
return QSize(100, 30)
class treeWidget(QTreeWidget):
def __init__(self, MW):
QTreeWidget.__init__(self)
self.setUniformRowHeights(False)
self.MW = MW
self.itemClicked.connect(self.printClick)
self.setHeaderItem(QTreeWidgetItem(["Name", "ProgressBar", "extra", "", "", "", "", "", "", "", ""]))
self.setDragDropMode(QAbstractItemView.InternalMove)
self.setSelectionMode(QAbstractItemView.ExtendedSelection)
self.dragItems = []
self.lastHidenItemsArray = []
self.hiddenItemsSet = []
self.delg = myDelegate(self)
self.setItemDelegate(self.delg)
self.addItems(self.invisibleRootItem())
def printClick(self, item, column):
print("Clicked")
def addItems(self, parent):
testParent = self.addSlave(parent, 'RandomA', 50)
testParent = self.addSlave(parent, 'RandomB', 60)
testParent = self.addSlave(parent, 'RandomBX', 60)
testParent = self.addSlave(parent, 'RandomBcvas', 60)
def addSlave(self, parent, title, id, expanded=True, visible=True):
item = widgetItemDictUpdate(parent, self, title, id)
myLayer = myWidget(item, self)
item.setText(0, title)
item.layer = myLayer
self.setItemWidget(item, 1, myLayer)
item.id = id
item.type = 1
return item
class Window(QWidget):
def __init__(self):
QWidget.__init__(self)
self.treeWidget = treeWidget(self)
layout = QVBoxLayout()
layout.addWidget(self.treeWidget)
self.setLayout(layout)
self.setGeometry(1000, 500, 500, 500)
if __name__ == "__main__":
app = QApplication(sys.argv)
window = Window()
window.show()
sys.exit(app.exec_())
As far as I am aware there is no way to resize rows directly though you can achieve a dynamic row height by implementing your table as a QTreeView + QSortFilterProxyModel subclass pair (PyQt: How Can I set row heights of QTreeView)
Alternatively I imagine removing the widget from the table (removeItemWidget) and then instantly re-adding it would cause a row size recalculation though I have not tested that.
As for my opinions regarding this setup of layouts inside tables, it is worth noting two things:
Qt layouts are slow to populate and resize.
Qt tables with variable row heights are slow in general.
That being said 500 isn't a huge number. I would probably knock together a quick prototype and see how it performs on a range of machines to see if the performance is acceptable and then worry about exploring other routes if it is too slow. I expect performance in this case will be most dependent on how complex the layouts are.

QT, Python, QTreeView, custom widget, setData - lose reference after drag and drop

This question is similar to the one in this this topic Preserve QStandardItem subclasses in drag and drop but with issue that I cant find a good solution for. That topic partially helps but fail on more complex task.
When I create an item in QTreeView I put that item in my array but when I use drag&Drop the item gets deleted and I no longer have access to it. I know that its because drag and drop copies the item and not moves it so I should use setData. I cant setData to be an object because even then the object gets copied and I lose reference to it.
Here is an example
itemsArray = self.addNewRow
def addNewRow(self)
'''some code with more items'''
itemHolder = QStandardItem("ProgressBarItem")
widget = QProgressBar()
itemHolder.setData(widget)
inx = self.model.rowCount()
self.model.setItem(inx, 0, itemIcon)
self.model.setItem(inx, 1, itemName)
self.model.setItem(inx, 2, itemHolder)
ix = self.model.index(inx,2,QModelIndex())
self.treeView.setIndexWidget(ix, widget)
return [itemHolder, itemA, itemB, itemC]
#Simplified functionality
data = [xxx,xxx,xxx]
for items in itemsArray:
items[0].data().setPercentage(data[0])
items[1].data().setText(data[1])
items[2].data().setChecked(data[2])
The code above works if I won't move the widget. The second I drag/drop I lose reference I lose updates on all my items and I get crash.
RuntimeError: wrapped C/C++ object of type QProgressBar has been deleted
The way I can think of of fixing this problem is to loop over entire treeview recursively over each row/child and on name match update item.... Problem is that I will be refreshing treeview every 0.5 second and have 500+ rows with 5-15 items each. Meaning... I don't think that will be very fast/efficient... if I want to loop over 5 000 items every 0.5 second...
Can some one suggest how I could solve this problem? Perhaps I can edit dropEvent so it does not copy/paste item but rather move item.... This way I would not lose my object in array
Qt can only serialize objects that can be stored in a QVariant, so it's no surprise that this won't work with a QWidget. But even if it could serialize widgets, I still don't think it would work, because index-widgets belong to the view, not the model.
Anyway, I think you will have to keep references to the widgets separately, and only store a simple key in the model items. Then once the items are dropped, you can retrieve the widgets and reset them in the view.
Here's a working demo script:
from PyQt4 import QtCore, QtGui
class TreeView(QtGui.QTreeView):
def __init__(self, *args, **kwargs):
super(TreeView, self).__init__(*args, **kwargs)
self.setDragDropMode(QtGui.QAbstractItemView.InternalMove)
self.setSelectionMode(QtGui.QAbstractItemView.ExtendedSelection)
self.setAllColumnsShowFocus(True)
self.setModel(QtGui.QStandardItemModel(self))
self._widgets = {}
self._dropping = False
self._droprange = range(0)
def dropEvent(self, event):
self._dropping = True
super(TreeView, self).dropEvent(event)
for row in self._droprange:
item = self.model().item(row, 2)
self.setIndexWidget(item.index(), self._widgets[item.data()])
self._droprange = range(0)
self._dropping = False
def rowsInserted(self, parent, start, end):
super(TreeView, self).rowsInserted(parent, start, end)
if self._dropping:
self._droprange = range(start, end + 1)
def addNewRow(self, name):
model = self.model()
itemIcon = QtGui.QStandardItem()
pixmap = QtGui.QPixmap(16, 16)
pixmap.fill(QtGui.QColor(name))
itemIcon.setIcon(QtGui.QIcon(pixmap))
itemName = QtGui.QStandardItem(name.title())
itemHolder = QtGui.QStandardItem('ProgressBarItem')
widget = QtGui.QProgressBar()
widget.setValue(5 * (model.rowCount() + 1))
key = id(widget)
self._widgets[key] = widget
itemHolder.setData(key)
model.appendRow([itemIcon, itemName, itemHolder])
self.setIndexWidget(model.indexFromItem(itemHolder), widget)
class Window(QtGui.QWidget):
def __init__(self):
super(Window, self).__init__()
self.treeView = TreeView()
for name in 'red yellow green purple blue orange'.split():
self.treeView.addNewRow(name)
layout = QtGui.QVBoxLayout(self)
layout.addWidget(self.treeView)
if __name__ == '__main__':
import sys
app = QtGui.QApplication(sys.argv)
window = Window()
window.setGeometry(500, 150, 600, 400)
window.show()
sys.exit(app.exec_())

Categories

Resources