【问题标题】:How to highlight an item present in Qlistwidget which is under a collapsed Qtoolbutton如何突出显示 Qlistwidget 中折叠的 Qtoolbutton 下的项目
【发布时间】:2021-10-07 21:18:08
【问题描述】:

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

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

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

我尝试了各种方法,例如使 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()

【问题讨论】:

  • 我看到您为动画设置了非常短的持续时间:5 毫秒是 1/200 秒,即使假设计算机速度如此之快以至于能够真正快速地显示动画,人眼无法看到(我们几乎看不到任何持续 20-30 毫秒的差异)。因此,在我看来,使用动画完全没有意义,这将解决您的问题,因为当您调用 scrollToItem 时,列表视图仍然折叠,因此无法滚动到任何内容。
  • 我认为保持低数字,即使它在用户眼中看起来是即时的,实际上应该有利于滚动的情况。仍然要测试,我在原始代码中将持续时间增加到 500,我还在滚动命令之前添加了 1 秒睡眠,但它仍然没有解决它。
  • 添加睡眠不会改变任何事情,因为阻塞函数会阻止事件循环正确处理所有内容(包括动画)。相信你还是不明白问题所在:动画是异步的,当你调用on_pressed时,动画开始,而scrollToItem之后就调用了,那时盒子仍然折叠,所以它不会滚动到任何东西。对动画使用较短的间隔不会解决问题,因为问题在于存在动画这一事实。即使将持续时间设置为 0 也不会改变这一点。
  • 还有另一个方面需要考虑:项目视图需要一些时间才能正确处理大小变化,并且滚动条不会立即更新:即使您不使用动画并立即调整框大小, scrollToItem 在任何情况下都不会像那样工作。因此,您必须澄清您是否仍然想要动画功能(具有适当的持续时间)或者您对此不感兴趣,因为解决方案会非常不同。
  • 知道了。我应该如何编写没有任何动画的 setContentLayout?它基本上应该在点击时在 content_height 和 collapsed_height 之间切换。

标签: python-3.x pyqt5 qlistwidget qlistwidgetitem


【解决方案1】:

有两个问题:

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

为了解决问题,解决方案是延迟使用singleShot QTimer 调用scrollToItem

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

一个可能的解决方案是创建一个本地函数来延迟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))

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

【讨论】:

  • 非常感谢您的帮助。我对你的代码和我的代码之间的差异感到不安。实际上on_pressed方法中的setFixedHeight的四行在没有使用singleShot的情况下解决了这个问题。我不知道为什么会这样。
  • @AbhinavSaini 请注意,您可以依赖它适用于您的情况这一事实,因为它可能只是适合您的情况的情况.事件在所有平台和任何情况下都不会以相同的方式触发,它们的顺序取决于很多事情(操作系统、计算机性能、调度、Qt 版本等)。如前所述,当您更改小部件的固定大小时,不能保证视图已正确调整大小(并且其滚动条已更新)。请注意,使用 QTimer.singleShot 实际上是一种(通常安全的)解决方法,但它不是完全正确的方法。
  • @AbhinavSaini 实际上,每当需要基于内容进行滚动时(如scrollToItem 那样),在实际处理之前强制等待事件被正确处理根据大小变化执行任何操作。在某些情况下,即使使用QTimer.singleShot 也是不够的,因为项目的加载和布局(例如,当模型尚未完全加载时,如 QFileSystemModel)或正在使用动画时,更改可能会延迟,这也可能是由于当前样式(某些样式在内部使用动画,而您对它们绝对无法控制)。
  • @AbhinavSaini 所以,我强烈建议使用 QTimer 作为最低安全预防措施。请记住:您在计算机上获得的内容其他人获得的内容。一种适当、更安全、更准确的方法是创建一个系统来监控视图大小并等待调整大小完成(这是我在第一个示例中所做的,因为我们知道调整大小是基于动画的) ,并且只有调用scrollToItem
  • 全心接受。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-05-03
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多