QTableView:类似于Excel的功能,可在单击时

时间:2018-10-29 14:08:16

标签: python pyqt pyqt5 qtableview

我正在尝试创建一个在某些方面与Excel相似的QTableView:

  • 如果单元格的值以=开头,它将被视为公式。
  • 这些公式可以引用同一表中的其他单元格(使用A1表示法,但这只是一个细节)。
  • 如果 1)正在编辑单元格, 2),则编辑值以=开头,而 3),您可以从同一QTableView中单击另一个单元格,然后然后 a)对该单元格的引用将自动添加到要编辑的公式中,并且< em> b)焦点停留在编辑模式下的单元格中,因此您可以继续编辑公式/为其他单元格添加更多引用。

我该怎么做最后一项?

理想情况下,我想先检测到某个单元格的点击,然后检查另一个单元格是否仍处于编辑模式,将引用附加到该单元格,并阻止编辑模式小部件(即QLineEdit)从失去焦点。但是,到检测到表格单击时,编辑模式窗口小部件已经失去焦点,即将被销毁。似乎也没有办法阻止该小部件失去焦点。

我找到的解决方法是:

  1. 当编辑小部件失去焦点时,将行/列存储在其所在位置,其最后光标位置等,并且不要尝试取消该事件
  2. 如果单击的下一个是一个单元格,请在编辑模式下重新打开该行/列中的单元格,并在光标位置所在的位置添加一个引用

这工作得相当不错,但是有时很难确保单元格确实是下一件事被点击。完全可以使用另一个控件来代替焦点,或者可以单击表的其他部分(例如标题/滚动条)。如果某个单元格在此之前处于编辑模式,则似乎不会触发QTableView的focusOutEvent

关于如何解决此问题的任何建议,或者我可以尝试的任何其他方法?


在下面查看我的代码示例。如果您双击编辑一个单元格,然后输入一个以=开头的字符串,然后单击另一个单元格,则实际上会将引用添加到表达式中。当您单击其他位置(滚动条或下面的按钮/ 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()

0 个答案:

没有答案