0

I have a custom QWidget that I have embedded into a QTableWidget. When I toggle the QCheckBoxes and modify the text in the QLineEdit widgets, the program is not able to distinguish the widgets in rows 2 and 1 from the widgets in row 0. How can I change the program so that it prints the correct row and column of the QLineEdit widget that is being edited or the Checkbox that is being toggled?

Figure 1 shows a screenshot of the program with the output after selecting the third checkbox many times in Visual Studio Code. The output is expected to read “2 0” repeatedly but instead it reads “0 0”.

Figure 2 Similarly, when I modify the text in the QLineEdit in cell 2,0 from “My Custom Text” to “Text” the program prints “Handle Cell Edited 0,0”, although it is expected to print “Handle Cell Edited 2,0 Cell 2,0 was changed to Text”.

Code:

# Much of this code is copy pasted form user: three_pineapples post on stackoverflow: 
#    https://stackoverflow.com/a/26311179/18914416
import sys
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import QWidget, QHBoxLayout, QTableWidget, \
    QApplication, QTableWidgetItem, QLineEdit, QCheckBox
from PyQt5 import QtGui

class SimpleTable(QTableWidget):
    def __init__(self,window):
        # Call the parent constructor
        QTableWidget.__init__(self)
        self.window = window
        

class myWidget(QWidget):
    #This code is adapted paritally form a post by user sebastian at:
    #https://stackoverflow.com/a/29764770/18914416
    def __init__(self,parent=None):
        super(myWidget,self).__init__()
        self.Layout1 = QHBoxLayout()
        self.item = QLineEdit("My custom text")
        #https://stackabuse.com/working-with-pythons-pyqt-framework/
        
        self.Checkbox = QCheckBox()
        self.Checkbox.setCheckState(Qt.CheckState.Unchecked)

        self.Layout1.addWidget(self.Checkbox)
        self.Layout1.addWidget(self.item)
        #https://stackoverflow.com/questions/29764395/adding-multiple-widgets-to-qtablewidget-cell-in-pyqt

        self.item.home(True)
        #https://www.qtcentre.org/threads/58387-Left-text-alignment-for-long-text-on-QLineEdit
        
        self.setLayout(self.Layout1)
        
class Window(QWidget):
    def __init__(self):
        super(Window, self).__init__()
        layout = QHBoxLayout()
        self.setLayout(layout)
        
        self.table_widget = SimpleTable(window=self)
        layout.addWidget(self.table_widget)
        self.table_widget.setColumnCount(3)
        self.table_widget.setHorizontalHeaderLabels(['Colour', 'Model'])

        items = [('Red', 'Toyota'), ('Blue', 'RV'), ('Green', 'Beetle')]
        for i in range(len(items)):
            c = QTableWidgetItem(items[i][0])
            m = QTableWidgetItem(items[i][1])
             
            self.table_widget.insertRow(self.table_widget.rowCount())
            self.table_widget.setItem(i, 1, c)
            self.table_widget.setItem(i, 2, m)

            myWidget1 = myWidget()
            myWidget1.Checkbox.stateChanged.connect(self.handleButtonClicked)
            myWidget1.item.editingFinished.connect(self.handle_cell_edited)      
            self.table_widget.setCellWidget(i,0,myWidget1)
            myWidget1.Layout1.setContentsMargins(50*i+10,0,0,0)

        self.show()

        self.table_widget.itemChanged.connect(self.handle_cell_edited)


    def handleButtonClicked(self):
        #Adapted from a post by user: Andy at:
        # https://stackoverflow.com/a/24149478/18914416    
        button = QApplication.focusWidget()
        # or button = self.sender()
        index = self.table_widget.indexAt(button.pos())
        if index.isValid():
            print(index.row(), index.column())
    
    # I added this fuction:
    def handle_cell_edited(self):
        if QApplication.focusWidget() != None:
            index = self.table_widget.indexAt(QApplication.focusWidget().pos())
            x,y = index.column(),index.row()
            if index.isValid():
                print("Handle Cell Edited",index.row(), index.column())
            if self.table_widget.item(y,x)!= None:
                print(f"Cell {x},{y} was changed to {self.table_widget.item(y,x).text()}.")

def main():
    app = QApplication(sys.argv)
    window = Window()
    sys.exit(app.exec_())
main()

What I've Tried So Far:

I learned that QT has two types of widgets that can be embedded in a table; a QTableWigetItem which can be inserted into a table using setItem()(3) and Qwidgets, which can be placed into a table using setCellWidget().(4) Generally, I know that using a QTableWigetItem one can set the item.setFlags(Qt.ItemFlag.ItemIsUserCheckable) flag to create a checkbox in the cell. (3) However, when using the QTableWigetItem, I wasn’t able to find a way to indent the checkboxes. Because giving each checkbox its own indentation level is important in the context of my program, I’ve decided to use Qwidgets instead of QTableWigetItems in the few select cells where indenting is important. I’ve read that by creating a QItemDelegate(5)(6), you can do a lot more with setting QWidgets in boxes. However, creating a delegate seems complicated, so I’d prefer to avoid this if possible. If there is no other way to make the program register the correct cell number of the cell being edited, creating a delegate will be the next thing I look into.

For anyone who might want to experiment with QTableWigetItems in this application, here is an equivalent program that uses QTableWigetItems instead of QWidgets but doesn't permit separate indentation or editing of the text field in column 0. For either and both of these two reasons, a QTableWigetItem seems not to be usable for the checkboxes in column 0.

Less Successful Attempt using QTableWidgetItem:

#Much of this code is copy pasted form user: three_pineapples post on stackoverflow:
     #  https://stackoverflow.com/a/26311179/18914416

import sys
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import QWidget, QHBoxLayout, QTableWidget, \
    QApplication, QTableWidgetItem, QLineEdit, QCheckBox
from PyQt5 import QtGui

class SimpleTable(QTableWidget):
    def __init__(self,window):
        QTableWidget.__init__(self)
        self.window = window
        
class Window(QWidget):
    def __init__(self):
        super(Window, self).__init__()
        layout = QHBoxLayout()
        self.setLayout(layout)
        
        self.table_widget = SimpleTable(window=self)
        layout.addWidget(self.table_widget)
        self.table_widget.setColumnCount(3)
        self.table_widget.setHorizontalHeaderLabels(['Colour', 'Model'])

        items = [('Red', 'Toyota'), ('Blue', 'RV'), ('Green', 'Beetle')]
        for i in range(len(items)):
            c = QTableWidgetItem(items[i][0])
            m = QTableWidgetItem(items[i][1])
             
            self.table_widget.insertRow(self.table_widget.rowCount())
            self.table_widget.setItem(i, 1, c)
            self.table_widget.setItem(i, 2, m)
            
            item = QTableWidgetItem("My Custom Text")
            item.setFlags(Qt.ItemFlag.ItemIsUserCheckable| Qt.ItemFlag.ItemIsEnabled)
            item.setCheckState(Qt.CheckState.Unchecked)
            self.table_widget.setItem(i,0,item)
            #https://youtu.be/DM8Ryoot7MI?t=251

        self.show()

        #I added this line:
        self.table_widget.itemChanged.connect(self.handle_cell_edited)



    def handleButtonClicked(self):
        #Adapted from a post by user: Andy at:
        # https://stackoverflow.com/a/24149478/18914416    
        button = QApplication.focusWidget()
        # or button = self.sender()
        index = self.table_widget.indexAt(button.pos())
        if index.isValid():
            print(index.row(), index.column())
    
    # I added this fuction:
    def handle_cell_edited(self):
        if QApplication.focusWidget() != None:
            index = self.table_widget.indexAt(QApplication.focusWidget().pos())
            x,y = index.column(),index.row()
            if index.isValid():
                print("Handle Cell Edited",index.row(), index.column())
            if self.table_widget.item(y,x)!= None:
                print(f"Cell {x},{y} was changed to {self.table_widget.item(y,x).text()}.")

def main():
    app = QApplication(sys.argv)
    window = Window()
    sys.exit(app.exec_())
main()

Bibliography:

1.https://i.stack.imgur.com/FudE3.png

2.https://i.stack.imgur.com/C2ypp.png

3.https://youtu.be/DM8Ryoot7MI?t=251

4.https://stackoverflow.com/questions/24148968/how-to-add-multiple-qpushbuttons-to-a-qtableview/24149478#24149478

5.Creating a QItemDelegate for QWidgets, https://stackoverflow.com/a/35418141/18914416

6.Need to create a QItemDelegate to add a stylesheet to QTableWidgetItems: https://forum.qt.io/topic/13124/solved-qtablewidgetitem-set-stylesheet

spamme180
  • 3
  • 2

1 Answers1

1

The geometry of a widget is always relative to its parent.

In your first example, the problem is that the pos() returned for the widget is relative to the myWidget container, and since the vertical position is always a few pixels below the top of the parent (the layout margin), you always get the same value.

The second example has another conceptual problem: the checkbox of a checkable item is not an actual widget, so the widget you get is the table itself.

    def handle_cell_edited(self):
        # this will print True
        print(isinstance(QApplication.focusWidget(), QTableWidget))

As explained above, the geometry is always relative to the parent, so you will actually get the position of the table relative to the window.

The solution to the first case is quite simple, as soon as you understand the relativity of coordinate systems. Note that you shall not rely on the focusWidget() (the widget might not accept focus), but actually get the sender(), which is the object that emitted the signal:

    def handleButtonClicked(self):
        sender = self.sender()
        if not self.table_widget.isAncestorOf(sender):
            return
        # the widget coordinates must *always* be mapped to the viewport
        # of the table, as the headers add margins
        pos = sender.mapTo(self.table_widget.viewport(), QPoint())
        index = self.table_widget.indexAt(pos)
        if index.isValid():
            print(index.row(), index.column())

In reality, this might not be that necessary, as an item delegate will suffice if the indentation is the only requirement: the solution is to properly set the option.rect() within initStyleOption() and use a custom role for the indentation:

IndentRole = Qt.UserRole + 1

class IndentDelegate(QStyledItemDelegate):
    def initStyleOption(self, opt, index):
        super().initStyleOption(opt, index)
        indent = index.data(IndentRole)
        if indent is not None:
            left = min(opt.rect.right(), 
                opt.rect.x() + indent)
            opt.rect.setLeft(left)


class SimpleTable(QTableWidget):
    def __init__(self,window):
        QTableWidget.__init__(self)
        self.window = window
        self.setItemDelegateForColumn(0, IndentDelegate(self))


class Window(QWidget):
    def __init__(self):
        # ...
        for i in range(len(items)):
            # ...
            item.setData(IndentRole, 20 * i)
musicamante
  • 41,230
  • 6
  • 33
  • 58