Python基于PyQt5和openpyxl实现Excel单元格合并工具

 更新时间:2025年11月28日 09:54:33   作者:温轻舟  
本文介绍了基于PyQt5和openpyxl的图形化界面工具,用于合并Excel文件中指定列的相同内容单元格,工具提供了文件选择、工作表选择、合并设置、合并执行和输出控制等功能,需要的朋友可以参考下

一:效果展示:

本项目是基于 PyQt5openpyxl 的图形化界面工具。用于合并 Excel 文件中指定列的相同内容单元格,允许用户选择 Excel 文件、指定工作表、设置起始行、选择要合并的列,并控制合并行为

二:功能描述:

1. 文件选择与工作表选择

  • 浏览按钮:允许用户通过文件对话框选择 Excel 文件(支持 .xlsx.xls 格式)
  • 自动读取工作表:选中文件后自动读取并显示所有工作表名称
  • 默认输出文件名:自动在原文件名后添加 “_wenqingzhou” 作为默认输出文件名

2. 合并设置

(1)起始行设置

  • 通过 QSpinBox 设置从哪一行开始处理数据

(2)列选择

  • 可以动态添加多个列选择控件
  • 每个列选择控件允许选择 A-CU 的列标识
  • 提供删除按钮移除不需要的列设置

(3)合并选项

  • "仅合并相同内容"复选框:控制是否只合并内容相同的相邻单元格
  • 如果勾选:只合并内容相同的相邻单元格
  • 如果未勾选:每个单元格单独处理(实际不合并,但保留结构以便统一处理)

3. 合并执行

(1)后台线程处

  • 使用 QThread 在后台执行合并操作,避免界面卡死

(2)进度显示

  • 通过 QProgressBar 显示合并进度

(3)结果反馈

  • 成功时显示完成消息和输出文件路径
  • 失败时显示错误详情

4. 输出控制

  • 可自定义输出文件名
  • 默认在原文件名后添加后缀保存

三:完整代码:

import sys
from PyQt5.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
                             QLabel, QLineEdit, QPushButton, QFileDialog, QComboBox,
                             QCheckBox, QMessageBox, QSpinBox, QProgressBar)
from PyQt5.QtCore import QThread, pyqtSignal
from openpyxl import load_workbook
from openpyxl.utils import get_column_letter
import os

class MergeThread(QThread):
    progress_signal = pyqtSignal(int)
    finished_signal = pyqtSignal(str)
    error_signal = pyqtSignal(str)

    def __init__(self, file_path, sheet_name, start_row, columns, merge_same_content, output_path, parent=None):
        super().__init__(parent)
        self.file_path = file_path
        self.sheet_name = sheet_name
        self.start_row = start_row
        self.columns = columns 
        self.merge_same_content = merge_same_content
        self.output_path = output_path

    def run(self):
        try:
            wb = load_workbook(self.file_path)
            ws = wb[self.sheet_name]
            total_steps = len(self.columns)
            current_step = 0
            
            for col_info in self.columns:
                col = col_info['col']
                target_list = []
                
                for row in range(self.start_row, ws.max_row + 1):
                    cell_value = ws[f"{col}{row}"].value
                    target_list.append(cell_value if cell_value is not None else "")
                    
                self._merge_cells(ws, target_list, self.start_row, col)
                current_step += 1
                progress = int(current_step / total_steps * 100)
                self.progress_signal.emit(progress)
                
            wb.save(self.output_path)
            self.finished_signal.emit(self.output_path)
            
        except Exception as e:
            self.error_signal.emit(str(e))

    def _merge_cells(self, ws, target_list, start_row, col):
        start = 0
        end = 0
        reference = target_list[0]

        for i in range(len(target_list)):
            if not self.merge_same_content and i > 0:
                if i > 0 and start != i - 1:
                    ws.merge_cells(f'{col}{start + start_row}:{col}{i - 1 + start_row}')
                start = i
            else:
                if i < len(target_list) - 1 and target_list[i] == target_list[i + 1]:
                    end = i
                    continue

                if start == i: 
                    pass
                else:
                    if target_list[start] == target_list[i]:
                        ws.merge_cells(f'{col}{start + start_row}:{col}{i + start_row}')

                start = i + 1

class ExcelMergeTool(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("Excel 单元格合并工具")
        self.setGeometry(100, 100, 600, 450)  
        self.file_path = ""
        self.sheet_names = []
        self.columns = []
        self.init_ui()

    def init_ui(self):
        main_widget = QWidget()
        main_layout = QVBoxLayout()
        file_layout = QHBoxLayout()
        file_layout.addWidget(QLabel("Excel文件:"))
        self.file_edit = QLineEdit()
        self.file_edit.setReadOnly(True)
        file_layout.addWidget(self.file_edit)
        browse_btn = QPushButton("浏览...")
        browse_btn.clicked.connect(self.browse_file)
        file_layout.addWidget(browse_btn)
        main_layout.addLayout(file_layout)
        sheet_layout = QHBoxLayout()
        sheet_layout.addWidget(QLabel("工作表:"))
        self.sheet_combo = QComboBox()
        self.sheet_combo.setEnabled(False)
        sheet_layout.addWidget(self.sheet_combo)
        main_layout.addLayout(sheet_layout)
        row_layout = QHBoxLayout()
        row_layout.addWidget(QLabel("起始行:"))
        self.start_row_spin = QSpinBox()
        self.start_row_spin.setMinimum(1)
        self.start_row_spin.setMaximum(9999)
        self.start_row_spin.setValue(1)
        row_layout.addWidget(self.start_row_spin)
        main_layout.addLayout(row_layout)
        col_layout = QVBoxLayout()
        col_layout.addWidget(QLabel("需要合并的列:"))
        self.col_list_widget = QWidget()
        self.col_list_layout = QVBoxLayout()
        self.col_list_widget.setLayout(self.col_list_layout)
        col_layout.addWidget(self.col_list_widget)
        add_col_btn = QPushButton("添加列")
        add_col_btn.clicked.connect(self.add_column_setting)
        col_layout.addWidget(add_col_btn)
        main_layout.addLayout(col_layout)
        option_layout = QHBoxLayout()
        self.merge_same_check = QCheckBox("仅合并相同内容")
        self.merge_same_check.setChecked(True)
        option_layout.addWidget(self.merge_same_check)
        main_layout.addLayout(option_layout)
        output_layout = QHBoxLayout()
        output_layout.addWidget(QLabel("输出文件名:"))
        self.output_edit = QLineEdit()
        self.output_edit.setPlaceholderText("自动在原文件名后添加'_wenqingzhou'")
        output_layout.addWidget(self.output_edit)
        main_layout.addLayout(output_layout)
        self.progress_bar = QProgressBar()
        self.progress_bar.setValue(0)
        main_layout.addWidget(self.progress_bar)
        execute_btn = QPushButton("执行合并")
        execute_btn.clicked.connect(self.execute_merge)
        main_layout.addWidget(execute_btn)
        main_widget.setLayout(main_layout)
        self.setCentralWidget(main_widget)

    def browse_file(self):
        file_path, _ = QFileDialog.getOpenFileName(
            self, "选择Excel文件", "", "Excel文件 (*.xlsx *.xls)"
        )

        if file_path:
            self.file_path = file_path
            self.file_edit.setText(file_path)

            try:
                wb = load_workbook(file_path, read_only=True)
                self.sheet_names = wb.sheetnames
                self.sheet_combo.clear()
                self.sheet_combo.addItems(self.sheet_names)
                self.sheet_combo.setEnabled(True)
                base, ext = os.path.splitext(file_path)
                default_output = f"{base}_wenqingzhou{ext}"
                self.output_edit.setText(default_output)

                for i in reversed(range(self.col_list_layout.count())):
                    widget = self.col_list_layout.itemAt(i).widget()
                    if widget is not None:
                        widget.deleteLater()

                self.columns = []

            except Exception as e:
                QMessageBox.critical(self, "错误", f"无法读取Excel文件:\n{str(e)}")

    def add_column_setting(self):
        if not self.sheet_names:
            QMessageBox.warning(self, "警告", "请先选择Excel文件")
            return

        col_widget = QWidget()
        col_layout = QHBoxLayout()
        col_layout.addWidget(QLabel("列:"))
        col_combo = QComboBox()
        col_combo.addItems([get_column_letter(i) for i in range(1, 100)])  # A-CZ
        col_layout.addWidget(col_combo)
        del_btn = QPushButton("删除")
        del_btn.clicked.connect(lambda: self.remove_column_setting(col_widget))
        col_layout.addWidget(del_btn)
        col_widget.setLayout(col_layout)
        self.col_list_layout.addWidget(col_widget)
        self.columns.append({
            'widget': col_widget,
            'col_combo': col_combo,
        })

    def remove_column_setting(self, widget):
        self.col_list_layout.removeWidget(widget)
        widget.deleteLater()
        self.columns = [col for col in self.columns if col['widget'] != widget]

    def execute_merge(self):
        if not self.file_path:
            QMessageBox.warning(self, "警告", "请先选择Excel文件")
            return

        if not self.columns:
            QMessageBox.warning(self, "警告", "请至少添加一列需要合并的列")
            return

        sheet_name = self.sheet_combo.currentText()
        start_row = self.start_row_spin.value()
        merge_same_content = self.merge_same_check.isChecked()

        columns_info = []
        for col in self.columns:
            col_letter = col['col_combo'].currentText()
            columns_info.append({'col': col_letter, 'name': ''})  

        output_path = self.output_edit.text().strip()
        if not output_path:
            base, ext = os.path.splitext(self.file_path)
            output_path = f"{base}_wenqingzhou{ext}"

        self.thread = MergeThread(
            file_path=self.file_path,
            sheet_name=sheet_name,
            start_row=start_row,
            columns=columns_info,
            merge_same_content=merge_same_content,
            output_path=output_path
        )

        self.thread.progress_signal.connect(self.update_progress)
        self.thread.finished_signal.connect(self.merge_completed)
        self.thread.error_signal.connect(self.merge_failed)
        self.thread.start()

    def update_progress(self, value):
        self.progress_bar.setValue(value)

    def merge_completed(self, output_path):
        QMessageBox.information(
            self,
            "完成",
            f"单元格合并完成!\n输出文件已保存为:\n{output_path}"
        )
        self.progress_bar.setValue(0)

    def merge_failed(self, error_msg):
        QMessageBox.critical(
            self,
            "错误",
            f"合并过程中发生错误:\n{error_msg}"
        )
        self.progress_bar.setValue(0)

if __name__ == "__main__":
    app = QApplication(sys.argv)
    window = ExcelMergeTool()
    window.show()
    sys.exit(app.exec_())

四:代码分析:

1. 线程处理模块

class MergeThread(QThread):
    
    """后台处理Excel合并的线程类,避免界面卡顿"""
    
    # 定义线程信号
    progress_signal = pyqtSignal(int)  # 进度信号
    finished_signal = pyqtSignal(str)  # 完成信号(带输出路径)
    error_signal = pyqtSignal(str)     # 错误信号

    def __init__(self, file_path, sheet_name, start_row, columns, merge_same_content, output_path, parent=None):
        super().__init__(parent)
        # 初始化参数
        self.file_path = file_path
        self.sheet_name = sheet_name
        self.start_row = start_row
        self.columns = columns  # 格式:[{'col': 'A'}, {'col': 'B'}]
        self.merge_same_content = merge_same_content
        self.output_path = output_path

    def run(self):
        """线程执行的主逻辑"""
        try:
            # 1. 加载Excel文件
            wb = load_workbook(self.file_path)
            ws = wb[self.sheet_name]
            
            # 2. 计算总进度步数(基于列数)
            total_steps = len(self.columns)
            current_step = 0
            
            # 3. 遍历处理每一列
            for col_info in self.columns:
                col = col_info['col']
                target_list = []
                
                # 3.1 收集该列所有单元格数据
                for row in range(self.start_row, ws.max_row + 1):
                    cell_value = ws[f"{col}{row}"].value
                    target_list.append(cell_value if cell_value is not None else "")
                
                # 3.2 执行单元格合并
                self._merge_cells(ws, target_list, self.start_row, col)
                
                # 3.3 更新进度
                current_step += 1
                progress = int(current_step / total_steps * 100)
                self.progress_signal.emit(progress)
            
            # 4. 保存结果
            wb.save(self.output_path)
            self.finished_signal.emit(self.output_path)
            
        except Exception as e:
            self.error_signal.emit(str(e))

    def _merge_cells(self, ws, target_list, start_row, col):
        """
        实际执行单元格合并的算法:
        :param ws: 工作表对象
        :param target_list: 该列所有单元格值列表
        :param start_row: 起始行号
        :param col: 列字母
        """
        start = 0
        reference = target_list[0]

        for i in range(len(target_list)):
            if not self.merge_same_content and i > 0:
                # 模式1:强制合并(无论内容是否相同)
                if i > 0 and start != i - 1:
                    ws.merge_cells(f'{col}{start + start_row}:{col}{i - 1 + start_row}')
                start = i
            else:
                # 模式2:仅合并相同内容
                if i < len(target_list) - 1 and target_list[i] == target_list[i + 1]:
                    continue  # 相同内容则继续扩展合并范围
                
                if start == i: 
                    pass  # 单个单元格无需合并
                else:
                    if target_list[start] == target_list[i]:
                        ws.merge_cells(f'{col}{start + start_row}:{col}{i + start_row}')
                start = i + 1

2. 主界面模块

class ExcelMergeTool(QMainWindow):
    
    """主窗口类,负责UI展示和用户交互"""
    def __init__(self):
        super().__init__()
        self.setWindowTitle("Excel 单元格合并工具")
        self.setGeometry(100, 100, 600, 450)  
        self.file_path = ""
        self.sheet_names = []
        self.columns = []  # 存储用户添加的列设置
        self.init_ui()

    def init_ui(self):
        
        """初始化用户界面"""
        main_widget = QWidget()
        main_layout = QVBoxLayout()
        
        # ------- 文件选择区域 -------
        file_layout = QHBoxLayout()
        file_layout.addWidget(QLabel("Excel文件:"))
        self.file_edit = QLineEdit()
        self.file_edit.setReadOnly(True)
        file_layout.addWidget(self.file_edit)
        
        browse_btn = QPushButton("浏览...")
        browse_btn.clicked.connect(self.browse_file)
        file_layout.addWidget(browse_btn)
        main_layout.addLayout(file_layout)
        
        # ------- 工作表选择区域 -------
        sheet_layout = QHBoxLayout()
        sheet_layout.addWidget(QLabel("工作表:"))
        self.sheet_combo = QComboBox()
        self.sheet_combo.setEnabled(False)  # 初始禁用,直到选择文件
        sheet_layout.addWidget(self.sheet_combo)
        main_layout.addLayout(sheet_layout)
        
        # ------- 起始行设置 -------
        row_layout = QHBoxLayout()
        row_layout.addWidget(QLabel("起始行:"))
        self.start_row_spin = QSpinBox()
        self.start_row_spin.setMinimum(1)
        self.start_row_spin.setMaximum(9999)
        self.start_row_spin.setValue(1)
        row_layout.addWidget(self.start_row_spin)
        main_layout.addLayout(row_layout)
        
        # ------- 列设置区域 -------
        col_layout = QVBoxLayout()
        col_layout.addWidget(QLabel("需要合并的列:"))
        
        # 动态列设置容器
        self.col_list_widget = QWidget()
        self.col_list_layout = QVBoxLayout()
        self.col_list_widget.setLayout(self.col_list_layout)
        col_layout.addWidget(self.col_list_widget)
        
        add_col_btn = QPushButton("添加列")
        add_col_btn.clicked.connect(self.add_column_setting)
        col_layout.addWidget(add_col_btn)
        main_layout.addLayout(col_layout)
        
        # ------- 选项设置 -------
        option_layout = QHBoxLayout()
        self.merge_same_check = QCheckBox("仅合并相同内容")
        self.merge_same_check.setChecked(True)
        option_layout.addWidget(self.merge_same_check)
        main_layout.addLayout(option_layout)
        
        # ------- 输出设置 -------
        output_layout = QHBoxLayout()
        output_layout.addWidget(QLabel("输出文件名:"))
        self.output_edit = QLineEdit()
        self.output_edit.setPlaceholderText("自动在原文件名后添加'_wenqingzhou'")
        output_layout.addWidget(self.output_edit)
        main_layout.addLayout(output_layout)
        
        # ------- 进度条 -------
        self.progress_bar = QProgressBar()
        self.progress_bar.setValue(0)
        main_layout.addWidget(self.progress_bar)
        
        # ------- 执行按钮 -------
        execute_btn = QPushButton("执行合并")
        execute_btn.clicked.connect(self.execute_merge)
        main_layout.addWidget(execute_btn)
        
        main_widget.setLayout(main_layout)
        self.setCentralWidget(main_widget)

3. 功能方法

    def browse_file(self):
        
        """打开文件对话框选择Excel文件"""
        file_path, _ = QFileDialog.getOpenFileName(
            self, "选择Excel文件", "", "Excel文件 (*.xlsx *.xls)"
        )

        if file_path:
            self.file_path = file_path
            self.file_edit.setText(file_path)

            try:
                # 读取工作表列表
                wb = load_workbook(file_path, read_only=True)
                self.sheet_names = wb.sheetnames
                self.sheet_combo.clear()
                self.sheet_combo.addItems(self.sheet_names)
                self.sheet_combo.setEnabled(True)
                
                # 设置默认输出文件名
                base, ext = os.path.splitext(file_path)
                default_output = f"{base}_wenqingzhou{ext}"
                self.output_edit.setText(default_output)

                # 清空已有列设置
                for i in reversed(range(self.col_list_layout.count())):
                    widget = self.col_list_layout.itemAt(i).widget()
                    if widget is not None:
                        widget.deleteLater()
                self.columns = []

            except Exception as e:
                QMessageBox.critical(self, "错误", f"无法读取Excel文件:\n{str(e)}")

    def add_column_setting(self):
        
        """添加一列合并设置"""
        if not self.sheet_names:
            QMessageBox.warning(self, "警告", "请先选择Excel文件")
            return

        # 创建列设置控件组
        col_widget = QWidget()
        col_layout = QHBoxLayout()
        col_layout.addWidget(QLabel("列:"))
        
        # 列选择下拉框(A-CZ)
        col_combo = QComboBox()
        col_combo.addItems([get_column_letter(i) for i in range(1, 100)])
        col_layout.addWidget(col_combo)
        
        # 删除按钮
        del_btn = QPushButton("删除")
        del_btn.clicked.connect(lambda: self.remove_column_setting(col_widget))
        col_layout.addWidget(del_btn)
        
        col_widget.setLayout(col_layout)
        self.col_list_layout.addWidget(col_widget)
        
        # 记录到列设置列表
        self.columns.append({
            'widget': col_widget,
            'col_combo': col_combo,
        })

    def remove_column_setting(self, widget):
        
        """删除指定的列设置"""
        self.col_list_layout.removeWidget(widget)
        widget.deleteLater()
        self.columns = [col for col in self.columns if col['widget'] != widget]

    def execute_merge(self):
        
        """执行合并操作"""
        # 参数验证
        if not self.file_path:
            QMessageBox.warning(self, "警告", "请先选择Excel文件")
            return

        if not self.columns:
            QMessageBox.warning(self, "警告", "请至少添加一列需要合并的列")
            return

        # 收集参数
        sheet_name = self.sheet_combo.currentText()
        start_row = self.start_row_spin.value()
        merge_same_content = self.merge_same_check.isChecked()

        # 准备列信息
        columns_info = []
        for col in self.columns:
            col_letter = col['col_combo'].currentText()
            columns_info.append({'col': col_letter, 'name': ''})  # name保留字段

        # 处理输出路径
        output_path = self.output_edit.text().strip()
        if not output_path:
            base, ext = os.path.splitext(self.file_path)
            output_path = f"{base}_wenqingzhou{ext}"

        # 创建并启动后台线程
        self.thread = MergeThread(
            file_path=self.file_path,
            sheet_name=sheet_name,
            start_row=start_row,
            columns=columns_info,
            merge_same_content=merge_same_content,
            output_path=output_path
        )

        # 连接信号槽
        self.thread.progress_signal.connect(self.update_progress)
        self.thread.finished_signal.connect(self.merge_completed)
        self.thread.error_signal.connect(self.merge_failed)
        self.thread.start()

4. 回调方法

    def update_progress(self, value):
        
        """更新进度条"""
        self.progress_bar.setValue(value)

    def merge_completed(self, output_path):
        
        """合并完成处理"""
        QMessageBox.information(
            self,
            "完成",
            f"单元格合并完成!\n输出文件已保存为:\n{output_path}"
        )
        self.progress_bar.setValue(0)

    def merge_failed(self, error_msg):
        
        """合并失败处理"""
        QMessageBox.critical(
            self,
            "错误",
            f"合并过程中发生错误:\n{error_msg}"
        )
        self.progress_bar.setValue(0)

以上就是Python基于PyQt5和openpyxl实现Excel单元格合并工具的详细内容,更多关于Python Excel单元格合并的资料请关注脚本之家其它相关文章!

相关文章

  • python安装twisted的问题解析

    python安装twisted的问题解析

    我们在这篇文章中给大家详细整理了python安装twisted时遇到的问题以及解决方法,有需要的朋友们参考下。
    2018-08-08
  • python处理html转义字符的方法详解

    python处理html转义字符的方法详解

    这篇文章主要介绍了python处理html转义字符的方法,结合实例形式较为详细的分析了Python针对常见HTML转义字符处理技巧,具有一定参考借鉴价值,需要的朋友可以参考下
    2016-07-07
  • 不知道这5种下划线的含义,你就不算真的会Python!

    不知道这5种下划线的含义,你就不算真的会Python!

    Python是一种高级程序语言,其核心设计哲学是代码可读性和语法,能够让程序员用很少的代码来表达自己的想法。这篇文章主要介绍了不知道这5种下划线的含义,你就不算真的会Python!对此标题感兴趣的朋友一起阅读本文吧
    2018-10-10
  • python用700行代码实现http客户端

    python用700行代码实现http客户端

    这篇文章主要介绍了python用700行代码实现http客户端的方法,帮助大家更好的理解和使用python,感兴趣的朋友可以了解下
    2021-01-01
  • Python实现PC屏幕截图并自动发送邮件

    Python实现PC屏幕截图并自动发送邮件

    在当前的数字化世界中,自动化已经成为我们日常生活和工作中的关键部分,本文我们将探讨如何使用Python来实现一个特定的自动化任务 - PC屏幕截图自动发送到指定的邮箱,感兴趣的可以了解下
    2023-11-11
  • Python threading模块condition原理及运行流程详解

    Python threading模块condition原理及运行流程详解

    这篇文章主要介绍了Python threading模块condition原理及运行流程详解,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下
    2020-10-10
  • python中内置库csv的使用及说明

    python中内置库csv的使用及说明

    这篇文章主要介绍了python中内置库csv的使用及说明,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2022-11-11
  • 使用Python和Tkinter实现html标签去除工具

    使用Python和Tkinter实现html标签去除工具

    本文介绍用Python和Tkinter开发的HTML标签去除工具,支持去除HTML标签、转义实体并输出纯文本,提供图形界面操作及复制功能,需要的朋友可以参考下
    2025-05-05
  • Python多线程threading和multiprocessing模块实例解析

    Python多线程threading和multiprocessing模块实例解析

    这篇文章主要介绍了Python多线程threading和multiprocessing模块等相关内容,分享了相关代码示例,小编觉得还是挺不错的,这里分享给大家,需要的朋友可以参考下
    2018-01-01
  • Python 复平面绘图实例

    Python 复平面绘图实例

    今天小编就为大家分享一篇Python 复平面绘图实例,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2019-11-11

最新评论