PyQt5实战项目:简易Excel表格数据管理器(完整可运行代码)

寒烟似雪
1月5日发布 /正在检测是否收录...

第10篇:阶段二实战项目:简易Excel表格数据管理器(完整代码)

哈喽~ 欢迎来到PyQt5系列的第10篇!这是阶段二的收官实战项目——我们将整合前9章的核心知识点(布局管理器、QTableWidget、容器控件、标准对话框、自定义交互),开发一个“仿照Excel的简易表格数据管理器”。这个项目覆盖Excel最常用的基础功能(新建/打开/保存、行列增删、数据排序、单元格格式设置等),全程代码可直接运行,帮你把零散的知识点串联成完整的项目开发能力!
mk0p53ij.png

一、项目核心目标与知识点整合

1. 项目目标

开发一个轻量级表格管理器,实现Excel的核心基础功能,满足日常简单的数据编辑/管理需求,界面风格贴近Excel,操作逻辑直观。
mk0ohoys.png

2. 整合的核心知识点

知识点应用场景
QTableWidget核心表格控件,承载数据展示/编辑
QTabWidget多工作表切换(模拟Excel的多sheet)
QGroupBox功能按钮分组(编辑区/格式区/数据区)
布局管理器(网格/线性)整体界面排版,保证自适应
标准对话框(文件/字体/颜色/消息)打开/保存文件、设置单元格格式、操作提示
信号与槽按钮交互、表格事件响应

3. 核心功能清单

  • ✅ 多工作表管理(新建/删除sheet、切换sheet);
  • ✅ 文件操作(新建空白表格、打开CSV文件、保存CSV文件);
  • ✅ 行列管理(插入/删除行/列、清空表格);
  • ✅ 数据编辑(单元格编辑、选中行/列高亮);
  • ✅ 格式设置(单元格字体、颜色、居中对齐);
  • ✅ 数据操作(按列排序、获取选中单元格数据);
  • ✅ 操作提示(成功/失败/警告类消息框)。

二、项目整体架构

1. 界面布局设计

整体界面分为3个区域:

  • 顶部功能区:文件操作按钮(新建/打开/保存) + 工作表切换(QTabWidget);
  • 中部功能按钮区:用QGroupBox分为“编辑区”“格式区”“数据区”,摆放功能按钮;
  • 底部表格区:QTableWidget核心表格,占界面主要空间,支持自适应缩放。

2. 核心功能模块

  • 文件模块:处理CSV文件的新建/打开/保存;
  • 工作表模块:管理多sheet的增删改查;
    mk0ois8v.png
  • 编辑模块:行列增删、清空表格;
    mk0ojcv5.png
  • 格式模块:单元格字体、颜色、对齐方式设置;
    mk0ojroj.png
  • 数据模块:数据排序、选中数据获取。
    mk0p33q1.png

三、完整代码实现

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功能,适合进阶学习:

  1. 单元格合并setSpan() 实现单元格合并/拆分;
  2. 公式计算:支持简单公式(如求和=SUM(A1:A5));
  3. 数据筛选:按条件筛选表格数据;
  4. 样式保存:支持单元格背景色、边框设置;
  5. Excel格式支持:用openpyxl/pandas库支持.xlsx文件(需额外安装)。

总结

  1. 项目核心价值:这是阶段二的收官项目,整合了前9章的所有核心知识点,从“零散知识点”到“完整项目”,帮你建立PyQt5项目开发的整体思维;
  2. 核心技术点:QTableWidget是表格开发的核心,QTabWidget实现多页面,QGroupBox规整界面,标准对话框处理交互,信号与槽串联所有功能;
  3. 实战思维:开发GUI项目需先设计界面布局→拆分功能模块→实现核心功能→处理异常→优化体验;
  4. 后续方向:阶段三将进入PyQt5进阶内容(信号与槽深入、自定义控件、多线程、数据库交互),进一步提升项目开发能力。

如果在运行代码时遇到问题,或者想拓展某个进阶功能(如单元格合并、Excel格式支持),欢迎在评论区留言讨论~

© 版权声明
THE END
喜欢就支持一下吧
点赞 0 分享 收藏
评论 抢沙发
OωO
取消
SSL