0

I am trying to use beginMoveColumns to move a single column over in a QTableView, but it doesn't work properly in my example below. The cell selections get shuffled and column widths don't move. Moving rows using the same logic seems to work correctly. What am I doing wrong?

Video: https://www.screencast.com/t/5UJ0iByZCEE

from PyQt5 import QtWidgets, QtCore, QtGui
import sys
from PyQt5.QtCore import QModelIndex, Qt


class MyTableModel(QtCore.QAbstractTableModel):
    def __init__(self, data=[[]], parent=None):
        super().__init__(parent)
        self.data = data

    def headerData(self, section: int, orientation: Qt.Orientation, role: int):
        if role == QtCore.Qt.DisplayRole:
            return "Header"

    def columnCount(self, parent=None):
        return len(self.data[0])

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

    def data(self, index: QModelIndex, role: int):
        if role == QtCore.Qt.DisplayRole:
            row = index.row()
            col = index.column()
            return str(self.data[row][col])

    def move_column(self, ix, new_ix):
        parent = QtCore.QModelIndex()
        if new_ix > ix:
            target = new_ix + 1
        else:
            target = new_ix
        self.beginMoveColumns(parent, ix, ix, parent, target)

        # Shift column in each row
        for row in self.data:
            row.insert(new_ix, row.pop(ix))

        self.endMoveColumns()

    def move_row(self, ix, new_ix):
        parent = QtCore.QModelIndex()
        if new_ix > ix:
            target = new_ix + 1
        else:
            target = new_ix
        self.beginMoveRows(parent, ix, ix, parent, target)

        # Shift row
        self.data.insert(new_ix, self.data.pop(ix))

        self.endMoveRows()


if __name__ == '__main__':
    app = QtWidgets.QApplication(sys.argv)

    data = []
    for i in range(12):
        row = []
        data.append(row)
        for j in range(6):
            row.append(f"R{i}_C{j}")

    model = MyTableModel(data)
    view = QtWidgets.QTableView()
    view.setModel(model)

    view.setColumnWidth(0, 50)
    view.setColumnWidth(1, 100)

    view.setRowHeight(0, 50)
    view.setRowHeight(1, 100)

    container = QtWidgets.QWidget()
    layout = QtWidgets.QVBoxLayout()
    container.setLayout(layout)

    layout.addWidget(view)

    button = QtWidgets.QPushButton("Move 1st column right by 1")
    button.clicked.connect(lambda: model.move_column(0, 1))
    layout.addWidget(button)

    button = QtWidgets.QPushButton("Move 1st column right by 2")
    button.clicked.connect(lambda: model.move_column(0, 2))
    layout.addWidget(button)

    button = QtWidgets.QPushButton("Move 1st row down by 1")
    button.clicked.connect(lambda: model.move_row(0, 1))
    layout.addWidget(button)

    button = QtWidgets.QPushButton("Move 1st row down by 2")
    button.clicked.connect(lambda: model.move_row(0, 2))
    layout.addWidget(button)

    container.resize(800, 800)
    container.show()


    sys.exit(app.exec_())
pyjamas
  • 4,608
  • 5
  • 38
  • 70
  • I'm trying to understand the odd selection behavior, as it *could* be a bug. That said, the behavior of the header is expected: moving rows or columns doesn't change the indexes of rows and columns (and thus, header sections): you're moving the internal data, not the "actual" row/column indexes. If what you need is to move the *displayed* order, then you should not change the data model, but move the header sections (using [`header.moveSection()`](https://doc.qt.io/qt-5/qheaderview.html#moveSection)). – musicamante Jun 11 '21 at 22:31
  • Hmm I may be misunderstanding more than I thought. Shouldn't moving the header data also be handled by the QAtbstractTableModel? My goal here is to completely move columns and rows (both the cell data and header data). This movement should be a modification of the underlying data, which should be reflected in the view, and I want the column and row sizes to follow the data. Here's a clip of me using Excel to do what I describe https://www.screencast.com/t/uajgleiH – pyjamas Jun 11 '21 at 23:45
  • 1. please try to be consistent, using pandas only complicates things unnecessarily. The main issue here is the inconsistency of the selection. 2. no, it shouldn't: it's called **abstract** exactly because **you** need to do that: since header data is completely *index* based (with index, in this case, a 0-based indexing), the header data section is *always* the *logical* index in the model, so reordering the header labels is up to you. 3. as said, the header view doesn't know anything about the change in the order, if you want to implement that, then you need to find your own way. – musicamante Jun 11 '21 at 23:56
  • 4. finally, there's a reason for which programs like Excel require hundreds of programmers and thousands of hours of coding: providing implementations like the one you want is not easy, and what you want to achieve is **not** the standard behavior provided by the **basic** abstract item models. The reason is simple and logic: that is what *you* need, not what everybody would; an abstract class provides the basic and generic default implementation, any advanced feature must be done in subclasses depending on the specific needs, which can be dramatically different depending on the requirements. – musicamante Jun 11 '21 at 23:59
  • 1
    Another important thing (which is valid for your original post as with the previous one), you should not name the data reference as `self.data`: `data()` is the method called for accessing the data model (and accessible through `model.data()`), so it should **not** be overwritten. – musicamante Jun 12 '21 at 00:18
  • I rolled back my latest edit to remove `pandas`. I'll make a new question about the header text disappearing if it's unrelated to the other visual bugs – pyjamas Jun 12 '21 at 00:34
  • "what you want to achieve is not the standard behavior" But it works fine with rows. When I reorder rows using `beginMoveRows` the row height moves around correctly along with the data without me touching `view.verticalHeader()`. The comment chain is getting long, I am free any time the next few hours to talk in chat if that's something you'd be open to – pyjamas Jun 12 '21 at 00:35
  • 1
    with "...is not the standard behavior" I was referring to the movement of the *headers* when the underlying data rows/columns is moved. While I can understand your point of view, that's not how the base implementation of Qt item models works. You can provide that feature by having a list of headers that is reordered when moving rows/columns, similarly to what `reindex` does. About that: the headers become invisible due to the type of the returned index by the dataframe; reindexing causes headers to become `int64` types, and PyQt knows nothing about that type: use `int()` to fix that. – musicamante Jun 12 '21 at 00:57
  • Ah yes you're right that fixed the invisible headers. I don't understand what you mean by "movement of the headers" though - it sounds like you're saying maintaining columns width when columns are moved is not a default behavior? But my example moves rows and maintains row heights and selections without me implementing that logic myself. Don't rows and columns both similarly have respective `QHeaderView` objects? (I'll leave my question without `pandas`, but for reference this is what I have right now https://www.screencast.com/t/ydF3wFktqx1 / https://pastebin.com/TzN2g0g0) – pyjamas Jun 12 '21 at 01:24
  • 1
    Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/233685/discussion-between-musicamante-and-esostack). – musicamante Jun 12 '21 at 01:26

1 Answers1

0

Turns out it was a bug, I made a bug report here: https://bugreports.qt.io/browse/QTBUG-94503

As a workaround I just clear cell selection on column move, and use this snippet to move column widths

model = view.model()
column_widths = [view.columnWidth(ix) for ix in range(model.columnCount())]
column_widths.insert(new_ix, column_widths.pop(ix))

# Set width of destination column to the width of the source column
for j in range(len(column_widths)):
    view.setColumnWidth(j, column_widths[j])

pyjamas
  • 4,608
  • 5
  • 38
  • 70