Search code examples
pythonpyqtpyqt5qwidgetqtablewidget

Crashes when moving cellWidgets around in a TableWidget


I am writing a tool that allows me to track some tasks along a path of predifined stages, from something on a backlog, to ToDo, through WIP, Review and finally to done.

I created a custom widget, that will eventually be yellow, not unlike a postit note and perhaps with a bit of formatting it to give it a nice frame, etc... but stopped before getting far enough to make it look right because of this issue.

The idea is that each of these yellow Task widgets will have a stage they are at, and that I can select them in a Table Widget, and move them onto the next or previous stage, which will update taht objects stage, then refresh the TableWidget, read all the widget and where thay should be and set them in their new place.

So I have it kind of working to some degree (below), where I can move the tasks forward and they update location, but I noticed when I click the cells that the widget was previously in, print statement still says that the cell still has a widget there (which kind of makes sense, as code below isn't removing the previous one, but I'd expect to visually still see it). And I can move them forward and backwards, and the information on the tasks does update correctly, but the table won't refresh unless the task moves to a cell that never had a cellWidget in it. Test this by moving it backwards. It works, movnig forward visually does nothing, but moving again, does show up.

I tried clearing the TableWidget and rebuilding from scratch and that crashes. The main issue I am having is that with all these crashes, which is an issue in itself as it makes debugging very tough... When I try and clear the TableWidget (with .clear()) before repopulating, I get this.

Process finished with exit code -1073741819 (0xC0000005)

Same error code if I try removing the old cells by setting the Table Widget to 0 rows before adding the correct number of rows.

A known issue that is less important is when I select a cell without a widget and try and move it, gies me this, but don't worry too much about that fix, as it's known issue.

Process finished with exit code -1073740791 (0xC0000409)

Also tried cleaning up by iterating every cell and if it has a cell widget, remove cell widget before re-setting them to correct place and it still crashes. I'm out of ideas.

Task Widget

import sys
from PyQt5.QtWidgets import (QApplication, QTableWidget, QWidget, QFrame, QHBoxLayout, QLabel,
                            QPushButton,QVBoxLayout)

class Task(QWidget):
    def __init__(self, ID, name, est):
        super(Task, self).__init__()
        # Creates a small widget that will be added to a table widget
        self.ID = ID
        self.name = name
        self.est = est
        #  These cell widgets represent tasks. So each task has a particular 'stage' it is at
        self.stage = 'ToDo'
        self.stages = ['Backlog', 'ToDo', 'WIP', 'Review', 'Done']
        self.objects_labels = {}
        self.initUI()

    def initUI(self):
        # adds a bunch of labels to the widget
        layout = QVBoxLayout()
        frame = QFrame()
        frame.setFrameShape(QFrame.StyledPanel)
        frame.setStyleSheet('background-color: red')
        frame.setLineWidth(2)
        layout.addWidget(frame)
        info = [self.ID, self.name, self.est]
        for section in info:
            self.objects_labels[section] = QLabel(str(section))
            layout.addWidget(self.objects_labels[section])
        self.setLayout(layout)
        self.setStyleSheet('background-color: yellow')

    def task_move(self, forward = True):
        # The main widget will allow me to change the stage of a particular Task
        # The idea is that I update the Table widget to show everything in the right place
        # This function finds out what stage it is at and increments/decrements by one
        index = self.stages.index(self.stage)
        print(self.stages)
        print(index)
        if forward:
            print('--->')
            if self.stage == self.stages[-1]:
                print('Already at the end of process')
                return
            self.stage = self.stages[index + 1]
        else:
            print('<---')
            if self.stage == self.stages[0]:
                print('Already at the start of process')
                return
            self.stage = self.stages[index - 1]

MainWidget

class MainWidget(QWidget):
    def __init__(self):
        super().__init__()
        self.tasks = self.make_tasks()
        self.init_ui()
        self.update_tw()

    def make_tasks(self):
        # Create a few tasks
        a = Task(0, 'Name_A', 44)
        b = Task(0, 'Name_B', 22)
        c = Task(0, 'Name_C', 66)
        d = Task(0, 'Name_D', 90)

        return [a, b, c, d]

    def init_ui(self):
        layout_main = QVBoxLayout()

        self.tw = QTableWidget()
        self.tw.cellClicked.connect(self.cell_clicked)
        self.tw.horizontalHeader().setDefaultSectionSize(120)
        self.tw.verticalHeader().setDefaultSectionSize(120)

        layout_main.addWidget(self.tw)
        layout_bottom_button_bar = QHBoxLayout()

        self.btn_task_backward = QPushButton('<--- Task')
        self.btn_task_backward.clicked.connect(lambda: self.move_task(forward=False))

        self.btn_task_forward = QPushButton('Task --->')
        self.btn_task_forward.clicked.connect(lambda: self.move_task())

        for widget in [self.btn_task_backward, self.btn_task_forward]:
            layout_bottom_button_bar.addWidget(widget)

        layout_main.addLayout(layout_bottom_button_bar)

        self.setLayout(layout_main)
        self.setGeometry(300, 300, 800, 600)
        self.setWindowTitle('MainWidget')
        self.show()

    @property
    def tw_header(self):
        return {'Backlog': 0, 'ToDo': 1, 'WIP': 2, 'Review': 3, 'Done': 4}

    @property
    def selected_indices(self):
        return [(x.row(), x.column()) for x in self.tw.selectedIndexes()]

    @property
    def selected_widgets(self):
        selected_widgets = [self.tw.cellWidget(x[0], x[1]) for x in self.selected_indices]
        print(selected_widgets)
        return selected_widgets


    def move_task(self, forward=True):
        # Crashes if you select a non-widget cell, but thats a known issue
        # Moves the task forward or backward and then prompts to update the TableWidget
        for object in self.selected_widgets:
            object.task_move(forward=forward)
        self.tw.clearSelection()
        self.update_tw()

    def cell_clicked(self, row, column):
        if self.tw.cellWidget(row, column):
            print(self.selected_indices)
            print(self.selected_widgets)
        else:
            print('No Cell Widget here')

    def update_tw(self):
        #I wanted to clear the Table widget and rebuild, but this crashes
        # self.tw.clear()
        self.tw.setHorizontalHeaderLabels(self.tw_header.keys())
        rows = len(self.tasks)
        columns = len(self.tw_header)
        self.tw.setRowCount(rows)
        self.tw.setColumnCount(columns)
        # Looks through each task, and then gets it's stage, and then adds the widget to the correct column
        for index, object in enumerate(self.tasks):
            column = self.tw_header[object.stage]
            print('Setting stage {} for {}\n...to r={}, c={}\n***'.format(object.stage, object, index, column))
            self.tw.setCellWidget(index, column, object)


if __name__ == '__main__':
    app = QApplication(sys.argv)
    ex = MainWidget()
    sys.exit(app.exec_())

Solution

  • It is not necessary to clean and create everything again, instead just move the widget for it we must know if it can be moved or not and for that task_move must indicate if the movement is valid or not. Considering the above, the solution is:

    def task_move(self, forward=True):
        # The main widget will allow me to change the stage of a particular Task
        # The idea is that I update the Table widget to show everything in the right place
        # This function finds out what stage it is at and increments/decrements by one
        index = self.stages.index(self.stage)
        print(self.stages)
        print(index)
        if forward:
            print("--->")
            if self.stage == self.stages[-1]:
                print("Already at the end of process")
                return False
            self.stage = self.stages[index + 1]
        else:
            print("<---")
            if self.stage == self.stages[0]:
                print("Already at the start of process")
                return False
            self.stage = self.stages[index - 1]
        return True
    def move_task(self, forward=True):
        for row, column in self.selected_indices:
            widget = self.tw.cellWidget(row, column)
            if isinstance(widget, Task) and widget.task_move(forward):
                next_column = column + (1 if forward else -1)
                # create new task widget
                task = Task(widget.ID, widget.name, widget.est)
                # remove all task widget
                self.tw.removeCellWidget(row, column)
                # move task widget
                self.tw.setCellWidget(row, next_column, task)
        self.tw.clearSelection()
    

    The crashed is because when using clear you are also removing the Task widget so "self.tasks" has objects deleted from C++ that you should not use.