python - 在 QHeaderView 和 QListWidget 之间拖放列

标签 python pyqt drag-and-drop qlistwidget qheaderview

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

下面我重新实现了几个拖动事件,以便在它们被调用时简单地打印出来。但是,只有 dragEnterEvent 成功。没有调用其他事件,例如 startDrag。我的最终目标是拥有一个 QTableView,我可以在其中将列拖入和拖入 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_())

最佳答案

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

鉴于此,有必要实现自定义拖放处理(使用 mousePressEventmouseMoveEventdropEvent),并提供用于编码和解码 Qt 用于在 View 之间传递项目的 mime 数据格式的函数。 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_())

关于python - 在 QHeaderView 和 QListWidget 之间拖放列,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/47291754/

相关文章:

python - 保持 Bokeh 小部件在应用程序中运行

python - 长序列在有注意力的 seq2seq 模型中?

python - 添加多个停靠的小部件

c++ - 我怎样才能将文件拖放到哪个 gtkmm 图像小部件

python - PyQt 项目 View 自定义拖放

python - 如何在 AllenNLP 中加载微调的 sciBERT 模型?

python - 如何避免在 python 中对键存在和值 True 进行捆绑检查

python - PyQt中带有QThread的后台线程

python - 隐藏新 QApplication() 的 Python 启动器图标

c++ - Os X Yosemite Qt 拖放文件名错误