Issue
I want to create a GUI that lists images in a directory along with a description for the images. So far I've been able to create a custom delegate for a QListView
to draw the image onto each list item, but any description longer than the designated size of a list item is truncated, how do I have it be scrollable? And also preferably selectable.
My code so far:
import os
from typing import Union
from PySide6 import QtWidgets as qtw
from PySide6 import QtGui as qtg
from PySide6 import QtCore as qtc
ITEMS_SPACING = 10
THUMBNAIL_SIZE = (200, 200)
class Delegate(qtw.QAbstractItemDelegate):
def paint(
self,
painter: qtg.QPainter,
option: qtw.QStyleOptionViewItem,
index: qtc.QModelIndex
) -> None:
thumbnail: qtg.QImage = index.model().data(
index, qtc.Qt.ItemDataRole.DecorationRole
)
description: str = index.model().data(
index, qtc.Qt.ItemDataRole.DisplayRole
)
if thumbnail is None:
return
old_painter = painter.device()
painter.drawImage(
qtc.QRect(
option.rect.left(),
option.rect.top(),
*THUMBNAIL_SIZE
),
thumbnail
)
painter.end()
text_edit = qtw.QPlainTextEdit(self.parent())
text_edit.setPlainText(description)
text_edit.setFixedSize(
option.rect.width() - THUMBNAIL_SIZE[0] - (ITEMS_SPACING * 2),
THUMBNAIL_SIZE[1]
)
text_edit.render(
painter.device(),
qtc.QPoint(
option.rect.left() + THUMBNAIL_SIZE[0] + 20,
option.rect.top()
)
)
painter.begin(old_painter)
def sizeHint(
self, option: qtw.QStyleOptionViewItem, index: qtc.QModelIndex
) -> int:
return qtc.QSize(*THUMBNAIL_SIZE)
class Model(qtc.QAbstractListModel):
def __init__(self, *args, **kwargs) -> None:
super().__init__(*args, **kwargs)
self._thumbnails = [
qtg.QImage(filename) for filename in os.listdir('.')
]
self._descriptions = ["this is text" * 5000 for _ in range(len(self._thumbnails))]
def rowCount(self, _: qtc.QModelIndex) -> int:
return len(self._thumbnails)
def data(
self, index: qtc.QModelIndex, role: qtc.Qt.ItemDataRole
) -> Union[int, None]:
if not index.isValid():
return None
if role == qtc.Qt.ItemDataRole.DisplayRole:
return self._descriptions[index.row()]
elif role == qtc.Qt.ItemDataRole.DecorationRole:
return self._thumbnails[index.row()]
return None
class MainWindow(qtw.QMainWindow):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
delegate = Delegate(self)
self._model = Model()
self._view = qtw.QListView(self)
self._view.setSpacing(ITEMS_SPACING)
self._view.setModel(self._model)
self._view.setItemDelegate(delegate)
self.setCentralWidget(self._view)
self.show()
app = qtw.QApplication()
mw = MainWindow()
app.exec()
Edit:
Why the code doesn't work:
I can't scroll the rendered QTextEdit
nor can I select the text. It's basically the same as painter.drawText
but with a scroll bar drawn below the text. Further more, in my actual code I can't get the QTextEdit
to align with the images, even though it has the same code as the one provided above. Also, the QTextArea
(s) seemingly randomly disappear/reappear following a paint event.
Solution
One possible solution is to use a delegate that will "scroll" the text whenever necessary, and also listen for mouse events that will eventually update the "hovered" index (if any) by scrolling its contents.
Note that we cannot just use the editorEvent()
of the delegate, because it can only track standard mouse events (press, release, move), which means that we cannot be notified whenever the mouse leaves an index for an empty area of the view.
So, we first need to install a separate a custom QObject that acts as filter on the viewport (and enable mouse tracking) and emits a signal whenever the hovered index changes (including invalid indexes - aka, the viewport).
Then, using a couple of timers, we update that index: the first timer is to delay the scrolling just a bit (so that it doesn't start scrolling unnecessarily when we move between many items), while the second is the actual scrolling timer.
Finally, the painting function actually draws an empty item based on the current style, then draws the text based on the scrolled value: if the text doesn't need scrolling, we just draw the text, if the scroll is active but the text has reached the right edge, we also stop the above timer to prevent further and unnecessary scrolling.
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
from PyQt5.QtGui import *
class HoverFilter(QObject):
indexHovered = pyqtSignal(QModelIndex)
def __init__(self, view):
viewport = view.viewport()
super().__init__(viewport)
self.view = view
viewport.setMouseTracking(True)
viewport.installEventFilter(self)
def eventFilter(self, obj, event):
if event.type() == event.MouseMove:
self.indexHovered.emit(self.view.indexAt(event.pos()))
elif event.type() == event.Leave:
self.indexHovered.emit(QModelIndex())
return super().eventFilter(obj, event)
class ScrollDelegate(QStyledItemDelegate):
hoverIndex = None
hoverX = 0
def __init__(self, view):
super().__init__(view)
self.hoverFilter = HoverFilter(view)
self.hoverFilter.indexHovered.connect(self.hoverIndexChanged)
self.scrollTimer = QTimer(self, interval=25, timeout=self.updateHoverIndex)
self.scrollDelay = QTimer(self, singleShot=True,
interval=500, timeout=self.scrollTimer.start)
def hoverIndexChanged(self, index):
if self.hoverIndex == index:
return
if self.hoverIndex and self.hoverIndex.isValid():
self.parent().update(self.hoverIndex)
self.hoverIndex = index
self.scrollTimer.stop()
if index.isValid():
self.scrollDelay.start()
self.hoverX = 0
else:
self.scrollDelay.stop()
def updateHoverIndex(self):
self.parent().update(self.hoverIndex)
self.hoverX += 1
def paint(self, qp, opt, index):
opt = opt.__class__(opt)
self.initStyleOption(opt, index)
widget = opt.widget
style = widget.style() if widget else QApplication.style()
style.drawPrimitive(style.PE_PanelItemViewItem, opt, qp, widget)
if not opt.text:
return
qp.save()
qp.setClipRect(opt.rect)
textRect = style.subElementRect(style.SE_ItemViewItemText, opt, widget)
margin = style.pixelMetric(style.PM_FocusFrameHMargin, opt, widget) + 1
left = textRect.x() + margin
if index == self.hoverIndex:
textWidth = opt.fontMetrics.boundingRect(opt.text).width()
if textWidth + margin * 2 > textRect.width():
left -= self.hoverX
if left + textWidth + margin <= textRect.right():
self.scrollTimer.stop()
textRect.setLeft(left)
alignment = index.data(Qt.TextAlignmentRole)
if alignment is None:
alignment = Qt.AlignLeft|Qt.AlignVCenter
if opt.state & style.State_Enabled:
if opt.state & style.State_Active:
colorGroup = QPalette.Normal
else:
colorGroup = QPalette.Inactive
else:
colorGroup = QPalette.Disabled
if opt.state & style.State_Selected:
qp.setPen(opt.palette.color(colorGroup, QPalette.HighlightedText))
else:
qp.setPen(opt.palette.color(colorGroup, QPalette.Text))
qp.drawText(textRect, alignment, opt.text)
qp.restore()
if __name__ == "__main__":
import sys
from random import choice, randrange
from string import ascii_lowercase, ascii_uppercase
letters = ascii_lowercase + ascii_uppercase
app = QApplication(sys.argv)
table = QTableWidget(15, 2)
table.setItemDelegate(ScrollDelegate(table))
for r in range(15):
for c in range(2):
text = ''.join(choice(letters) for i in range(randrange(40)))
table.setItem(r, c, QTableWidgetItem(text))
table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch)
table.show()
sys.exit(app.exec())
Note that, while the above works, there are some important caveats:
- I didn't really implement the alignment part: I didn't test it with right alignment nor (most importantly) RTL languages; I'll leave that to the reader;
- it doesn't provide any text selection; if you want that (and don't need editing), consider overriding the
createEditor()
of the delegate and provide areadOnly
line edit, then useopenPersistentEditor()
on the view: it won't provide actual scrolling, but that's another story (see the following point); - the whole scrolling concept might be fine for simple and controlled scenarios, but it's generally not a very good idea from the UX perspective: if the text is very long, users will have to wait for a lot of time, and if they accidentally move the mouse away while waiting for the whole text to appear, they are back to square one - that's extremely annoying; I strongly suggest other ways to show the content, like using the
Qt.ToolTipRole
or override thehelpEvent()
of the delegate to show a custom (eventually formatted) tool tip;
Answered By - musicamante
0 comments:
Post a Comment
Note: Only a member of this blog may post a comment.