我正在尝试创建一个在某些方面与Excel相似的QTableView:
=
开头,它将被视为公式。=
开头,而 3),您可以从同一QTableView中单击另一个单元格,然后然后 a)对该单元格的引用将自动添加到要编辑的公式中,并且< em> b)焦点停留在编辑模式下的单元格中,因此您可以继续编辑公式/为其他单元格添加更多引用。我该怎么做最后一项?
理想情况下,我想先检测到某个单元格的点击,然后检查另一个单元格是否仍处于编辑模式,将引用附加到该单元格,并阻止编辑模式小部件(即QLineEdit
)从失去焦点。但是,到检测到表格单击时,编辑模式窗口小部件已经失去焦点,即将被销毁。似乎也没有办法阻止该小部件失去焦点。
我找到的解决方法是:
这工作得相当不错,但是有时很难确保单元格确实是下一件事被点击。完全可以使用另一个控件来代替焦点,或者可以单击表的其他部分(例如标题/滚动条)。如果某个单元格在此之前处于编辑模式,则似乎不会触发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()