ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

用Python+PySide6+openpyxl打造一个可用的Excel编辑器

用Python+PySide6+openpyxl打造一个可用的Excel编辑器 说句实话“用Python做Excel编辑器”这个标题第一眼看上去挺唬人第二眼就容易让人想偏。很多人以为是拿Python去调Excel的VBA或者做个命令行工具批量改数据其实都不是。这里说的编辑器是真真正正带图形界面、能双击单元格改内容、能增删行列、能保存回文件的桌面软件。我在实际做这类工具的时候最常用的一套组合是PySide6 QTableWidget openpyxl界面交互用Qt文件读写用openpyxl数据解析用pandas辅助。这篇文章把整个实现思路、核心代码、踩过的坑全部拆开讲一遍目标是让一个有一定Python基础的人能照着搭出一个能用的Excel编辑器雏形。这个项目适合谁两类人最合适。一类是经常跟Excel打交道、但嫌WPS和Office太重或者涉及批量处理太麻烦的办公人员自己做个小工具把高频操作收进去另一类是Python初学者想找个有实际价值、能练GUI和数据处理结合的练手项目。文里的代码不是玩具是能跑、能扩展、能继续往深处加功能的那种。1. 方案选型为什么是PySide6 QTableWidget openpyxl1.1 界面层用哪个GUI框架才像“编辑器”做编辑器第一步是选图形界面框架。Python的GUI方案就那么几个Tkinter、PyQt5/PySide6、wxPython、Kivy。我直接说结论现阶段做Excel编辑器PySide6是最合适的选择没有之一。Tkinter虽然内置、零安装但它的表格控件Treeview说白了就是个列表单元格编辑要靠绑定事件自己模拟复制粘贴、选区高亮、键盘导航这些编辑器的基础体验要全部手写。写成能用没问题写成好用要脱一层皮。PySide6是Qt官方的Python绑定里面的QTableWidget是天生为表格编辑而生的控件。它自带单元格编辑、选中区域、行列拖拽、键盘导航连复制粘贴都有默认行为。做Excel编辑器的UI层QTableWidget几乎就是照着需求设计的。选PySide6而不选PyQt5主要是授权协议的问题。PySide6是LGPL协议做内部工具或者开源项目都更省心PyQt5的GPL协议在某些商业场景下有坑。功能上两者高度重叠没必要在授权上给自己埋雷。1.2 数据层openpyxl与pandas怎么分工数据读写这一层有两种方案容易纠结openpyxl还是pandas。我两个都用但分工明确。openpyxl是直接操作xlsx文件的库能读单元格、写单元格、保留公式、读取单元格样式。它的强项是“保真”——读进来的是什么写出去的大体上还是什么。pandas的read_excel和to_excel用起来很爽尤其是数据处理场景一行代码把整个Sheet变成DataFrame然后随便groupby、pivot。但pandas的读写是“表格化”的它读进来会做类型推断、会丢公式、会把索引列写进文件这在“编辑器”这个场景下是不合格的。编辑器的核心要求是用户打开一个Excel文件看到的内容应该和Excel里几乎一模一样改完保存后公式和格式不能稀里哗啦掉一地。所以文件读写用openpyxl作为主力pandas只在需要做数据透视、批量清洗、类型转换的时候作为辅助工具拿出来用。一句话总结pandas适合算数openpyxl适合做编辑器。1.3 功能范围先定死避免陷进Excel全功能泥潭做编辑器最容易犯的错是想复刻Excel的全部功能。我建议一开始就把范围锁死。第一版不要碰图表、透视表、条件格式、宏、多Sheet联动这些大功能。核心就是五件事打开xlsx文件把数据加载到界面在表格里直接编辑单元格支持复制粘贴增删行、增删列排序保存回xlsx文件这五件事做扎实了就已经是一个非常能用的表格编辑器了。后面的功能比如查找替换、撤销重做、数据校验是在这个地基上一层层加的。我见过不少项目挂在半路上不是因为难是因为一开始就想什么都做结果每个功能都做了一半。2. 环境准备与框架搭建半个小时跑起一个能双击编辑的表格2.1 安装依赖正式开始前先把环境准备好。Python版本建议3.9以上PySide6目前对旧版Python的兼容性一般。直接pip安装pip install PySide6 openpyxl pandas如果网络比较慢可以用国内镜像源pip install PySide6 openpyxl pandas -i https://pypi.tuna.tsinghua.edu.cn/simple提示一下PySide6的安装包比较大一百多兆第一次装的时候要有心理准备耐心等它跑完。装完验证一下python -c import PySide6, openpyxl, pandas; print(ok)输出ok了环境就算齐了。2.2 基础窗口骨架先写一个最小可运行的窗口程序。这个程序不加载文件只是弹出一个带QTableWidget的空白窗口。界面布局大概是这样的顶部一个菜单栏中间一个QTableWidget铺满整个窗口底部一个状态栏显示当前的行列信息。import sys from PySide6.QtWidgets import QMainWindow, QTableWidget, QApplication from PySide6.QtCore import Qt class ExcelEditor(QMainWindow): def __init__(self): super().__init__() self.setWindowTitle(Python Excel Editor) self.resize(1200, 800) self.table QTableWidget(50, 10) self.setCentralWidget(self.table) # 基础设置 self.table.setEditTriggers(QTableWidget.EditTrigger.DoubleClicked | QTableWidget.EditTrigger.EditKeyPressed) self.table.setSelectionMode(QTableWidget.SelectionMode.ExtendedSelection) self.table.setSelectionBehavior(QTableWidget.SelectionBehavior.SelectItems) if __name__ __main__: app QApplication(sys.argv) window ExcelEditor() window.show() sys.exit(app.exec())这里的setEditTriggers是核心设置它决定了用户怎么进入编辑状态。我用的值是DoubleClicked和EditKeyPressed也就是双击进入编辑或者在选中单元格后按回车、F2也可以编辑。默认情况下QTableWidget的回车键行为是把焦点移到下一行设置EditKeyPressed之后回车就能直接进入编辑而不是跳走这个细节对编辑体验影响很大。行数和列数暂时写死了50行10列这只是为了让窗口启动时有个初始大小。后面加载文件时会根据文件内容动态调整。2.3 内存里的数据模型类型记忆与数据分离这一步是很多人会忽略的关键设计。QTableWidget的单元格显示的是字符串但Excel里的单元格是有类型的整数、浮点数、日期、公式、文本。如果直接把所有东西转成字符串塞进表格保存的时候就会出问题——数字变成了文本数字日期变成了英文日期字符串Excel打开会一脸茫然。我的做法是维护一个数据矩阵和一个类型矩阵。数据矩阵存每个单元格真正的值类型矩阵记录每个单元格的类型标记。界面显示的时候把值转成字符串显示的形态保存的时候根据类型矩阵重新构造单元格的数据类型。# 用两个二维列表维护数据模型 self.data [[None for _ in range(10)] for _ in range(50)] # 原始数据 self.data_type [[0 for _ in range(10)] for _ in range(50)] # 0默认 1数字 2日期 3公式 4文本为什么不能直接把值写成单元格的displayText因为QTableWidgetItem存的是QVariant虽然它有setData方法可以通过自定义角色保存原始数据。但是这种做法在排序的时候会出问题——排序是按显示文本排的还是按原始数值排的表现不一致。用独立的数据矩阵虽然多了同步的麻烦但逻辑清爽不会被控件的行为绑架。3. 核心功能实现读写、编辑、增删、排序与查找3.1 打开与保存Excel文件文件读取用openpyxl的load_workbook注意两个参数。一个是data_onlyFalse默认值保证公式能读出来而不是只读缓存值另一个是keep_vbaFalse加载含宏的文件时不去动它的VBA工程。第二个参数在处理一些老文件时很重要如果文件里嵌了宏openpyxl默认保留但编辑器用不上反而增加出错概率。from openpyxl import load_workbook def open_excel_file(self, file_path): wb load_workbook(file_path, data_onlyFalse) ws wb.active row_count ws.max_row col_count ws.max_column self.table.setRowCount(row_count) self.table.setColumnCount(col_count) # 重建数据与类型矩阵 self.data [[None for _ in range(col_count)] for _ in range(row_count)] self.data_type [[0 for _ in range(col_count)] for _ in range(row_count)] for row_idx in range(1, row_count 1): for col_idx in range(1, col_count 1): cell ws.cell(rowrow_idx, columncol_idx) value cell.value self.data[row_idx - 1][col_idx - 1] value # 判断类型 if cell.data_type n: self.data_type[row_idx - 1][col_idx - 1] 1 elif cell.data_type d: self.data_type[row_idx - 1][col_idx - 1] 2 elif cell.data_type f: self.data_type[row_idx - 1][col_idx - 1] 3 elif cell.data_type s: self.data_type[row_idx - 1][col_idx - 1] 4读取的循环看着简单但对理解Excel的数据模型非常有帮助。openpyxl里单元格的data_type有几种取值n代表数字d代表日期f代表公式s代表字符串还有b代表布尔值。逐个单元格判断类型存到类型矩阵就是为保存时能精准还原类型。保存的逻辑是逆操作遍历所有单元格根据类型矩阵决定怎么写入def save_excel_file(self, file_path): wb load_workbook(file_path) if hasattr(self, current_file) and self.current_file else Workbook() ws wb.active for row_idx in range(self.table.rowCount()): for col_idx in range(self.table.columnCount()): value self.data[row_idx][col_idx] cell_type self.data_type[row_idx][col_idx] if value is None: ws.cell(rowrow_idx 1, columncol_idx 1).value None elif cell_type 1: ws.cell(rowrow_idx 1, columncol_idx 1).value float(value) elif cell_type 2: # 日期类型需要转成datetime if isinstance(value, datetime): ws.cell(rowrow_idx 1, columncol_idx 1).value value else: ws.cell(rowrow_idx 1, columncol_idx 1).value parse_date(value) elif cell_type 3: ws.cell(rowrow_idx 1, columncol_idx 1).value value # 公式原样存回 else: ws.cell(rowrow_idx 1, columncol_idx 1).value str(value) wb.save(file_path)这个反序列化的过程建议留一个独立的方法比如def _apply_value_with_type(cell, value, cell_type)。后续新增类型判断逻辑只改这一个方法就行。3.2 单元格编辑与类型识别读取的时候类型是openpyxl告诉我们的但用户在界面上编辑新内容时类型就得靠自己判断。我常用的策略是这样用户编辑完单元格触发itemChanged信号时拿到QTableWidgetItem里用户输入的新字符串。先用openpyxl自带的方法试着转数字和日期转成功就算对应类型如果拿不准默认按字符串存。这个逻辑看起来朴素但实际使用中比想象中靠谱——因为人类输入的数字和日期格式总是那么几种。from PySide6.QtWidgets import QTableWidgetItem from datetime import datetime def on_item_changed(self, item, row, col): # 获取用户输入 new_text item.text().strip() new_value, new_type self._infer_value_type(new_text) # 同步到数据矩阵 self.data[row][col] new_value self.data_type[row][col] new_type def _infer_value_type(self, text): if text : return None, 0 # 先试数字整数、浮点数、科学计数法 try: if text.replace(., , 1).replace(-, , 1).isdigit(): return float(text), 1 # 或者用roundtrip判断 num float(text) return num, 1 except ValueError: pass # 再试日期常见的几种格式2024-01-01、2024/01/01、01/01/2024 for fmt in (%Y-%m-%d, %Y/%m/%d, %m/%d/%Y): try: dt datetime.strptime(text, fmt) return dt, 2 except ValueError: continue # 默认算文本 return text, 4数值判断这里有一个常见的坑用户输入“001”如果不加判断会被当成数字1存进Excel这在处理学号、工号、编号类数据时会闯祸。我的经验是数字判断时先看一眼字符串最前面的字符如果是以“0”开头且后面还有数字就不要转浮点数直接按文本存。3.3 复制粘贴从Excel到表格的桥复制粘贴是表格编辑器里使用频率最高的操作QTableWidget虽然没有直接提供全功能的复制粘贴但通过重写keyPressEvent可以很方便地实现。核心原理是读取选区内的单元格用制表符\t分隔列用换行符\r\n分隔行凑成一段CSV格式的纯文本放进剪贴板。粘贴的时候反向解析。from PySide6.QtGui import QGuiApplication from PySide6.QtCore import Qt def keyPressEvent(self, event): if (event.modifiers() Qt.KeyboardModifier.ControlModifier) and event.key() Qt.Key.Key_C: self._copy_selection() elif (event.modifiers() Qt.KeyboardModifier.ControlModifier) and event.key() Qt.Key.Key_V: self._paste_from_clipboard() else: super().keyPressEvent(event) def _copy_selection(self): items self.table.selectedItems() if not items: return rows sorted(set(item.row() for item in items)) cols sorted(set(item.column() for item in items)) clipboard_lines [] for row in rows: line_parts [] for col in cols: value self.data[row][col] line_parts.append( if value is None else str(value)) clipboard_lines.append(\t.join(line_parts)) QGuiApplication.clipboard().setText(\r\n.join(clipboard_lines))这里面最重要的细节是选区可能不是矩形。用selectedItems取到的单元格如果用户用Ctrl键点了不相邻的几个格子那行列集合是不规则的。我做了一个保守方案取所有选中单元格的行集合和列集合然后把它们拼成完整的矩形区域。虽然复制了用户没选的部分但保证了粘贴时数据结构一定是规则的。这个取舍在实际使用中很合理——编辑器不是剪贴板工具数据完整性优先。粘贴的代码是复制代码的逆过程。拿到剪贴板文本按\r\n分行按\t分列从当前焦点单元格开始逐个写入def _paste_from_clipboard(self): clipboard_text QGuiApplication.clipboard().text() lines clipboard_text.split(\r\n) rows_data [line.split(\t) for line in lines] current_row self.table.currentRow() current_col self.table.currentColumn() for i, row_data in enumerate(rows_data): for j, cell_text in enumerate(row_data): target_row current_row i target_col current_col j if target_row self.table.rowCount() or target_col self.table.columnCount(): continue # 类型推断后再写入 value, cell_type self._infer_value_type(cell_text) self.data[target_row][target_col] value self.data_type[target_row][target_col] cell_type self.table.setItem(target_row, target_col, QTableWidgetItem(cell_text))粘贴时有个隐藏问题如果复制的是从Excel里来的区域数字可能带有千分位分隔符比如“1,234”。这种字符串infer_value_type会判断成文本。我建议在解析时先把逗号去掉再试一次数字。另一个坑是Excel复制带公式的单元格时剪贴板里可能是计算结果也可能是公式取决于用户复制的方式。这里默认按纯文本处理不做公式的跨表复制。3.4 行/列操作、排序、查找替换增删行和增删列是编辑器的基础操作。QTableWidget本身提供了insertRow、removeRow、insertColumn、removeColumn等方法但用法上有个关键点改完表格结构后必须同步修改数据矩阵和类型矩阵否则后续保存会用错位的数据。def insert_row_after(self): current_row self.table.currentRow() new_row current_row 1 self.table.insertRow(new_row) # 在数据矩阵中插入一行 self.data.insert(new_row, [None for _ in range(self.table.columnCount())]) self.data_type.insert(new_row, [0 for _ in range(self.table.columnCount())])删除行的同步逻辑更麻烦因为QTableWidget删除行之后selectedItems里的行号会变化如果先删除表格行再处理数据矩阵数据矩阵的索引就会错位。我惯用的顺序是先算出要删除的行的集合先从数据矩阵里删再从表格控件里删。反过来容易出bug。排序的实现可以用QTableWidget的sortItems但默认排序是按字符串排的数字会排成1、10、11、2这种诡异顺序。所以要指定SortRole并保证QTableWidgetItem里存的是原始值而不是显示字符串def sort_by_column(self, col, ascendingTrue): self.table.sortItems(col, Qt.SortOrder.AscendingOrder if ascending else Qt.SortOrder.DescendingOrder)但是这里有个数据不同步的问题QTableWidget排序之后界面显示的顺序变了但数据矩阵没有跟着变。实际使用中必须在排序后遍历表格控件把新的顺序回写到数据矩阵中。这个坑我踩过两次后才彻底记住否则就会出现“界面上已经排好序了一保存又变回原来的顺序”的诡异现象。排序后的同步方法def sync_data_after_sort(self): for row in range(self.table.rowCount()): for col in range(self.table.columnCount()): item self.table.item(row, col) if item: # 从item的UserRole里取原始值 self.data[row][col] item.data(Qt.ItemDataRole.UserRole)查找替换的套路比较简单。查找就是在数据矩阵里遍历文本用QTableWidget找到对应行后滚动到可见区域并选中。替换就是把查到的值设置成新值同时更新界面和数据矩阵。过程中有个体验细节值得注意查找时默认按精确匹配还是模糊包含我提供两个按钮分开处理“查找”按包含匹配“查找下一个”连续查找“替换全部”遍历全表一次性处理。一次替换几百个单元格时建议关掉表格的刷新替换完成后再统一刷新速度快很多。4. 进阶体验撤销重做、数据校验与性能优化4.1 撤销与重做用快照栈实现很多人一上来就想去实现Excel那种“一步一步撤销”的复杂机制用命令模式维护一堆操作反操作。我的经验是普通编辑器做个快照栈就行简单实用够用。维护两个栈undo_stack和redo_stack。每次有批量修改动作时比如粘贴、删除行、排序、替换全部在操作之前先记录整张表的状态快照也就是复制一份data和data_type。操作完成后把旧快照压入undo_stack。撤销时做逆操作把当前状态压入redo_stack从undo_stack弹出旧快照恢复。def snapshot(self): return [row[:] for row in self.data], [row[:] for row in self.data_type] def restore(self, snap): data_snap, type_snap snap self.data [row[:] for row in data_snap] self.data_type [row[:] for row in type_snap] self.refresh_table_view() def undo(self): if self.undo_stack: redo_snap self.snapshot() old_snap self.undo_stack.pop() self.redo_stack.append(redo_snap) self.restore(old_snap) def redo(self): if self.redo_stack: undo_snap self.snapshot() new_snap self.redo_stack.pop() self.undo_stack.append(undo_snap) self.restore(new_snap)这个方案最怕的是用户频繁编辑单个单元格时快照数量爆炸。我做了个优化单格编辑不立即压栈而是等用户停止输入2秒后再压栈或者累积50次编辑后再合并成一次撤销记录。具体的触发时机可以在itemChanged信号里加一个QTimer的singleShot实现。效果就是连续按十几次退格修改同一个单元格撤销时一步就回到原始值体验比一次一格还顺手。4.2 数据校验与输入限制数据校验是编辑器区别于记事本的重要分界线。在编辑的场景里校验不应该是事后提醒而是提前预防。关键动作是在itemChanged信号触发时对新值和类型做合法性检查。比如这个单元格的类型是日期用户输入“abc”应该直接拒绝并恢复原值。数字类型的单元格用户输入文本应该弹出提示并保持编辑状态。这种逻辑放在itemChanged信号里需要注意避免死循环——恢复原值会再次触发itemChanged。我通常用一个标志位is_programmatic_change来区分是用户编辑还是程序写入。def on_item_changed(self, item): if self.is_programmatic_change: return row, col item.row(), item.column() old_type self.data_type[row][col] # 已有固定类型的单元格做类型校验 if old_type in (1, 2): # 数字或日期 new_value, new_type self._infer_value_type(item.text()) if new_type ! old_type: QMessageBox.warning(self, 提示, 该单元格类型为数字/日期输入内容不匹配) self.is_programmatic_change True self.table.item(row, col).setText(str(self.data[row][col])) self.is_programmatic_change False这个体验比Excel自带的还顺滑Excel的默认行为是允许你输入任何内容只是把单元格标记成文本后续公式计算时再撞墙。提前拦截可以在源头上减少脏数据。4.3 大数据量卡顿的优化方案QTableWidget在大数据量下的表现是个老大难。行数超过两万行之后一次性setItem几万个单元格界面直接卡成PPT滚动也是卡顿明显。我的优化思路是分三步走由浅入深。第一步加载数据时关闭表格刷新self.table.setUpdatesEnabled(False) # 批量setItem... self.table.setUpdatesEnabled(True) self.table.viewport().update()这个操作简单效果立竿见影。本质上是一次性把几万个单元格的set操作攒到一起最后统一重绘避免每设置一格都触发一次界面重绘。第二步加载时只填充可见区域附近的数据配合滚动事件动态加载。这个方案要复杂不少需要监听滚动条位置变化根据可视行范围计算需要setItem的区间。我建议先不做因为第一步已经解决了大部分体验问题。第三步真正的大数据量场景超过10万行就不要再硬磕QTableWidget了改用QAbstractTableModel配合QTableView把数据模型和视图彻底分离由Model的data方法按需返回数据界面滚动时只渲染看得见的那些单元格性能会有质的提升。代码量会多不少但这是唯一能做“百万行不卡”的正路。实际项目中我通常会加一个行数阈值判断少于3万行用QTableWidget超过3万行自动切换成QTableViewModel模式。这样普通用户和数据处理用户都能照顾到。4.4 公式兼容性细节公式是Excel手里最锋利的刀编辑器如果处理不好公式就只能是半个玩具。openpyxl读公式单元格时如果load_workbook时不加data_only参数cell.value返回的是公式字符串比如“SUM(A1:A10)”如果加了data_onlyTrue返回的是公式的缓存值。编辑器场景下我的选择是默认读公式字符串界面上显示公式保存时原样写回。原因很直接Excel打开文件时会自动重算公式我们没必要在编辑器里再造一个计算引擎但如果我们把公式的缓存值当普通值保存那公式就永久丢失了。还有个细节容易忽略openpyxl保存公式单元格时缓存值字段是空的Excel打开后第一次显示可能要用F9手动重算才能出结果。通常Excel打开会自动重算但为了保险保存时设置一下工作簿的计算属性wb.calculation.fullCalcOnLoad True这样每次打开文件都会全量重算公式结果一定是对的。代价是大文件打开稍慢但正确性优先。5. 常见问题与排查技巧实录5.1 日期被读成浮点数这是做Excel相关工具时遇到最多的坑。Excel内部存储日期时用的是序列号比如2024-01-01的序列号是45292。openpyxl读取时如果你不处理拿到手的就可能是个float。虽然openpyxl在data_type为d时会自动转成datetime但有些文件里的日期单元格data_type可能是n数字因为它实际上是数字格式只是套了一个日期显示样式。这时候就要靠判断单元格number_format来识别。def is_date_cell(cell): fmt cell.number_format if yyyy in fmt.lower() or dd in fmt.lower() or mm in fmt.lower(): return True return False这个判断不是绝对准确但日常处理已经很能打了。识别出来之后再用datetime.fromordinal或者直接转成datetime。5.2 复制粘贴时从Excel带进来的不可见字符从Excel复制数据到编辑器剪贴板文本里经常带着制表符、换行符混在一起甚至还有不换行空格\xa0。如果直接用\t和\r\n做分隔容易解析出多一个空列或者多一行空行。我的做法是解析前先做一轮清洗把\r\n替换成\n再按行处理每行结尾的\t或空格全部去掉clipboard_text clipboard_text.replace(\r\n, \n).replace(\r, \n) lines [line.rstrip(\t ) for line in clipboard_text.split(\n) if line.rstrip(\t )]这一行清洗代码能省掉后续排查“为什么粘贴出来多了几列”的大量时间。5.3 大文件保存时内存飙升openpyxl保存大文件时会把整个工作簿的数据都加载到内存。如果文件本身有几十个大Sheet再加上编辑器里的数据矩阵、类型矩阵、界面控件里的数据三层数据同时驻留内存32G内存的机器也能吃紧。我踩坑之后改成了这样保存前先清空QTableWidget里已编辑控件的数据引用让界面控件和数据矩阵解耦。具体做法是暂时把表格的行列数变小保存完成后再重新加载回来。当然这个方法只适用在数据已经同步到data矩阵后。更本质的方案是保存的时候分Sheet处理一次只维护一个Sheet的数据不要全部堆积在内存里。5.4 单元格样式确实会丢必须先把丑话说在前面openpyxl读xlsx文件时能读取样式但编辑器保存后绝大多数单元格样式还是会丢掉。原因很简单openpyxl在这个版本对样式支持不完整尤其是条件格式、数据条、图标集这些高阶能力。我实际保存后对比过字体、颜色、边框、填充色这几种基础样式如果原始文件比较简单保存后大概率保留但如果有合并单元格、条件格式、数据验证下拉列表这些东西就凶多吉少了。在功能设计上要把预期管理做好。编辑器做的是“数据编辑”不是“格式编辑”这一点在文档和界面里都要明确提示用户。5.5 快捷键冲突与按键事件最后一个比较容易忽视的坑是按键事件被QTableWidget吞掉。QTableWidget默认情况下按上下左右箭头、Tab、回车都有既定行为。我重写keyPressEvent的时候如果只是挂一个copy/paste的快捷键没什么问题但如果要把F2改成其他功能就可能覆盖掉QTableWidget内部的编辑触发逻辑。我的经验是重写keyPressEvent时先处理自己关心的快捷键处理完直接调用super().keyPressEvent(event)把其他按键交还给父类千万不要在else里什么都不干。很多人写出来“为什么按方向键没反应”基本都是这个原因。最后再说一个实际操作中的体会。这个项目做下来最花时间的其实不是代码而是型别判断和异常处理的边界。Excel文件格式看着规范实际工作里的文件什么怪样子都有——合并单元格里套公式、单元格里有富文本、日期格式五花八门、负数用括号表示。编辑器每处理一种怪格式都是往上补一层判断逻辑。我后来把这套判断收敛到一个统一的normalize方法里所有进入数据矩阵的单元格都必须过这个方法。为了让代码的扩展更从容后续如果要做Excel转CSV、Excel转JSON、报表导出这类衍生功能时核心编辑器的代码几乎不太需要动只需要在导出模块里把数据矩阵和类型矩阵解析到目标格式就行。这个项目做到这里我的建议是先把核心的读写和编辑做扎实其他功能慢慢加不迟。
返回列表