python-3.x QListWidgetItem内的小部件在内部移动后消失

qhhrdooz  于 2023-03-31  发布在  Python
关注(0)|答案(2)|浏览(241)

我有一个QListWidget,它通过.setItemWidget()和拖放模式InternalMoveQLabel填充,当我在列表中移动一个项目时,它的标签就会消失。
我该如何解决这个问题?

一个最小的例子来重现

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()
所以我想解决这个问题的一个丑陋的方法是通过QMimeData.setData()QListWidget.mimeData()中手动序列化的小部件存储为自定义mimetype,并在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())
u4dcyp6a

u4dcyp6a1#

这是由一个Qt bug引起的,它只影响最近的版本。我可以在使用Qt-5.15.6和Qt-6.4.0时一直重现它-但不是例如Qt-5.12.1。这个问题似乎与QTBUG-100128密切相关。
PyQt 5/6(基于solution by 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)

旧答案

不幸的是,经过今天的进一步实验,似乎下面给出的建议的解决方案不是一个有效的解决方案。我发现也可以通过拖放到非空区域来使项目小部件消失。
在测试了Qt 5的其他版本后,我可以确认该错误在5.12.x,5.13.x,5.14.x,5.15.0和5.15.1中完全不存在。这与上面现有的Qt错误报告一致,该报告将Qt-5.15.2确定为引入错误的版本。
与问题中的建议相反,没有任何理由不将标签用作项目小部件。术语“静态内容”只是意味着“不通过用户定义的自定义绘图更新”。
这个bug似乎是QTBUG-87057的回归,QTBUG-87057在拖放过程中对列表视图行的移动方式进行了大量的内部更改。这些更改的复杂性可能意味着不可能有一个简单的解决方案来消除其负面影响。这些更改影响所有Qt 5版本5.15.1以上和Qt 6版本6.0以上。
AFAICS,这只影响将视图中的当前最后一项拖放到空白区域。其他项和多项选择不受影响。这建议使用以下解决方法:

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

t9eec4r02#

这里有一个方法可以防止bug的发生:

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

相关问题