I have a rather big project, written in Python 3.4 and PyQt5 5.5.1. After updating to PyQt5 version 5.5.1 from PyQt5 version 5.4.1 I have strange memory leaks, there are no any leaks on 5.4.1 version. I have custom QAbstractTableModel and custom QTableView in my project with delegates for data types. Here is example code, which has memory leaks in lines 98 and 117 (it can be found using tracemalloc). Memory leak occur when you select cells in table.
Why it can be so?
#!/usr/bin/python3
import tracemalloc
tracemalloc.start()
import sys
from PyQt5.QtCore import *
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *
RW = (Qt.ItemIsSelectable | Qt.ItemIsEditable | Qt.ItemIsEnabled)
my_array = [['00','01','02'],
['10','11','12'],
['20','21','22']]
class MyWindow(QWidget):
def __init__(self, *args):
QWidget.__init__(self, *args)
tablemodel = MyTableModel(my_array, self)
tableview = QTableView()
tableview.setModel(tablemodel)
self.cdelegates = []
for i in range(3):
d = RSpinDelegate(self)
self.cdelegates.append(d)
tableview.setItemDelegateForColumn(i, d)
layout = QVBoxLayout(self)
layout.addWidget(tableview)
self.setLayout(layout)
self.startTimer(5000)
def timerEvent(self, e):
snapshot = tracemalloc.take_snapshot()
top_stats = snapshot.statistics('lineno')
print("[ Top 10 ]")
for stat in top_stats[:10]:
print(stat)
class RSpinDelegate(QItemDelegate):
def __init__(self, parent=None, decimals=0, step=1, range_=(0, 1e9), edit=RW, suffix='', colorfill=None, stepFilter=None):
super(RSpinDelegate, self).__init__(parent)
self.decimals = decimals
self.step = step
self.range_ = range_
self.edit = edit
self.suffix = suffix
self.colorfill = colorfill
self.stepFilter = stepFilter
def setDecimals(self, decimals):
self.decimals = decimals
def createEditor(self, parent, option, index):
if self.edit == RW:
if self.decimals:
decimals = self.decimals
dec = int(index.model().data(index, RBaseTableModel.DECIMALS_ROLE))
decimals = dec
d = 10 ** (-decimals)
editor = RDoubleSpinBox(parent)
if self.stepFilter != None:
editor.installEventFilter(self.stepFilter)
editor.setSingleStep(d)
editor.setDecimals(decimals)
editor.setRange(self.range_[0], self.range_[1])
editor.setSuffix(self.suffix)
self._editor = editor
return editor
else:
editor = RSpinBox(parent)
if self.stepFilter != None:
editor.installEventFilter(self.stepFilter)
editor.setSingleStep(self.step)
editor.setRange(self.range_[0], self.range_[1])
editor.setSuffix(self.suffix)
self._editor = editor
return editor
return None
return None
def setEditorData(self, editor, index):
val = index.model().data(index, Qt.EditRole)
try:
editor.setValue(float(val.replace(' ', '')) if self.decimals != 0 else int(val.replace(' ', '')))
except:
editor.setValue(editor.minimum())
def setModelData(self, editor, model, index):
model.setData(index, editor.value(), Qt.EditRole)
def getBrush(self, option):
brush = option.palette.base()
if option.state & QStyle.State_Selected:# memory leak is here!
if option.state & QStyle.State_Active:
brush = option.palette.highlight()
else:
brush = option.palette.light()
return brush
def updateEditorGeometry(self, editor, option, index):
editor.setGeometry(option.rect)
def paint(self, painter, option, index):
opt = QStyleOptionViewItem(option)
if self.colorfill:
brush = self.colorfill(index.model().data(index, RBaseTableModel.INDEX_ROLE), option)
if not(option.state & QStyle.State_Selected):
painter.fillRect(option.rect, brush)
opt.palette.setBrush(QPalette.Highlight, brush)
else:
brush = self.getBrush(option)
painter.fillRect(option.rect, brush)# memory leak is here!
super(RSpinDelegate, self).paint(painter, opt, index)
# création du modèle
class MyTableModel(QAbstractTableModel):
refreshTable = pyqtSignal()
def __init__(self, datain, parent = None, *args):
QAbstractTableModel.__init__(self, parent, *args)
self.arraydata = datain
self.timer = self.startTimer(300)
def timerEvent(self, e):
if self.timer == e.timerId():
self.refreshTable.emit()
else:
super(RBaseTableView, self).timerEvent(e)
def refreshTableSlot(self):
self.layoutAboutToBeChanged.emit()
self.layoutChanged.emit()
def rowCount(self, parent):
return len(self.arraydata)
def columnCount(self, parent):
return len(self.arraydata[0])
def data(self, index, role):
if not index.isValid():
return None
elif role != Qt.DisplayRole:
return None
return (self.arraydata[index.row()][index.column()])
if __name__ == "__main__":
app = QApplication(sys.argv)
w = MyWindow()
w.show()
sys.exit(app.exec_())
Related
I am new to Qt. Currently I am trying to learn how to update a table model from a different thread and then how to get an immediate display update for it. I read the documentation and found the dataChanged() and layoutChanged() signals. While dataChanged() works fine, any attempt to emit layoutChanged() fails with:
'QObject::connect: Cannot queue arguments of type 'QList<QPersistentModelIndex>' (Make sure 'QList<QPersistentModelIndex>' is registered using qRegisterMetaType().)
Searching for this particular error didn't give me anything that I could turn into working code. I am not using any QList or QPersistentModelIndex explicitly, but of course that can be implicitly used due to the constructs that I chose.
What am I doing wrong?
class TimedModel(QtCore.QAbstractTableModel):
def __init__(self, table, view):
super(TimedModel, self).__init__()
self.table = table
self.view = view
self.setHeaderData(0, Qt.Horizontal, Qt.AlignLeft, Qt.TextAlignmentRole)
self.rows = 6
self.columns = 4
self.step = 5
self.timer = Thread(
name = "Timer",
target = self.tableTimer,
daemon = True)
self.timer.start()
self.random = Random()
self.updated = set()
#staticmethod
def encode(row, column):
return row << 32 | column
def data(self, index, role):
if role == Qt.DisplayRole or role == Qt.EditRole:
return f'Data-{index.row()}-{index.column()}'
if role == Qt.ForegroundRole:
encoded = TimedModel.encode(index.row(), index.column())
return QBrush(Qt.red if encoded in self.updated else Qt.black)
return None
def rowCount(self, index):
return self.rows
def columnCount(self, index):
return self.columns
def headerData(self, col, orientation, role):
if orientation == Qt.Vertical:
# Vertical
return super().headerData(col, orientation, role)
# Horizontal
if not 0 <= col < self.columns:
return None
if role == Qt.DisplayRole:
return f'Data-{col}'
if role == Qt.TextAlignmentRole:
return int(Qt.AlignLeft | Qt.AlignVCenter)
return super().headerData(col, orientation, role)
def tableTimer(self):
while True:
time.sleep(5.0)
randomRow = self.random.randint(0, self.rows)
randomColumn = self.random.randint(0, self.columns)
encodedRandom = TimedModel.encode(randomRow, randomColumn)
if encodedRandom in self.updated:
self.updated.remove(encodedRandom)
else:
self.updated.add(encodedRandom)
updatedIndex = self.createIndex(randomRow, randomColumn)
self.dataChanged.emit(updatedIndex, updatedIndex)
'''this here does not work:'''
self.layoutAboutToBeChanged.emit()
self.rows += self.step
self.layoutChanged.emit()
class MainWindow(QtWidgets.QMainWindow):
def __init__(self):
QtWidgets.QMainWindow.__init__(self)
self.timedTable = QTableView()
self.model = TimedModel(self.timedTable, self)
self.timedTable.setModel(self.model)
headerView = self.timedTable.horizontalHeader()
headerView.setStretchLastSection(True)
self.setCentralWidget(self.timedTable)
self.setGeometry(300, 300, 1000, 600)
self.setWindowTitle('Timed Table')
self.show()
if __name__ == '__main__':
app = QtWidgets.QApplication(sys.argv)
app.name = "Timed Table Application"
window = MainWindow()
window.show()
app.exec_()
The following code:
self.layoutAboutToBeChanged.emit()
self.rows += self.step
self.layoutChanged.emit()
create new model elements that have QPersistentModelIndex associated that are not thread-safe and that Qt monitors its creation to warn its misuse as in this case since modifying that element is unsafe since it implies modifying the GUI from another thread (Read here for more information).
So you see that message warning that what you are trying to do is unsafe.
Instead dataChanged only emits a signal, does not create any element belonging to Qt, and you have been lucky that the modification of "self.updated" has not generated bottlenecks since you modify a property that belongs to the main thread from a secondary thread without use guards as mutexes.
Qt points out that the GUI and the elements that the GUI uses should only be updated in the GUI thread, and if you want to modify the GUI with information from another thread, then you must send that information, for example, using the signals that are thread- safe:
import random
import sys
import threading
import time
from PySide2 import QtCore, QtGui, QtWidgets
class TimedModel(QtCore.QAbstractTableModel):
random_signal = QtCore.Signal(object)
def __init__(self, table, view):
super(TimedModel, self).__init__()
self.table = table
self.view = view
self.setHeaderData(
0, QtCore.Qt.Horizontal, QtCore.Qt.AlignLeft, QtCore.Qt.TextAlignmentRole
)
self.rows = 6
self.columns = 4
self.step = 5
self.updated = set()
self.random_signal.connect(self.random_slot)
self.timer = threading.Thread(name="Timer", target=self.tableTimer, daemon=True)
self.timer.start()
#staticmethod
def encode(row, column):
return row << 32 | column
def data(self, index, role):
if role in (QtCore.Qt.DisplayRole, QtCore.Qt.EditRole):
return f"Data-{index.row()}-{index.column()}"
if role == QtCore.Qt.ForegroundRole:
encoded = TimedModel.encode(index.row(), index.column())
return QtGui.QBrush(
QtCore.Qt.red if encoded in self.updated else QtCore.Qt.black
)
return None
def rowCount(self, index):
return self.rows
def columnCount(self, index):
return self.columns
def headerData(self, col, orientation, role):
if orientation == QtCore.Qt.Vertical:
# Vertical
return super().headerData(col, orientation, role)
# Horizontal
if not 0 <= col < self.columns:
return None
if role == QtCore.Qt.DisplayRole:
return f"Data-{col}"
if role == QtCore.Qt.TextAlignmentRole:
return QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter
return super().headerData(col, orientation, role)
def tableTimer(self):
while True:
time.sleep(5.0)
randomRow = random.randint(0, self.rows)
randomColumn = random.randint(0, self.columns)
encodedRandom = TimedModel.encode(randomRow, randomColumn)
self.random_signal.emit(encodedRandom)
#QtCore.Slot(object)
def random_slot(self, encodedRandom):
if encodedRandom in self.updated:
self.updated.remove(encodedRandom)
else:
self.updated.add(encodedRandom)
self.layoutAboutToBeChanged.emit()
self.rows += self.step
self.layoutChanged.emit()
class MainWindow(QtWidgets.QMainWindow):
def __init__(self, parent=None):
super().__init__(parent)
self.timedTable = QtWidgets.QTableView()
self.model = TimedModel(self.timedTable, self)
self.timedTable.setModel(self.model)
headerView = self.timedTable.horizontalHeader()
headerView.setStretchLastSection(True)
self.setCentralWidget(self.timedTable)
self.setGeometry(300, 300, 1000, 600)
self.setWindowTitle("Timed Table")
self.show()
if __name__ == "__main__":
app = QtWidgets.QApplication(sys.argv)
app.name = "Timed Table Application"
window = MainWindow()
window.show()
app.exec_()
I am looking for a widget which is a list box, and one entry has different attributes represented by column names.
I want to be able to select multiple entries from the list using a check box on the left side.
Please, also tell me how to configure it. Thanks.
The solution in this case is to use a QTableView (customizing some aspects like the header checkbox):
import sys
from PyQt5 import QtCore, QtGui, QtWidgets
class HeaderView(QtWidgets.QHeaderView):
checked = QtCore.pyqtSignal(bool)
def __init__(self, orientation, parent=None):
super().__init__(orientation, parent)
self._checkable_column = -1
self._state = False
self._column_down = -1
#property
def checkable_column(self):
return self._checkable_column
#checkable_column.setter
def checkable_column(self, c):
self._checkable_column = c
#property
def state(self):
return self._state
#state.setter
def state(self, c):
if self.checkable_column == -1:
return
self._state = c
self.checked.emit(c)
self.updateSection(self.checkable_column)
def paintSection(self, painter, rect, logicalIndex):
painter.save()
super().paintSection(painter, rect, logicalIndex)
painter.restore()
if logicalIndex != self.checkable_column:
return
opt = QtWidgets.QStyleOptionButton()
checkbox_rect = self.style().subElementRect(
QtWidgets.QStyle.SE_CheckBoxIndicator, opt, None
)
checkbox_rect.moveCenter(rect.center())
opt.rect = checkbox_rect
opt.state = QtWidgets.QStyle.State_Enabled | QtWidgets.QStyle.State_Active
if logicalIndex == self._column_down:
opt.state |= QtWidgets.QStyle.State_Sunken
opt.state |= (
QtWidgets.QStyle.State_On if self.state else QtWidgets.QStyle.State_Off
)
self.style().drawPrimitive(QtWidgets.QStyle.PE_IndicatorCheckBox, opt, painter)
def mousePressEvent(self, event):
super().mousePressEvent(event)
li = self.logicalIndexAt(event.pos())
self._column_down = li
self.updateSection(li)
def mouseReleaseEvent(self, event):
super().mouseReleaseEvent(event)
li = self.logicalIndexAt(event.pos())
self._column_down = -1
if li == self.checkable_column:
self.state = not self.state
class Dialog(QtWidgets.QDialog):
def __init__(self, parent=None):
super().__init__(parent)
self.model = QtGui.QStandardItemModel(0, 4, self)
self.model.setHorizontalHeaderLabels(["", "First Name", "Last Name", "Company"])
for state, firstname, lastname, company in (
(True, "Larry", "Ellison", "Oracle"),
(True, "Steve", "Jobs", "Apple"),
(True, "Steve", "Ballmer", "Microsoft"),
(True, "Bill", "Gates", "Microsoft"),
):
it_state = QtGui.QStandardItem()
it_state.setEditable(False)
it_state.setCheckable(True)
it_state.setCheckState(QtCore.Qt.Checked if state else QtCore.Qt.UnChecked)
it_firstname = QtGui.QStandardItem(firstname)
it_lastname = QtGui.QStandardItem(lastname)
it_company = QtGui.QStandardItem(company)
self.model.appendRow([it_state, it_firstname, it_lastname, it_company])
self.view = QtWidgets.QTableView(
showGrid=False, selectionBehavior=QtWidgets.QAbstractItemView.SelectRows
)
self.view.setModel(self.model)
headerview = HeaderView(QtCore.Qt.Horizontal, self.view)
headerview.checkable_column = 0
headerview.checked.connect(self.change_state_of_model)
self.view.setHorizontalHeader(headerview)
self.view.verticalHeader().hide()
self.view.horizontalHeader().setMinimumSectionSize(0)
self.view.horizontalHeader().setSectionResizeMode(
0, QtWidgets.QHeaderView.ResizeToContents
)
self.view.horizontalHeader().setStretchLastSection(True)
self.box = QtWidgets.QDialogButtonBox(
QtWidgets.QDialogButtonBox.Ok
| QtWidgets.QDialogButtonBox.Cancel
| QtWidgets.QDialogButtonBox.NoButton
| QtWidgets.QDialogButtonBox.Help,
QtCore.Qt.Vertical,
)
hlay = QtWidgets.QHBoxLayout(self)
hlay.addWidget(self.view)
hlay.addWidget(self.box)
self.box.accepted.connect(self.accept)
self.box.rejected.connect(self.reject)
self.box.helpRequested.connect(self.on_helpRequested)
self.resize(500, 240)
#QtCore.pyqtSlot()
def on_helpRequested(self):
QtWidgets.QMessageBox.aboutQt(self)
#QtCore.pyqtSlot(bool)
def change_state_of_model(self, state):
for i in range(self.model.rowCount()):
it = self.model.item(i)
if it is not None:
it.setCheckState(QtCore.Qt.Checked if state else QtCore.Qt.Unchecked)
if __name__ == "__main__":
app = QtWidgets.QApplication(sys.argv)
w = Dialog()
w.show()
sys.exit(app.exec_())
I am using PySide2 and I cant find any documentation on how to use the paint() function in a QStyledItemDelegate subclass. I am rather new to classes but is so far understandable but having trouble with PySide2.
I would like to replace my QtWidgets.QListWidgetItem with my own ListWidgetItem and display them correctly, like this:
So on the left of the ListWidgetItem an icon a bit to the right the name of the ListWidgetItem and underneath the description.
Here is the code:
from PySide2 import QtWidgets, QtCore, QtGui
from PySide2.QtGui import *
import sys
class MainWindow(QtWidgets.QMainWindow):
def __init__(self, parent=None):
super(MainWindow, self).__init__()
self.setWindowTitle('Test Window')
self.setStyleSheet("background-color: rgb(65, 65, 65);")
mainWidget = QtWidgets.QWidget(self)
self.setCentralWidget(mainWidget)
self.boxLayout = QtWidgets.QVBoxLayout()
mainWidget.setLayout(self.boxLayout)
# Add Widgets
self.textField = QtWidgets.QLineEdit()
self.listView = QtWidgets.QListWidget()
self.textField.textChanged.connect(self.onTextChanged)
self.boxLayout.addWidget(self.textField)
self.boxLayout.addWidget(self.listView)
self.textField.setFocus()
def onTextChanged(self, ):
titles = ['Monkey', 'Giraffe', 'Dragon', 'Bull']
descriptions = ['Almost a homo sapiens sapiens', 'I am a Giraffe!', 'Can fly and is hot on spices', 'Horny...']
if self.textField.text() == '' or self.textField.text().isspace() or self.textField.text() == ' ':
if self.listView.count() > 0:
self.listView.clear()
else:
if self.listView.count() > 0:
self.listView.clear()
for x in range(len(titles)):
if self.textField.text() in titles[x]:
item = ListWidgetItem(titles[x])
self.listView.addItem(item)
self.listView.setCurrentRow(0)
continue
class ListWidgetItem(QtWidgets.QListWidgetItem):
def __init__(self, title = '', description = '', icon = QtGui.QIcon()):
super(ListWidgetItem, self).__init__()
self.title = title
self.description = description
self.icon = icon
class ListViewStyle(QtWidgets.QStyledItemDelegate):
def __init__(self, parent, itemModel):
super(ListViewStyle, self).__init__(parent)
self.itemModel = itemModel
def sizeHint(self, option, index):
if index:
return QtCore.QSize(40, 40)
def paint(self, painter, option, index):
super(ListViewStyle, self).paint(painter, option, index)
if __name__ == '__main__':
app = QtWidgets.QApplication.instance()
if app is None:
app = QtWidgets.QApplication(sys.argv)
w = MainWindow()
w.show()
#sys.exit(app.exec_())
Info: In onTextChanged() the ListWidgetItem will be added to the QListWidget but not drawn correctly, basically empty.
Does QListWidgetItem have any notable difference to QListView?
The delegate only paints and is not interested in what element provides the information since that class uses the QModelIndex and the same model to obtain the information, so in my previous solution I used a QStandardItemModel that uses QStandardItem and in your current case a QListWidget with QListWidgetItem is indifferent. My delegate expects only that the information of the title, description and icon are related to TitleRole, DescriptionRole and IconRole, respectively.
On the other hand it is not good to delete the items but it is better to hide or make them visible when necessary.
Considering the above, the solution with QListWidget is as follows:
import sys
from PySide2 import QtWidgets, QtCore, QtGui
TitleRole = QtCore.Qt.UserRole + 1000
DescriptionRole = QtCore.Qt.UserRole + 1001
IconRole = QtCore.Qt.UserRole + 1002
class ListWidgetItem(QtWidgets.QListWidgetItem):
def __init__(self, title="", description="", icon=QtGui.QIcon()):
super(ListWidgetItem, self).__init__()
self.title = title
self.description = description
self.icon = icon
#property
def title(self):
return self.data(TitleRole)
#title.setter
def title(self, title):
self.setData(TitleRole, title)
#property
def description(self):
return self.data(DescriptionRole)
#description.setter
def description(self, description):
self.setData(DescriptionRole, description)
#property
def icon(self):
return self.data(IconRole)
#icon.setter
def icon(self, icon):
self.setData(IconRole, icon)
class StyledItemDelegate(QtWidgets.QStyledItemDelegate):
def sizeHint(self, option, index):
return QtCore.QSize(50, 50)
def paint(self, painter, option, index):
super(StyledItemDelegate, self).paint(painter, option, index)
title = index.data(TitleRole)
description = index.data(DescriptionRole)
icon = index.data(IconRole)
mode = QtGui.QIcon.Normal
if not (option.state & QtWidgets.QStyle.State_Enabled):
mode = QtGui.QIcon.Disabled
elif option.state & QtWidgets.QStyle.State_Selected:
mode = QtGui.QIcon.Selected
state = (
QtGui.QIcon.On
if option.state & QtWidgets.QStyle.State_Open
else QtGui.QIcon.Off
)
iconRect = QtCore.QRect(option.rect)
iconRect.setSize(QtCore.QSize(40, 40))
icon.paint(
painter, iconRect, QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter, mode, state
)
titleFont = QtGui.QFont(option.font)
titleFont.setPixelSize(20)
fm = QtGui.QFontMetrics(titleFont)
titleRect = QtCore.QRect(option.rect)
titleRect.setLeft(iconRect.right())
titleRect.setHeight(fm.height())
color = (
option.palette.color(QtGui.QPalette.BrightText)
if option.state & QtWidgets.QStyle.State_Selected
else option.palette.color(QtGui.QPalette.WindowText)
)
painter.save()
painter.setFont(titleFont)
pen = painter.pen()
pen.setColor(color)
painter.setPen(pen)
painter.drawText(titleRect, title)
painter.restore()
descriptionFont = QtGui.QFont(option.font)
descriptionFont.setPixelSize(15)
fm = QtGui.QFontMetrics(descriptionFont)
descriptionRect = QtCore.QRect(option.rect)
descriptionRect.setTopLeft(titleRect.bottomLeft())
descriptionRect.setHeight(fm.height())
painter.save()
painter.setFont(descriptionFont)
pen = painter.pen()
pen.setColor(color)
painter.setPen(pen)
painter.drawText(
descriptionRect,
fm.elidedText(description, QtCore.Qt.ElideRight, descriptionRect.width()),
)
painter.restore()
class MainWindow(QtWidgets.QMainWindow):
def __init__(self, parent=None):
super(MainWindow, self).__init__()
self.setWindowTitle("Test Window")
self.setStyleSheet("background-color: rgb(65, 65, 65);")
mainWidget = QtWidgets.QWidget(self)
self.setCentralWidget(mainWidget)
self.boxLayout = QtWidgets.QVBoxLayout()
mainWidget.setLayout(self.boxLayout)
# Add Widgets
self.textField = QtWidgets.QLineEdit()
self.listView = QtWidgets.QListWidget()
self.textField.textChanged.connect(self.onTextChanged)
self.boxLayout.addWidget(self.textField)
self.boxLayout.addWidget(self.listView)
self.fill_model()
self.textField.setFocus()
self.listView.setItemDelegate(StyledItemDelegate(self))
def fill_model(self):
titles = ["Monkey", "Giraffe", "Dragon", "Bull"]
descriptions = [
"Almost a homo sapiens sapiens",
"I am a Giraffe!",
"Can fly and is hot on spices",
"Horny...",
]
for title, description in zip(titles, descriptions):
it = ListWidgetItem(title=title, description=description)
self.listView.addItem(it)
#QtCore.Slot(str)
def onTextChanged(self, text):
text = text.strip()
if text:
for i in range(self.listView.count()):
it = self.listView.item(i)
if it is not None:
it.setHidden(text.lower() not in it.title.lower())
else:
for i in range(self.listView.count()):
it = self.listView.item(i)
if it is not None:
it.setHidden(False)
if __name__ == "__main__":
app = QtWidgets.QApplication.instance()
if app is None:
app = QtWidgets.QApplication(sys.argv)
w = MainWindow()
w.show()
sys.exit(app.exec_())
I want to search a QTableWidget-Table by a list of words, if they've been found i want them to bee highlighted.
I tried to modify the code from here so the table is beeing searched by a list of words, not just one. Unfortunatly my results keep getting overwritten. I always only get the result for the last word in the list.
Does anyone know how to modify the code so it will show the result of the whole list of words ?
Here is the code:
from PyQt5 import QtCore, QtGui, QtWidgets
import random
import html
words_1 = ["Hello dseerfd", "world sdfsdf sdfgsdf sdfsdf", "Stack dasdf", "Overflow", "Hello world", """<font color="red">Hello world</font>"""]
class HTMLDelegate(QtWidgets.QStyledItemDelegate):
def __init__(self, parent=None):
super(HTMLDelegate, self).__init__(parent)
self.doc = QtGui.QTextDocument(self)
def paint(self, painter, option, index):
substring = index.data(QtCore.Qt.UserRole)
painter.save()
options = QtWidgets.QStyleOptionViewItem(option)
self.initStyleOption(options, index)
res = ""
color = QtGui.QColor("red")
if substring:
substrings = options.text.split(substring)
res = """<font color="{}">{}</font>""".format(color.name(QtGui.QColor.HexRgb), substring).join(list(map(html.escape, substrings)))
else:
res = html.escape(options.text)
self.doc.setHtml(res)
options.text = ""
style = QtWidgets.QApplication.style() if options.widget is None \
else options.widget.style()
style.drawControl(QtWidgets.QStyle.CE_ItemViewItem, options, painter)
ctx = QtGui.QAbstractTextDocumentLayout.PaintContext()
if option.state & QtWidgets.QStyle.State_Selected:
ctx.palette.setColor(QtGui.QPalette.Text, option.palette.color(
QtGui.QPalette.Active, QtGui.QPalette.HighlightedText))
else:
ctx.palette.setColor(QtGui.QPalette.Text, option.palette.color(
QtGui.QPalette.Active, QtGui.QPalette.Text))
textRect = style.subElementRect(
QtWidgets.QStyle.SE_ItemViewItemText, options)
if index.column() != 0:
textRect.adjust(5, 0, 0, 0)
the_constant = 4
margin = (option.rect.height() - options.fontMetrics.height()) // 2
margin = margin - the_constant
textRect.setTop(textRect.top() + margin)
painter.translate(textRect.topLeft())
painter.setClipRect(textRect.translated(-textRect.topLeft()))
self.doc.documentLayout().draw(painter, ctx)
painter.restore()
class Widget(QtWidgets.QWidget):
def __init__(self, parent=None):
super(Widget, self).__init__(parent)
hlay = QtWidgets.QHBoxLayout()
lay = QtWidgets.QVBoxLayout(self)
self.table = QtWidgets.QTableWidget(5, 5)
lay.addLayout(hlay)
lay.addWidget(self.table)
self.table.setItemDelegate(HTMLDelegate(self.table))
for i in range(self.table.rowCount()):
for j in range(self.table.columnCount()):
it = QtWidgets.QTableWidgetItem(random.choice(words_1))
self.table.setItem(i, j, it)
text_list = ['ello', 'ack']
# clear
allitems = self.table.findItems("", QtCore.Qt.MatchContains)
selected_items =[]
for words in text_list:
for item in allitems:
selected_items = self.table.findItems(words, QtCore.Qt.MatchContains)
selected_items.append(self.table.findItems(words, QtCore.Qt.MatchContains)) ## i tried to make a list which is beeing appened but using this list it returns only the same as the input
item.setData(QtCore.Qt.UserRole, words if item in selected_items else None)
if __name__ == '__main__':
import sys
app = QtWidgets.QApplication(sys.argv)
w = Widget()
w.show()
sys.exit(app.exec_())
In the previous case I wanted to filter the cases so as not to have to paint unnecessarily but in this case because it was more complex I decided to implement the highlight logic using QTextCharFormat and not HTML as I show below:
from PyQt5 import QtCore, QtGui, QtWidgets
import random
words = ["Hello dseerfd", "world sdfsdf sdfgsdf sdfsdf", "Stack dasdf", "Overflow", "Hello world", """<font color="red">Hello world</font>"""]
class HighlightDelegate(QtWidgets.QStyledItemDelegate):
def __init__(self, parent=None):
super(HighlightDelegate, self).__init__(parent)
self.doc = QtGui.QTextDocument(self)
self._filters = []
def paint(self, painter, option, index):
painter.save()
options = QtWidgets.QStyleOptionViewItem(option)
self.initStyleOption(options, index)
self.doc.setPlainText(options.text)
self.apply_highlight()
options.text = ""
style = QtWidgets.QApplication.style() if options.widget is None \
else options.widget.style()
style.drawControl(QtWidgets.QStyle.CE_ItemViewItem, options, painter)
ctx = QtGui.QAbstractTextDocumentLayout.PaintContext()
if option.state & QtWidgets.QStyle.State_Selected:
ctx.palette.setColor(QtGui.QPalette.Text, option.palette.color(
QtGui.QPalette.Active, QtGui.QPalette.HighlightedText))
else:
ctx.palette.setColor(QtGui.QPalette.Text, option.palette.color(
QtGui.QPalette.Active, QtGui.QPalette.Text))
textRect = style.subElementRect(
QtWidgets.QStyle.SE_ItemViewItemText, options)
if index.column() != 0:
textRect.adjust(5, 0, 0, 0)
the_constant = 4
margin = (option.rect.height() - options.fontMetrics.height()) // 2
margin = margin - the_constant
textRect.setTop(textRect.top() + margin)
painter.translate(textRect.topLeft())
painter.setClipRect(textRect.translated(-textRect.topLeft()))
self.doc.documentLayout().draw(painter, ctx)
painter.restore()
def apply_highlight(self):
cursor = QtGui.QTextCursor(self.doc)
cursor.beginEditBlock()
fmt = QtGui.QTextCharFormat()
fmt.setForeground(QtCore.Qt.red)
for f in self.filters():
highlightCursor = QtGui.QTextCursor(self.doc)
while not highlightCursor.isNull() and not highlightCursor.atEnd():
highlightCursor = self.doc.find(f, highlightCursor)
if not highlightCursor.isNull():
highlightCursor.mergeCharFormat(fmt)
cursor.endEditBlock()
#QtCore.pyqtSlot(list)
def setFilters(self, filters):
if self._filters == filters: return
self._filters = filters
def filters(self):
return self._filters
class Widget(QtWidgets.QWidget):
def __init__(self, parent=None):
super(Widget, self).__init__(parent)
self.table = QtWidgets.QTableWidget(30, 6)
self._delegate = HighlightDelegate(self.table)
self.table.setItemDelegate(self._delegate)
for i in range(self.table.rowCount()):
for j in range(self.table.columnCount()):
it = QtWidgets.QTableWidgetItem(random.choice(words))
self.table.setItem(i, j, it)
self.table.horizontalHeader().setSectionResizeMode(QtWidgets.QHeaderView.ResizeToContents)
le = QtWidgets.QLineEdit()
le.textChanged.connect(self.on_textChanged)
lay = QtWidgets.QVBoxLayout(self)
lay.addWidget(le)
lay.addWidget(self.table)
le.setText("ello ack")
#QtCore.pyqtSlot(str)
def on_textChanged(self, text):
self._delegate.setFilters(list(set(text.split())))
self.table.viewport().update()
if __name__ == '__main__':
import sys
app = QtWidgets.QApplication(sys.argv)
w = Widget()
w.showMaximized()
sys.exit(app.exec_())
I really like a clean result of self-adjusting to the QTableView's width of the columns using:
self.view.horizontalHeader().setResizeMode(QHeaderView.Stretch)
But unfortunately with this flag used the columns width doesn't stay adjustable any longer (the user is not able to resize the columns width).
I wonder if there is an alternative way to set the columns width to the width of the QTableView and yet keeping the columns width "user-adjustable"?
from PyQt4.QtCore import *
from PyQt4.QtGui import *
import sys
class Model(QAbstractTableModel):
def __init__(self, parent=None, *args):
QAbstractTableModel.__init__(self, parent, *args)
self.items = ['Item_A_001','Item_A_002','Item_B_001','Item_B_002']
self.totalColumn=10
def rowCount(self, parent=QModelIndex()):
return len(self.items)
def columnCount(self, parent=QModelIndex()):
return self.totalColumn
def setColumnCount(self, number):
self.totalColumn=number
self.reset()
def data(self, index, role):
if not index.isValid(): return QVariant()
elif role != Qt.DisplayRole:
return QVariant()
row=index.row()
if row<len(self.items):
return QVariant(self.items[row])
else:
return QVariant()
class MyWindow(QWidget):
def __init__(self, *args):
QWidget.__init__(self, *args)
tableModel=Model(self)
self.view=QTableView(self)
self.view.setModel(tableModel)
self.view.horizontalHeader().setResizeMode(QHeaderView.Stretch)
hideButton=QPushButton('Hide Column')
hideButton.clicked.connect(self.hideColumn)
unhideButton=QPushButton('Unhide Column')
unhideButton.clicked.connect(self.unhideColumn)
layout = QVBoxLayout(self)
layout.addWidget(self.view)
layout.addWidget(hideButton)
layout.addWidget(unhideButton)
self.setLayout(layout)
def hideColumn(self):
self.view.model().setColumnCount(1)
def unhideColumn(self):
self.view.model().setColumnCount(10)
if __name__ == "__main__":
app = QApplication(sys.argv)
w = MyWindow()
w.show()
sys.exit(app.exec_())
so here is a little trick I came up with. I modified the resize event of MyWindow to resize your QTableWidget.
class MyWindow(QWidget):
def __init__(self, *args):
QWidget.__init__(self, *args)
self.tableModel=Model(self) #Set the model as part of your class to access it in the event handler
self.view=QTableView(self)
self.view.setModel(self.tableModel) #Modified here too
self.view.horizontalHeader().setResizeMode(QHeaderView.Interactive) #Mode set to Interactive to allow resizing
hideButton=QPushButton('Hide Column')
hideButton.clicked.connect(self.hideColumn)
unhideButton=QPushButton('Unhide Column')
unhideButton.clicked.connect(self.unhideColumn)
layout = QVBoxLayout(self)
layout.addWidget(self.view)
layout.addWidget(hideButton)
layout.addWidget(unhideButton)
self.setLayout(layout)
def hideColumn(self):
self.view.model().setColumnCount(1)
def unhideColumn(self):
self.view.model().setColumnCount(10)
#Added a reimplementation of the resize event
def resizeEvent(self, event):
tableSize = self.view.width() #Retrieves your QTableView width
sideHeaderWidth = self.view.verticalHeader().width() #Retrieves the left header width
tableSize -= sideHeaderWidth #Perform a substraction to only keep all the columns width
numberOfColumns = self.tableModel.columnCount() #Retrieves the number of columns
for columnNum in range( self.tableModel.columnCount()): #For each column
self.view.setColumnWidth(columnNum, int(tableSize/numberOfColumns) ) #Set the width = tableSize / nbColumns
super(MyWindow, self).resizeEvent(event) #Restores the original behaviour of the resize event
The only downfall is that the scrollbar is flicking. I think this can be solved with some adjustment.
Update:
Get a cleaner look when resizing, no more flicking, disabled the scrollbar.
Modified initialization of the QTableView:
self.view=QTableView(self)
self.view.setModel(self.tableModel)
self.view.horizontalHeader().setResizeMode(QHeaderView.Interactive)
#Added these two lines
self.view.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
self.view.horizontalHeader().setStretchLastSection(True)
Modified resizeEvent:
def resizeEvent(self, event):
super(MyWindow, self).resizeEvent(event)
tableSize = self.view.width()
sideHeaderWidth = self.view.verticalHeader().width()
tableSize -= sideHeaderWidth
numberOfColumns = self.tableModel.columnCount()
remainingWidth = tableSize % numberOfColumns
for columnNum in range( self.tableModel.columnCount()):
if remainingWidth > 0:
self.view.setColumnWidth(columnNum, int(tableSize/numberOfColumns) + 1 )
remainingWidth -= 1
else:
self.view.setColumnWidth(columnNum, int(tableSize/numberOfColumns) )