在 QHeaderView class 中由 Button 触发的插槽中获得正确的 parent

Get correct parent in a slot triggered by Button inside a QHeaderView class

只是为了好玩,我正在玩 @ekhumoro(原始 Qt4 代码的所有功劳都归他所有),他在其中插入了一个QLineEdit 小部件的新行合并到 QTableViewQHeaderview 中。我将代码移植到 Qt5 并开始向 header 添加不同的小部件。 QComboBoxQCheckBox、空 space (QWidget) 和 QPushButton.

没有问题

但是,当我创建一个组合 QWidget 时,它包含一个 QHBoxLayout 和一个 QPushButton(它是带有“=”符号的那个,在“三”列中)和一个 QLineEdit。所有控件都链接到相关插槽并且运行良好,包括来自第三列中组合字段的 QLineEdit,但来自该组合小部件的 QPushButton 除外。 ChangeIntButtonSymbol(self) 插槽定义应在 <|=|> 值之间循环按钮的 Text。我总是得到一个错误:

AttributeError: 'FilterHeader' object has no attribute 'text'

这表明,与其他情况不同,此处 parent(由 self.sender() 检索)小部件的上下文不同,接收到的 def FilterHeader class作为 parent 而不是 btn。我还尝试使用 lambda 传递参数:

self.btn.clicked.connect(lambda: self.changebuttonsymbol.emit(self.btn))

...但结果完全一样(错误中的措辞不同)。

很明显,我没有完全理解这个 QHeaderView 扩展的架构并且犯了一些基本错误。完整演示如下,单击“=”按钮时出现问题,感谢任何解决方案或提示。

import sys
from PyQt5 import QtCore, QtGui
from PyQt5.QtWidgets import QHeaderView, QWidget, QLineEdit, QApplication, QTableView, QVBoxLayout,QHBoxLayout, QLineEdit, QComboBox, QPushButton, QCheckBox
from PyQt5.QtCore import pyqtSignal

class FilterHeader(QHeaderView):
    filterActivated = QtCore.pyqtSignal()
    changebuttonsymbol = QtCore.pyqtSignal()

    def __init__(self, parent):
        super().__init__(QtCore.Qt.Horizontal, parent)
        self._editors = []
        self._padding = 4
        self.setStretchLastSection(True)
        #self.setResizeMode(QHeaderView.Stretch)
        self.setDefaultAlignment(QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter)
        self.setSortIndicatorShown(False)
        self.sectionResized.connect(self.adjustPositions)
        parent.horizontalScrollBar().valueChanged.connect(self.adjustPositions)

    def setFilterBoxes(self, count):
        while self._editors:
            editor = self._editors.pop()
            editor.deleteLater()
        for index in range(count):
            if index == 1:     # Empty
                editor = QWidget()
            elif index == 2:   # Number filter (>|=|<)
                editor = QWidget(self.parent())
                edlay = QHBoxLayout()
                edlay.setContentsMargins(0, 0, 0, 0)
                edlay.setSpacing(0)
                self.btn = QPushButton()
                self.btn.setText("=")     
                self.btn.setFixedWidth(20)
                #self.btn.clicked.connect(lambda: self.changebuttonsymbol.emit(self.btn))
                self.btn.clicked.connect(self.changebuttonsymbol.emit)
                #btn.setViewportMargins(0, 0, 0, 0)
                linee = QLineEdit(self.parent())
                linee.setPlaceholderText('Filter')
                linee.returnPressed.connect(self.filterActivated.emit) 
                #linee.setViewportMargins(0, 0, 0, 0)
                edlay.addWidget(self.btn)
                edlay.addWidget(linee)
                editor.setLayout(edlay)
            elif index == 3:
                editor = QComboBox(self.parent())
                editor.addItems(["", "Combo", "One", "Two", "Three"])
                editor.currentIndexChanged.connect(self.filterActivated.emit)
            elif index == 4:
                editor = QPushButton(self.parent())
                editor.clicked.connect(self.filterActivated.emit)
                editor.setText("Button")
            elif index == 5:
                editor = QCheckBox(self.parent())
                editor.clicked.connect(self.filterActivated.emit)
                editor.setTristate(True)
                editor.setCheckState(1)
                editor.setText("CheckBox")
            else:   # string filter
                editor = QLineEdit(self.parent())
                editor.setPlaceholderText('Filter')
                editor.returnPressed.connect(self.filterActivated.emit)                
            self._editors.append(editor)
        self.adjustPositions()


    def sizeHint(self):
        size = super().sizeHint()
        if self._editors:
            height = self._editors[0].sizeHint().height()
            size.setHeight(size.height() + height + self._padding)
        return size

    def updateGeometries(self):
        if self._editors:
            height = self._editors[0].sizeHint().height()
            self.setViewportMargins(0, 0, 0, height + self._padding)
        else:
            self.setViewportMargins(0, 0, 0, 0)
        super().updateGeometries()
        self.adjustPositions()

    def adjustPositions(self):
        for index, editor in enumerate(self._editors):
            height = editor.sizeHint().height()
            CompensateY = 0
            CompensateX = 0
            if self._editors[index].__class__.__name__ == "QComboBox":
                CompensateY = +2
            elif self._editors[index].__class__.__name__ == "QWidget":
                CompensateY = -1
            elif self._editors[index].__class__.__name__ == "QPushButton":
                CompensateY = -1
            elif self._editors[index].__class__.__name__ == "QCheckBox":
                CompensateY = 4
                CompensateX = 4
            editor.move( self.sectionPosition(index) - self.offset() + 1 + CompensateX, height + (self._padding // 2) + 2 + CompensateY)
            editor.resize(self.sectionSize(index), height)

    def filterText(self, index):
        if 0 <= index < len(self._editors): 
            if self._editors[index].__class__.__name__ == "QLineEdit":
                return self._editors[index].text()
        return ''

    def setFilterText(self, index, text):
        if 0 <= index < len(self._editors):
            self._editors[index].setText(text)

    def clearFilters(self):
        for editor in self._editors:
            editor.clear()


class Window(QWidget):
    def __init__(self):
        super(Window, self).__init__()
        self.view = QTableView()
        layout = QVBoxLayout(self)
        layout.addWidget(self.view)
        header = FilterHeader(self.view)
        self.view.setHorizontalHeader(header)
        model = QtGui.QStandardItemModel(self.view)
        model.setHorizontalHeaderLabels('One Two Three Four Five Six Seven'.split())
        self.view.setModel(model)
        header.setFilterBoxes(model.columnCount())
        header.filterActivated.connect(self.handleFilterActivated)
        header.changebuttonsymbol.connect(self.ChangeIntButtonSymbol)

    def handleFilterActivated(self):
        header = self.view.horizontalHeader()
        for index in range(header.count()):
            if index != 4:
                print((index, header.filterText(index)))
            else:
                print("Button")
        
    def ChangeIntButtonSymbol(self):
        print("Int button triggered")
        nbtn = self.sender()
        print(str(nbtn))
        if nbtn.text() == "=":
            nbtn.setText(">")
        elif nbtn.text() == ">":
            nbtn.setText("<")
        else:
            nbtn.setText("=")

if __name__ == '__main__':

    app = QApplication(sys.argv)
    window = Window()
    window.setGeometry(800, 100, 600, 300)
    window.show()
    sys.exit(app.exec_())

sender() 是指示信号属于哪个 object 的方法,很明显 changebuttonsymbol 属于 header 显然没有 text() 方法。另一方面,每个 class 最好管理自己的 object,因此按钮文本的更改必须在 header.

中实现

最后,如果使用了复杂的小部件,最好将它放在 class 中。

import sys

from PyQt5.QtCore import pyqtSignal, Qt
from PyQt5.QtGui import QStandardItemModel
from PyQt5.QtWidgets import (
    QHeaderView,
    QWidget,
    QLineEdit,
    QApplication,
    QTableView,
    QVBoxLayout,
    QHBoxLayout,
    QComboBox,
    QPushButton,
    QCheckBox,
)


class Widget(QWidget):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.btn = QPushButton()
        self.btn.setText("=")
        self.btn.setFixedWidth(20)

        self.linee = QLineEdit()
        self.linee.setPlaceholderText("Filter")

        lay = QHBoxLayout(self)
        lay.setContentsMargins(0, 0, 0, 0)
        lay.setSpacing(0)
        lay.addWidget(self.btn)
        lay.addWidget(self.linee)


class FilterHeader(QHeaderView):
    filterActivated = pyqtSignal()

    def __init__(self, parent):
        super().__init__(Qt.Horizontal, parent)
        self._editors = []
        self._padding = 4
        self.setStretchLastSection(True)
        # self.setResizeMode(QHeaderView.Stretch)
        self.setDefaultAlignment(Qt.AlignLeft | Qt.AlignVCenter)
        self.setSortIndicatorShown(False)
        self.sectionResized.connect(self.adjustPositions)
        parent.horizontalScrollBar().valueChanged.connect(self.adjustPositions)

    def setFilterBoxes(self, count):
        while self._editors:
            editor = self._editors.pop()
            editor.deleteLater()
        for index in range(count):
            editor = self.create_editor(self.parent(), index)
            self._editors.append(editor)
        self.adjustPositions()

    def create_editor(self, parent, index):
        if index == 1:  # Empty
            editor = QWidget()
        elif index == 2:  # Number filter (>|=|<)
            editor = Widget(parent)
            editor.linee.returnPressed.connect(self.filterActivated)
            editor.btn.clicked.connect(self.changebuttonsymbol)
        elif index == 3:
            editor = QComboBox(parent)
            editor.addItems(["", "Combo", "One", "Two", "Three"])
            editor.currentIndexChanged.connect(self.filterActivated)
        elif index == 4:
            editor = QPushButton(parent)
            editor.clicked.connect(self.filterActivated)
            editor.setText("Button")
        elif index == 5:
            editor = QCheckBox(parent)
            editor.clicked.connect(self.filterActivated)
            editor.setTristate(True)
            editor.setCheckState(Qt.Checked)
            editor.setText("CheckBox")
        else:
            editor = QLineEdit(parent)
            editor.setPlaceholderText("Filter")
            editor.returnPressed.connect(self.filterActivated)
        return editor

    def sizeHint(self):
        size = super().sizeHint()
        if self._editors:
            height = self._editors[0].sizeHint().height()
            size.setHeight(size.height() + height + self._padding)
        return size

    def updateGeometries(self):
        if self._editors:
            height = self._editors[0].sizeHint().height()
            self.setViewportMargins(0, 0, 0, height + self._padding)
        else:
            self.setViewportMargins(0, 0, 0, 0)
        super().updateGeometries()
        self.adjustPositions()

    def adjustPositions(self):
        for index, editor in enumerate(self._editors):
            if not isinstance(editor, QWidget):
                continue
            height = editor.sizeHint().height()
            compensate_y = 0
            compensate_x = 0
            if type(editor) is QComboBox:
                compensate_y = +2
            elif type(editor) in (QWidget, Widget):
                compensate_y = -1
            elif type(editor) is QPushButton:
                compensate_y = -1
            elif type(editor) is QCheckBox:
                compensate_y = 4
                compensate_x = 4
            editor.move(
                self.sectionPosition(index) - self.offset() + 1 + compensate_x,
                height + (self._padding // 2) + 2 + compensate_y,
            )
            editor.resize(self.sectionSize(index), height)

    def filterText(self, index):
        for editor in self._editors:
            if hasattr(editor, "text") and callable(editor.text):
                return editor.text()
        return ""

    def setFilterText(self, index, text):
        for editor in self._editors:
            if hasattr(editor, "setText") and callable(editor.setText):
                editor.setText(text)

    def clearFilters(self):
        for editor in self._editors:
            editor.clear()

    def changebuttonsymbol(self):
        nbtn = self.sender()
        if nbtn.text() == "=":
            nbtn.setText(">")
        elif nbtn.text() == ">":
            nbtn.setText("<")
        else:
            nbtn.setText("=")


class Window(QWidget):
    def __init__(self):
        super(Window, self).__init__()
        self.view = QTableView()
        layout = QVBoxLayout(self)
        layout.addWidget(self.view)
        header = FilterHeader(self.view)
        self.view.setHorizontalHeader(header)
        model = QStandardItemModel(self.view)
        model.setHorizontalHeaderLabels("One Two Three Four Five Six Seven".split())
        self.view.setModel(model)
        header.setFilterBoxes(model.columnCount())
        header.filterActivated.connect(self.handleFilterActivated)

    def handleFilterActivated(self):
        header = self.view.horizontalHeader()
        for index in range(header.count()):
            if index != 4:
                print(index, header.filterText(index))
            else:
                print("Button")


if __name__ == "__main__":

    app = QApplication(sys.argv)
    window = Window()
    window.setGeometry(800, 100, 600, 300)
    window.show()
    sys.exit(app.exec_())