from tkinter import ttk, filedialog, messagebox, scrolledtext
import re
import xlwings as xw
import os
from os import path as osp
import subprocess
import glob
import sys
from datetime import datetime
class ExcelConverterApp:
def __init__(self, root):
self.root = root
self.root.title("CSV批量转Excel工具V1.0")
self.root.geometry("700x600") # 增大窗口高度以容纳日志框
self.root.resizable(False, False)
# 初始化变量
self.folder_path = tk.StringVar()
self.output_format = tk.StringVar(value="xlsx") # 默认xlsx格式
self.app = None
self.setup_ui()
def setup_ui(self):
# 设置主题风格
style = ttk.Style()
style.theme_use('clam')
# 主框架
main_frame = ttk.Frame(self.root, padding="10")
main_frame.pack(fill=tk.BOTH, expand=True)
# 标题
title_label = ttk.Label(
main_frame,
text="CSV批量转Excel转换工具",
font=("微软雅黑", 16, "bold"),
foreground="#2c3e50"
)
title_label.pack(pady=(0, 15))
# 文件夹选择部分
folder_frame = ttk.LabelFrame(main_frame, text="选择文件夹", padding=10)
folder_frame.pack(fill=tk.X, pady=5)
ttk.Label(folder_frame, text="文件夹路径:").grid(row=0, column=0, sticky=tk.W)
folder_entry = ttk.Entry(folder_frame, textvariable=self.folder_path, width=40)
folder_entry.grid(row=0, column=1, padx=5, sticky=tk.EW)
browse_btn = ttk.Button(
folder_frame,
text="浏览...",
command=self.browse_folder,
style="Accent.TButton"
)
browse_btn.grid(row=0, column=2, padx=5)
folder_frame.columnconfigure(1, weight=1) # 让输入框可以扩展
# 输出格式选择
format_frame = ttk.LabelFrame(main_frame, text="输出格式", padding=10)
format_frame.pack(fill=tk.X, pady=10)
ttk.Radiobutton(
format_frame,
text="Excel 2007及以上 (.xlsx)",
variable=self.output_format,
value="xlsx"
).pack(anchor=tk.W, pady=2)
ttk.Radiobutton(
format_frame,
text="Excel 97-2003 (.xls)",
variable=self.output_format,
value="xls"
).pack(anchor=tk.W, pady=2)
# 转换按钮
button_frame = ttk.Frame(main_frame)
button_frame.pack(pady=10)
convert_btn = ttk.Button(
button_frame,
text="开始转换",
command=self.convert_files,
style="Accent.TButton"
)
convert_btn.pack(side=tk.LEFT, padx=5)
clear_log_btn = ttk.Button(
button_frame,
text="清空日志",
command=self.clear_log
)
clear_log_btn.pack(side=tk.LEFT, padx=5)
# 日志框
log_frame = ttk.LabelFrame(main_frame, text="转换日志", padding=5)
log_frame.pack(fill=tk.BOTH, expand=True, pady=(10, 0))
self.log_text = scrolledtext.ScrolledText(
log_frame,
wrap=tk.WORD,
width=50,
height=5,
font=('Consolas', 10)
)
self.log_text.pack(fill=tk.BOTH, expand=True)
# 状态栏
self.status_var = tk.StringVar(value="准备就绪")
status_bar = ttk.Label(
main_frame,
textvariable=self.status_var,
anchor=tk.W,
padding=5
)
status_bar.pack(fill=tk.X, pady=(10, 0))
# 配置样式
self.configure_styles()
def configure_styles(self):
style = ttk.Style()
# 主颜色
style.configure('.', background="#ecf0f1", foreground="#2c3e50")
style.configure('TLabel', background="#ecf0f1")
style.configure('TFrame', background="#ecf0f1")
style.configure('TLabelframe', background="#ecf0f1")
style.configure('TLabelframe.Label', background="#ecf0f1")
# 按钮样式
style.configure('TButton', padding=6, font=("微软雅黑", 10))
style.configure('Accent.TButton', foreground="white", background="#3498db")
style.map('Accent.TButton',
background=[('active', '#2980b9'), ('pressed', '#2c3e50')])
# 输入框样式
style.configure('TEntry', fieldbackground="white")
# 单选按钮样式
style.configure('TRadiobutton', background="#ecf0f1")
# 日志框样式
self.log_text.configure(
background="white",
foreground="black",
insertbackground="black"
)
def log_message(self, message):
"""向日志框添加消息"""
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
log_entry = f"[{timestamp}] {message}\n"
self.log_text.insert(tk.END, log_entry)
self.log_text.see(tk.END) # 自动滚动到底部
self.root.update() # 更新界面
def clear_log(self):
"""清空日志框"""
self.log_text.delete(1.0, tk.END)
self.log_message("日志已清空")
def browse_folder(self):
folder_selected = filedialog.askdirectory()
if folder_selected:
self.folder_path.set(folder_selected)
self.log_message(f"已选择文件夹: {folder_selected}")
self.status_var.set(f"已选择文件夹: {osp.basename(folder_selected)}")
def load_excel(self):
"""加载Excel应用"""
self.log_message("正在启动Excel应用...")
try:
app = xw.App(visible=False)
self.log_message("Excel应用启动成功")
return app
except Exception as e:
self.log_message(f"打开Excel程序失败: {str(e)}")
raise Exception("打开Excel程序失败") from e
def natural_sort_key(self, s):
"""自然排序的键生成函数"""
return [int(text) if text.isdigit() else text.lower()
for text in re.split(r'(\d+)', s)]
def convert_files(self):
"""执行转换操作"""
folder = self.folder_path.get()
if not folder:
messagebox.showwarning("警告", "请先选择包含CSV文件的文件夹")
self.log_message("警告: 未选择文件夹")
return
try:
self.status_var.set("正在初始化Excel...")
self.log_message("开始转换过程...")
self.root.update()
self.app = self.load_excel()
self.status_var.set("正在查找CSV文件...")
self.log_message(f"正在搜索文件夹: {folder}")
self.root.update()
csv_files = glob.glob(osp.join(folder, '**', '*.csv'), recursive=True)
csv_files.sort(key=lambda x: self.natural_sort_key(x), reverse=False)
if not csv_files:
messagebox.showinfo("提示", "没有找到任何CSV文件")
self.log_message("未找到任何CSV文件")
return
total_files = len(csv_files)
format_name = "XLSX" if self.output_format.get() == "xlsx" else "XLS"
self.log_message(f"共找到 {total_files} 个CSV文件,将转换为{format_name}格式")
confirm = messagebox.askyesno(
"确认",
f"找到 {total_files} 个CSV文件,将转换为{format_name}格式,是否继续?"
)
if not confirm:
self.log_message("用户取消了转换操作")
return
success_count = 0
for i, csv in enumerate(csv_files, 1):
try:
current_status = f"正在处理 ({i}/{total_files}): {osp.basename(csv)}"
self.status_var.set(current_status)
self.log_message(current_status)
self.root.update()
self.log_message(f"打开文件: {csv}")
excel_path = osp.splitext(csv)[0] + ('.xlsx' if self.output_format.get() == "xlsx" else '.xls')
#打开csv
wb = self.app.books.open(csv)
self.log_message(f"正在保存为: {excel_path}")
# 设置文件格式
file_format = 51 if self.output_format.get() == "xlsx" else 56 # 51=xlsx, 56=xls
wb.save(excel_path)
wb.close()
success_count += 1
self.log_message(f"成功转换: {osp.basename(csv)}")
except Exception as e:
error_msg = f"处理文件 {osp.basename(csv)} 时出错: {str(e)}"
self.log_message(error_msg)
print(error_msg)
result_msg = f"转换完成!成功: {success_count}/{total_files} 个文件"
messagebox.showinfo("完成", result_msg)
self.log_message(result_msg)
self.status_var.set(f"转换完成,共处理 {success_count}/{total_files} 个文件")
except Exception as e:
error_msg = f"转换过程中出错: {str(e)}"
messagebox.showerror("错误", error_msg)
self.log_message(error_msg)
self.status_var.set("转换出错")
finally:
if hasattr(self, 'app') and self.app:
self.log_message("正在退出Excel应用...")
self.app.quit()
def main():
root = tk.Tk()
app = ExcelConverterApp(root)
root.mainloop()
if __name__ == "__main__":
main()
运行后界面是这样:
