Python PyQt5 - 是否可以在 QTreeView 中添加一个按钮来按下?

Python PyQt5 - Is it possible to add a Button to press inside QTreeView?

我想在以 .pdf 结尾的树视图中添加一个 QPushButton,当我单击它时,我想 return 它所分配的索引的路径。

这在 Native QTreeView 中甚至是不可能的,但如果有人能指导我正确的方向,那就太棒了!

总而言之,我想要的是 QPushButton 出现在红色方块下方的位置。

"Tree View"的当前代码:

from PyQt5.QtMultimediaWidgets import *
from PyQt5.QtMultimedia import *
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
from PyQt5.QtGui import *
from PyQt5 import *
import os, sys
class MainMenu(QWidget):
    def __init__(self, parent = None):
        super(MainMenu, self).__init__(parent)
        self.model = QFileSystemModel()
        self.model.setRootPath(QDir.rootPath())
        self.model.setFilter(QDir.NoDotAndDotDot | QDir.AllEntries | QDir.Dirs | QDir.Files)
        self.proxy_model = QSortFilterProxyModel(recursiveFilteringEnabled = True, filterRole = QFileSystemModel.FileNameRole)
        self.proxy_model.setSourceModel(self.model)
        self.model.setReadOnly(False)
        self.model.setNameFilterDisables(False)

        self.indexRoot = self.model.index(self.model.rootPath())

        self.treeView = QTreeView(self)
        self.treeView.setModel(self.proxy_model)

        self.treeView.setRootIndex(self.indexRoot)
        self.treeView.setAnimated(True)
        self.treeView.setIndentation(20)
        self.treeView.setSortingEnabled(True)
        self.treeView.setDragEnabled(False)
        self.treeView.setAcceptDrops(False)
        self.treeView.setDropIndicatorShown(True)
        self.treeView.setEditTriggers(QTreeView.NoEditTriggers)
        for i in range(1, self.treeView.model().columnCount()):
            self.treeView.header().hideSection(i)


if __name__ == '__main__':
    app = QApplication(sys.argv)
    main = MainMenu()
    main.show()
    sys.exit(app.exec_())

为此,您可能需要一个项目代理。

我们的想法是将基本项目绘制留给基本 class paint() 函数,然后在其上绘制一个虚拟按钮。
为了实现这一点,QStyleOptionButton 用于视图样式(从 option 参数获得):您创建一个样式选项,init 它来自视图(option.widget,这将应用小部件的基本矩形、字体、调色板等),调整矩形以适合您的需要,最后绘制它。

为了更好地实现绘图(鼠标悬停效果,同时确保正确的绘图更新),您还需要为树视图将鼠标跟踪设置为 True。这与代码中解释的其他检查一起,允许您绘制虚拟按钮,包括其悬停或按下状态。

最后,当释放按钮并且鼠标在其边界内时,将发出 buttonClicked 信号,并将当前索引作为参数。

class TreeButtonDelegate(QtWidgets.QStyledItemDelegate):
    buttonClicked = QtCore.pyqtSignal(QtCore.QModelIndex, int)

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

        self.clickedPaths = {}
        self._mousePos = None
        self._pressed = False
        self.minimumButtonWidth = 32

    def getOption(self, option, index):
        btnOption = QtWidgets.QStyleOptionButton()
        # initialize the basic options with the view
        btnOption.initFrom(option.widget)

        clickedCount = self.clickedPaths.get(self.fsModel.filePath(index), 0)
        if clickedCount:
            btnOption.text = '{}'.format(clickedCount)
        else:
            btnOption.text = 'NO'

        # the original option properties should never be touched, so we can't
        # directly use it's "rect"; let's create a new one from it
        btnOption.rect = QtCore.QRect(option.rect)

        # adjust it to the minimum size
        btnOption.rect.setLeft(option.rect.right() - self.minimumButtonWidth)

        style = option.widget.style()
        # get the available space for the contents of the button
        textRect = style.subElementRect(
            QtWidgets.QStyle.SE_PushButtonContents, btnOption)
        # get the margins between the contents and the border, multiplied by 2
        # since they're used for both the left and right side
        margin = style.pixelMetric(
            QtWidgets.QStyle.PM_ButtonMargin, btnOption) * 2

        # the width of the current button text
        textWidth = btnOption.fontMetrics.width(btnOption.text)

        if textRect.width() < textWidth + margin:
            # if the width is too small, adjust the *whole* button rect size
            # to fit the contents
            btnOption.rect.setLeft(btnOption.rect.left() - (
                textWidth - textRect.width() + margin))

        return btnOption

    def editorEvent(self, event, model, option, index):
        # map the proxy index to the fsModel
        srcIndex = index.model().mapToSource(index)
        # I'm just checking if it's a file, if you want to check the extension
        # you might need to use fsModel.fileName(srcIndex)
        if not self.fsModel.isDir(srcIndex):
            if event.type() in (QtCore.QEvent.Enter, QtCore.QEvent.MouseMove):
                self._mousePos = event.pos()
                # request an update of the current index
                option.widget.update(index)
            elif event.type() == QtCore.QEvent.Leave:
                self._mousePos = None
            elif (event.type() in (QtCore.QEvent.MouseButtonPress, QtCore.QEvent.MouseButtonDblClick)
                and event.button() == QtCore.Qt.LeftButton):
                    # check that the click is within the virtual button rectangle
                    if event.pos() in self.getOption(option, srcIndex).rect:
                        self._pressed = True
                    option.widget.update(index)
                    if event.type() == QtCore.QEvent.MouseButtonDblClick:
                        # do not send double click events
                        return True
            elif event.type() == QtCore.QEvent.MouseButtonRelease:
                if self._pressed and event.button() == QtCore.Qt.LeftButton:
                    # emit the click only if the release is within the button rect
                    if event.pos() in self.getOption(option, srcIndex).rect:
                        filePath = self.fsModel.filePath(srcIndex)
                        count = self.clickedPaths.setdefault(filePath, 0)
                        self.buttonClicked.emit(index, count + 1)
                        self.clickedPaths[filePath] += 1
                self._pressed = False
                option.widget.update(index)
        return super().editorEvent(event, model, option, index)

    def paint(self, painter, option, index):
        super().paint(painter, option, index)
        srcIndex = index.model().mapToSource(index)
        if not self.fsModel.isDir(srcIndex):
            btnOption = self.getOption(option, srcIndex)

            # remove the focus rectangle, as it will be inherited from the view
            btnOption.state &= ~QtWidgets.QStyle.State_HasFocus
            if self._mousePos is not None and self._mousePos in btnOption.rect:
                # if the style supports it, some kind of "glowing" border
                # will be shown on the button
                btnOption.state |= QtWidgets.QStyle.State_MouseOver
                if self._pressed == QtCore.Qt.LeftButton:
                    # set the button pressed state
                    btnOption.state |= QtWidgets.QStyle.State_On
            else:
                # ensure that there's no mouse over state (see above)
                btnOption.state &= ~QtWidgets.QStyle.State_MouseOver

            # finally, draw the virtual button
            option.widget.style().drawControl(
                QtWidgets.QStyle.CE_PushButton, btnOption, painter)


class MainMenu(QWidget):
    def __init__(self, parent = None):
        super(MainMenu, self).__init__(parent)
        # ...
        self.treeView = QTreeView(self)
        self.treeView.setMouseTracking(True)
        # ...
        self.treeDelegate = TreeDelegate(self.model)
        self.treeView.setItemDelegateForColumn(0, self.treeDelegate)
        self.treeDelegate.buttonClicked.connect(self.treeButtonClicked)
        # ...


    def treeButtonClicked(self, index, count):
        print('{} clicked {} times'.format(index.data(), count))

注意:我按照您在评论中的要求实现了点击计数器(并使用辅助函数来容纳相应地计算按钮大小的较长函数),请记住,这没有考虑到的可能性文件重命名,删除 and/or 重新创建(或重命名的文件覆盖现有文件)。要获得它,您需要使用比简单的基于路径的字典更复杂的方法,可能通过实现 QFileSystemWatcher 并检查文件 removed/renamed.
另请注意,为了稍微加快速度,我将源文件系统模型添加到委托的初始化中,这样就不需要在每次需要绘制或鼠标跟踪时都找到它。