pythonpyqtpysideqtableviewqtablewidgetitem

How to get plain text from a cell QTableView


I'm trying to do a drag and drop between two items, but I'm having trouble getting the information out of the cell I click on in the table. I have a custom table that has a custom model.

class PyTableWidget(QTableView):
    def __init__(self, *args):
        super().__init__()
        self.setDragEnabled(True)

        self.set_stylesheet(*args)

    def onLeftClick(self, index):
                print('onClick index.row: %s, index.col: %s' % (index.row(), index.column()))

    def mouseMoveEvent(self, event):
        if event.buttons() == Qt.LeftButton:
            drag = QDrag(self)
            mime = QMimeData()
            drag.setMimeData(mime)
            drag.exec(Qt.MoveAction)

Here's the custom model for the table:

class CustomizedNumpyModel(QAbstractTableModel):
    def __init__(self, data, parent=None):
        QAbstractTableModel.__init__(self, parent)
        self._data = np.array(data.values)
        self._cols = data.columns
        self.r, self.c = np.shape(self._data)

    def data(self, index, role=Qt.DisplayRole):

        if role == Qt.DisplayRole:
            value = self._data[index.row(), index.column()]

            if isinstance(value, float):
                return "%.2f" % value

            if isinstance(value, str):
                return '%s' % value

            return unicode(value)

    def rowCount(self, parent=None):
        return self.r

    def columnCount(self, parent=None):
        return self.c

    def headerData(self, p_int, orientation, role):
        if role == Qt.DisplayRole:
            if orientation == Qt.Horizontal:
                return str(self._cols[p_int])

            if orientation == Qt.Vertical:
                return p_int
        return None

    def flags(self, index):
        return Qt.ItemIsEnabled | Qt.ItemIsSelectable | Qt.ItemIsEditable

    def setData(self, index, value, role=Qt.EditRole):
        index_value = None
        based_columns = [6, 8, 10, 12]
        if role == Qt.EditRole:
            row = index.row()
            column = index.column()
            tmp = str(value)
            if tmp != '':
                if column in based_columns:
                    if column == 6 and tmp in self._cols:
                        index_no = np.where(self._cols == tmp)[0][0]
                        self._data[row, column + 1] = self._data[row, index_no]
                        self._data[row, column] = tmp
                    elif column in [8, 10, 12]:
                        for x in range(97, 181):
                            if self._data[row, x] == int(tmp):
                                index_value = x
                                break

                        col_name = self._cols[index_value]
                        col_name = col_name.removesuffix('_rank')
                        self._data[row, column + 1] = col_name
                        self._data[row, column] = tmp
                    self.dataChanged.emit(index, index)
                return True
            else:
                return False

And here the code in the other widget which accept the drop

class PyCustomButton(QPushButton):
    def __init__(self, *args):
        super().__init__()
        self.setCursor(Qt.PointingHandCursor)
        self.setAcceptsDrops(True)
        self._lista = []

    def dragEnterEvent(self, event):
        if event.mimeData().hasText():
            event.accept()
        else:
            event.ignore()

    def dropEvent(self, event):
        self._lista.append(event.mimeData().text())
        print(self._lista)

The table has following aspect:

Names
First
Second

And I want plain text i.e. First or Second drag from table and drop in custom button.


Solution

  • The simple solution is to get the "selected" index using indexAt(). Note that it's generally better to start a drag action when the user has moved the mouse by a certain amount of pixels, which defaults to the QApplication startDragDistance() property.

    class PyTableWidget(QTableView):
        _startPos = None
        def mousePressEvent(self, event):
            if event.button() == Qt.LeftButton:
                index = self.indexAt(event.pos())
                if index.isValid():
                    self._startPos = event.pos()
            super().mousePressEvent(event)
    
        def mouseMoveEvent(self, event):
            if event.buttons() == Qt.LeftButton and self._startPos is not None:
                delta = (event.pos() - self._startPos).manhattanLength()
                if delta >= QApplication.startDragDistance():
                    drag = QDrag(self)
                    mime = QMimeData()
                    mime.setText(self.indexAt(self._startPos).data())
                    drag.setMimeData(mime)
                    drag.exec(Qt.MoveAction)
                    self._startPos = None
                    return
            super().mouseMoveEvent(event)
    
        def mouseReleaseEvent(self, event):
            self._startPos = None
            super().mouseReleaseEvent(event)
    

    Note that Qt item views already support drag and drop: to enable that support in custom models, you need to also add Qt.ItemIsDragEnabled in flags() and ensure that the table supports drag either by using setDragEnabled(True) (as you did) or by setting the dragDropMode of the view at least to DragOnly.

    The only remaining "issue" is getting the data from outside the view from the drag mime data, which uses an internal format to serialize the content.

    While this approach might seem more complex and difficult, it's also a better solution whenever you intend to support drag and drop between multiple item views, since that serialization format is common and standard to all Qt views.

    class CustomizedNumpyModel(QAbstractTableModel):
        # ...
        def flags(self, index):
            return (Qt.ItemIsEnabled 
                | Qt.ItemIsSelectable 
                | Qt.ItemIsEditable 
                | Qt.ItemIsDragEnabled)
    
    
    class PyTableWidget(QTableView):
        def __init__(self, *args):
            super().__init__(*args)
            self.setDragEnabled(True)
    
        # nothing else to implement in this class
    
    
    class PyCustomButton(QPushButton):
        def __init__(self, *args):
            super().__init__()
            self.setCursor(Qt.PointingHandCursor)
            self.setAcceptDrops(True)
            self._lista = []
    
        def dragEnterEvent(self, event):
            if 'application/x-qabstractitemmodeldatalist' in event.mimeData().formats():
                event.accept()
            else:
                event.ignore()
    
        def dropEvent(self, event):
            mimeData = event.mimeData()
            if mimeData.hasText():
                self._lista.append(mimeData.text())
            elif 'application/x-qabstractitemmodeldatalist' in mimeData.formats():
                names = []
                stream = QDataStream(mimeData.data(
                    'application/x-qabstractitemmodeldatalist'))
                while not stream.atEnd():
                    # all fields must be read, even if not used
                    row = stream.readInt()
                    column = stream.readInt()
                    for _ in range(stream.readInt()):
                        role = stream.readInt()
                        value = stream.readQVariant()
                        if role == Qt.DisplayRole:
                            names.append(value)
                self._lista.extend(names)
            print(self._lista)
    

    Note that setData() must always return a bool. Remove the last else: and push the return False back to the main indentation level of the function.