Сбои при перемещении cellWidget вокруг в TableWidget - PullRequest
1 голос
/ 28 октября 2019

Я пишу инструмент, который позволяет мне отслеживать некоторые задачи по пути предопределенных этапов, от чего-то из невыполненной работы до ToDo, через WIP, Review и, наконец, до выполнения.

Я создал пользовательскийвиджет, который в конечном итоге станет желтым, мало чем отличающимся от заметки postit и, возможно, с небольшим форматированием, чтобы придать ей красивый кадр и т. д. ... но остановился, прежде чем получить достаточно далеко, чтобы она выглядела правильно из-за этой проблемы.

Идея состоит в том, что у каждого из этих желтых виджетов задач будет стадия, на которой они находятся, и что я могу выбрать их в виджете таблицы и переместить их на следующую или предыдущую стадию, которая обновит стадию объектов taht,затем обновите TableWidget, прочитайте все виджеты и где они должны быть и установите их на новом месте.

Так что у меня есть вид работы до некоторой степени (ниже), где я могу перемещать задачи вперед иони обновляют местоположение, но я заметил, что когда я щелкаю по ячейкам, в которых ранее был виджет, оператор print по-прежнему говорит, что ячейкаУ ill там есть виджет (который имеет смысл, так как приведенный ниже код не удаляет предыдущий, но я ожидаю увидеть его все еще визуально). И я могу перемещать их вперед и назад, и информация о задачах обновляется правильно, но таблица не будет обновляться, если задача не перемещается в ячейку, в которой никогда не было cellWidget. Проверьте это, сдвинув его назад. Это работает, движение вперед визуально ничего не делает, но движение снова появляется.

Я попытался очистить TableWidget и восстановить с нуля, и это вылетело. Основная проблема, с которой я сталкиваюсь, заключается в том, что со всеми этими сбоями, которая сама по себе является проблемой, поскольку делает отладку очень сложной ... Когда я пытаюсь очистить TableWidget (с помощью .clear ()) перед повторным заполнением, я получаю это.

Process finished with exit code -1073741819 (0xC0000005)

Тот же код ошибки, если я пытаюсь удалить старые ячейки, установив виджет таблицы в 0 строк перед добавлением правильного количества строк.

Известная проблема, которая менее важна, - это когда я выбираю ячейку без виджета и пытаюсь переместить ее, мне это кажется, но я не слишком беспокоюсь об этом исправлении, поскольку это известная проблема.

Process finished with exit code -1073740791 (0xC0000409)

Также попытался выполнить очистку путем итерации каждой ячейки, и, если у нее есть виджет ячейки, удалите виджет ячейки перед тем, как переустановить их в правильное место, и он все еще падает. У меня нет идей.

Виджет задач

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_())

Ответы [ 2 ]

1 голос
/ 28 октября 2019

Из моего предыдущего опыта я всегда находил использование setCellWidget неуклюжим, неэффективным и с ошибками. В большинстве случаев мои виджеты были утеряны или потеряны при обновлении таблицы так же, как вы это делаете. Кроме того, я полагаю, вы захотите использовать этот «Task Mover» в большем масштабе, и, как я мог видеть, установка отдельных виджетов внутри QWidgetItems становится довольно медленной, когда выполняется для множества элементов.

Мое предложениебудет использовать делегатов стилей, так что вы сможете настроить внешний вид ваших элементов по своему вкусу, без необходимости иметь дело с вещами setCellWidget, которые доставляют вам проблемы.

Как только у вас есть собственный делегат, и рисуйтеэлементы, как вы хотите, вы можете просто продолжать обновлять эти данные элемента и перемещать элементы вокруг таблицы, используя «take» и «set».

Я не уверен, будет ли это лучшим способомвыполнение этой конкретной задачи, но движение в этом направлении, вероятно, даст вам большую гибкость и возможности настройки в долгосрочной перспективе.

import sys
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *


class TaskProperty():
    properties = ["ID", "name", "est", "stage"]
    count = 4
    ID, Name, Est, Stage =  [Qt.UserRole + x for x in range(count)]


STAGES = ['Backlog', 'ToDo', 'WIP', 'Review', 'Done']

class MainWidget(QWidget):
    def __init__(self):
        super(MainWidget, self).__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()
        # create and set the delegate to the TableWidget
        self.delegate = TaskDelegate(self.tw )
        self.tw.setItemDelegate(self.delegate)

        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()]


    def move_task(self, forward=True):
        '''
        To move the task to the next step, we iterate all the items selected.
        If the task can be moved, we take the corresponding item from its current cell and move it to the destination.
        :param forward:
        :return:
        '''
        selected =self.tw.selectedItems()
        for item in selected:
            item.setSelected(False)
            result = item.task_move(forward=forward)
            if result:
                next = 1 if forward else -1
                row = item.row()
                column = item.column()
                moveItem = self.tw.takeItem(row, column)
                self.tw.setItem(row, column + next, moveItem)
                moveItem.setSelected(True)

    def cell_clicked(self, row, column):
        item = self.tw.item(row, column)
        if not isinstance(item, TaskItem):
            print "No Task Item Here"

    def update_tw(self):
        # I wanted to clear the Table widget and rebuild, but this crashes
        # self.tw.clear()
        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 row, object in enumerate(self.tasks):
            # create items of our custom type only for the column that need to be filled.
            # the other cells will be filled with null items.
            column = STAGES.index(object.stage)
            print('Setting stage {} for {}\n...to r={}, c={}\n***'.format(object.stage, object, row, column))
            item = TaskItem(object)
            self.tw.setItem(row, column, item)



class TaskDelegate(QStyledItemDelegate):
    '''
    This delegate take care of Drawing our cells the way we want it to be.
    '''
    def paint(self, painter, option, index):
        '''
        Override the Paint function to draw our own cell.
        If the QTableWidgetItem does not have our Data stored in it, we do a default paint
        :param painter:
        :param option:
        :param index:
        :return:
        '''
        painter.save()
        rect = option.rect

        status = index.data(TaskProperty.Stage)
        if status is None:
            return super(TaskDelegate, self).paint(painter, option, index)
        else:
            id = STAGES.index(status)
            pen = painter.pen()
            pen.setBrush(Qt.black)
            painter.setPen(pen)
            if id == index.column():
                rect.translate(3, 3)
                newRect = QRect(rect.x(), rect.y(), rect.width() - 6, 20)
                infos = [index.data(TaskProperty.ID), index.data(TaskProperty.Name), index.data(TaskProperty.Est)]
                painter.setBrush(Qt.red)
                painter.drawRect(newRect)
                painter.setBrush(Qt.yellow)
                for info in infos:
                    newRect.translate(0, 25)
                    painter.drawRect(newRect)
                    painter.drawText(newRect, Qt.AlignHCenter | Qt.AlignVCenter,
                                 str(info))



class TaskItem(QTableWidgetItem):
    '''
    Subclass QTableWidgetItem.
    Probably not needed, since we can set the property when we create the item instead of in the init,
    and keep track of which item is attached to which task object using the Column Index of the table.
    However, this can be useful if you want to attach more specific procedures to your items
    '''
    def __init__(self, task):
        super(TaskItem, self).__init__()
        self._task = task
        self.setData(TaskProperty.ID, task.ID)
        self.setData(TaskProperty.Name, task.name)
        self.setData(TaskProperty.Est, task.est)
        self.setData(TaskProperty.Stage, task.stage)

        self.objects_labels = {}

    def task_move(self, forward=True):
        result = self._task.task_move(forward=forward)
        self.setData(TaskProperty.Stage, self._task.stage)
        return result

class Task(object):
    '''
    The Task class is now just an object, not a widget.
    '''

    def __init__(self, ID, name, est):
        # 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 = {}

    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)
        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

if __name__ == '__main__':
    app = QApplication(sys.argv)
    ex = MainWidget()
    sys.exit(app.exec_())
0 голосов
/ 28 октября 2019

Нет необходимости чистить и создавать все заново, вместо этого просто переместите виджет, для этого мы должны знать, может ли он быть перемещен или нет, и для этого task_move должен указывать, является ли движение допустимым или нет. Учитывая вышеизложенное, решение выглядит следующим образом:

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("---&gt")
        if self.stage == self.stages[-1]:
            print("Already at the end of process")
            <b>return False</b>
        self.stage = self.stages[index + 1]
    else:
        print("&lt---")
        if self.stage == self.stages[0]:
            print("Already at the start of process")
            <b>return False</b>
        self.stage = self.stages[index - 1]
    <b>return True</b>
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()

Сбой произошел из-за того, что при использовании clear вы также удаляете виджет Task, поэтому в файле self.tasks удаляются объекты из C ++, которые вы не должны использовать.

...