具有大量项目的 QComboBox 的初始显示性能较慢

Slow initial show performance of QComboBox with large item count

看起来在 widget/window 上调用 show 非常慢,其中包含一个 QComboBox,项目数很大。

采用以下代码段比较使用 QComboBox 与 QTreeWidget 的性能

from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
from PyQt5.QtGui import *
from time import perf_counter

import sys

class MainWindowComboBox(QMainWindow):

    def __init__(self, items, *args, **kwargs):
        super().__init__(*args, **kwargs)

        widget = QComboBox()
        widget.addItems(items)
        self.setCentralWidget(widget)

class MainWindowTreeWidget(QMainWindow):

    def __init__(self, items, *args, **kwargs):
        super().__init__(*args, **kwargs)

        widget = QTreeWidget()
        items = [QTreeWidgetItem([item]) for item in items]
        widget.addTopLevelItems(items)
        self.setCentralWidget(widget)

items = [f'item {i}' for i in range(100_000)]
app = QApplication(sys.argv)

window = MainWindowTreeWidget(items)

s = perf_counter()
window.show()
print('took', perf_counter()-s)
app.exec_()

我得到以下时间:

  1. 使用 QComboBOx -> 8.09s
  2. 使用 QTreeWidget -> 0.06s

使用 QComboBox 的速度要慢几个数量级。

QTreeWidget 与 QAbstractItemView 的其他子视图一样,不知道它们的内容,因此它们通常默认使用小部件大小的最小尺寸提示,并且最终能够调整自己的大小(通常通过如果有可用的 space).

QComboBox,另一方面,有sizeAdjustPolicy 属性根据其内部模型的内容提供不同大小的提示。

属性 的默认值为 AdjustToContentsOnFirstShow,这会导致小部件浏览模型的整个内容以找到最大的项目尺寸并将该尺寸用作尺寸提示第一次显示组合后。
为了获得每个项目的大小,Qt 使用为每个项目初始化的 QItemDelegate,计算文本大小,添加图标(如果存在)以及图标和文本之间所需的间距,并将其调整为代表的边距。可以想象,对大量项目执行该过程需要大量时间。

作为 AdjustToMinimumContentsLengthWithIcon value 报告的文档:

The combobox will adjust to minimumContentsLength plus space for an icon. For performance reasons use this policy on large models.

一旦您将策略 属性 设置为此值,上述大小计算将不会发生,小部件将立即显示。

当然,这样做的缺点是,如果组合位于非常小的 window 中,或者在包含其他需要更多 space 的小部件的布局中,它会非常小,甚至可能根本不显示任何文本内容。

为防止这种情况,您可以根据项目文本为组合设置任意最小宽度;它不是完美的(因为Qt在文本周围添加了一些边距,你还应该考虑向下箭头),但它会快得多。
请注意,根据字体的不同,这可能会给您带来意想不到的结果,因为我使用 max 来表示字符串长度,而不是实际的字体宽度:具有 8 "i" 的字符串将被视为大于一个有 7 "w",但如果你不使用 monospaced 字体,后者可能会更大。

    combo = QComboBox()
    combo.setSizeAdjustPolicy(combo.AdjustToMinimumContentsLengthWithIcon)
    combo.addItems(items)
    # use font metrics to obtain the pixel width of the (possibly) longest
    # text in the list;
    textWidth = self.fontMetrics().width(max(items))
    # add some arbitrary margin
    combo.setMinimumWidth(textWidth + 20)