Reputation: 487
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.
This might not even be possible with the Native QTreeView
but if anyone could guide me in the right direction that would be awesome!
To conclude more of what I would want is to have a QPushButton
appear where that red square is below.
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_())
Upvotes: 2
Views: 2820
Reputation: 48489
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.
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.
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. 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.
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.
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.
Upvotes: 2