简体   繁体   中英

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

Just for a sport of it I am playing around with a demo code from @ekhumoro (all credits for original Qt4 code goes to him), where he inserted a new line of QLineEdit widgets into QHeaderview of QTableView . I ported the code to Qt5 and started to add different widget to the header. No problems with a QComboBox , QCheckBox , an empty space ( QWidget ) and a QPushButton .

在此处输入图像描述

However, when I created a composed QWidget containting a QHBoxLayout with a QPushButton (it's the one with "=" sign, in column "Three") and a QLineEdit . All the controls are linked to relevant slots and it runs fine, including the QLineEdit from the composed field in column Three, but except the QPushButton from that composed widget. The ChangeIntButtonSymbol(self) slot def should cycle the button's Text between <|=|> values. I always get an error:

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

which indicates, that unlike in other cases, here the context of the parent (retrieved by self.sender() ) widget is different, the def received FilterHeader class as a parent instead the btn . I tried also passing an argument using lambda:

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

...but the result was exactly the same (with different wording in the error).

Clearly, I'm not getting fully the architecture of this QHeaderView extension and making some basic mistake. Full demo bellow, the problem occures when "=" button is clicked, any solutions or hints appreciated.

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() is a method that indicates to which object the signal belongs, and it is obvious changebuttonsymbol belongs to the header that obviously does not have a text() method. On the other hand, it is better for each class to manage its own objects, so the change of the button text must be implemented in the header.

Finally, if a complex widget is used, it is better to have it in a 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_())

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

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