如何突出显示在折叠的 Qtoolbutton 下的 Qlistwidget 中的项目

How to highlight an item present in Qlistwidget which is under a collapsed Qtoolbutton

基于 by eyllanesc 的精彩回答,我已将 QListWidget 添加到可折叠框中。

下面有一个文本框,用户可以在其中提供搜索字符串,我的目的是在 QListWidget(如果存在)中突出显示该项目。

当前代码工作正常,即如果 QToolButton 已经展开,它将突出显示文本并将其滚动到顶部,但它只会打开并突出显示该项目,但如果尚未展开则不会将其滚动到顶部。 (所以用户现在不知道他是否找到了该项目,因为他看不到它突出显示。) 奇怪的是,如果我再次按回车键,它会滚动到顶部。

我尝试了各种方法,例如使 QlistWidget 处于活动状态、处于焦点状态等,但没有帮助。

请告诉我我遗漏了什么,这样我就不需要按两次 Enter 以防 QToolButton 尚未展开。

编辑:按照建议从代码中删除动画部分。


import time
from PyQt5 import QtCore
from PyQt5.QtWidgets import QMainWindow, QListWidget, QLabel, QApplication, QVBoxLayout, \
    QWidget, QSizePolicy, QToolButton, QScrollArea, QFrame, QDockWidget, QLineEdit, QHBoxLayout, QAbstractItemView

collapsed_list = ['What', 'should', 'be', 'done', 'to', 'fix', 'this', 'issue?', 'I', 'am', 'confused']


class CollapsibleDemo(QWidget):
    def __init__(self, title="", parent=None):
        super(CollapsibleDemo, self).__init__(parent)
        self.toggle_button = QToolButton(
            text=title, checkable=True, checked=False
        )
        self.toggle_button.setSizePolicy(
            QSizePolicy.Expanding, QSizePolicy.Fixed
        )
        self.toggle_button.setToolButtonStyle(
            QtCore.Qt.ToolButtonTextBesideIcon
        )
        self.toggle_button.setArrowType(QtCore.Qt.RightArrow)
        self.toggle_button.pressed.connect(self.on_pressed)

        self.content_area = QScrollArea(
            maximumHeight=0, minimumHeight=0
        )
        self.content_area.setSizePolicy(
            QSizePolicy.Expanding, QSizePolicy.Fixed
        )

        self.content_area.setFrameShape(QFrame.NoFrame)

        lay = QVBoxLayout(self)
        lay.setSpacing(0)
        lay.setContentsMargins(10, 10, 1, 1)
        lay.addWidget(self.toggle_button)
        lay.addWidget(self.content_area)


    @QtCore.pyqtSlot()
    def on_pressed(self):
        checked = self.toggle_button.isChecked()
        self.toggle_button.setArrowType(
            QtCore.Qt.DownArrow if not checked else QtCore.Qt.RightArrow
        )

        if not checked:
            self.content_area.setMaximumHeight(self.content_height + self.collapsed_height)
        else:
            self.content_area.setMaximumHeight(0)


    def setContentLayout(self, layout):
        self.content_area.setLayout(layout)
        self.collapsed_height = (
                self.sizeHint().height() - self.content_area.maximumHeight()
        )
        self.content_height = layout.sizeHint().height()

    def set_text(self, title):
        self.toggle_button.setText(title)


class Try(QMainWindow):

    def __init__(self, ):
        super().__init__()
        self.width = 800
        self.height = 800
        self.init_ui()

    def init_ui(self):
        self.resize(self.width, self.height)
        self.create_background()
        self.add_collapsed_list_box()
        self.add_find_text_box()
        self.vlay.addStretch()
        self.find_text_box.setFocus()

    def create_background(self):
        dock = QDockWidget()
        self.setCentralWidget(dock)
        dock.setFeatures(QDockWidget.NoDockWidgetFeatures)
        scroll = QScrollArea()
        dock.setWidget(scroll)
        content = QWidget()
        scroll.setWidget(content)
        scroll.setWidgetResizable(True)
        self.vlay = QVBoxLayout(content)
        self.vlay.setSpacing(10)

    def add_collapsed_list_box(self):
        self.box = CollapsibleDemo(f"Whats inside here!")
        self.vlay.addWidget(self.box)
        lay_diff = QVBoxLayout()

        self.qlist = QListWidget()
        self.qlist.addItems(collapsed_list)

        lay_diff.addWidget(self.qlist)
        self.box.setContentLayout(lay_diff)


    def add_find_text_box(self):
        self.find_text_box = QLineEdit("am")

        find_label = QLabel("    Search text:   ")
        enter_label = QLabel(" (Press Enter)")
        hlayout = QHBoxLayout()
        hlayout.addWidget(find_label)
        hlayout.addWidget(self.find_text_box)
        hlayout.addWidget(enter_label)
        hlayout.addStretch(3)
        self.vlay.addLayout(hlayout)
        self.find_text_box.returnPressed.connect(self.find_selected)

    def find_selected(self):
        user_text = self.find_text_box.text()

        if user_text in collapsed_list:
            if not self.box.toggle_button.isChecked():
                self.box.on_pressed()
                self.box.toggle_button.setChecked(True)
            item = self.qlist.findItems(user_text, QtCore.Qt.MatchRegExp)[0]
            item.setSelected(True)
            self.qlist.scrollToItem(item, QAbstractItemView.PositionAtTop)


if __name__ == "__main__":
    app = QApplication([])
    window = Try()
    window.show()
    app.exec()

有两个问题:

  1. 动画是异步的:当它们启动时,控制权立即返回,因此在 animation.start() 之后调用的任何函数都会立即执行;这意味着此时动画的值仍然是 start 值;
  2. 项目视图(和一般的滚动区域)需要一些时间才能正确更新它们的滚动条;即使框立即折叠(没有动画),scrollToItem 不会 工作,因为滚动区域尚未布置其内容;

为了解决问题,解决方法是delay the scrollToItem call using a singleShot QTimer.

如果正在使用动画,则必须等待动画结束,因为滚动条会随框大小一起更新。

一个可能的解决方案是创建一个延迟 scrollToItem 的本地函数,并且如果框折叠,则仅在通过连接到动画 finished 信号完成动画时调用它,然后断开连接(这 非常 重要!)。

def find_selected(self):
    user_text = self.find_text_box.text()

    if user_text in collapsed_list:
        collapsed = not self.box.toggle_button.isChecked()
        if collapsed:
            self.box.on_pressed()
            self.box.toggle_button.setChecked(True)
        item = self.qlist.findItems(user_text, QtCore.Qt.MatchRegExp)[0]
        item.setSelected(True)

        def scrollTo():
            QtCore.QTimer.singleShot(1, 
                lambda: self.qlist.scrollToItem(item, QAbstractItemView.PositionAtTop))
            if collapsed:
                # disconnect the function!!!
                self.box.toggle_animation.finished.disconnect(scrollTo)
        if collapsed:
            self.box.toggle_animation.finished.connect(scrollTo)
        else:
            scrollTo()

如果你对动画不感兴趣,那就更简单了:

class CollapsibleDemo(QWidget):
    def __init__(self, title="", parent=None):
        # ...
        # remove all the animations in here

    @QtCore.pyqtSlot()
    def on_pressed(self):
        checked = self.toggle_button.isChecked()
        self.toggle_button.setArrowType(
            QtCore.Qt.DownArrow if not checked else QtCore.Qt.RightArrow
        )
        if checked:
            self.setFixedHeight(self.collapsed_height)
            self.content_area.setFixedHeight(0)
        else:
            self.setFixedHeight(self.expanded_height)
            self.content_area.setFixedHeight(self.content_height)

    def setContentLayout(self, layout):
        self.content_area.setLayout(layout)
        self.collapsed_height = self.sizeHint().height() - self.content_area.maximumHeight()
        self.content_height = layout.sizeHint().height()
        self.expanded_height = self.collapsed_height + self.content_height

    def set_text(self, title):
        self.toggle_button.setText(title)


class Try(QMainWindow):
    def find_selected(self):
        user_text = self.find_text_box.text()

        if user_text in collapsed_list:
            if not self.box.toggle_button.isChecked():
                self.box.on_pressed()
                self.box.toggle_button.setChecked(True)
            item = self.qlist.findItems(user_text, QtCore.Qt.MatchRegExp)[0]
            item.setSelected(True)
            QtCore.QTimer.singleShot(1, lambda:
                self.qlist.scrollToItem(item, QAbstractItemView.PositionAtTop))

如评论中所述,动画持续时间至少为 50-100 毫秒才有意义;使用太短的持续时间会使动画无用(计算机无法显示效果并且我们的眼睛无论如何也无法看到它)并且只会使事情复杂化(您必须等待动画结束)。