QTableView: Excel-подобная функциональность для добавления ссылок на ячейки в другие ячейки при клике - PullRequest
0 голосов
/ 29 октября 2018

Я пытаюсь создать QTableView, который ведет себя подобно Excel в некоторых аспектах:

  • Если значение ячейки начинается с =, оно будет рассматриваться как формула.
  • Формулы могут ссылаться на другие ячейки в той же таблице (в обозначениях A1, но это просто деталь).
  • Если 1) редактируемая ячейка, 2) значение редактирования начинается с = и 3) вы щелкаете по другой ячейке из того же QTableView, затем a) ссылка на эту ячейку автоматически добавляется в редактируемую формулу, а b) фокус остается в ячейке в режиме редактирования, так что вы можете продолжать редактировать формулу / добавлять дополнительные ссылки на другие ячейки.

Как я могу сделать этот последний элемент?

В идеале я хотел бы сначала обнаружить щелчок по ячейке, затем проверить, находится ли еще одна ячейка в режиме редактирования, добавить ссылку на эту ячейку и предотвратить потерю виджета режима редактирования (т. Е. QLineEdit). фокус. Однако к тому времени, когда щелчок таблицы обнаружен, виджет режима редактирования уже потерял фокус и собирается быть уничтоженным. Похоже, не существует способа предотвратить потерю фокуса этим виджетом.

Обходной путь, который я нашел:

  1. Когда виджет редактирования теряет фокус, сохраните строку / столбец там, где он был, его последнюю позицию курсора и т. Д., И не пытайтесь отменить событие
  2. Если следующее нажатие - это ячейка, заново откройте ячейку в этой строке / столбце в режиме редактирования и добавьте ссылку, где позиция курсора была

Это работает достаточно хорошо, но иногда трудно убедиться, что ячейка действительно была , когда в следующий раз щелкнули . Вместо этого можно было бы сфокусироваться на совершенно другом элементе управления или щелкнуть другие части таблицы (например, заголовки / полосы прокрутки). focusOutEvent QTableView, похоже, не срабатывает, если перед этим ячейка находилась в режиме редактирования.

Любые предложения о том, как это исправить, или любой другой подход, который я мог бы попробовать?


См. Пример моего кода ниже. Если вы редактируете ячейку двойным щелчком, затем вводите строку, которая начинается с =, и щелкаете по другой ячейке, эта ссылка действительно добавляется к выражению. Проблемы начинаются, когда вы щелкаете где-то еще (полосы прокрутки или кнопка / QLineEdit ниже), а затем нажимаете обратно на ячейку таблицы (вы не ожидаете, что вернетесь в режим редактирования с добавленной ссылкой на эту ячейку). Я работаю с mouseReleaseEvent, потому что таким образом я могу определить выделение области таблицы, а не только ячейки.

Примечание: Пример приведен в Python / PyQt5, но проблема должна быть такой же в C ++

import sys

from PyQt5 import QtCore, QtWidgets

Qt = QtCore.Qt


def num2col(num):
    if num <= 0:
        raise ValueError('The column index must be > 0')

    letters = ''
    while num:
        mod = (num - 1) % 26
        letters += chr(mod + 65)
        num = (num - 1) // 26
    return ''.join(reversed(letters))


def index2ref(row, col):
    return '{}{}'.format(num2col(col + 1), row + 1)


class CellEdit(QtWidgets.QLineEdit):

    def __init__(self, table, index, parent=None):
        super(CellEdit, self).__init__(parent)
        self._table = table  # type: MyTableView
        self._table.editingWidget = self
        self.index = index  # type: QtCore.QModelIndex
        self._leavingNormally = False

    def focusInEvent(self, event):
        print('Focus gained for {},{}'.format(self.index.row(), self.index.column()))

    def focusOutEvent(self, event):
        print('Focus lost for {},{}'.format(self.index.row(), self.index.column()))
        if self._leavingNormally:
            return
        if self._table.lastCell is None and self.text().startswith('='):
            if self.hasSelectedText():
                selection = (self.selectionStart(), len(self.selectedText()))
            else:
                selection = (self.cursorPosition(), 0)
            self._table.lastCell = (
                self.index,
                self.text(),
                selection
            )

    def keyPressEvent(self, e):
        if e.key() in (Qt.Key_Enter, Qt.Key_Return):
            print('Closing CellEdit normally by pressing Return')
            self._leavingNormally = True
        super(CellEdit, self).keyPressEvent(e)

    def addReference(self, ref):
        txt = self.text()
        if self.hasSelectedText():
            start = self.selectionStart()
            end = start + len(self.selectedText())
        else:
            start = self.cursorPosition()
            end = start
        txt = txt[:start] + ref + txt[end:]
        self.setText(txt)
        self.setCursorPosition(start + len(ref))


class MyItemDelegate(QtWidgets.QItemDelegate):

    def __init__(self, table, parent=None):
        super(MyItemDelegate, self).__init__(parent)
        self._table = table  # type: MyTableView

    def createEditor(self, parent, option, index):
        if not index.isValid():
            return super(MyItemDelegate, self).createEditor(parent, option, index)
        edit = CellEdit(self._table, index, parent)
        return edit


class MyTableModel(QtCore.QAbstractTableModel):

    def __init__(self, rows, cols, parent=None):
        super(MyTableModel, self).__init__(parent)

        self._rows = rows
        self._cols = cols
        self._data = [
            [index2ref(r, c) for c in range(cols)]
            for r in range(rows)
        ]

    def flags(self, index):
        if not index.isValid():
            return Qt.ItemIsEditable
        return super(MyTableModel, self).flags(index) | Qt.ItemIsEditable

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

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

    def data(self, index, role=Qt.DisplayRole):
        if not index.isValid():
            return None
        if role in (Qt.DisplayRole, Qt.EditRole):
            return self._data[index.row()][index.column()]
        return None

    def setData(self, index, value, role=Qt.EditRole):
        if not index.isValid():
            return False
        if role != Qt.EditRole:
            return False
        self._data[index.row()][index.column()] = value
        return True


class MyTableView(QtWidgets.QTableView):

    def __init__(self, rows, cols, parent=None):
        super(MyTableView, self).__init__(parent)

        # This will store, in order:
        #  - The index of cell whose edit mode just lost focus
        #  - The text when that happened
        #  - The cursor position when that happened
        self.lastCell = None  # type: tuple

        # This will be set as soon as the editing widget gets created, not when
        # it loses focus
        self.editingWidget = None  # type: CellEdit

        self._model = MyTableModel(rows, cols)
        self.setModel(self._model)

        self._delegate = MyItemDelegate(self)
        self.setItemDelegate(self._delegate)

    def mouseReleaseEvent(self, event):
        index = self.selectionModel().selectedIndexes()
        if len(index) == 0:
            super(MyTableView, self).mousePressEvent(event)
            self.lastCell = None
            return
        print('Cell clicked')
        if self.lastCell is None:
            print('No cell was being edited')
            super(MyTableView, self).mousePressEvent(event)
            return
        if len(index) == 1 and self.lastCell[0].row() == index[0].row() and \
                self.lastCell[0].column() == index[0].column():
            # We are clicking the widget we are just editing, so no reference
            # is added
            print('We clicked the cell that was being edited')
            self.lastCell = None
            super(MyTableView, self).mousePressEvent(event)
            return
        if not self.lastCell[1].startswith('='):
            # Excel wouldn't put references in cells which do not contain
            # formulas, so neither do we
            print('The cell that was being edited did not contain a formula')
            super(MyTableView, self).mousePressEvent(event)
            return
        # Add the reference to this cell to the formula, and ignore the click
        self.openAndAddReference(index)
        event.accept()

    def openAndAddReference(self, index):
        # Store this locally because the focusOutEvent will reset it
        lastCell = self.lastCell
        self.edit(lastCell[0])
        self.editingWidget.setSelection(*lastCell[2])
        ref = index2ref(index[0].row(), index[0].column())
        if len(index) > 1:
            ref = '{}:{}'.format(
                ref,
                index2ref(index[-1].row(), index[-1].column())
            )
        self.editingWidget.addReference(ref)
        self.lastCell = None

    def focusOutEvent(self, e):
        print('Focus lost for entire table')
        self.lastCell = None


class MainWindow(QtWidgets.QWidget):

    def __init__(self, parent=None):
        super(MainWindow, self).__init__(parent)
        layout = QtWidgets.QVBoxLayout()
        table = MyTableView(5, 4)
        layout.addWidget(table)
        # This push button is added just to have something to focus on other
        # than the table
        layout.addWidget(QtWidgets.QPushButton())
        layout.addWidget(QtWidgets.QLineEdit())
        self.setLayout(layout)


def main():
    app = QtWidgets.QApplication(sys.argv)
    widget = MainWindow()
    widget.show()
    sys.exit(app.exec_())


if __name__ == '__main__':
    main()
...