自定义QTableView-多行选择和自定义上下文菜单

时间:2019-08-23 09:11:20

标签: pyqt5

我从一个用于只读单元格的示例开始,并且天生具有选择多行的能力(使用CTRL +左键单击)。 customContextMenu也可以直接使用(即右键单击将其打开)。 “天生”的意思是,没有代码专门用于处理这些东西(customMenuEvent的实现除外):它们是QTableView类本身提供的功能。

我构建了所需的东西,但是在此过程中我失去了这两个功能。如果运行示例代码,您将看到右键单击打开我认为是LineEdit小部件上下文菜单的菜单。我尝试使用右键单击或CTRL强制将小部件设为只读,以查看是否对只读有所帮助,但事实并非如此。 我也真的无法再得到那个蓝色的背景来指定正在“选择”的单元格,我认为这可能是我问题的真正核心:我没有打开TableView上下文菜单,因为我从来没有在表上下文中因为我从来没有真正“选择”行。

回顾一下,我正在寻求帮助:

  1. 理解为什么我不能选择(和多次选择)行。

  2. 理解为什么右键单击时不调用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_())

0 个答案:

没有答案