一:效果展示:
本项目采用直观的用户界面,交互式数据预览,精确的单元格定位。支持.xlsx和.xls格式文件的读取;可将修改后的内容保存为新的Excel文件,界面上方显示当前加载的文件路径
二:功能描述
(1)项目特色
- 采用PyQt5构建的现代化GUI界面,布局清晰,操作便捷
- 交互式数据预览,以表格形式展示Excel数据,支持点击单元格自动填充地址和值
- 通过鼠标点击位置可以精确判断选择的列(单元格定位)
- 对文件操作、单元格更新等关键操作提供错误提示,防止程序因异常情况崩溃
(2)核心功能
1. 文件管理功能
- 打开文件:支持.xlsx和.xls格式文件的读取
- 保存文件:可将修改后的内容保存为新的Excel文件
- 文件状态显示:界面上方显示当前加载的文件路径
2. 工作表管理
- 工作表选择:通过下拉框选择工作簿中的不同工作表
- 自动加载:打开文件后自动加载第一个工作表的内容
3. 单元格编辑功能
- 单元格定位,可通过点击数据预览区域自动填充单元格地址,也可手动输入单元格地址
- 内容编辑
- 可修改选定单元格的值
- 支持文本和数字内容的更新
- 提供"清空内容"按钮快速清除输入
- 即时反馈
- 更新成功后显示确认消息
- 自动刷新数据预览区域
4. 数据预览功能
- 智能列显示
- 自动检测工作表的最大列数
- 默认显示前10列,可根据内容自动调整
- 交互式选择
- 点击任意单元格可自动填充地址和当前值到输入框
- 鼠标位置精确判断选择的列
(3)技术实现亮点
- openpyxl集成
- 使用openpyxl库进行Excel文件的读写操作
- 正确处理单元格引用和值转换
- 动态UI更新
- 工作表切换时自动更新预览区域
- 列标题根据实际数据动态生成
- 响应式设计
- 数据预览区域固定高度,避免界面过度拉伸
- 列宽根据内容自动调整
- 用户体验优化
- 操作前后提供明确的反馈信息
- 错误处理完善,避免程序意外终止
(4)扩展性
当前设计采用选项卡式布局,可以方便地添加和扩展其它功能,如:
- 批量编辑功能
- 数据筛选和排序
- 图表生成
- 公式支持
- 多文件操作等高级功能
这个Excel编辑器适合需要快速查看和修改Excel文件内容的场景,特别适合不需要完整Excel功能但又要比纯文本编辑更直观的用户使用
三:完整代码
python
import sys
from PyQt5.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
QLabel, QLineEdit, QPushButton, QFileDialog, QMessageBox,
QComboBox, QListWidget, QTabWidget, QGridLayout)
from PyQt5.QtCore import Qt
from openpyxl import load_workbook
from openpyxl.utils import get_column_letter
class ExcelEditor(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("Excel文件编辑器")
self.setGeometry(100, 100, 800, 600)
self.wb = None
self.current_file = ""
self.current_sheet = None
self.preview_columns = 10
self.init_ui()
def init_ui(self):
main_widget = QWidget()
self.setCentralWidget(main_widget)
main_layout = QVBoxLayout()
main_widget.setLayout(main_layout)
file_layout = QHBoxLayout()
self.file_label = QLabel("未选择文件")
file_layout.addWidget(self.file_label)
open_btn = QPushButton("打开文件")
open_btn.clicked.connect(self.open_file)
file_layout.addWidget(open_btn)
save_btn = QPushButton("保存文件")
save_btn.clicked.connect(self.save_file)
file_layout.addWidget(save_btn)
main_layout.addLayout(file_layout)
self.tabs = QTabWidget()
main_layout.addWidget(self.tabs)
self.sheet_tab = QWidget()
self.tabs.addTab(self.sheet_tab, "工作表编辑")
self.setup_sheet_tab()
def setup_sheet_tab(self):
layout = QVBoxLayout()
sheet_layout = QHBoxLayout()
sheet_layout.addWidget(QLabel("选择工作表:"))
self.sheet_combo = QComboBox()
self.sheet_combo.currentIndexChanged.connect(self.load_sheet_data)
sheet_layout.addWidget(self.sheet_combo)
layout.addLayout(sheet_layout)
edit_layout = QHBoxLayout()
left_layout = QVBoxLayout()
left_layout.addWidget(QLabel("单元格地址:"))
self.cell_input = QLineEdit("A1")
left_layout.addWidget(self.cell_input)
right_layout = QVBoxLayout()
right_layout.addWidget(QLabel("新值:"))
self.value_input = QLineEdit()
right_layout.addWidget(self.value_input)
edit_layout.addLayout(left_layout)
edit_layout.addLayout(right_layout)
layout.addLayout(edit_layout)
btn_layout = QHBoxLayout()
update_btn = QPushButton("更新单元格")
update_btn.clicked.connect(self.update_cell)
btn_layout.addWidget(update_btn)
clear_btn = QPushButton("清空内容")
clear_btn.clicked.connect(self.clear_cell)
btn_layout.addWidget(clear_btn)
layout.addLayout(btn_layout)
self.preview_container = QWidget()
self.preview_layout = QVBoxLayout()
self.preview_container.setLayout(self.preview_layout)
self.column_header = QWidget()
self.header_layout = QHBoxLayout()
self.header_layout.setContentsMargins(0, 0, 0, 0)
self.header_layout.setSpacing(0)
self.column_header.setLayout(self.header_layout)
self.column_labels = []
self.preview_list = QListWidget()
self.preview_list.setFixedHeight(400)
self.preview_list.itemClicked.connect(self.on_preview_item_clicked)
self.preview_layout.addWidget(QLabel("工作表数据预览 (点击单元格可选择地址):"))
self.preview_layout.addWidget(self.column_header)
self.preview_layout.addWidget(self.preview_list)
layout.addWidget(self.preview_container)
self.sheet_tab.setLayout(layout)
def open_file(self):
file_path, _ = QFileDialog.getOpenFileName(self, "打开Excel文件", "", "Excel文件 (*.xlsx *.xls)")
if file_path:
try:
self.wb = load_workbook(file_path)
self.current_file = file_path
self.file_label.setText(f"已加载: {file_path}")
self.sheet_combo.clear()
self.sheet_combo.addItems(self.wb.sheetnames)
if self.wb.sheetnames:
self.load_sheet_data(0)
except Exception as e:
QMessageBox.critical(self, "错误", f"无法加载文件:\n{str(e)}")
def save_file(self):
if not self.wb:
QMessageBox.warning(self, "警告", "没有打开的文件可保存")
return
save_path, _ = QFileDialog.getSaveFileName(self, "保存Excel文件", "", "Excel文件 (*.xlsx)")
if save_path:
try:
if not save_path.endswith('.xlsx'):
save_path += '.xlsx'
self.wb.save(save_path)
QMessageBox.information(self, "成功", f"文件已保存到:\n{save_path}")
except Exception as e:
QMessageBox.critical(self, "错误", f"保存文件失败:\n{str(e)}")
def load_sheet_data(self, index):
if not self.wb or index < 0:
return
sheet_name = self.sheet_combo.itemText(index)
self.current_sheet = self.wb[sheet_name]
max_row = 1
max_col = 1
for row in self.current_sheet.iter_rows(max_row=20):
for cell in row:
if cell.value is not None:
max_col = max(max_col, cell.column)
max_row += 1
if max_row > 20:
break
self.preview_columns = min(max_col, self.preview_columns)
self.update_column_headers()
self.preview_list.clear()
self.preview_data = []
for row_idx, row in enumerate(self.current_sheet.iter_rows(max_row=20, max_col=self.preview_columns), start=1):
row_data = []
for col_idx, cell in enumerate(row, start=1):
row_data.append({
'col_idx': col_idx,
'row_idx': row_idx,
'value': str(cell.value) if cell.value is not None else "",
'col_letter': get_column_letter(col_idx)
})
self.preview_data.append(row_data)
display_parts = []
for item in row_data:
cell_width = max(len(item['value']), len(item['col_letter'])) + 2
display_parts.append(f"{item['value']:<{cell_width}}")
self.preview_list.addItem("".join(display_parts))
def update_column_headers(self):
for label in self.column_labels:
self.header_layout.removeWidget(label)
label.deleteLater()
self.column_labels.clear()
for col_idx in range(1, self.preview_columns + 1):
col_letter = get_column_letter(col_idx)
label = QLabel(f" {col_letter} ")
label.setAlignment(Qt.AlignCenter)
label.setStyleSheet("""
QLabel {
font-weight: bold;
border: 1px solid lightgray;
background-color: #f0f0f0;
min-width: 40px;
}
""")
self.header_layout.addWidget(label)
self.column_labels.append(label)
self.header_layout.addStretch()
def on_preview_item_clicked(self, item):
if not hasattr(self, 'preview_data') or not self.preview_data:
return
row_index = self.preview_list.row(item)
if 0 <= row_index < len(self.preview_data):
row_data = self.preview_data[row_index]
pos = self.preview_list.visualItemRect(item).topLeft()
mouse_x = self.preview_list.mapFromGlobal(self.cursor().pos()).x() - pos.x()
clicked_col = 0
current_x = 0
col_positions = []
for i, item_data in enumerate(row_data):
col_letter = item_data['col_letter']
cell_width = max(len(item_data['value']), len(col_letter)) + 2
col_width_pixels = cell_width * 8 # 近似像素宽度
col_positions.append((current_x, current_x + col_width_pixels, i))
current_x += col_width_pixels
for start, end, col_idx in col_positions:
if start <= mouse_x < end:
clicked_col = col_idx
break
else:
if col_positions:
clicked_col = col_positions[-1][2]
if 0 <= clicked_col < len(row_data):
col_idx = row_data[clicked_col]['col_idx']
row_idx = row_data[clicked_col]['row_idx']
cell_ref = f"{get_column_letter(col_idx)}{row_idx}"
self.cell_input.setText(cell_ref)
cell_value = self.current_sheet.cell(row=row_idx, column=col_idx).value
self.value_input.setText(str(cell_value) if cell_value is not None else "")
def update_cell(self):
if not self.wb:
QMessageBox.warning(self, "警告", "请先打开一个Excel文件")
return
if not self.current_sheet:
QMessageBox.warning(self, "警告", "请先选择一个工作表")
return
cell_ref = self.cell_input.text().strip()
new_value = self.value_input.text()
if not cell_ref:
QMessageBox.warning(self, "警告", "请输入单元格地址")
return
try:
self.current_sheet[cell_ref].value = new_value
QMessageBox.information(self, "成功", f"已更新工作表 '{self.current_sheet.title}' 中的单元格 {cell_ref}")
current_index = self.sheet_combo.currentIndex()
self.load_sheet_data(current_index)
except Exception as e:
QMessageBox.critical(self, "错误", f"更新单元格失败:\n{str(e)}")
def clear_cell(self):
self.value_input.clear()
if __name__ == "__main__":
app = QApplication(sys.argv)
editor = ExcelEditor()
editor.show()
sys.exit(app.exec_())
四:代码分析
1.主窗口类定义
python
class ExcelEditor(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("Excel文件编辑器")
self.setGeometry(100, 100, 800, 600)
self.wb = None # 存储Excel工作簿对象
self.current_file = "" # 当前打开的文件路径
self.current_sheet = None # 当前工作表对象
self.preview_columns = 10 # 预览显示的列数
self.init_ui() # 初始化UI界面
2.初始化用户界面
python
def init_ui(self):
main_widget = QWidget()
self.setCentralWidget(main_widget)
main_layout = QVBoxLayout()
main_widget.setLayout(main_layout)
# 文件操作区域布局
file_layout = QHBoxLayout()
self.file_label = QLabel("未选择文件") # 显示当前文件状态
file_layout.addWidget(self.file_label)
# 打开文件按钮
open_btn = QPushButton("打开文件")
open_btn.clicked.connect(self.open_file) # 绑定打开文件事件
file_layout.addWidget(open_btn)
# 保存文件按钮
save_btn = QPushButton("保存文件")
save_btn.clicked.connect(self.save_file) # 绑定保存文件事件
file_layout.addWidget(save_btn)
main_layout.addLayout(file_layout)
# 标签页控件
self.tabs = QTabWidget()
main_layout.addWidget(self.tabs)
# 工作表编辑标签页
self.sheet_tab = QWidget()
self.tabs.addTab(self.sheet_tab, "工作表编辑")
self.setup_sheet_tab() # 设置工作表编辑界面
3.设置工作表编辑标签页的UI
python
def setup_sheet_tab(self):
layout = QVBoxLayout()
# 工作表选择区域
sheet_layout = QHBoxLayout()
sheet_layout.addWidget(QLabel("选择工作表:"))
self.sheet_combo = QComboBox() # 工作表选择下拉框
self.sheet_combo.currentIndexChanged.connect(self.load_sheet_data) # 选择变化时加载数据
sheet_layout.addWidget(self.sheet_combo)
layout.addLayout(sheet_layout)
# 单元格编辑区域(地址输入+值输入)
edit_layout = QHBoxLayout()
left_layout = QVBoxLayout()
left_layout.addWidget(QLabel("单元格地址:"))
self.cell_input = QLineEdit("A1") # 默认单元格地址
left_layout.addWidget(self.cell_input)
right_layout = QVBoxLayout()
right_layout.addWidget(QLabel("新值:"))
self.value_input = QLineEdit() # 单元格新值输入框
right_layout.addWidget(self.value_input)
edit_layout.addLayout(left_layout)
edit_layout.addLayout(right_layout)
layout.addLayout(edit_layout)
# 按钮区域
btn_layout = QHBoxLayout()
update_btn = QPushButton("更新单元格")
update_btn.clicked.connect(self.update_cell) # 绑定更新事件
btn_layout.addWidget(update_btn)
clear_btn = QPushButton("清空内容")
clear_btn.clicked.connect(self.clear_cell) # 绑定清空事件
btn_layout.addWidget(clear_btn)
layout.addLayout(btn_layout)
# 数据预览区域
self.preview_container = QWidget()
self.preview_layout = QVBoxLayout()
self.preview_container.setLayout(self.preview_layout)
# 列标题
self.column_header = QWidget()
self.header_layout = QHBoxLayout()
self.header_layout.setContentsMargins(0, 0, 0, 0)
self.header_layout.setSpacing(0)
self.column_header.setLayout(self.header_layout)
self.column_labels = [] # 存储列标题标签
# 数据预览列表
self.preview_list = QListWidget()
self.preview_list.setFixedHeight(400)
self.preview_list.itemClicked.connect(self.on_preview_item_clicked) # 点击预览项事件
self.preview_layout.addWidget(QLabel("工作表数据预览 (点击单元格可选择地址):"))
self.preview_layout.addWidget(self.column_header)
self.preview_layout.addWidget(self.preview_list)
layout.addWidget(self.preview_container)
self.sheet_tab.setLayout(layout)
4.打开与保存文件功能
python
# 打开文件功能
def open_file(self):
file_path, _ = QFileDialog.getOpenFileName(self, "打开Excel文件", "", "Excel文件 (*.xlsx *.xls)")
if file_path:
try:
self.wb = load_workbook(file_path) # 加载工作簿
self.current_file = file_path
self.file_label.setText(f"已加载: {file_path}")
self.sheet_combo.clear()
self.sheet_combo.addItems(self.wb.sheetnames) # 添加所有工作表名到下拉框
if self.wb.sheetnames:
self.load_sheet_data(0) # 默认加载第一个工作表
except Exception as e:
QMessageBox.critical(self, "错误", f"无法加载文件:\n{str(e)}")
# 保存文件功能
def save_file(self):
if not self.wb:
QMessageBox.warning(self, "警告", "没有打开的文件可保存")
return
save_path, _ = QFileDialog.getSaveFileName(self, "保存Excel文件", "", "Excel文件 (*.xlsx)")
if save_path:
try:
if not save_path.endswith('.xlsx'):
save_path += '.xlsx'
self.wb.save(save_path) # 保存工作簿
QMessageBox.information(self, "成功", f"文件已保存到:\n{save_path}")
except Exception as e:
QMessageBox.critical(self, "错误", f"保存文件失败:\n{str(e)}")
5.工作表
python
# 加载工作表数据
def load_sheet_data(self, index):
if not self.wb or index < 0:
return
sheet_name = self.sheet_combo.itemText(index)
self.current_sheet = self.wb[sheet_name] # 获取当前工作表对象
# 计算实际数据范围(最多检查20行)
max_row = 1
max_col = 1
for row in self.current_sheet.iter_rows(max_row=20):
for cell in row:
if cell.value is not None:
max_col = max(max_col, cell.column)
max_row += 1
if max_row > 20:
break
self.preview_columns = min(max_col, self.preview_columns) # 确定预览列数
self.update_column_headers() # 更新列标题
self.preview_list.clear()
self.preview_data = [] # 存储预览数据
# 加载前20行数据用于预览
for row_idx, row in enumerate(self.current_sheet.iter_rows(max_row=20, max_col=self.preview_columns), start=1):
row_data = []
for col_idx, cell in enumerate(row, start=1):
row_data.append({
'col_idx': col_idx,
'row_idx': row_idx,
'value': str(cell.value) if cell.value is not None else "",
'col_letter': get_column_letter(col_idx)
})
self.preview_data.append(row_data)
# 格式化显示行数据
display_parts = []
for item in row_data:
cell_width = max(len(item['value']), len(item['col_letter'])) + 2
display_parts.append(f"{item['value']:<{cell_width}}")
self.preview_list.addItem("".join(display_parts))
# 更新列标题
def update_column_headers(self):
# 清除现有列标题
for label in self.column_labels:
self.header_layout.removeWidget(label)
label.deleteLater()
self.column_labels.clear()
# 添加新列标题
for col_idx in range(1, self.preview_columns + 1):
col_letter = get_column_letter(col_idx)
label = QLabel(f" {col_letter} ")
label.setAlignment(Qt.AlignCenter)
label.setStyleSheet("""
QLabel {
font-weight: bold;
border: 1px solid lightgray;
background-color: #f0f0f0;
min-width: 40px;
}
""")
self.header_layout.addWidget(label)
self.column_labels.append(label)
self.header_layout.addStretch()
# 预览项点击事件处理
def on_preview_item_clicked(self, item):
if not hasattr(self, 'preview_data') or not self.preview_data:
return
row_index = self.preview_list.row(item)
if 0 <= row_index < len(self.preview_data):
row_data = self.preview_data[row_index]
# 计算鼠标点击位置对应的列
pos = self.preview_list.visualItemRect(item).topLeft()
mouse_x = self.preview_list.mapFromGlobal(self.cursor().pos()).x() - pos.x()
clicked_col = 0
current_x = 0
col_positions = []
# 计算每列的像素位置范围
for i, item_data in enumerate(row_data):
col_letter = item_data['col_letter']
cell_width = max(len(item_data['value']), len(col_letter)) + 2
col_width_pixels = cell_width * 8 # 近似像素宽度
col_positions.append((current_x, current_x + col_width_pixels, i))
current_x += col_width_pixels
# 确定点击的是哪一列
for start, end, col_idx in col_positions:
if start <= mouse_x < end:
clicked_col = col_idx
break
else:
if col_positions:
clicked_col = col_positions[-1][2]
if 0 <= clicked_col < len(row_data):
col_idx = row_data[clicked_col]['col_idx']
row_idx = row_data[clicked_col]['row_idx']
cell_ref = f"{get_column_letter(col_idx)}{row_idx}"
self.cell_input.setText(cell_ref) # 更新单元格地址输入框
cell_value = self.current_sheet.cell(row=row_idx, column=col_idx).value
self.value_input.setText(str(cell_value) if cell_value is not None else "") # 更新值输入框
# 更新单元格值
def update_cell(self):
if not self.wb:
QMessageBox.warning(self, "警告", "请先打开一个Excel文件")
return
if not self.current_sheet:
QMessageBox.warning(self, "警告", "请先选择一个工作表")
return
cell_ref = self.cell_input.text().strip()
new_value = self.value_input.text()
if not cell_ref:
QMessageBox.warning(self, "警告", "请输入单元格地址")
return
try:
self.current_sheet[cell_ref].value = new_value # 更新单元格值
QMessageBox.information(self, "成功", f"已更新工作表 '{self.current_sheet.title}' 中的单元格 {cell_ref}")
# 刷新预览数据
current_index = self.sheet_combo.currentIndex()
self.load_sheet_data(current_index)
except Exception as e:
QMessageBox.critical(self, "错误", f"更新单元格失败:\n{str(e)}")
# 清空值输入框
def clear_cell(self):
self.value_input.clear()

