我从一个用于只读单元格的示例开始,并且天生具有选择多行的能力(使用CTRL +左键单击)。 customContextMenu也可以直接使用(即右键单击将其打开)。 “天生”的意思是,没有代码专门用于处理这些东西(customMenuEvent的实现除外):它们是QTableView类本身提供的功能。
我构建了所需的东西,但是在此过程中我失去了这两个功能。如果运行示例代码,您将看到右键单击打开我认为是LineEdit小部件上下文菜单的菜单。我尝试使用右键单击或CTRL强制将小部件设为只读,以查看是否对只读有所帮助,但事实并非如此。 我也真的无法再得到那个蓝色的背景来指定正在“选择”的单元格,我认为这可能是我问题的真正核心:我没有打开TableView上下文菜单,因为我从来没有在表上下文中因为我从来没有真正“选择”行。
回顾一下,我正在寻求帮助:
理解为什么我不能选择(和多次选择)行。
理解为什么右键单击时不调用TableView contextMenuEvent的原因。
由于这两个功能在我发现的一些示例代码中都不需要从QTableView继承的任何代码。
接下来是我程序的框架。我猜问题出在TableView中,除非只读很重要,否则LineEdit和来自Model的标志功能也可能如此。
#!/usr/bin/env python3
from collections import OrderedDict
from PyQt5 import QtCore, QtWidgets
import pandas as pd
def set_read_only(event):
modifiers = QtWidgets.QApplication.keyboardModifiers()
return event.button() == QtCore.Qt.RightButton or modifiers == QtCore.Qt.ControlModifier
class LineEdit(QtWidgets.QLineEdit):
def __init__(self, parent=None):
super().__init__(parent)
self.setAlignment(QtCore.Qt.AlignLeft)
def data(self):
return str(self.text())
def set_data(self, value):
if isinstance(value, QtCore.QVariant):
value = value.value()
self.setText(value)
self.update()
def mousePressEvent(self, event):
if set_read_only(event):
self.setReadOnly(True)
return
super().mousePressEvent(event)
def focusOutEvent(self, event):
super().focusOutEvent(event)
self.setReadOnly(False)
class Delegate(QtWidgets.QStyledItemDelegate):
def __init__(self, model):
super().__init__()
self.model = model
def createEditor(self, parent, option, index):
value = self.model.data(index)
widget = LineEdit(parent)
widget.set_data(value)
return widget
def setModelData(self, widget, model, index):
self.model.setData(index, widget.data())
def setEditorData(self, widget, index):
widget.set_data(index.data())
class TableView(QtWidgets.QTableView):
cellClicked = QtCore.pyqtSignal(int, int)
def __init__(self, dataframe, parent=None):
super().__init__(parent)
self.setSortingEnabled(True)
self.setObjectName("Example")
self.model = SerializableModel(dataframe, self)
# Delegate Setup
delegate = Delegate(self.model)
self.setItemDelegate(delegate)
self.setEditTriggers(self.CurrentChanged)
self.setModel(self.model)
def init(self):
for row in range(self.model.rowCount()):
for column in range(self.model.columnCount()):
self.enable_cell(row, column)
def row_add(self):
row = self.model.rowCount()
self.model.row_append()
for column in range(self.model.columnCount()):
self.enable_cell(row, column)
def row_delete(self):
print("Get the lines selected and call model.row_delete(row, count, parent=QtCore.QModelIndex())")
return
def enable_cell(self, row, column):
index = self.model.index(row, column)
self.openPersistentEditor(index)
def get_icon(self, icon_name):
if not icon_name.startswith('SP_'):
icon_name = 'SP_' + icon_name
icon = getattr(QtWidgets.QStyle, icon_name, None)
if icon is None:
raise Exception("Unknown icon %s" % icon_name)
return self.style().standardIcon(icon)
def make_cell_context_menu(self, menu):
""" Creates the menu displayed when right-clicking on a cell. """
menu.addAction(self.get_icon('CommandLink'), "Add Row(s)", self.row_add)
menu.addAction(self.get_icon('DialogResetButton'), "Delete Row(s)", self.row_delete)
return menu
def contextMenuEvent(self, event):
""" Implements right-clicking on cell. """
if self.rowAt(event.y()) < 0 or self.columnAt(event.x()) < 0:
return
menu = QtWidgets.QMenu(self)
menu = self.make_cell_context_menu(menu)
menu.exec_(self.mapToGlobal(event.pos()))
class SerializableModel(QtCore.QAbstractTableModel):
def __init__(self, dataframe, table_view, parent=None):
"empty_row is a list of types ordered according to the columns in the dataframe, which represents an empty row"
QtCore.QAbstractTableModel.__init__(self, parent=parent)
self.empty_row = pd.DataFrame(data=DF_PROTOTYPE_GLOBAL)
self.filtered = dataframe
self.table_view = table_view
def flags(self, index):
defaults = super().flags(index)
return defaults | QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsEditable | QtCore.Qt.ItemIsSelectable
def data(self, index, role=QtCore.Qt.DisplayRole):
if role == QtCore.Qt.TextAlignmentRole:
return QtCore.Qt.AlignCenter
if not index.isValid() or role != QtCore.Qt.DisplayRole or index.row() >= self.filtered.shape[0]:
return QtCore.QVariant()
value = self.filtered.iat[index.row(), index.column()]
return QtCore.QVariant(str(value))
def setData(self, index, value, role=QtCore.Qt.EditRole):
column = self.filtered.columns[index.column()]
column_dtype = self.filtered[column].dtype
if column_dtype != object:
value = None if value == '' else column_dtype.type(value)
row = self.filtered.index.values[index.row()]
self.filtered.iat[row, index.column()] = value
self.dataChanged.emit(index, index)
return True
@QtCore.pyqtSlot(int, QtCore.Qt.Orientation, result=str)
def headerData(self, section: int, orientation: QtCore.Qt.Orientation, role: int = QtCore.Qt.DisplayRole):
if role != QtCore.Qt.DisplayRole:
return QtCore.QVariant()
if orientation == QtCore.Qt.Horizontal:
return self.filtered.columns[section]
else:
return str(self.filtered.index[section])
def rowCount(self, parent=QtCore.QModelIndex()):
return self.filtered.shape[0] if not parent.isValid() else 0
def columnCount(self, parent=QtCore.QModelIndex()):
return self.filtered.shape[1] if not parent.isValid() else 0
def insertRows(self, row, count, parent=QtCore.QModelIndex()):
""" Since we only need append, we assume row == rowCount. """
for _ in range(count):
self.filtered = self.filtered.append(self.empty_row, ignore_index=True)
return True
def row_append(self, count=1, parent=QtCore.QModelIndex()):
""" Reset filters, to ensure the new rows are not hidden by them, and appends rows with default values. """
rows = self.filtered.shape[0]
self.beginInsertRows(parent, rows, rows + count - 1)
self.insertRows(rows, count, parent)
self.endInsertRows()
self.layoutChanged.emit()
def removeRows(self, row, count, parent=QtCore.QModelIndex()):
self.filtered.drop(range(row, row + count), inplace=True).reset_index(drop=True)
return True
def row_delete(self, row, count=1, parent=QtCore.QModelIndex()):
""" Removes count rows starting with row. """
rows = self.filtered.shape[0]
self.beginRemoveRows(parent, row, count)
self.removeRows(rows, count, parent)
self.endRemoveRows()
self.layoutChanged.emit()
DF_PROTOTYPE_GLOBAL = pd.DataFrame(data=OrderedDict((
("Details", [" "]),
)))
DF_PROTOTYPE_GLOBAL_SAVED = pd.DataFrame(data=OrderedDict((
("Details", ["item 1", "item 2"]),
)))
class InfoManager:
def __init__(self):
self.window = QtWidgets.QMainWindow()
self.ui = Ui_InfoWindow()
self.ui.setupUi(self.window)
# Overwrites
self.ui.view_global.setParent(None)
self.ui.view_global = TableView(DF_PROTOTYPE_GLOBAL_SAVED, parent=self.ui.tab_global)
self.ui.verticalLayout_4.addWidget(self.ui.view_global)
self.ui.view_global.init()
def show(self):
self.window.show()
class Ui_InfoWindow(object):
def setupUi(self, InfoWindow):
InfoWindow.setObjectName("InfoWindow")
InfoWindow.resize(1000, 748)
self.centralwidget = QtWidgets.QWidget(InfoWindow)
self.centralwidget.setObjectName("centralwidget")
self.verticalLayout_5 = QtWidgets.QVBoxLayout(self.centralwidget)
self.verticalLayout_5.setObjectName("verticalLayout_5")
self.info_tabs = QtWidgets.QTabWidget(self.centralwidget)
self.info_tabs.setObjectName("info_tabs")
self.tab_global = QtWidgets.QWidget()
self.tab_global.setObjectName("tab_global")
self.verticalLayout_4 = QtWidgets.QVBoxLayout(self.tab_global)
self.verticalLayout_4.setContentsMargins(0, 0, 0, 0)
self.verticalLayout_4.setObjectName("verticalLayout_4")
self.view_global = QtWidgets.QTableView(self.tab_global)
self.view_global.setSortingEnabled(True)
self.view_global.setObjectName("view_global")
self.verticalLayout_4.addWidget(self.view_global)
self.info_tabs.addTab(self.tab_global, "")
self.verticalLayout_5.addWidget(self.info_tabs)
InfoWindow.setCentralWidget(self.centralwidget)
self.menubar = QtWidgets.QMenuBar(InfoWindow)
self.menubar.setGeometry(QtCore.QRect(0, 0, 1000, 20))
self.menubar.setObjectName("menubar")
InfoWindow.setMenuBar(self.menubar)
self.statusbar = QtWidgets.QStatusBar(InfoWindow)
self.statusbar.setObjectName("statusbar")
InfoWindow.setStatusBar(self.statusbar)
self.action_email_tab = QtWidgets.QAction(InfoWindow)
self.action_email_tab.setObjectName("action_email_tab")
self.action_exit = QtWidgets.QAction(InfoWindow)
self.action_exit.setObjectName("action_exit")
self.retranslateUi(InfoWindow)
self.info_tabs.setCurrentIndex(0)
QtCore.QMetaObject.connectSlotsByName(InfoWindow)
def retranslateUi(self, InfoWindow):
_translate = QtCore.QCoreApplication.translate
InfoWindow.setWindowTitle(_translate("InfoWindow", "Info Window"))
self.info_tabs.setTabText(self.info_tabs.indexOf(self.tab_global), _translate("InfoWindow", "Global"))
self.action_email_tab.setText(_translate("InfoWindow", "Email Current Tab"))
self.action_exit.setText(_translate("InfoWindow", "Exit"))
if __name__ == '__main__':
import sys
app = QtWidgets.QApplication(sys.argv)
info_manager = InfoManager()
info_manager.show()
sys.exit(app.exec_())