PyQt6 优化操作:QTableWidget 增加右键菜单,支持对选中区域内容的复制、粘贴、清空、插入、删除、设置对齐方式等操作

1. 部分功能效果展示

表格选中区域后,右键显示菜单,菜单项包括:复制、粘贴、清空、删除,插入,设置对齐方式,高亮背景等;未适配多重选择,需选择连续单元格或整行、整列;部分功能,如复制粘贴支持使用热键。

1.1 复制粘贴操作

可以直接复制选中区域表格内容,以便粘贴到外部 Excel 表。

可以将表格中某个矩形区域的内容粘贴到其他位置,超出表格范围时自动增加行或列。下述示例中,将从 '62' 开始的 3*3 内容粘贴到第10行第2列处,并自动增加2行,保证待粘贴内容无丢失。

1.2 清空操作

右键 -> 清空

1.3 删除操作

选中后右键‘删除’,显示<删除>弹窗(类似于 Excel 风格),用户可根据实际情况选择所需操作。

1.4 插入操作

如果选中整行,右键点击‘插入’,将在上方插入空白行;如果选中整列,右键点击‘插入’,将在左侧插入空白行;如果选中连续多个单元格,显示<插入>弹窗(类似于 Excel 风格),用户可根据实际情况选择所需操作。

2. 代码分享

import sys
from PyQt6.QtCore import Qt, pyqtSignal, pyqtSlot
from PyQt6.QtGui import QAction, QKeySequence, QShortcut, QColor
from PyQt6.QtWidgets import QTableWidget, QTableWidgetItem, QDialog, QVBoxLayout, QApplication, QMenu, QMessageBox, \
    QGroupBox, QRadioButton, QPushButton, QHBoxLayout, QHeaderView


class CleverTableWidget(QTableWidget):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.copy_action = QAction("复制", self)
        self.paste_action = QAction("粘贴", self)
        self.clear_action = QAction("清空", self)
        self.delete_action = QAction("删除", self)
        self.align_left_action = QAction("左对齐", self)
        self.align_right_action = QAction("右对齐", self)
        self.align_center_action = QAction("居中对齐", self)
        self.highlight_action = QAction("高亮", self)
        self.cancle_highlight_action = QAction("取消高亮", self)
        self.bold_action = QAction("加粗", self)
        self.cancle_bold_action = QAction("取消加粗", self)

        self.copy_action.triggered.connect(self.copy_selection)
        self.paste_action.triggered.connect(self.paste_selection)
        self.clear_action.triggered.connect(self.clear_selection)
        self.delete_action.triggered.connect(self.delete_selection)
        self.align_left_action.triggered.connect(self.align_left)
        self.align_right_action.triggered.connect(self.align_right)
        self.align_center_action.triggered.connect(self.align_center)
        self.highlight_action.triggered.connect(self.highlight_background(choice='Y'))
        self.cancle_highlight_action.triggered.connect(self.highlight_background(choice='T'))
        self.bold_action.triggered.connect(self.bold_text(choice='Y'))
        self.cancle_bold_action.triggered.connect(self.bold_text(choice='N'))

        # 标题行增加筛选按钮

        # 设置快捷键
        self.copy_action.setShortcut('Ctrl+C')
        self.paste_action.setShortcut('Ctrl+V')
        self.clear_action.setShortcut('Ctrl+0')
        self.delete_action.setShortcut('Del')
        self.align_left_action.setShortcut('Ctrl+L')
        self.align_right_action.setShortcut('Ctrl+R')
        self.align_center_action.setShortcut('Ctrl+E')
        QShortcut(QKeySequence("Ctrl+C"), self).activated.connect(self.copy_selection)
        QShortcut(QKeySequence("Ctrl+V"), self).activated.connect(self.paste_selection)
        QShortcut(QKeySequence("Ctrl+0"), self).activated.connect(self.clear_selection)
        QShortcut(QKeySequence("Del"), self).activated.connect(self.delete_selection)
        QShortcut(QKeySequence("Ctrl+L"), self).activated.connect(self.align_left)
        QShortcut(QKeySequence("Ctrl+R"), self).activated.connect(self.align_right)
        QShortcut(QKeySequence("Ctrl+E"), self).activated.connect(self.align_center)

        self.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
        self.customContextMenuRequested.connect(self.showContextMenu)

    def showContextMenu(self, pos):
        menu = QMenu(self)
        align_menu = QMenu('对齐方式', self)

        insert_action = QAction("插入", self)
        insert_action_common = QAction("插入", self)
        insert_action_common.triggered.connect(self.insert_base_on_selection)

        menu.addAction(self.copy_action)
        menu.addAction(self.paste_action)
        menu.addAction(self.clear_action)
        menu.addAction(self.delete_action)
        align_menu.addAction(self.align_center_action)
        align_menu.addAction(self.align_left_action)
        align_menu.addAction(self.align_right_action)

        # 插入操作
        selected = self.selectedRanges()
        if len(selected) == 0:
            return
        elif len(selected) > 1:  # 不支持多重选择场景
            return
        else:
            s = selected[0]
            srn = s.bottomRow() - s.topRow() + 1
            scn = s.rightColumn() - s.leftColumn() + 1
            if srn == self.rowCount():  # 选中区域行数等于表格最大行数, 选中整列, 向左侧插入新列
                insert_action.triggered.connect(self.insert_whole_base_on_selection(insert_type='C'))
                menu.addAction(insert_action)
            elif scn == self.columnCount():  # 选中区域列数等于表格最大列数, 选中整行, 向上方插入新行
                insert_action.triggered.connect(self.insert_whole_base_on_selection(insert_type='R'))
                menu.addAction(insert_action)
            else:
                menu.addAction(insert_action_common)

        menu.addMenu(align_menu)
        menu.addAction(self.highlight_action)
        menu.addAction(self.bold_action)
        menu.addAction(self.cancle_highlight_action)
        menu.addAction(self.cancle_bold_action)
        menu.exec(self.viewport().mapToGlobal(pos))

    def get_first_empty_row_id(self):
        first_empty_row_id = 0
        for row in range(self.rowCount()):
            exist_item = False
            for column in range(self.columnCount()):
                if self.item(row, column) is not None and self.item(row, column).text() not in ['', ' ']:
                    exist_item = True
                    break
            if not exist_item:
                return first_empty_row_id
            first_empty_row_id += 1
        return first_empty_row_id

    def read_table_context(self):
        context = [[self.item(row, col).text() if self.item(row, col) is not None else ''
                    for col in range(self.columnCount())] for row in range(self.rowCount())]
        return context

    def _insert_add_row_helper(self, selected_top, selected_bottom):
        finaly_unempty_row_id = self.rowCount()
        end = False
        for row in sorted(range(self.rowCount()), reverse=True):
            finaly_unempty_row_id -= 1
            for column in range(selected_top, selected_bottom + 1):
                if self.item(row, column) is not None and self.item(row, column).text() not in ['', ' ']:
                    end = True
                    break
            if end:
                break
        return finaly_unempty_row_id

    def _insert_add_col_helper(self, selected_left, selected_right):
        finaly_unempty_col_id = self.columnCount()
        end = False
        for column in sorted(range(self.columnCount()), reverse=True):
            finaly_unempty_col_id -= 1
            for row in range(selected_left, selected_right + 1):
                if self.item(row, column) is not None and self.item(row, column).text() not in ['', ' ']:
                    end = True
                    break
            if end:
                break
        return finaly_unempty_col_id

    def _judge_rectangular_selected(self):
        selection = self.selectedRanges()
        if len(selection) != 1:
            return False
        return True

    def copy_selection(self):
        if not self._judge_rectangular_selected():
            return
        selected = self.selectedRanges()[0]
        text = "\n".join(['\t'.join([self.item(row, col).text() if self.item(row, col) is not None else ''
                                     for col in range(selected.leftColumn(), selected.rightColumn() + 1)])
                          for row in range(selected.topRow(), selected.bottomRow() + 1)])
        QApplication.clipboard().setText(text)

    def paste_selection(self):
        if not self._judge_rectangular_selected():
            return
        selected = self.selectedRanges()[0]
        text = QApplication.clipboard().text()
        rows = text.split('\n')
        if '' in rows:
            rows.remove('')

        for r, row in enumerate(rows):
            if selected.topRow() + r >= self.rowCount():
                self.insertRow(selected.topRow() + r)
            cols = row.split('\t')
            for c, text in enumerate(cols):
                if selected.leftColumn() + c >= self.columnCount():
                    self.insertColumn(selected.leftColumn() + c)
                self.setItem(selected.topRow() + r, selected.leftColumn() + c, QTableWidgetItem(text))

    def clear_selection(self):
        for item in self.selectedItems():
            self.setItem(item.row(), item.column(), QTableWidgetItem(""))

    def delete_selection(self):
        if not self._judge_rectangular_selected():
            return
        delete_dialog = DeleteInsertDialog(dialog_type='delete')
        delete_dialog.DelSignal.connect(self._delete_operations)
        delete_dialog.setWindowModality(Qt.WindowModality.ApplicationModal)
        delete_dialog.exec()

    @pyqtSlot(str)
    def _delete_operations(self, message):
        selected = self.selectedRanges()[0]
        if message == 'Move Left':
            selected_cols_num = selected.rightColumn() - selected.leftColumn() + 1
            start_col_index = selected.leftColumn() + selected_cols_num
            for col in range(start_col_index, self.columnCount() + selected_cols_num):
                ori_col = col - selected_cols_num
                for row in range(selected.topRow(), selected.bottomRow() + 1):
                    if col < self.columnCount():
                        text = self.item(row, col).text() if self.item(row, col) is not None \
                            else ''
                        self.setItem(row, ori_col, QTableWidgetItem(text))
                    else:
                        self.setItem(row, ori_col, QTableWidgetItem(''))
        elif message == 'Move Up':
            selected_rows_num = selected.bottomRow() - selected.topRow() + 1
            start_row_index = selected.topRow() + selected_rows_num
            for row in range(start_row_index, self.rowCount() + selected_rows_num):
                ori_row = row - selected_rows_num
                for col in range(selected.leftColumn(), selected.rightColumn() + 1):
                    if row < self.rowCount():
                        text = self.item(row, col).text() if self.item(row, col) is not None \
                            else ''
                        self.setItem(ori_row, col, QTableWidgetItem(text))
                    else:
                        self.setItem(ori_row, col, QTableWidgetItem(''))
        elif message == 'Delete Selected Rows':
            # 从最后一列开始删除,避免删除后索引变化
            for row in sorted(range(selected.topRow(), selected.bottomRow() + 1), reverse=True):
                self.removeRow(row)
        elif message == 'Delete Selected Cols':
            # 从最后一列开始删除,避免删除后索引变化
            for column in sorted(range(selected.leftColumn(), selected.rightColumn() + 1), reverse=True):
                self.removeColumn(column)
        else:
            print('Empty Message')

    def insert_whole_base_on_selection(self, insert_type):
        def insert():
            selected = self.selectedRanges()[0]
            if insert_type == 'R':
                row_id = selected.topRow()
                for i in range(selected.bottomRow() - selected.topRow() + 1):
                    self.insertRow(row_id)
            elif insert_type == 'C':
                col_id = selected.leftColumn()
                for i in range(selected.rightColumn() - selected.leftColumn() + 1):
                    self.insertColumn(col_id)

        return insert

    def insert_base_on_selection(self):
        if not self._judge_rectangular_selected():
            return
        insert_dialog = DeleteInsertDialog(dialog_type='insert')
        insert_dialog.InsertSignal.connect(self._insert_operations)
        insert_dialog.setWindowModality(Qt.WindowModality.ApplicationModal)
        insert_dialog.exec()

    @pyqtSlot(str)
    def _insert_operations(self, message):
        selected = self.selectedRanges()[0]
        if message == 'Move Right':
            selected_cols_num = selected.rightColumn() - selected.leftColumn() + 1
            final_col = self._insert_add_col_helper(selected.topRow(), selected.bottomRow()) + 1 + selected_cols_num
            while self.columnCount() < final_col:
                self.insertColumn(self.columnCount())
            for col in sorted(range(selected.leftColumn(), final_col), reverse=True):
                ori_col = col - selected_cols_num
                for row in range(selected.topRow(), selected.bottomRow() + 1):
                    if col >= selected.leftColumn() + selected_cols_num:
                        text = self.item(row, ori_col).text() if self.item(row, ori_col) is not None else ''
                        self.setItem(row, col, QTableWidgetItem(text))
                    else:
                        self.setItem(row, col, QTableWidgetItem(''))
            print('OK')
        elif message == 'Move Down':
            selected_rows_num = selected.bottomRow() - selected.topRow() + 1
            final_row = self._insert_add_row_helper(selected.leftColumn(),
                                                    selected.rightColumn()) + 1 + selected_rows_num
            while self.rowCount() < final_row:
                self.insertRow(self.rowCount())
            for row in sorted(range(selected.topRow(), final_row), reverse=True):
                ori_row = row - selected_rows_num
                for col in range(selected.leftColumn(), selected.rightColumn() + 1):
                    if row >= selected.topRow() + selected_rows_num:
                        text = self.item(ori_row, col).text() if self.item(ori_row, col) is not None else ''
                        self.setItem(row, col, QTableWidgetItem(text))
                    else:
                        self.setItem(row, col, QTableWidgetItem(''))
        elif message == 'Insert Rows Above':
            self.insert_whole_base_on_selection('R')()
        elif message == 'Insert Cols Left':
            self.insert_whole_base_on_selection('C')()
        else:
            print('Empty Message')

    def _set_null_item(self):
        if not self._judge_rectangular_selected():
            return
        s = self.selectedRanges()[0]
        for row in range(s.topRow(), s.bottomRow() + 1):
            for col in range(s.leftColumn(), s.rightColumn() + 1):
                item = self.item(row, col)
                if item is None:
                    self.setItem(row, col, QTableWidgetItem(''))

    def align_right(self):
        self._set_null_item()
        for item in self.selectedItems():
            item.setTextAlignment(Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter)

    def align_left(self):
        self._set_null_item()
        for item in self.selectedItems():
            item.setTextAlignment(Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter)

    def align_center(self):
        self._set_null_item()
        for item in self.selectedItems():
            item.setTextAlignment(Qt.AlignmentFlag.AlignCenter)

    def highlight_background(self, choice):
        def highlight_operation():
            self._set_null_item()
            yellow_bg = QColor(255, 255, 0)
            transparent_bg = QColor(255, 255, 255, 0)
            for item in self.selectedItems():
                if choice == 'Y':
                    item.setBackground(yellow_bg)
                elif choice == 'T':
                    item.setBackground(transparent_bg)
                else:
                    print('highlight background error input')
        return highlight_operation

    def bold_text(self, choice):
        def bold_operation():
            self._set_null_item()
            for item in self.selectedItems():
                item_font = item.font()
                if choice == 'Y':
                    item_font.setBold(True)
                    item.setFont(item_font)
                elif choice == 'N':
                    item_font.setBold(False)
                    item.setFont(item_font)
                else:
                    print('bold operation error input')
        return bold_operation

    @staticmethod
    def write_dim2_list_to_table(dim2_list, tablewidget_object):
        for row, dim1_list in enumerate(dim2_list):
            for col, text in enumerate(dim1_list):
                item = QTableWidgetItem(str(text))
                item.setTextAlignment(Qt.AlignmentFlag.AlignCenter)
                tablewidget_object.setItem(row, col, item)

    def filter_operation(self, col, filter_list):
        for row in range(self.rowCount()):
            item = self.item(row, col)
            if item is not None and item.text() in filter_list:
                continue
            else:
                self.setRowHidden(row, True)


class DeleteInsertDialog(QDialog):
    DelSignal = pyqtSignal(str)
    InsertSignal = pyqtSignal(str)

    def __init__(self, dialog_type):
        super().__init__()
        self.dialog_type = dialog_type
        self.setWindowFlags(Qt.WindowType.Drawer | Qt.WindowType.WindowCloseButtonHint)
        self.dialog_typ_ch = '删除' if dialog_type.lower() == 'delete' else '插入'
        self.setWindowTitle(self.dialog_typ_ch)
        self.resize(250, 150)

        # 1. 创建GroupBox
        self.groupBox = QGroupBox(self.dialog_typ_ch)
        self.groupBox.setFlat(True)

        # 1.1 创建RadioButton
        self.radioButtonA = QRadioButton()
        self.radioButtonB = QRadioButton()
        self.radioButtonC = QRadioButton()
        self.radioButtonD = QRadioButton()

        # 1.2 将RadioButton添加到GroupBox中
        gb_vbox = QVBoxLayout()
        gb_vbox.addWidget(self.radioButtonA)
        gb_vbox.addWidget(self.radioButtonB)
        gb_vbox.addWidget(self.radioButtonC)
        gb_vbox.addWidget(self.radioButtonD)
        self.groupBox.setLayout(gb_vbox)

        # 2. 创建确定和取消按钮
        self.buttonOK = QPushButton("确定")
        self.buttonCancel = QPushButton("取消")

        # 2.1 将按钮添加到水平布局中
        hbox = QHBoxLayout()
        hbox.addWidget(self.buttonOK)
        hbox.addWidget(self.buttonCancel)

        # 3. 将GroupBox和按钮添加到垂直布局中
        vbox = QVBoxLayout()
        vbox.addWidget(self.groupBox)
        vbox.addLayout(hbox)

        # 4. 设置对话框的布局
        self.setLayout(vbox)
        self.buttonCancel.clicked.connect(self.close)
        self._preperation()

    def _preperation(self):
        if self.dialog_type.lower() == 'delete':
            # 1. 设置文本
            self.radioButtonA.setText('右侧单元格左移(L)')
            self.radioButtonB.setText('下方单元格上移(U)')
            self.radioButtonC.setText('整行(R)')
            self.radioButtonD.setText('整列(C)')

            # 2 设置热键
            QShortcut(QKeySequence("L"), self).activated.connect(self.radioButtonA.toggle)
            QShortcut(QKeySequence("U"), self).activated.connect(self.radioButtonB.toggle)
            QShortcut(QKeySequence("R"), self).activated.connect(self.radioButtonC.toggle)
            QShortcut(QKeySequence("C"), self).activated.connect(self.radioButtonD.toggle)

            self.buttonOK.clicked.connect(self.delete_button_ok_clicked)
        else:
            # 1. 设置文本
            self.radioButtonA.setText('活动单元格右移(R)')
            self.radioButtonB.setText('活动单元格下移(D)')
            self.radioButtonC.setText('整行(R)')
            self.radioButtonD.setText('整列(C)')

            # 2 设置热键
            QShortcut(QKeySequence("R"), self).activated.connect(self.radioButtonA.toggle)
            QShortcut(QKeySequence("D"), self).activated.connect(self.radioButtonB.toggle)
            QShortcut(QKeySequence("R"), self).activated.connect(self.radioButtonC.toggle)
            QShortcut(QKeySequence("C"), self).activated.connect(self.radioButtonD.toggle)

            self.buttonOK.clicked.connect(self.insert_button_ok_clicked)

    def delete_button_ok_clicked(self):
        if self.radioButtonA.isChecked():
            self.DelSignal.emit('Move Left')
        elif self.radioButtonB.isChecked():
            self.DelSignal.emit('Move Up')
        elif self.radioButtonC.isChecked():
            self.DelSignal.emit('Delete Selected Rows')
        elif self.radioButtonD.isChecked():
            self.DelSignal.emit('Delete Selected Cols')
        else:
            self.DelSignal.emit('')
        self.close()

    def insert_button_ok_clicked(self):
        if self.radioButtonA.isChecked():
            self.InsertSignal.emit('Move Right')
        elif self.radioButtonB.isChecked():
            self.InsertSignal.emit('Move Down')
        elif self.radioButtonC.isChecked():
            self.InsertSignal.emit('Insert Rows Above')
        elif self.radioButtonD.isChecked():
            self.InsertSignal.emit('Insert Cols Left')
        else:
            self.InsertSignal.emit('')
        self.close()


class UsingCleverTW(QDialog):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.setWindowTitle("CleverTableWidget")
        self.setWindowFlags(
            Qt.WindowType.WindowMinimizeButtonHint | Qt.WindowType.WindowMaximizeButtonHint | Qt.WindowType.WindowCloseButtonHint)
        self.tableWidget = CleverTableWidget()
        self.tableWidget.setRowCount(10)
        self.tableWidget.setColumnCount(5)

        layout = QVBoxLayout()
        layout.addWidget(self.tableWidget)
        self.setLayout(layout)
        self.add_table_content()
        self.resize(700, 400)
        self.show()

    def add_table_content(self):
        for i in range(10):
            for j in range(5):
                item = QTableWidgetItem('{}{}'.format(i + 1, j + 1))
                self.tableWidget.setItem(i, j, item)


if __name__ == '__main__':
    app = QApplication(sys.argv)
    ui = UsingCleverTW()
    ui.show()
    sys.exit(app.exec())

3. 代码细节说明

3.1 模态窗口

模态窗口是指打开后,用户必须先处理该窗口中的内容,才能继续进行其他操作。在模态窗口打开时,用户无法与其他窗口进行交互操作,直到该模态窗口被关闭或者完成操作。

非模态窗口可以与其他窗口同时存在,用户可以在不关闭该窗口的情况下进行其他操作。非模态窗口通常用于提供一些辅助功能或者信息展示,而不是必须要用户操作的任务。

可以通过下述代码,将子窗口设置为模态窗口:

.setWindowModality(Qt.WindowModality.ApplicationModal)

3.2 子窗口与父窗口的信息交互

在子窗口中用 pyqtSignal 定义新的信号(注意:信号需作为类属性,不要写成实例属性),通过信号的 emit() 函数将需传递的信息(信号)发射给主窗口;主窗口上将该信号绑定到槽函数,从而实现获取并处理来自子窗口的消息。

3.3 ContextMenuPolicy

3.3.1 CustomContextMenu

ContextMenuPolicy 表示如何显示上下文菜单,默认为 DefaultContextMenu,更多信息可参考<参考链接1>。

DefaultContextMenu默认菜单,重写 contextMenuEvent() 实现自定义。
NoContextMenu无菜单,事件响应传递给部件父级。
PreventContextMenu无菜单,事件响应不继续传递。
ActionsContextMenu事件菜单,只响应部件事件,部件子件的事件不响应。
CustomContextMenu用户自定义菜单,自定义 customContextMenuRequested 信号的槽函数。

当前代码中设置为 CustomContextMenu 策略,表示当用户在右键单击时,会触发 customContextMenuRequested 信号,显示自定义上下文菜单。

self.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)

3.3.2 ActionsContextMenu

代码分享
import sys
from PyQt6.QtCore import Qt, pyqtSignal, pyqtSlot
from PyQt6.QtGui import QAction, QKeySequence, QShortcut, QColor
from PyQt6.QtWidgets import QTableWidget, QTableWidgetItem, QDialog, QVBoxLayout, QApplication, QMenu, QMessageBox, \
    QGroupBox, QRadioButton, QPushButton, QHBoxLayout


class CleverTableWidget(QTableWidget):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.copy_action = QAction("复制", self)
        self.paste_action = QAction("粘贴", self)
        self.clear_action = QAction("清空", self)
        self.delete_action = QAction("删除", self)
        self.align_left_action = QAction("左对齐", self)
        self.align_right_action = QAction("右对齐", self)
        self.align_center_action = QAction("居中对齐", self)
        self.highlight_action = QAction("高亮", self)
        self.cancle_highlight_action = QAction("取消高亮", self)
        self.bold_action = QAction("加粗", self)
        self.cancle_bold_action = QAction("取消加粗", self)

        self.copy_action.triggered.connect(self.copy_selection)
        self.paste_action.triggered.connect(self.paste_selection)
        self.clear_action.triggered.connect(self.clear_selection)
        self.delete_action.triggered.connect(self.delete_selection)
        self.align_left_action.triggered.connect(self.align_left)
        self.align_right_action.triggered.connect(self.align_right)
        self.align_center_action.triggered.connect(self.align_center)
        self.highlight_action.triggered.connect(self.highlight_background(choice='Y'))
        self.cancle_highlight_action.triggered.connect(self.highlight_background(choice='T'))
        self.bold_action.triggered.connect(self.bold_text(choice='Y'))
        self.cancle_bold_action.triggered.connect(self.bold_text(choice='N'))

        # 设置快捷键
        self.copy_action.setShortcut('Ctrl+C')
        self.paste_action.setShortcut('Ctrl+V')
        self.clear_action.setShortcut('Ctrl+0')
        self.delete_action.setShortcut('Del')
        self.align_left_action.setShortcut('Ctrl+L')
        self.align_right_action.setShortcut('Ctrl+R')
        self.align_center_action.setShortcut('Ctrl+E')
        QShortcut(QKeySequence("Ctrl+C"), self).activated.connect(self.copy_selection)
        QShortcut(QKeySequence("Ctrl+V"), self).activated.connect(self.paste_selection)
        QShortcut(QKeySequence("Ctrl+0"), self).activated.connect(self.clear_selection)
        QShortcut(QKeySequence("Del"), self).activated.connect(self.delete_selection)
        QShortcut(QKeySequence("Ctrl+L"), self).activated.connect(self.align_left)
        QShortcut(QKeySequence("Ctrl+R"), self).activated.connect(self.align_right)
        QShortcut(QKeySequence("Ctrl+E"), self).activated.connect(self.align_center)

        self.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
        self.customContextMenuRequested.connect(self.showContextMenu)

    def showContextMenu(self, pos):
        menu = QMenu(self)
        align_menu = QMenu('对齐方式', self)

        insert_action = QAction("插入", self)
        insert_action_common = QAction("插入", self)
        insert_action_common.triggered.connect(self.insert_base_on_selection)

        menu.addAction(self.copy_action)
        menu.addAction(self.paste_action)
        menu.addAction(self.clear_action)
        menu.addAction(self.delete_action)
        align_menu.addAction(self.align_center_action)
        align_menu.addAction(self.align_left_action)
        align_menu.addAction(self.align_right_action)

        # 插入操作
        selected = self.selectedRanges()
        if len(selected) == 0:
            return
        elif len(selected) > 1:  # 不支持多重选择场景
            return
        else:
            s = selected[0]
            srn = s.bottomRow() - s.topRow() + 1
            scn = s.rightColumn() - s.leftColumn() + 1
            if srn == self.rowCount():  # 选中区域行数等于表格最大行数, 选中整列, 向左侧插入新列
                insert_action.triggered.connect(self.insert_whole_base_on_selection(insert_type='C'))
                menu.addAction(insert_action)
            elif scn == self.columnCount():  # 选中区域列数等于表格最大列数, 选中整行, 向上方插入新行
                insert_action.triggered.connect(self.insert_whole_base_on_selection(insert_type='R'))
                menu.addAction(insert_action)
            else:
                menu.addAction(insert_action_common)

        menu.addMenu(align_menu)
        menu.addAction(self.highlight_action)
        menu.addAction(self.bold_action)
        menu.addAction(self.cancle_highlight_action)
        menu.addAction(self.cancle_bold_action)
        menu.exec(self.viewport().mapToGlobal(pos))

    def get_first_empty_row_id(self):
        first_empty_row_id = 0
        for row in range(self.rowCount()):
            exist_item = False
            for column in range(self.columnCount()):
                if self.item(row, column) is not None and self.item(row, column).text() not in ['', ' ']:
                    exist_item = True
                    break
            if not exist_item:
                return first_empty_row_id
            first_empty_row_id += 1
        return first_empty_row_id

    def read_table_context(self):
        context = [[self.item(row, col).text() if self.item(row, col) is not None else ''
                    for col in range(self.columnCount())] for row in range(self.rowCount())]
        return context

    def _insert_add_row_helper(self, selected_top, selected_bottom):
        finaly_unempty_row_id = self.rowCount()
        end = False
        for row in sorted(range(self.rowCount()), reverse=True):
            finaly_unempty_row_id -= 1
            for column in range(selected_top, selected_bottom + 1):
                if self.item(row, column) is not None and self.item(row, column).text() not in ['', ' ']:
                    end = True
                    break
            if end:
                break
        return finaly_unempty_row_id

    def _insert_add_col_helper(self, selected_left, selected_right):
        finaly_unempty_col_id = self.columnCount()
        end = False
        for column in sorted(range(self.columnCount()), reverse=True):
            finaly_unempty_col_id -= 1
            for row in range(selected_left, selected_right + 1):
                if self.item(row, column) is not None and self.item(row, column).text() not in ['', ' ']:
                    end = True
                    break
            if end:
                break
        return finaly_unempty_col_id

    def _judge_rectangular_selected(self):
        selection = self.selectedRanges()
        if len(selection) != 1:
            QMessageBox.warning(self, "选中区域非法", "多重选择或未选中任何区域, 当前操作不支持多重选择")
            return False
        return True

    def copy_selection(self):
        if not self._judge_rectangular_selected():
            return
        selected = self.selectedRanges()[0]
        text = "\n".join(['\t'.join([self.item(row, col).text() if self.item(row, col) is not None else ''
                                     for col in range(selected.leftColumn(), selected.rightColumn() + 1)])
                          for row in range(selected.topRow(), selected.bottomRow() + 1)])
        QApplication.clipboard().setText(text)

    def paste_selection(self):
        if not self._judge_rectangular_selected():
            return
        selected = self.selectedRanges()[0]
        text = QApplication.clipboard().text()
        rows = text.split('\n')
        if '' in rows:
            rows.remove('')

        for r, row in enumerate(rows):
            if selected.topRow() + r >= self.rowCount():
                self.insertRow(selected.topRow() + r)
            cols = row.split('\t')
            for c, text in enumerate(cols):
                if selected.leftColumn() + c >= self.columnCount():
                    self.insertColumn(selected.leftColumn() + c)
                self.setItem(selected.topRow() + r, selected.leftColumn() + c, QTableWidgetItem(text))

    def clear_selection(self):
        for item in self.selectedItems():
            self.setItem(item.row(), item.column(), QTableWidgetItem(""))

    def delete_selection(self):
        if not self._judge_rectangular_selected():
            return
        delete_dialog = DeleteInsertDialog(dialog_type='delete')
        delete_dialog.DelSignal.connect(self._delete_operations)
        delete_dialog.setWindowModality(Qt.WindowModality.ApplicationModal)
        delete_dialog.exec()

    @pyqtSlot(str)
    def _delete_operations(self, message):
        selected = self.selectedRanges()[0]
        if message == 'Move Left':
            selected_cols_num = selected.rightColumn() - selected.leftColumn() + 1
            start_col_index = selected.leftColumn() + selected_cols_num
            for col in range(start_col_index, self.columnCount() + selected_cols_num):
                ori_col = col - selected_cols_num
                for row in range(selected.topRow(), selected.bottomRow() + 1):
                    if col < self.columnCount():
                        text = self.item(row, col).text() if self.item(row, col) is not None \
                            else self.item(row, col).text
                        self.setItem(row, ori_col, QTableWidgetItem(text))
                    else:
                        self.setItem(row, ori_col, QTableWidgetItem(''))
        elif message == 'Move Up':
            selected_rows_num = selected.bottomRow() - selected.topRow() + 1
            start_row_index = selected.topRow() + selected_rows_num
            for row in range(start_row_index, self.rowCount() + selected_rows_num):
                ori_row = row - selected_rows_num
                for col in range(selected.leftColumn(), selected.rightColumn() + 1):
                    if row < self.rowCount():
                        text = self.item(row, col).text() if self.item(row, col) is not None \
                            else self.item(row, col).text
                        self.setItem(ori_row, col, QTableWidgetItem(text))
                    else:
                        self.setItem(ori_row, col, QTableWidgetItem(''))
        elif message == 'Delete Selected Rows':
            # 从最后一列开始删除,避免删除后索引变化
            for row in sorted(range(selected.topRow(), selected.bottomRow() + 1), reverse=True):
                self.removeRow(row)
        elif message == 'Delete Selected Cols':
            # 从最后一列开始删除,避免删除后索引变化
            for column in sorted(range(selected.leftColumn(), selected.rightColumn() + 1), reverse=True):
                self.removeColumn(column)
        else:
            print('Empty Message')

    def insert_whole_base_on_selection(self, insert_type):
        def insert():
            selected = self.selectedRanges()[0]
            if insert_type == 'R':
                row_id = selected.topRow()
                for i in range(selected.bottomRow() - selected.topRow() + 1):
                    self.insertRow(row_id)
            elif insert_type == 'C':
                col_id = selected.leftColumn()
                for i in range(selected.rightColumn() - selected.leftColumn() + 1):
                    self.insertColumn(col_id)

        return insert

    def insert_base_on_selection(self):
        if not self._judge_rectangular_selected():
            return
        insert_dialog = DeleteInsertDialog(dialog_type='insert')
        insert_dialog.InsertSignal.connect(self._insert_operations)
        insert_dialog.setWindowModality(Qt.WindowModality.ApplicationModal)
        insert_dialog.exec()

    @pyqtSlot(str)
    def _insert_operations(self, message):
        selected = self.selectedRanges()[0]
        if message == 'Move Right':
            selected_cols_num = selected.rightColumn() - selected.leftColumn() + 1
            final_col = self._insert_add_col_helper(selected.topRow(), selected.bottomRow()) + 1 + selected_cols_num
            while self.columnCount() < final_col:
                self.insertColumn(self.columnCount())
            for col in sorted(range(selected.leftColumn(), final_col), reverse=True):
                ori_col = col - selected_cols_num
                for row in range(selected.topRow(), selected.bottomRow() + 1):
                    if col >= selected.leftColumn() + selected_cols_num:
                        text = self.item(row, ori_col).text() if self.item(row, ori_col) is not None else ''
                        self.setItem(row, col, QTableWidgetItem(text))
                    else:
                        self.setItem(row, col, QTableWidgetItem(''))
            print('OK')
        elif message == 'Move Down':
            selected_rows_num = selected.bottomRow() - selected.topRow() + 1
            final_row = self._insert_add_row_helper(selected.leftColumn(),
                                                    selected.rightColumn()) + 1 + selected_rows_num
            while self.rowCount() < final_row:
                self.insertRow(self.rowCount())
            for row in sorted(range(selected.topRow(), final_row), reverse=True):
                ori_row = row - selected_rows_num
                for col in range(selected.leftColumn(), selected.rightColumn() + 1):
                    if row >= selected.topRow() + selected_rows_num:
                        text = self.item(ori_row, col).text() if self.item(ori_row, col) is not None else ''
                        self.setItem(row, col, QTableWidgetItem(text))
                    else:
                        self.setItem(row, col, QTableWidgetItem(''))
        elif message == 'Insert Rows Above':
            self.insert_whole_base_on_selection('R')()
        elif message == 'Insert Cols Left':
            self.insert_whole_base_on_selection('C')()
        else:
            print('Empty Message')

    def _set_null_item(self):
        if not self._judge_rectangular_selected():
            return
        s = self.selectedRanges()[0]
        for row in range(s.topRow(), s.bottomRow() + 1):
            for col in range(s.leftColumn(), s.rightColumn() + 1):
                item = self.item(row, col)
                if item is None:
                    self.setItem(row, col, QTableWidgetItem(''))

    def align_right(self):
        self._set_null_item()
        for item in self.selectedItems():
            item.setTextAlignment(Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter)

    def align_left(self):
        self._set_null_item()
        for item in self.selectedItems():
            item.setTextAlignment(Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter)

    def align_center(self):
        self._set_null_item()
        for item in self.selectedItems():
            item.setTextAlignment(Qt.AlignmentFlag.AlignCenter)

    def highlight_background(self, choice):
        def highlight_operation():
            self._set_null_item()
            yellow_bg = QColor(255, 255, 0)
            transparent_bg = QColor(255, 255, 255, 0)
            for item in self.selectedItems():
                if choice == 'Y':
                    item.setBackground(yellow_bg)
                elif choice == 'T':
                    item.setBackground(transparent_bg)
                else:
                    print('highlight background error input')
        return highlight_operation

    def bold_text(self, choice):
        def bold_operation():
            self._set_null_item()
            for item in self.selectedItems():
                item_font = item.font()
                if choice == 'Y':
                    item_font.setBold(True)
                    item.setFont(item_font)
                elif choice == 'N':
                    item_font.setBold(False)
                    item.setFont(item_font)
                else:
                    print('bold operation error input')
        return bold_operation


class DeleteInsertDialog(QDialog):
    DelSignal = pyqtSignal(str)
    InsertSignal = pyqtSignal(str)

    def __init__(self, dialog_type):
        super().__init__()
        self.dialog_type = dialog_type
        self.setWindowFlags(Qt.WindowType.Drawer | Qt.WindowType.WindowCloseButtonHint)
        self.dialog_typ_ch = '删除' if dialog_type.lower() == 'delete' else '插入'
        self.setWindowTitle(self.dialog_typ_ch)
        self.resize(250, 150)

        # 1. 创建GroupBox
        self.groupBox = QGroupBox(self.dialog_typ_ch)
        self.groupBox.setFlat(True)

        # 1.1 创建RadioButton
        self.radioButtonA = QRadioButton()
        self.radioButtonB = QRadioButton()
        self.radioButtonC = QRadioButton()
        self.radioButtonD = QRadioButton()

        # 1.2 将RadioButton添加到GroupBox中
        gb_vbox = QVBoxLayout()
        gb_vbox.addWidget(self.radioButtonA)
        gb_vbox.addWidget(self.radioButtonB)
        gb_vbox.addWidget(self.radioButtonC)
        gb_vbox.addWidget(self.radioButtonD)
        self.groupBox.setLayout(gb_vbox)

        # 2. 创建确定和取消按钮
        self.buttonOK = QPushButton("确定")
        self.buttonCancel = QPushButton("取消")

        # 2.1 将按钮添加到水平布局中
        hbox = QHBoxLayout()
        hbox.addWidget(self.buttonOK)
        hbox.addWidget(self.buttonCancel)

        # 3. 将GroupBox和按钮添加到垂直布局中
        vbox = QVBoxLayout()
        vbox.addWidget(self.groupBox)
        vbox.addLayout(hbox)

        # 4. 设置对话框的布局
        self.setLayout(vbox)
        self.buttonCancel.clicked.connect(self.close)
        self._preperation()

    def _preperation(self):
        if self.dialog_type.lower() == 'delete':
            # 1. 设置文本
            self.radioButtonA.setText('右侧单元格左移(L)')
            self.radioButtonB.setText('下方单元格上移(U)')
            self.radioButtonC.setText('整行(R)')
            self.radioButtonD.setText('整列(C)')

            # 2 设置热键
            QShortcut(QKeySequence("L"), self).activated.connect(self.radioButtonA.toggle)
            QShortcut(QKeySequence("U"), self).activated.connect(self.radioButtonB.toggle)
            QShortcut(QKeySequence("R"), self).activated.connect(self.radioButtonC.toggle)
            QShortcut(QKeySequence("C"), self).activated.connect(self.radioButtonD.toggle)

            self.buttonOK.clicked.connect(self.delete_button_ok_clicked)
        else:
            # 1. 设置文本
            self.radioButtonA.setText('活动单元格右移(R)')
            self.radioButtonB.setText('活动单元格下移(D)')
            self.radioButtonC.setText('整行(R)')
            self.radioButtonD.setText('整列(C)')

            # 2 设置热键
            QShortcut(QKeySequence("R"), self).activated.connect(self.radioButtonA.toggle)
            QShortcut(QKeySequence("D"), self).activated.connect(self.radioButtonB.toggle)
            QShortcut(QKeySequence("R"), self).activated.connect(self.radioButtonC.toggle)
            QShortcut(QKeySequence("C"), self).activated.connect(self.radioButtonD.toggle)

            self.buttonOK.clicked.connect(self.insert_button_ok_clicked)

    def delete_button_ok_clicked(self):
        if self.radioButtonA.isChecked():
            self.DelSignal.emit('Move Left')
        elif self.radioButtonB.isChecked():
            self.DelSignal.emit('Move Up')
        elif self.radioButtonC.isChecked():
            self.DelSignal.emit('Delete Selected Rows')
        elif self.radioButtonD.isChecked():
            self.DelSignal.emit('Delete Selected Cols')
        else:
            self.DelSignal.emit('')
        self.close()

    def insert_button_ok_clicked(self):
        if self.radioButtonA.isChecked():
            self.InsertSignal.emit('Move Right')
        elif self.radioButtonB.isChecked():
            self.InsertSignal.emit('Move Down')
        elif self.radioButtonC.isChecked():
            self.InsertSignal.emit('Insert Rows Above')
        elif self.radioButtonD.isChecked():
            self.InsertSignal.emit('Insert Cols Left')
        else:
            self.InsertSignal.emit('')
        self.close()


class UsingCleverTW(QDialog):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.setWindowTitle("CleverTableWidget")
        self.setWindowFlags(
            Qt.WindowType.WindowMinimizeButtonHint | Qt.WindowType.WindowMaximizeButtonHint | Qt.WindowType.WindowCloseButtonHint)
        self.tableWidget = CleverTableWidget()
        self.tableWidget.setRowCount(10)
        self.tableWidget.setColumnCount(5)

        layout = QVBoxLayout()
        layout.addWidget(self.tableWidget)
        self.setLayout(layout)
        self.add_table_content()
        self.resize(700, 400)
        self.show()

    def add_table_content(self):
        for i in range(10):
            for j in range(5):
                item = QTableWidgetItem('{}{}'.format(i + 1, j + 1))
                self.tableWidget.setItem(i, j, item)


if __name__ == '__main__':
    app = QApplication(sys.argv)
    ui = UsingCleverTW()
    ui.show()
    sys.exit(app.exec())
 效果展示

其他

本文代码暂不涉及的其他tablewidget 常用命令

self.tableWidget.setHorizontalHeaderLabels(['列1', '列2', '列3', '列4', '列5']) # 设置表头(列名)
self.tableWidget.setVerticalHeaderLabels(['行{}'.format(i+1) for i in range(10)]) # 设置行名
self.tableWidget.setAlternatingRowColors(True) # 设置隔行变色
self.tableWidget.horizontalHeader().setSectionsMovable(True) # 可以拖动列,改变列顺序
self.tableWidget.setEditTriggers(QTableWidget.EditTrigger.NoEditTriggers) # 禁止用户编辑表格内容

4. 参考链接

PyQt5 父窗口和子窗口信息通信

https://www.cnblogs.com/codedingzhen/p/17913655.html

  • 7
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
您可以使用QMenu和QAction来创建右键菜单,并在QTableWidget中的相关项上设置ContextMenuPolicy以显示右键菜单。然后,您可以将QAction与槽函数连接,以便在菜单时执行删除操作并更新sqlite3数据库。 以下是一个简的示例代码: ```python import sqlite3 from PyQt5.QtWidgets import QTableWidget, QTableWidgetItem, QMenu, QAction class MyTableWidget(QTableWidget): def __init__(self): super().__init__() self.setContextMenuPolicy(Qt.CustomContextMenu) self.customContextMenuRequested.connect(self.showContextMenu) # 设置表格列数和行数 self.setColumnCount(2) self.setRowCount(3) # 设置表格内容 self.setItem(0, 0, QTableWidgetItem("row1, column1")) self.setItem(0, 1, QTableWidgetItem("row1, column2")) self.setItem(1, 0, QTableWidgetItem("row2, column1")) self.setItem(1, 1, QTableWidgetItem("row2, column2")) self.setItem(2, 0, QTableWidgetItem("row3, column1")) self.setItem(2, 1, QTableWidgetItem("row3, column2")) def showContextMenu(self, pos): # 创建右键菜单 menu = QMenu(self) deleteAction = QAction("Delete", self) menu.addAction(deleteAction) # 连接删除操作的槽函数 deleteAction.triggered.connect(self.deleteItem) # 显示右键菜单 menu.exec_(self.mapToGlobal(pos)) def deleteItem(self): # 获取当前选中的行 selectedRow = self.currentRow() # 从sqlite3数据库中删除该行 conn = sqlite3.connect('your_database.db') c = conn.cursor() c.execute("DELETE FROM your_table WHERE id=?", (selectedRow + 1,)) conn.commit() conn.close() # 从表格中删除该行 self.removeRow(selectedRow) ``` 在这个示例中,我们首先创建一个QTableWidget并为其设置ContextMenuPolicy以显示右键菜单。然后,我们在右键菜单中添加了一个名为“Delete”的QAction,并将其连接到槽函数self.deleteItem()。在槽函数中,我们首先获取当前选中的行,然后使用sqlite3模块从数据库中删除该行。最后,我们从表格中删除该行。请注意,在这个示例中,我们假设您已经已经连接到sqlite3数据库并具有相应的表。 希望这可以帮助您实现QTableWidget 表格右键菜单delete功能,并及时删除sqlite3中的数据。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值