3

I am having troubled using the QHeaderView drag & drop feature. When I subclass a QHeaderView, I am able to accept drops with no issue. However, when I click on the QHeaderView and try to drag from one of the columns, nothing appears to happen.

Below I have re-implemented several drag events to simply print if they were called. However, only the dragEnterEvent is successful. No other event such as startDrag is ever called. My ultimate goal is to have a QTableView where I can drag columns from and to a QListWidget (essentially hiding the column) and the user can then drag the QListWidget item back onto the QTableView if they want the column and its data to be visible again. However, I can’t move forward until I can understand why the QHeaderView is not allowing me to drag. Any help would be greatly appreciated.

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

1 回答 1

3

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

鉴于此,有必要实现自定义拖放处理(使用mousePressEventmouseMoveEventdropEvent),并且还提供用于编码和解码 Qt 用于在视图之间传递项目的 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_())
于 2017-11-15T04:25:43.137 回答