Important: Please read the Qt Code of Conduct - https://forum.qt.io/topic/113070/qt-code-of-conduct

Drag and Drop QLabel to QHeaderView



  • Hi,

    I am currently trying to create a gui to allow a user to assign predetermined headers to a specific column in some data presented in a QTableView. Something like this:

    ffc9a9ef-80af-4778-bdeb-fd3aae26f565-image.png

    The headers available to the user will be available in a QListWidget and the data in the QTableView is coming from a modified version of QAbstractTableModel which allows a direct import of a pandas dataframe:

    import pandas as pd
    from PyQt5.QtWidgets import QApplication, QTableView
    from PyQt5.QtCore import QAbstractTableModel, Qt
    
    
    class pandasModel(QAbstractTableModel):
        
        def __init__(self, data):
            QAbstractTableModel.__init__(self)
            self._data = data
    
        def rowCount(self, parent=None):
            return self._data.shape[0]
    
        def columnCount(self, parent=None):
            return self._data.shape[1]
    
        def data(self, index, role=Qt.DisplayRole):
            if index.isValid():
                if role == Qt.DisplayRole:
                    return str(self._data.iloc[index.row(), index.column()])
            return None
    
        def headerData(self, col, orientation, role):
            if orientation == Qt.Horizontal and role == Qt.DisplayRole:
                return self._data.columns[col]
            return None
    

    I would like to do this using the Drag and Drop functionality where the user can drag a containing the header name onto a specific column in the QTableView and then have the column header take the text of the QListWidgetItem.

    This is the closest I have found (https://stackoverflow.com/questions/47291754/drag-and-drop-columns-between-qheaderview-and-qlistwidget) and this simply hides the column but I can't get the header text to change to the QListWidgetItem which is beign dragged:

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

    Any help on how to change the column header text is appreciated on this thank you.


  • Lifetime Qt Champion

    Hi,

    Why not call the header model's setHeaderData method when dropping ?



  • Hi so I tried that and reimplemented the setHeaderData method and explictly emited the headerChangedSignal as below but this will always return false even if I try to do it explcitly by calling it in the code.

        def setHeaderData(self, section, orientation, value, role=Qt.EditRole):
            self.headerDataChanged.emit(orientation,section,section+1)
            return super().setHeaderData(section, orientation, value, role)
    

  • Lifetime Qt Champion

    Since you have also a custom headerData method, I would expect that you would store the new value yourself in setHeaderData.


Log in to reply