在 QHeaderView 和 QListWidget 之间拖放列

Drag and drop columns between QHeaderView and QListWidget

我在使用 QHeaderView 拖放功能时遇到问题。当我对 QHeaderView 进行子类化时,我可以毫无问题地接受掉落。但是,当我单击 QHeaderView 并尝试从其中一列拖动时,似乎没有任何反应。

下面我重新实现了几个拖动事件,以便在它们被调用时简单地打印出来。但是,只有 dragEnterEvent 是成功的。没有调用其他事件,例如 startDrag。我的最终目标是有一个 QTableView ,我可以在其中将列从 QListWidget 拖动到 QListWidget (基本上隐藏该列),然后用户可以将 QListWidget 项目拖回 QTableView 如果他们希望列及其数据再次可见。但是,在我理解为什么 QHeaderView 不允许我拖动之前,我无法继续前进。任何帮助将不胜感激。

class MyHeader(QHeaderView):
    def __init__(self, parent=None):
        super().__init__(Qt.Horizontal, parent)
        self.setDragEnabled(True)
        self.setAcceptDrops(True)

    def startDrag(self, *args, **kwargs):
        print('start drag success')

    def dragEnterEvent(self, event):
        print('drag enter success')

    def dragLeaveEvent(self, event):
        print('drag leave success')

    def dragMoveEvent(self, event):
        print('drag move success')

class Form(QDialog):
    def __init__(self, parent=None):
        super().__init__(parent)

        listWidget = QListWidget()
        listWidget.setDragEnabled(True)
        listWidget.setAcceptDrops(True)
        listWidget.addItem('item #1')
        listWidget.addItem('item #2')

        tableWidget = QTableWidget()
        header = MyHeader()
        tableWidget.setHorizontalHeader(header)
        tableWidget.setRowCount(5)
        tableWidget.setColumnCount(2)
        tableWidget.setHorizontalHeaderLabels(["Column 1", "Column 2"])

        splitter = QSplitter(Qt.Horizontal)
        splitter.addWidget(listWidget)
        splitter.addWidget(tableWidget)
        layout = QHBoxLayout()
        layout.addWidget(splitter)
        self.setLayout(layout)

if __name__=='__main__':
    import sys
    app = QApplication(sys.argv)
    form= Form()
    form.show()
    sys.exit(app.exec_())

QHeaderViewclass不使用继承自QAbstractItemView的拖放方法,因为它永远不需要发起拖拽操作。拖放仅用于重新排列列,没有必要为此使用 QDrag 机制。

鉴于此,有必要实现自定义拖放处理(使用 mousePressEventmouseMoveEventdropEvent),并提供编码和解码 mime 的函数-Qt 用于在视图之间传递项目的数据格式。 table-widget 需要一个事件过滤器,因此当所有列都被隐藏时仍然可以删除;以及列表小部件,以阻止它向自身复制项目。

下面的演示脚本实现了所有这些。可能还需要进行更多改进,但这应该足以让您入门:

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

class MyHeader(QHeaderView):
    MimeType = 'application/x-qabstractitemmodeldatalist'
    columnsChanged = pyqtSignal(int)

    def __init__(self, parent=None):
        super().__init__(Qt.Horizontal, parent)
        self.setDragEnabled(True)
        self.setAcceptDrops(True)
        self._dragstartpos = None

    def encodeMimeData(self, items):
        data = QByteArray()
        stream = QDataStream(data, QIODevice.WriteOnly)
        for column, label in items:
            stream.writeInt32(0)
            stream.writeInt32(column)
            stream.writeInt32(2)
            stream.writeInt32(int(Qt.DisplayRole))
            stream.writeQVariant(label)
            stream.writeInt32(int(Qt.UserRole))
            stream.writeQVariant(column)
        mimedata = QMimeData()
        mimedata.setData(MyHeader.MimeType, data)
        return mimedata

    def decodeMimeData(self, mimedata):
        data = []
        stream = QDataStream(mimedata.data(MyHeader.MimeType))
        while not stream.atEnd():
            row = stream.readInt32()
            column = stream.readInt32()
            item = {}
            for count in range(stream.readInt32()):
                key = stream.readInt32()
                item[key] = stream.readQVariant()
            data.append([item[Qt.UserRole], item[Qt.DisplayRole]])
        return data

    def mousePressEvent(self, event):
        if event.button() == Qt.LeftButton:
            self._dragstartpos = event.pos()
        super().mousePressEvent(event)

    def mouseMoveEvent(self, event):
        if (event.buttons() & Qt.LeftButton and
            self._dragstartpos is not None and
            (event.pos() - self._dragstartpos).manhattanLength() >=
            QApplication.startDragDistance()):
            column = self.logicalIndexAt(self._dragstartpos)
            data = [column, self.model().headerData(column, Qt.Horizontal)]
            self._dragstartpos = None
            drag = QDrag(self)
            drag.setMimeData(self.encodeMimeData([data]))
            action = drag.exec(Qt.MoveAction)
            if action != Qt.IgnoreAction:
                self.setColumnHidden(column, True)

    def dropEvent(self, event):
        mimedata = event.mimeData()
        if mimedata.hasFormat(MyHeader.MimeType):
            if event.source() is not self:
                for column, label in self.decodeMimeData(mimedata):
                    self.setColumnHidden(column, False)
                event.setDropAction(Qt.MoveAction)
                event.accept()
            else:
                event.ignore()
        else:
            super().dropEvent(event)

    def setColumnHidden(self, column, hide=True):
        count = self.count()
        if 0 <= column < count and hide != self.isSectionHidden(column):
            if hide:
                self.hideSection(column)
            else:
                self.showSection(column)
            self.columnsChanged.emit(count - self.hiddenSectionCount())

class Form(QDialog):
    def __init__(self, parent=None):
        super().__init__(parent)

        self.listWidget = QListWidget()
        self.listWidget.setAcceptDrops(True)
        self.listWidget.setDragEnabled(True)
        self.listWidget.viewport().installEventFilter(self)

        self.tableWidget = QTableWidget()
        header = MyHeader(self)
        self.tableWidget.setHorizontalHeader(header)
        self.tableWidget.setRowCount(5)
        self.tableWidget.setColumnCount(4)

        labels = ["Column 1", "Column 2", "Column 3", "Column 4"]
        self.tableWidget.setHorizontalHeaderLabels(labels)
        for column, label in enumerate(labels):
            if column > 1:
                item = QListWidgetItem(label)
                item.setData(Qt.UserRole, column)
                self.listWidget.addItem(item)
                header.hideSection(column)

        header.columnsChanged.connect(
            lambda count: self.tableWidget.setAcceptDrops(not count))
        self.tableWidget.viewport().installEventFilter(self)

        splitter = QSplitter(Qt.Horizontal)
        splitter.addWidget(self.listWidget)
        splitter.addWidget(self.tableWidget)
        layout = QHBoxLayout()
        layout.addWidget(splitter)
        self.setLayout(layout)

    def eventFilter(self, source, event):
        if event.type() == QEvent.Drop:
            if source is self.tableWidget.viewport():
                self.tableWidget.horizontalHeader().dropEvent(event)
                return True
            else:
                event.setDropAction(Qt.MoveAction)
        return super().eventFilter(source, event)

if __name__=='__main__':

    app = QApplication(sys.argv)
    form = Form()
    form.setGeometry(600, 50, 600, 200)
    form.show()
    sys.exit(app.exec_())