Python自动办公工具01-Excel文件编辑器

一:效果展示:

本项目采用直观的用户界面,交互式数据预览,精确的单元格定位。支持.xlsx和.xls格式文件的读取;可将修改后的内容保存为新的Excel文件,界面上方显示当前加载的文件路径

二:功能描述

(1)项目特色
  1. 采用PyQt5构建的现代化GUI界面,布局清晰,操作便捷
  2. 交互式数据预览,以表格形式展示Excel数据,支持点击单元格自动填充地址和值
  3. 通过鼠标点击位置可以精确判断选择的列(单元格定位)
  4. 对文件操作、单元格更新等关键操作提供错误提示,防止程序因异常情况崩溃
(2)核心功能
1. 文件管理功能
  • 打开文件:支持.xlsx和.xls格式文件的读取
  • 保存文件:可将修改后的内容保存为新的Excel文件
  • 文件状态显示:界面上方显示当前加载的文件路径
2. 工作表管理
  • 工作表选择:通过下拉框选择工作簿中的不同工作表
  • 自动加载:打开文件后自动加载第一个工作表的内容
3. 单元格编辑功能
  • 单元格定位,可通过点击数据预览区域自动填充单元格地址,也可手动输入单元格地址
  • 内容编辑
    • 可修改选定单元格的值
    • 支持文本和数字内容的更新
    • 提供"清空内容"按钮快速清除输入
  • 即时反馈
    • 更新成功后显示确认消息
    • 自动刷新数据预览区域
4. 数据预览功能
  • 智能列显示
    • 自动检测工作表的最大列数
    • 默认显示前10列,可根据内容自动调整
  • 交互式选择
    • 点击任意单元格可自动填充地址和当前值到输入框
    • 鼠标位置精确判断选择的列
(3)技术实现亮点
  1. openpyxl集成
    • 使用openpyxl库进行Excel文件的读写操作
    • 正确处理单元格引用和值转换
  2. 动态UI更新
    • 工作表切换时自动更新预览区域
    • 列标题根据实际数据动态生成
  3. 响应式设计
    • 数据预览区域固定高度,避免界面过度拉伸
    • 列宽根据内容自动调整
  4. 用户体验优化
    • 操作前后提供明确的反馈信息
    • 错误处理完善,避免程序意外终止
(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()
相关推荐
星星上的吴彦祖36 分钟前
多模态感知驱动的人机交互决策研究综述
python·深度学习·计算机视觉·人机交互
爱笑的眼睛111 小时前
PyTorch Lightning:重新定义深度学习工程实践
java·人工智能·python·ai
0思必得01 小时前
[Web自动化] HTTP/HTTPS协议
前端·python·http·自动化·网络基础·web自动化
纵有疾風起2 小时前
C++——多态
开发语言·c++·经验分享·面试·开源
rgb2gray2 小时前
增强城市数据分析:多密度区域的自适应分区框架
大数据·python·机器学习·语言模型·数据挖掘·数据分析·llm
氵文大师3 小时前
A机通过 python -m http.server 下载B机的文件
linux·开发语言·python·http
程序员爱钓鱼3 小时前
用 Python 批量生成炫酷扫光 GIF 动效
后端·python·trae
封奚泽优3 小时前
下降算法(Python实现)
开发语言·python·算法
java1234_小锋3 小时前
基于Python深度学习的车辆车牌识别系统(PyTorch2卷积神经网络CNN+OpenCV4实现)视频教程 - 自定义字符图片数据集
python·深度学习·cnn·车牌识别