移动单元格时崩溃在TableWidget中四处移动



我正在编写一个工具,该工具允许我沿着预先定义的阶段的路径跟踪一些任务,从积压工作到待办事项,通过在制品、审查,最后完成。

我创建了一个自定义的小部件,它最终将是黄色的,与postit注释没有什么不同,也许还需要对它进行一些格式化,以使它有一个漂亮的框架,等等……但由于这个问题,它在足够长的距离使它看起来正确之前就停止了。

这个想法是,每个黄色的Task小部件都有一个它们所处的阶段,我可以在Table Widget中选择它们,并将它们移动到下一个或上一个阶段,这将更新taht对象阶段,然后刷新TableWidget,读取所有小部件以及它们应该在哪里,并将其设置在新的位置。

所以我让它在某种程度上起作用(如下(,我可以向前移动任务,它们会更新位置,但我注意到,当我单击小部件以前所在的单元格时,print语句仍然显示该单元格仍然有一个小部件(这有点道理,因为下面的代码没有删除前一个,但我希望在视觉上仍然可以看到它(。我可以向前和向后移动它们,任务的信息确实会正确更新,但除非任务移动到一个从未包含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]

主窗口小部件

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

根据我以前的经验,我总是发现使用setCellWidget很笨拙、性能不佳且有缺陷。大多数时候,我的小工具都丢失或放错地方了,而刷新桌子的方式与你现在的方式类似。此外,我想你会想在更大范围内使用这个"任务移动器",从我所看到的,在QWidgetItems中设置单独的Widgets在加载项目时会变得非常慢。

我的建议是使用样式委托,这样你就可以根据自己的喜好定制物品的外观,而不必处理setCellWidget的东西,这会给你带来问题。

一旦你有了自己的委托,并按照你想要的方式绘制项目,你就可以通过使用"take"one_answers"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_())

没有必要再次清理和创建所有内容,只需移动小部件即可。我们必须知道它是否可以移动,并且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("--->")
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()

崩溃是因为在使用clear时,您还删除了Task小部件,因此"self.tasks"从C++中删除了不应该使用的对象。

最新更新