Qt中项目视图的默认委托机制是什么?

9

简要版

QTreeView使用的默认委托是什么?特别是我想找到它的paint()方法。

详细版

我是Python用户(Pyside/PyQt),正在使用自定义委托来重新创建QTreeView的一些功能。因此,我正在尝试找到在QTreeView中使用的默认委托和其paint()方法。更好的是解释一下它是如何工作的。

转帖

我在Qt Centre上发布了同样的问题(http://www.qtcentre.org/threads/64458-Finding-default-delegate-for-QTreeView)。

1个回答

25
默认的项目视图委托是 QStyledItemDelegate。它的 paint() 方法调用 qcommonstyle.cpp 中定义的 drawControl() 来绘制每个项目。因此,请仔细查看 qcommonstyle.cpp 以了解每个项目绘制的详细信息。

tl;dr


长篇回答

喜欢简洁的人应该阅读上面的tl;drItem Views中样式的文档。如果你仍然困惑(很多Python用户可能会),那么这个回答的剩余部分应该会有所帮助。

I. 默认委托是QStyledItemDelegate

QStyledItemDelegate是视图项目的默认委托。这在Qt Model/View编程概述中明确说明:

自Qt 4.4以来,默认的委托实现由QStyledItemDelegate提供,并且这是Qt标准视图使用的默认委托。

QStyledItemDelegate文档提供了更详细的信息:

在 Qt 的项目视图中显示模型数据时(例如 QTableView),单个项目是由代理绘制的。当项目被编辑时,它会提供一个编辑器部件,在编辑过程中放置在项目视图顶部。QStyledItemDelegate 是所有 Qt 项目视图的默认委托,并在创建它们时安装。
总之,如果你想了解任何项目视图(不仅是树形视图,还包括表格和列表)的底层机制,请研究 QStyledItemDelegate。
qstyleditemdelegate.cpp 中的 paint() 方法在代码库的 Doxygenated 版本上 位于第419行。让我们看一下最后两行:
    QStyle *style = widget ? widget->style() : QApplication::style();
    style->drawControl(QStyle::CE_ItemViewItem, &opt, painter, widget);

这里发生了两件事情。首先,样式被设置 -- 通常是QApplication.style()。其次,该样式的drawControl()方法被调用以绘制正在绘制的项目。就是这样。这实际上是QStyledItemDelegate.paint()的最后一行!

因此,如果您真的想弄清默认委托如何绘制东西,我们实际上必须研究样式,因为它才是做所有真正工作的地方。这就是我们在本文档的其余部分要做的。

II. QStyle:是什么赋予委托其样式?

当使用Qt显示任何内容时,它将根据在实例化QApplication时选择的某种系统特定方式的样式进行绘制。来自QStyle文档

在Qt中,QStyle类是一个抽象基类,用于封装GUI的外观和感觉。Qt包含一组QStyle子类,模拟Qt支持的不同平台的样式(QWindowsStyle、QMacStyle、QMotifStyle等)。默认情况下,这些样式都内置在QtGui库中。
在Qt中,您可以在src/gui/styles/N.cpp中找到样式N的源代码。
每种样式都包含用于绘制GUI中的所有内容(从树视图到下拉菜单)所使用的基本操作的实现。标准样式,例如QWindowsStyle,大多数方法都继承自QCommonStyle。每个特定的样式通常只包括与该公共基础的轻微偏差。因此,仔细研究qcommonstyle.cpp将揭示Qt开发人员发现用于绘制GUI的所有部分的基本功能。它的重要性难以高估。
接下来,我们将研究绘制视图项的相关部分。
III. QStyle.drawControl(): 对委托进行内窥镜检查
如上所述,理解绘制视图的基本机制需要检查qcommonstyle.cppdrawControl()的实现,该实现从第1197行开始。请注意,在接下来的内容中,当我提到一个行号而没有提到文件名时,按照惯例我是指doxygen化的代码库中的qcommonstyle.cppQStyle.drawControl()的文档很有教育意义:

QStyle.drawControl(element, option, painter)

参数:

  • element – QStyle.ControlElement

  • option – QtGui.QStyleOption

  • painter – PySide.QtGui.QPainter

使用提供的painter以及由option指定的样式选项绘制给定的元素...option参数是一个指向QStyleOption对象的指针,包含了绘制所需的所有信息。

调用者通过传递一个QStyle.ControlElement标志告诉drawControl()它要绘制哪种类型的元素。控件元素是窗口的高级组件,用于向用户显示信息:如复选框、按钮和菜单项。所有控件元素在此处枚举:

http://pyqt.sourceforge.net/Docs/PyQt4/qstyle.html#ControlElement-enum

请回忆一下在调用QStyledItemDelegate.paint()时发送的控制元素是CE_ItemViewItem,它只是要在项视图内显示的一个项目。在QCommonStyle.drawControl()中,CE_ItemViewItem情况从第2153行开始。让我们深入了解。

A. subElementRect(): 大小很重要

正确获取每个项目的大小和布局非常关键。这是drawControl()计算的第一件事。为了获取此信息,它调用subElementRect()(定义于第2313行,并在第2158行首次调用)。例如,我们有:

QRect textRect = subElementRect(SE_ItemViewItemText, vopt, widget);

第一个参数是 QStyle.SubElement 标志,此处为 SE_ItemViewItemText。样式子元素代表控制元素的组成部分。视图中的每个项目有三个可能的子元素:复选框、图标和文本;显然,SE_ItemViewItemText 子元素代表文本。所有可能的子元素在此处枚举:

http://pyqt.sourceforge.net/Docs/PyQt4/qstyle.html#SubElement-enum

subElementRect()方法包含子元素枚举中的所有情况:我们的SE_ItemViewItemText从3015行开始。

请注意,subElementRect()返回一个QRect,它使用整数精度在平面上定义矩形,并可以用四个整数(左、上、宽度、高度)构造。例如,r1 = QRect(100, 200, 11, 16)。这指定了子元素的大小以及它在视口中将被绘制的x、y位置。

subElementRect()实际上调用viewItemLayout()(在999行定义)来执行真正的工作,这是一个两步过程。首先,viewItemLayout()使用viewItemSize()计算子元素的高度和宽度。其次,它计算子元素的x和y位置。让我们依次考虑每个操作。

1. viewItemLayout():计算子元素的宽度和高度

从第1003行开始,viewItemLayout()调用viewItemSize()(在第838行定义),该函数计算子元素所需的高度和宽度。 viewItemSize()从哪里获取诸如标题栏高度之类的默认数字?这是像素度量的领域。像素度量是一个依赖于样式的大小,由单个像素值表示。例如,Style.PM_IndicatorWidth返回复选框指示器的宽度,QStyle.PM_TitleBarHeight返回应用程序样式的标题栏高度。所有不同的QStyle.PixelMetric都在此处枚举:

http://pyqt.sourceforge.net/Docs/PyQt4/qstyle.html#PixelMetric-enum

您可以使用QStyle.pixelMetric()检索给定像素度量的值,它在viewItemSize()中经常使用。 pixelMetric()的第一个输入是枚举中的QStyle.PixelMetric之一。 在qcommonstyle.cpp中,pixelMetric()的实现从第4367行开始。
例如,viewItemSize()使用以下内容计算复选框的宽度和高度(如果需要):
return QSize(proxyStyle->pixelMetric(QStyle::PM_IndicatorWidth, option, widget),
    proxyStyle->pixelMetric(QStyle::PM_IndicatorHeight, option, widget));

请注意,pixelMetric()不仅用于viewItemSize(),而且是普遍存在的。它用于计算许多GUI元素的度量属性,从窗口边框到图标,并且在qcommonstyle.cpp中随处可见。基本上,每当您需要知道样式为某些图形元素使用了多少像素,而其大小不会更改(例如复选框)时,样式将调用像素度量。 viewItemLayout()的第二部分致力于组织刚刚计算出宽度和高度的子元素的布局。也就是说,它需要找到它们的x和y位置,以完成将值填充到QRect(如textRect)中。子元素的组织方式将根据视图的一般设置而有所不同(例如,它是右-左还是左-右导向)。因此,viewItemLayout()根据这些因素计算每个子元素的最终停放位置。
如果您需要关于如何重新实现自定义委托中的sizeHint()的线索,subElementRect()可能是一个有用的技巧来源。特别是viewItemSize()可能包含有用的提示和相关的像素度量,当您想要自定义视图与默认视图紧密匹配时,可能需要查询它们。
一旦文本、图标和复选框子元素的QRect被计算出来,drawControl()就会继续,并最终开始绘制。 B. 对背景进行着色:获得原始的颜色 首先为项目填充背景(第2163行):
drawPrimitive(PE_PanelItemViewItem, option, painter);

这与调用QStyle.drawControl()非常相似,但第一个参数不是控件元素,而是一个原始元素(PE)。就像drawControl()是一个庞大的方法,控制所有高级控件元素的绘制一样,QStyle.drawPrimitive()绘制GUI中大部分低级原始图形元素(例如项视图中的矩形背景)。这些低级单元,即原始元素,在此处枚举:

http://pyqt.sourceforge.net/Docs/PyQt4/qstyle.html#PrimitiveElement-enum

文档中说,“原始元素是常见的GUI元素,例如复选框指示器或按钮斜面。”例如,PE_PanelItemViewItem原始元素是“[T]项目视图中项目的背景。”

每种样式都必须有一个drawPrimitive()的实现,我们的实现从第140行开始。在那里,您可以详细了解它如何执行其原始绘画操作。这是使用paint()命令在自定义委托中实践的提示的有用来源。

对于PE_PanelItemViewItem情况的QStyle.drawPrimitive()的重新实现从第773行开始。它首先根据项目的状态选择适当的背景颜色。它通过查询项目的QStyle.StateFlag来完成此操作。 option.state包含描述该项目状态的状态标志(是否启用,已选择,正在编辑等)。这些状态不仅在后端中使用,而且您可能需要在自定义委托中重新实现QStyledItemDelegate.paint()时使用它。您可以在此处找到QStyle.StateFlag的枚举:

http://pyqt.sourceforge.net/Docs/PyQt4/qstyle.html#StateFlag-enum

在选择正确的颜色后,drawPrimitive() 接着使用 QPainter.fillRect() 以该颜色填充相应的区域(第786行):
p->fillRect(vopt->rect, vopt->backgroundBrush);

QPainter.fillRect() 是实现自定义委托时非常有用的方法。

在处理背景后,drawControl() 接着完成绘制项目,从复选框 (第 2165 行) 开始,然后是图标 (第 2185 行),最后是文本 (第 2194 行)。我们不会详细讨论这个过程,但我会简要讨论一下它如何绘制文本。

C. 绘制文本:走自己的路

首先,使用项目的状态来指定文本的颜色。这使用了刚刚讨论的 QStyle.StateFlags。然后,drawControl() 将文本绘制的责任交给一个自定义方法 viewItemDrawText() (在第 921 行定义):

viewItemDrawText(painter, vopt, textRect);

该方法接受绘图器、选项和上述文本矩形(部分A)作为参数。请注意,选项参数非常重要:它是一个QStyleOption类,通过该类在样式中传递内容(包括图标、检查状态和文本属性)。
在从选项中获取文本后,将其合并到一个QtGui.QTextLine中,并添加到QtGui.QTextLayout中。最终的省略文本(即根据换行设置带有省略号的文本)由QPainter.drawText()绘制(参见第983行),这是Qt的原始绘画操作之一。
坦率地说,viewItemDrawText()的很大一部分时间都花在处理换行上。这就是我们开始涉及一些Qt的内部机制,没有任何Python用户曾经打算看到,更不用说动手修改了。例如,它使用QStackTextEngine类。我鼓励您搜索“qstacktextengine pyqt”,看看这对Python用户来说有多少次出现。如果您感兴趣,请尽管去试试吧!
IV. 总结
如果您想访问默认委托的底层绘图机制,您将需要研究qcommonstyle.cppQStyle.drawControl()的实现,该文件有6000行代码。这一练习对于弄清楚用于计算大小和绘制项目包含的基本图形元素的确切过程非常有帮助。然而,在处理换行等情况时,这个“野兽”可能会令人生畏且无助。在这些情况下,您可能需要为您的委托找到所需功能的自定义实现。
最后,既然我们已经看到了事物如何运作的整体情况,我们可以更好地欣赏QStyle文档的帮助,特别是项视图中的样式部分。在那里,我们发现以下启示性的信息:
视图中项目的绘制由委托执行。Qt的默认委托QStyledItemDelegate也用于计算项目(及其子元素)的边界矩形……当QStyledItemDelegate绘制其项目时,它会绘制CE_ItemViewItems…当实现样式以自定义项目视图的绘制时,您需要检查QCommonStyle的实现(以及从中继承您的样式的任何其他子类)。这样,您就可以找出已经绘制了哪些其他样式元素,以及如何绘制它们,然后重新实现应该以不同方式绘制的元素的绘制。

因此,原帖的答案基本上是“他们说的”。


1
我认为这篇文章非常有见地,感谢您花时间写下它!我对Qt的委托框架有些失望,但最终使用了QAbstractItemView.openPersistentEditor来跳过绘制,并且在实现QStyledItemDelegate.sizeHint时参考了您的大部分内容,然后在QStyledItemDelegate.setEditorData(index)的结尾调用了QStyledItemDelegate.emit(index) - hansonap
@hansonap 非常感谢,我很高兴它有所帮助——这篇文章绝对是一份用心的劳动。 - eric

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接