我有一个
QListWidget
,它由QLabel
通过.setItemWidget()
和拖放模式InternalMove
填充,当我在列表中移动一个项目时,它的标签消失了。
我该如何解决这个问题?
重现的最小示例
from PyQt5.QtWidgets import (
QApplication, QLabel, QStyle,
QListWidget, QListWidgetItem
)
from PyQt5.QtCore import QSize
import sys
if __name__ == '__main__':
app = QApplication(sys.argv)
list = QListWidget()
list.setFixedHeight(400)
list.setDragDropMode(QListWidget.DragDropMode.InternalMove)
for _ in range(8):
item = QListWidgetItem()
item.setSizeHint(QSize(40, 40))
list.addItem(item)
label = QLabel()
label.setPixmap(list.style().standardIcon(
QStyle.StandardPixmap.SP_ArrowUp).pixmap(QSize(40,40)))
list.setItemWidget(item, label)
list.show()
sys.exit(app.exec())
编辑
阅读
.setItemWidget()
的文档后,其中指出:
此函数只能用于显示静态内容以代替列表小部件项目。如果您想显示自定义动态内容或实现自定义编辑器小部件,请改用 QListView 和子类 QStyledItemDelegate。
我想知道这是否与问题有关,“静态内容”在这种情况下是什么意思,
QLabel
是否被视为“动态内容”?
编辑#2
问题是在一个
dropEvent()
里面一个dropMimeData()
被调用,这又创建了一个全新的项目? (rowsInserted
被调用),我猜这不应该发生在 self 项目上,因为拖动项目中的小部件集没有序列化并存储在 mimedata
中所以小部件是解耦的,dropMimeData()
是通常在您从不同列表中拖放项目时调用。
所以我想解决这个问题的一个丑陋的方法是通过
QListWidget.mimeData()
将手动序列化的小部件存储在mimetype
中作为自定义QMimeData.setData()
并在放入QListWidget.dropMimeData()
后重新创建小部件。
例如:
from PyQt5.QtWidgets import (
QApplication, QLabel, QStyle,
QListWidget, QListWidgetItem
)
from PyQt5.QtCore import QSize, QMimeData, QBuffer, QIODevice
from PyQt5.QtGui import QPixmap
import pickle
import sys
class ListWidget(QListWidget):
def mimeData(self, items:list[QListWidgetItem]) -> QMimeData:
mimedata = QListWidget.mimeData(self, items)
# e.g. serialize pixmap
custommime = []
for item in items:
label:QLabel = self.itemWidget(item)
buff = QBuffer()
buff.open(QIODevice.OpenModeFlag.WriteOnly)
label.pixmap().save(buff, 'PNG')
buff.close()
custommime.append(buff.data())
mimedata.setData('application/custommime', pickle.dumps(custommime))
#
return mimedata
def dropMimeData(self, index:int, mimedata:QMimeData, action) -> bool:
result = QListWidget.dropMimeData(self, index, mimedata, action)
# e.g. recreate pixmap
if mimedata.hasFormat('application/custommime'):
for i, data in enumerate(
pickle.loads(mimedata.data('application/custommime')),
start=index):
pixmap = QPixmap()
pixmap.loadFromData(data, 'PNG')
label = QLabel()
label.setPixmap(pixmap)
self.setItemWidget(self.item(i), label)
#
return result
if __name__ == '__main__':
app = QApplication(sys.argv)
list = ListWidget()
list.setFixedHeight(400)
list.setDragDropMode(QListWidget.DragDropMode.InternalMove)
list.setSelectionMode(QListWidget.SelectionMode.ExtendedSelection)
for i in range(8):
item = QListWidgetItem()
item.setSizeHint(QSize(40, 40))
list.addItem(item)
label = QLabel()
label.setPixmap(list.style().standardIcon(
QStyle.StandardPixmap.SP_DialogOkButton + i).pixmap(QSize(40,40)))
list.setItemWidget(item, label)
list.show()
sys.exit(app.exec())
更新
该错误现已在 Qt-6.6.0(和 Qt-6.2.8 - LTS Commercial)中修复。它可能永远不会向后移植到 Qt5,因此仍然需要下面给出的解决方法。
这是由 Qt 错误引起的,该错误仅影响相当新的版本。我可以在使用 Qt-5.15.6 和 Qt-6.4.0 时始终如一地重现它 - 但不是例如Qt-5.12.1。这个问题似乎与QTBUG-100128密切相关。
PyQt5/6 的解决方法(基于 PaddleStroke 的解决方案)如下:
class ListWidget(QListWidget):
def dragMoveEvent(self, event):
if ((target := self.row(self.itemAt(event.pos()))) ==
(current := self.currentRow()) + 1 or
(current == self.count() - 1 and target == -1)):
event.ignore()
else:
super().dragMoveEvent(event)
旧答案:
不幸的是,经过今天的一些进一步实验,下面给出的建议解决方法似乎不是一个有效的解决方案。我发现也可以通过拖放到非空白区域来使项目小部件消失。
在测试Qt5的其他一些版本后,我可以确认5.12.x、5.13.x、5.14.x、5.15.0和5.15.1完全没有这个bug。这与上面现有的 Qt 错误报告一致,该报告将 Qt-5.15.2 确定为引入错误的版本。
与问题中的建议相反,没有理由不将标签用作项目小部件。术语“静态内容”,仅表示“不由用户定义的自定义绘图更新”。
这个错误似乎是 QTBUG-87057 的回归,它对拖放过程中列表视图行的移动方式进行了大量内部更改。这些更改的复杂性可能意味着无法通过简单的解决方法来消除其负面影响。这些更改会影响所有大于 5.15.1 的 Qt5 版本和大于 6.0 的 Qt6 版本。
class ListWidget(QListWidget):
def dropEvent(self, event):
if (self.currentRow() < self.count() - 1 or
self.itemAt(event.pos()) is not None):
super().dropEvent(event)
list = ListWidget()
...
或使用事件过滤器:
class Monitor(QObject):
def eventFilter(self, source, event):
if event.type() == QEvent.Drop:
view = source.parent()
if (view.currentRow() == view.count() - 1 and
view.itemAt(event.pos()) is None):
return True
return super().eventFilter(source, event)
monitor = Monitor()
list = QListWidget()
list.viewport().installEventFilter(monitor)
...
这是防止错误发生的方法:
class QListWidgetDragBugFix : public QListWidget
{
Q_OBJECT
public:
QListWidgetDragBugFix(QWidget *parent);
~QListWidgetDragBugFix() override;
protected:
void dragMoveEvent(QDragMoveEvent *e) override;
};
QListWidgetDragBugFix::QListWidgetDragBugFix(QWidget * parent)
: QListWidget(parent)
{
}
QListWidgetDragBugFix::~QListWidgetDragBugFix()
{
}
/* Qt has a recent bug (2023, https://bugreports.qt.io/browse/QTBUG-100128)
* where the items disappears in certain conditions when drag and dropping.
* Here we prevent the situation where this happens.
* 1 - If the item is dropped on the item below such that the item doesn't move (ie superior half of the below item)
* 2 - The item is the last one and user drop it on the empty space below.
* In both those cases the item widget was lost.
*/
void QListWidgetCustom::dragMoveEvent(QDragMoveEvent *e)
{
if ((row(itemAt(e->pos())) == currentRow() + 1)
|| (currentRow() == count() - 1 && row(itemAt(e->pos())) == -1)) {
e->ignore();
}
else {
QListWidget::dragMoveEvent(e);
}
}