第10篇:阶段二实战项目:简易Excel表格数据管理器(完整代码)
哈喽~ 欢迎来到PyQt5系列的第10篇!这是阶段二的收官实战项目——我们将整合前9章的核心知识点(布局管理器、QTableWidget、容器控件、标准对话框、自定义交互),开发一个“仿照Excel的简易表格数据管理器”。这个项目覆盖Excel最常用的基础功能(新建/打开/保存、行列增删、数据排序、单元格格式设置等),全程代码可直接运行,帮你把零散的知识点串联成完整的项目开发能力!
一、项目核心目标与知识点整合
1. 项目目标
开发一个轻量级表格管理器,实现Excel的核心基础功能,满足日常简单的数据编辑/管理需求,界面风格贴近Excel,操作逻辑直观。
2. 整合的核心知识点
| 知识点 | 应用场景 |
|---|---|
| QTableWidget | 核心表格控件,承载数据展示/编辑 |
| QTabWidget | 多工作表切换(模拟Excel的多sheet) |
| QGroupBox | 功能按钮分组(编辑区/格式区/数据区) |
| 布局管理器(网格/线性) | 整体界面排版,保证自适应 |
| 标准对话框(文件/字体/颜色/消息) | 打开/保存文件、设置单元格格式、操作提示 |
| 信号与槽 | 按钮交互、表格事件响应 |
3. 核心功能清单
- ✅ 多工作表管理(新建/删除sheet、切换sheet);
- ✅ 文件操作(新建空白表格、打开CSV文件、保存CSV文件);
- ✅ 行列管理(插入/删除行/列、清空表格);
- ✅ 数据编辑(单元格编辑、选中行/列高亮);
- ✅ 格式设置(单元格字体、颜色、居中对齐);
- ✅ 数据操作(按列排序、获取选中单元格数据);
- ✅ 操作提示(成功/失败/警告类消息框)。
二、项目整体架构
1. 界面布局设计
整体界面分为3个区域:
- 顶部功能区:文件操作按钮(新建/打开/保存) + 工作表切换(QTabWidget);
- 中部功能按钮区:用QGroupBox分为“编辑区”“格式区”“数据区”,摆放功能按钮;
- 底部表格区:QTableWidget核心表格,占界面主要空间,支持自适应缩放。
2. 核心功能模块
- 文件模块:处理CSV文件的新建/打开/保存;
- 工作表模块:管理多sheet的增删改查;

- 编辑模块:行列增删、清空表格;

- 格式模块:单元格字体、颜色、对齐方式设置;

- 数据模块:数据排序、选中数据获取。

三、完整代码实现
import sys
import os
import csv
from PyQt5.QtWidgets import (
QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
QGridLayout, QGroupBox, QPushButton, QTableWidget, QTableWidgetItem,
QTabWidget, QFileDialog, QFontDialog, QColorDialog, QMessageBox
)
from PyQt5.QtGui import QFont, QColor, QIcon
from PyQt5.QtCore import Qt, pyqtSlot
# 主窗口(继承QMainWindow,支持菜单栏/工具栏,更贴近Excel)
class SimpleExcel(QMainWindow):
def __init__(self):
super().__init__()
self.init_ui()
self.current_file_path = None # 记录当前打开的文件路径
def init_ui(self):
# 基础窗口设置
self.setWindowTitle("简易Excel表格管理器")
self.resize(1000, 700)
self.setMinimumSize(800, 500) # 设置最小尺寸,避免缩放过小
# ---------- 中心控件(所有内容放在中心控件中) ----------
central_widget = QWidget()
self.setCentralWidget(central_widget)
# ---------- 主布局(垂直) ----------
main_layout = QVBoxLayout(central_widget)
main_layout.setSpacing(15)
main_layout.setContentsMargins(15, 15, 15, 15)
# ---------- 1. 顶部功能区:文件操作 + 工作表切换 ----------
top_layout = QVBoxLayout()
# 1.1 文件操作按钮(水平布局)
file_btn_layout = QHBoxLayout()
self.new_file_btn = QPushButton("新建")
self.open_file_btn = QPushButton("打开")
self.save_file_btn = QPushButton("保存")
# 按钮样式
for btn in [self.new_file_btn, self.open_file_btn, self.save_file_btn]:
btn.setFixedSize(80, 35)
btn.setStyleSheet("QPushButton { font-size: 14px; }")
file_btn_layout.addWidget(self.new_file_btn)
file_btn_layout.addWidget(self.open_file_btn)
file_btn_layout.addWidget(self.save_file_btn)
file_btn_layout.addStretch() # 右侧留白
# 1.2 工作表切换(QTabWidget)
self.sheet_tab = QTabWidget()
self.sheet_tab.setTabPosition(QTabWidget.South) # 标签在下方(贴近Excel)
self.sheet_tab.setTabsClosable(True) # 显示关闭按钮
# 添加默认工作表
self.add_new_sheet("Sheet1")
# 顶部布局组合
top_layout.addLayout(file_btn_layout)
top_layout.addWidget(self.sheet_tab)
# ---------- 2. 中部功能按钮区(分组) ----------
btn_group_layout = QGridLayout()
btn_group_layout.setSpacing(10)
# 2.1 编辑区分组
edit_group = QGroupBox("编辑区")
edit_layout = QVBoxLayout(edit_group)
# 编辑区按钮
self.add_row_btn = QPushButton("插入行")
self.del_row_btn = QPushButton("删除行")
self.add_col_btn = QPushButton("插入列")
self.del_col_btn = QPushButton("删除列")
self.clear_btn = QPushButton("清空表格")
edit_btns = [self.add_row_btn, self.del_row_btn, self.add_col_btn, self.del_col_btn, self.clear_btn]
for btn in edit_btns:
btn.setFixedSize(80, 30)
edit_layout.addWidget(btn)
btn_group_layout.addWidget(edit_group, 0, 0)
# 2.2 格式区分组
format_group = QGroupBox("格式区")
format_layout = QVBoxLayout(format_group)
# 格式区按钮
self.font_btn = QPushButton("字体设置")
self.color_btn = QPushButton("字体颜色")
self.align_btn = QPushButton("居中对齐")
format_btns = [self.font_btn, self.color_btn, self.align_btn]
for btn in format_btns:
btn.setFixedSize(80, 30)
format_layout.addWidget(btn)
btn_group_layout.addWidget(format_group, 0, 1)
# 2.3 数据区分组
data_group = QGroupBox("数据区")
data_layout = QVBoxLayout(data_group)
# 数据区按钮
self.sort_asc_btn = QPushButton("升序排序")
self.sort_desc_btn = QPushButton("降序排序")
self.get_data_btn = QPushButton("选中数据")
self.add_sheet_btn = QPushButton("新建Sheet")
data_btns = [self.sort_asc_btn, self.sort_desc_btn, self.get_data_btn, self.add_sheet_btn]
for btn in data_btns:
btn.setFixedSize(80, 30)
data_layout.addWidget(btn)
btn_group_layout.addWidget(data_group, 0, 2)
# ---------- 3. 底部表格区 ----------
# 表格区占主要空间,设置拉伸权重
table_layout = QVBoxLayout()
table_layout.setStretchFactor(table_layout, 1)
# ---------- 组合所有布局 ----------
main_layout.addLayout(top_layout)
main_layout.addLayout(btn_group_layout)
main_layout.addLayout(table_layout, stretch=1) # 表格区拉伸权重1,占更多空间
# ---------- 信号绑定 ----------
self.bind_signals()
# ---------- 核心方法:添加新工作表 ----------
def add_new_sheet(self, sheet_name):
"""添加新的工作表(QTableWidget)"""
# 创建表格控件
table = QTableWidget()
table.setRowCount(10) # 默认10行
table.setColumnCount(5) # 默认5列
# 设置表格样式(贴近Excel)
table.setAlternatingRowColors(True) # 隔行变色
table.horizontalHeader().setStretchLastSection(True) # 最后一列拉伸
table.setSelectionBehavior(QTableWidget.SelectRows) # 选中整行
# 添加到标签页
self.sheet_tab.addTab(table, sheet_name)
# 切换到新工作表
self.sheet_tab.setCurrentWidget(table)
return table
# ---------- 信号绑定 ----------
def bind_signals(self):
# 文件操作
self.new_file_btn.clicked.connect(self.on_new_file)
self.open_file_btn.clicked.connect(self.on_open_file)
self.save_file_btn.clicked.connect(self.on_save_file)
# 工作表操作
self.add_sheet_btn.clicked.connect(self.on_add_sheet)
self.sheet_tab.tabCloseRequested.connect(self.on_close_sheet)
# 编辑操作
self.add_row_btn.clicked.connect(self.on_add_row)
self.del_row_btn.clicked.connect(self.on_del_row)
self.add_col_btn.clicked.connect(self.on_add_col)
self.del_col_btn.clicked.connect(self.on_del_col)
self.clear_btn.clicked.connect(self.on_clear_table)
# 格式操作
self.font_btn.clicked.connect(self.on_set_font)
self.color_btn.clicked.connect(self.on_set_color)
self.align_btn.clicked.connect(self.on_set_align)
# 数据操作
self.sort_asc_btn.clicked.connect(lambda: self.on_sort_data(Qt.AscendingOrder))
self.sort_desc_btn.clicked.connect(lambda: self.on_sort_data(Qt.DescendingOrder))
self.get_data_btn.clicked.connect(self.on_get_selected_data)
# ---------- 文件操作槽函数 ----------
def on_new_file(self):
"""新建空白表格"""
# 清空所有工作表
while self.sheet_tab.count() > 0:
self.sheet_tab.removeTab(0)
# 添加默认工作表
self.add_new_sheet("Sheet1")
self.current_file_path = None
QMessageBox.information(self, "成功", "新建空白表格完成!")
def on_open_file(self):
"""打开CSV文件"""
file_path, _ = QFileDialog.getOpenFileName(
self, "打开CSV文件", "", "CSV Files (*.csv);;All Files (*.*)"
)
if not file_path:
return
try:
# 清空现有工作表
while self.sheet_tab.count() > 0:
self.sheet_tab.removeTab(0)
# 创建新工作表并加载数据
table = self.add_new_sheet(os.path.basename(file_path))
# 读取CSV文件
with open(file_path, "r", encoding="utf-8") as f:
reader = csv.reader(f)
# 获取所有行数据
rows = list(reader)
if not rows:
QMessageBox.warning(self, "提示", "文件为空!")
return
# 设置表格行列数
table.setRowCount(len(rows))
table.setColumnCount(len(rows[0]))
# 填充数据
for row_idx, row_data in enumerate(rows):
for col_idx, col_data in enumerate(row_data):
item = QTableWidgetItem(str(col_data))
item.setTextAlignment(Qt.AlignCenter)
table.setItem(row_idx, col_idx, item)
self.current_file_path = file_path
QMessageBox.information(self, "成功", f"已打开文件:{os.path.basename(file_path)}")
except Exception as e:
QMessageBox.critical(self, "错误", f"打开文件失败:{str(e)}")
def on_save_file(self):
"""保存CSV文件"""
# 获取当前表格
current_table = self.sheet_tab.currentWidget()
if not isinstance(current_table, QTableWidget):
QMessageBox.warning(self, "提示", "无有效表格可保存!")
return
# 获取保存路径
if self.current_file_path:
save_path = self.current_file_path
else:
save_path, _ = QFileDialog.getSaveFileName(
self, "保存CSV文件", "", "CSV Files (*.csv)"
)
if not save_path:
return
# 补充后缀
if not save_path.endswith(".csv"):
save_path += ".csv"
try:
# 写入CSV文件
with open(save_path, "w", encoding="utf-8", newline="") as f:
writer = csv.writer(f)
# 遍历表格所有行
for row in range(current_table.rowCount()):
row_data = []
for col in range(current_table.columnCount()):
item = current_table.item(row, col)
row_data.append(item.text() if item else "")
writer.writerow(row_data)
self.current_file_path = save_path
QMessageBox.information(self, "成功", f"文件已保存到:{save_path}")
except Exception as e:
QMessageBox.critical(self, "错误", f"保存文件失败:{str(e)}")
# ---------- 工作表操作槽函数 ----------
def on_add_sheet(self):
"""新建工作表"""
sheet_count = self.sheet_tab.count() + 1
self.add_new_sheet(f"Sheet{sheet_count}")
QMessageBox.information(self, "成功", f"新建工作表Sheet{sheet_count}完成!")
def on_close_sheet(self, index):
"""关闭指定工作表"""
if self.sheet_tab.count() <= 1:
QMessageBox.warning(self, "提示", "至少保留一个工作表!")
return
self.sheet_tab.removeTab(index)
QMessageBox.information(self, "成功", "工作表已删除!")
# ---------- 编辑操作槽函数 ----------
def get_current_table(self):
"""获取当前选中的表格,辅助函数"""
table = self.sheet_tab.currentWidget()
if not isinstance(table, QTableWidget):
QMessageBox.warning(self, "提示", "无有效表格!")
return None
return table
def on_add_row(self):
"""插入行"""
table = self.get_current_table()
if not table:
return
# 在选中行下方插入,无选中则在最后插入
current_row = table.currentRow()
insert_row = current_row + 1 if current_row >= 0 else table.rowCount()
table.insertRow(insert_row)
QMessageBox.information(self, "成功", f"在第{insert_row+1}行插入新行!")
def on_del_row(self):
"""删除行"""
table = self.get_current_table()
if not table:
return
current_row = table.currentRow()
if current_row < 0:
QMessageBox.warning(self, "提示", "请先选中要删除的行!")
return
table.removeRow(current_row)
QMessageBox.information(self, "成功", f"第{current_row+1}行已删除!")
def on_add_col(self):
"""插入列"""
table = self.get_current_table()
if not table:
return
current_col = table.currentColumn()
insert_col = current_col + 1 if current_col >= 0 else table.columnCount()
table.insertColumn(insert_col)
QMessageBox.information(self, "成功", f"在第{insert_col+1}列插入新列!")
def on_del_col(self):
"""删除列"""
table = self.get_current_table()
if not table:
return
current_col = table.currentColumn()
if current_col < 0:
QMessageBox.warning(self, "提示", "请先选中要删除的列!")
return
table.removeColumn(current_col)
QMessageBox.information(self, "成功", f"第{current_col+1}列已删除!")
def on_clear_table(self):
"""清空表格(保留行列数)"""
table = self.get_current_table()
if not table:
return
# 确认清空
reply = QMessageBox.question(self, "确认", "确定要清空表格所有数据吗?", QMessageBox.Yes | QMessageBox.No)
if reply != QMessageBox.Yes:
return
# 清空所有单元格内容
for row in range(table.rowCount()):
for col in range(table.columnCount()):
table.setItem(row, col, QTableWidgetItem(""))
QMessageBox.information(self, "成功", "表格数据已清空!")
# ---------- 格式操作槽函数 ----------
def on_set_font(self):
"""设置选中单元格字体"""
table = self.get_current_table()
if not table:
return
# 获取选中单元格
selected_items = table.selectedItems()
if not selected_items:
QMessageBox.warning(self, "提示", "请先选中要设置格式的单元格!")
return
# 弹出字体选择对话框
font, ok = QFontDialog.getFont(QFont("微软雅黑", 12), self, "选择字体")
if ok:
for item in selected_items:
item.setFont(font)
QMessageBox.information(self, "成功", "字体设置完成!")
def on_set_color(self):
"""设置选中单元格字体颜色"""
table = self.get_current_table()
if not table:
return
selected_items = table.selectedItems()
if not selected_items:
QMessageBox.warning(self, "提示", "请先选中要设置格式的单元格!")
return
# 弹出颜色选择对话框
color = QColorDialog.getColor(Qt.black, self, "选择字体颜色")
if color.isValid():
for item in selected_items:
item.setForeground(color)
QMessageBox.information(self, "成功", "字体颜色设置完成!")
def on_set_align(self):
"""设置选中单元格居中对齐"""
table = self.get_current_table()
if not table:
return
selected_items = table.selectedItems()
if not selected_items:
QMessageBox.warning(self, "提示", "请先选中要设置格式的单元格!")
return
for item in selected_items:
item.setTextAlignment(Qt.AlignCenter)
QMessageBox.information(self, "成功", "居中对齐设置完成!")
# ---------- 数据操作槽函数 ----------
def on_sort_data(self, order):
"""数据排序(按选中列)"""
table = self.get_current_table()
if not table:
return
current_col = table.currentColumn()
if current_col < 0:
QMessageBox.warning(self, "提示", "请先选中要排序的列!")
return
# 排序
table.sortItems(current_col, order)
sort_type = "升序" if order == Qt.AscendingOrder else "降序"
QMessageBox.information(self, "成功", f"按第{current_col+1}列{sort_type}排序完成!")
def on_get_selected_data(self):
"""获取选中单元格数据"""
table = self.get_current_table()
if not table:
return
selected_items = table.selectedItems()
if not selected_items:
QMessageBox.warning(self, "提示", "请先选中单元格!")
return
# 整理选中数据
data_text = "选中数据:\n"
for item in selected_items:
data_text += f"行{item.row()+1}列{item.column()+1}:{item.text()}\n"
QMessageBox.information(self, "选中数据", data_text)
# ---------- 程序入口 ----------
if __name__ == "__main__":
# 解决中文显示问题(可选)
QApplication.setStyle("Fusion")
app = QApplication(sys.argv)
window = SimpleExcel()
window.show()
sys.exit(app.exec_())四、核心功能解析
1. 多工作表管理
- 核心方法:
add_new_sheet()创建新的QTableWidget并添加到QTabWidget; - 关闭保护:
on_close_sheet()中判断工作表数量,确保至少保留1个; - 标签位置:
setTabPosition(QTabWidget.South)把标签放在下方,贴近Excel的sheet位置。
2. CSV文件读写
- 读取:用
csv.reader()读取所有行,再逐行填充到QTableWidget; - 写入:遍历表格所有单元格,收集数据后用
csv.writer()写入; - 编码:统一用
encoding="utf-8",避免中文乱码(Windows若乱码可尝试gbk)。
3. 单元格格式设置
- 选中单元格:
selectedItems()获取所有选中的单元格项; - 字体设置:
QFontDialog.getFont()弹出字体选择框,返回选中的字体对象; - 颜色设置:
QColorDialog.getColor()返回颜色对象,用setForeground()应用; - 对齐方式:
setTextAlignment(Qt.AlignCenter)设置居中,支持左/右对齐。
4. 行列操作
- 插入行/列:
insertRow()/insertColumn(),优先在选中位置插入,无选中则在末尾; - 删除行/列:
removeRow()/removeColumn(),需先判断是否选中; - 清空表格:遍历所有单元格,设置为空字符串(保留行列数)。
五、常见问题排查
1. 文件相关问题
- CSV打开乱码:读写时确保
encoding="utf-8",Windows系统可替换为encoding="gbk"; - 保存文件失败:检查文件路径是否有权限(如系统盘根目录),建议保存到用户目录;
- 文件为空提示:读取CSV时判断
rows是否为空,避免表格无数据。
2. 表格操作问题
- 无有效表格提示:所有表格操作前调用
get_current_table(),判断是否为QTableWidget; - 选中行/列无响应:确保表格
setSelectionBehavior(QTableWidget.SelectRows),支持整行选中; - 排序无效果:排序需指定列(
currentColumn()),确保选中了有效列。
3. 界面适配问题
- 窗口缩放过小:设置
setMinimumSize(800, 500),避免控件重叠; - 表格不自适应:
horizontalHeader().setStretchLastSection(True)让最后一列拉伸; - 按钮排版混乱:用QGridLayout管理分组按钮,固定按钮大小,避免拉伸变形。
六、功能拓展建议(进阶方向)
这个基础版本可拓展更多Excel功能,适合进阶学习:
- 单元格合并:
setSpan()实现单元格合并/拆分; - 公式计算:支持简单公式(如求和
=SUM(A1:A5)); - 数据筛选:按条件筛选表格数据;
- 样式保存:支持单元格背景色、边框设置;
- Excel格式支持:用
openpyxl/pandas库支持.xlsx文件(需额外安装)。
总结
- 项目核心价值:这是阶段二的收官项目,整合了前9章的所有核心知识点,从“零散知识点”到“完整项目”,帮你建立PyQt5项目开发的整体思维;
- 核心技术点:QTableWidget是表格开发的核心,QTabWidget实现多页面,QGroupBox规整界面,标准对话框处理交互,信号与槽串联所有功能;
- 实战思维:开发GUI项目需先设计界面布局→拆分功能模块→实现核心功能→处理异常→优化体验;
- 后续方向:阶段三将进入PyQt5进阶内容(信号与槽深入、自定义控件、多线程、数据库交互),进一步提升项目开发能力。
如果在运行代码时遇到问题,或者想拓展某个进阶功能(如单元格合并、Excel格式支持),欢迎在评论区留言讨论~