【问题标题】:Python PyQt5 - Is it possible to add a Button to press inside QTreeView?Python PyQt5 - 是否可以在 QTreeView 中添加要按下的按钮?
【发布时间】:2026-01-28 12:45:01
【问题描述】:

我想将QPushButton 添加到以.pdf 结尾的树视图中,当我单击它时,我想返回分配给它的那个索引的路径。

这对于 Native QTreeView 来说甚至是不可能的,但如果有人能引导我朝着正确的方向前进,那就太棒了!

总结一下我想要的更多内容是在红色方块下方出现QPushButton

“树视图”的当前代码:

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_())

【问题讨论】:

    标签: python pyqt5 qtreeview qpushbutton


    【解决方案1】:

    为此,您可能需要一个项目委托。

    我们的想法是,您将把基本项目绘制留给基类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))
    
    

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

    【讨论】:

    • 这很好用!!非常感谢你做的这些。您将如何更改刚刚按下的按钮的名称?例如,如果我按了 5 次,我想显示我按了多少次。这可能吗?
    • 您需要图标和文本还是只需要文本?因为有很多差异,特别是在计算按钮边框、图标和文本之间的边距时,必须考虑这些边距以确保始终显示文本(或者至少是其中最重要的部分)。
    • 我只需要文字..我认为我不需要图标..但我只需要文字。
    • @JareBear 查看更新。请注意,我更改了逻辑的一个重要部分,请注意 paint()editorEvent() 中的新更新。
    • 这看起来很完美,我不打算更改文件名或类似的东西。非常感谢您在这方面的帮助。这真是太有见地了。 (: