简体   繁体   English

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

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

I want to add a QPushButton to the tree view that ends with .pdf and when I click it I want to return the path for that Index it's assigned at.我想将QPushButton添加到以.pdf结尾的树视图中,当我单击它时,我想返回分配给它的索引的路径。

This might not even be possible with the Native QTreeView but if anyone could guide me in the right direction that would be awesome!使用本机QTreeView甚至可能无法做到这QTreeView但如果有人能指导我朝着正确的方向前进,那就太棒了!

To conclude more of what I would want is to have a QPushButton appear where that red square is below.总结更多我想要的是让QPushButton出现在下面红色方块的位置。

看法

Current code for the "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_())

For this you'll probably need an item delegate.为此,您可能需要一个项目委托。

The idea is that you are going to leave basic item painting to the base class paint() function, and then paint a virtual button over it.这个想法是你将把基本的项目绘制留给基类paint()函数,然后在它上面绘制一个虚拟按钮。
To achieve that, QStyleOptionButton is used against the view style (obtained from the option argument): you create a style option, init it from the view ( option.widget , which will apply the basic rectangle of the widget, the font, palette, etc.), adjust the rectangle to suit your needs and finally paint it.为了实现这一目标, QStyleOptionButton是用来对付视图样式(从获得的option参数):创建一个样式选项,从视图(INIToption.widget ,这将适用于小部件的基本矩形,字体,调色板,等),调整矩形以满足您的需要,最后绘制它。

To better implement drawing (mouse hover effects, but also to ensure correct painting update), you'll also need to set mouse tracking to True for the tree view.为了更好地实现绘图(鼠标悬停效果,同时确保正确的绘图更新),您还需要将树视图的鼠标跟踪设置为 True。 This, amongst other checks explained in the code, allows you to draw the virtual button, including its hover or pressed states.这与代码中解释的其他检查一起,允许您绘制虚拟按钮,包括其悬停或按下状态。

Finally, when the button is released and the mouse is within its boundaries, a buttonClicked signal is emitted, with the current index as argument.最后,当按钮被释放并且鼠标在其边界内时,将发出一个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))

Note: I implemented the click counter as you asked in the comments (and used an helper function to accomodate the longer function that computes the button size accordingly), just remember that this doesn't take into account the possibility of files renamed, removed and/or recreated (or files renamed overwriting an existing one).注意:我按照您在评论中的要求实现了点击计数器(并使用了一个辅助函数来适应相应地计算按钮大小的较长函数),请记住,这并没有考虑到文件重命名、删除和/ 或重新创建(或重命名的文件覆盖现有文件)。 To obtain that you'll need to use a more complex approach than a simple path-based dictionary, possibly by implementing QFileSystemWatcher and checking for files removed/renamed.要获得它,您需要使用比简单的基于路径的字典更复杂的方法,可能是通过实现 QFileSystemWatcher 并检查已删除/重命名的文件。
Also note that to speed up things a bit I'm adding the source filesystem model to the init of the delegate so that it doesn't need to be found each time it's required for painting or mouse tracking.另请注意,为了加快速度,我将源文件系统模型添加到委托的 init 中,这样每次需要绘制或鼠标跟踪时都不需要找到它。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM