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) # 禁止用户编辑表格内容