代码:
import tkinter as tk
from tkinter import filedialog, messagebox, ttk
import pandas as pd
from docx import Document
from docx.shared import Cm, Pt
from docx.enum.text import WD_ALIGN_PARAGRAPH
import os
import traceback
import threading
import queue
import sys
import re # 导入正则表达式库,用于文件名清理
import math # 导入数学库,用于向上取整
# 尝试导入pywin32库,如果失败则提示用户安装
try:
import win32com.client
import pythoncom # 导入pythoncom模块以支持多线程COM
except ImportError:
# 在GUI完全加载前,无法直接使用messagebox
# 这种启动时依赖检查的错误,最好在主程序入口处处理
print("错误:缺少关键库 pywin32。请在命令行运行 'pip install pywin32' 来安装。")
# 实际应用中可以创建一个简单的tk窗口来显示这个错误
# 此处为简化,直接退出
sys.exit(1)
# --- 配置区 ---
FONT_SIZES_CN = {
'一号': 26, '小一': 24, '二号': 22, '小二': 18,
'三号': 16, '小三': 15, '四号': 14, '五号': 10.5,
}
OUTPUT_FOLDER_NAME = "FD箱唛导出文件"
class LabelGeneratorApp:
def __init__(self, root):
self.root = root
self.root.title("Word & PDF 标签生成器 (双方案)")
self.root.geometry("500x400") # 增加窗口高度以容纳新选项
self.excel_path = ""
self.task_queue = queue.Queue()
# --- GUI 组件 ---
main_frame = ttk.Frame(self.root, padding="10 10 10 10")
main_frame.pack(fill=tk.BOTH, expand=True)
# 1. 文件选择区
select_file_button = ttk.Button(main_frame, text="1. 选择 Excel 文件", command=self.select_file)
select_file_button.pack(pady=5, fill=tk.X)
self.file_label = ttk.Label(main_frame, text="尚未选择文件")
self.file_label.pack(pady=5)
# 2. 方案选择区
scheme_frame = ttk.LabelFrame(main_frame, text="2. 选择生成方案")
scheme_frame.pack(pady=10, fill=tk.X, padx=5)
self.scheme_var = tk.StringVar(value="scheme1") # 默认选择方案一
# --- 修改点:更新方案一的描述 ---
scheme1_rb = ttk.Radiobutton(scheme_frame, text="方案一:按序号生成", variable=self.scheme_var, value="scheme1")
scheme1_rb.pack(anchor=tk.W, padx=10, pady=5)
# --- 修改点:更新方案二的描述 ---
scheme2_rb = ttk.Radiobutton(scheme_frame, text="方案二:按总数生成", variable=self.scheme_var, value="scheme2")
scheme2_rb.pack(anchor=tk.W, padx=10, pady=5)
# 3. 操作按钮区
button_frame = ttk.Frame(main_frame)
button_frame.pack(pady=10, fill=tk.X, expand=True)
self.generate_word_button = ttk.Button(button_frame, text="3. 生成 Word (.docx)", command=lambda: self.start_task('word'))
self.generate_word_button.pack(side=tk.LEFT, padx=5, expand=True, fill=tk.X)
self.generate_pdf_button = ttk.Button(button_frame, text="3. 生成 PDF (.pdf)", command=lambda: self.start_task('pdf'))
self.generate_pdf_button.pack(side=tk.RIGHT, padx=5, expand=True, fill=tk.X)
# 4. 状态栏
self.status_label = ttk.Label(main_frame, text="准备就绪", relief=tk.SUNKEN, anchor=tk.W)
self.status_label.pack(side=tk.BOTTOM, fill=tk.X, pady=(10,0))
self.process_queue()
def select_file(self):
path = filedialog.askopenfilename(
title="请选择包含标签数据的 Excel 文件",
filetypes=[("Excel Files", "*.xlsx *.xls")]
)
if path:
self.excel_path = path
filename = os.path.basename(path)
self.file_label.config(text=f"已选择: {filename}")
self.status_label.config(text="文件已加载,请选择生成方案和格式。")
def process_queue(self):
try:
task = self.task_queue.get(block=False)
# 根据任务类型更新GUI
if task[0] == 'update_status':
self.status_label.config(text=task[1])
elif task[0] == 'task_done':
self.enable_buttons()
messagebox.showinfo(task[1], task[2])
self.status_label.config(text=task[3])
elif task[0] == 'task_error':
self.enable_buttons()
messagebox.showerror(task[1], task[2])
self.status_label.config(text=task[3])
except queue.Empty:
pass
finally:
self.root.after(100, self.process_queue)
def start_task(self, task_type):
if not self.excel_path:
messagebox.showerror("错误", "请先选择一个 Excel 文件!")
return
self.disable_buttons()
# 将选择的方案和任务类型传递给后台线程
scheme = self.scheme_var.get()
thread = threading.Thread(target=self.run_generation_task, args=(scheme, task_type))
thread.daemon = True
thread.start()
def disable_buttons(self):
self.generate_word_button.config(state=tk.DISABLED)
self.generate_pdf_button.config(state=tk.DISABLED)
def enable_buttons(self):
self.generate_word_button.config(state=tk.NORMAL)
self.generate_pdf_button.config(state=tk.NORMAL)
def add_formatted_paragraph(self, document, text, font_size_pt, is_bold=False):
p = document.add_paragraph()
p.alignment = WD_ALIGN_PARAGRAPH.LEFT
p_format = p.paragraph_format
p_format.line_spacing = 1.0
p_format.space_before = Pt(0)
p_format.space_after = Pt(0)
run = p.add_run(text)
font = run.font
font.name = 'Calibri'
font.size = Pt(font_size_pt)
font.bold = is_bold
def run_generation_task(self, scheme, task_type):
"""根据选择的方案执行不同的生成逻辑"""
# 为后台线程初始化COM库
pythoncom.CoInitialize()
try:
self.task_queue.put(('update_status', "正在读取Excel文件..."))
df = pd.read_excel(self.excel_path, engine='openpyxl')
required_columns = ['PI', 'ASIN', 'SKU', '箱数']
if not all(col in df.columns for col in required_columns):
raise ValueError(f"Excel 文件必须包含以下列: {', '.join(required_columns)}")
# 清理'箱数'列,将非数字转为0
df['箱数'] = pd.to_numeric(df['箱数'], errors='coerce').fillna(0).astype(int)
# 过滤掉箱数为0的行
df = df[df['箱数'] > 0]
if df.empty:
raise ValueError("未找到任何箱数大于0的有效数据行。")
if scheme == 'scheme1':
self.run_scheme1(df, task_type)
elif scheme == 'scheme2':
self.run_scheme2(df, task_type)
except Exception as e:
error_message = f"处理过程中出现错误: \n{traceback.format_exc()}"
self.task_queue.put(('task_error', "发生错误", error_message, "操作失败。"))
finally:
# 确保COM库被正确释放
pythoncom.CoUninitialize()
def convert_to_pdf_single(self, docx_path, pdf_path):
"""
启动一个独立的Word进程来执行单个文件转换,以确保最大稳定性。
"""
word_app = None
word_doc = None
try:
word_app = win32com.client.gencache.EnsureDispatch("Word.Application")
word_app.Visible = False
abs_docx_path = os.path.abspath(docx_path)
abs_pdf_path = os.path.abspath(pdf_path)
word_doc = word_app.Documents.Open(abs_docx_path)
# 强制设置为10x10cm
points_10_cm = 283.5
word_doc.PageSetup.PageWidth = points_10_cm
word_doc.PageSetup.PageHeight = points_10_cm
word_doc.SaveAs(abs_pdf_path, FileFormat=17) # 17代表PDF格式
finally:
if word_doc:
word_doc.Close(False) # 关闭文档,不保存更改
if word_app:
word_app.Quit()
def run_scheme1(self, df, task_type):
"""方案一:按序号生成"""
doc = Document()
section = doc.sections[0]
section.page_width = Cm(10); section.page_height = Cm(10)
section.left_margin = Cm(1.27); section.right_margin = Cm(1.27)
section.top_margin = Cm(1.27); section.bottom_margin = Cm(1.27)
self.task_queue.put(('update_status', "方案一:正在生成标签内容..."))
total_labels_generated = 0
for _, row in df.iterrows():
pi = str(row['PI']); asin = str(row['ASIN']); sku = str(row['SKU'])
carton_count = row['箱数']
for i in range(1, carton_count + 1):
if total_labels_generated > 0: doc.add_page_break()
self.add_formatted_paragraph(doc, "Customer ID: FD", FONT_SIZES_CN['二号'])
doc.add_paragraph().paragraph_format.space_after = Pt(0)
self.add_formatted_paragraph(doc, f"ASIN: {asin}", FONT_SIZES_CN['二号'], is_bold=True)
sku_len = len(sku)
if sku_len <= 9: sku_font_size = FONT_SIZES_CN['一号']
elif sku_len <= 10: sku_font_size = FONT_SIZES_CN['二号']
elif sku_len <= 14: sku_font_size = FONT_SIZES_CN['小二']
elif sku_len <= 18: sku_font_size = FONT_SIZES_CN['三号']
else: sku_font_size = FONT_SIZES_CN['小三']
self.add_formatted_paragraph(doc, f"SKU: {sku}", sku_font_size, is_bold=False)
pi_len = len(pi)
if pi_len <= 12: pi_font_size = 28
elif pi_len <= 15: pi_font_size = 22
elif pi_len <= 19: pi_font_size = 18
elif pi_len <= 24: pi_font_size = 14
else: pi_font_size = 12
self.add_formatted_paragraph(doc, f"PI: {pi}", pi_font_size, is_bold=True)
self.add_formatted_paragraph(doc, f"CARTON QTY: {i}/{carton_count}", 20)
self.add_formatted_paragraph(doc, "MADE IN CHINA", FONT_SIZES_CN['二号'])
total_labels_generated += 1
if task_type == 'word':
output_path = filedialog.asksaveasfilename(title="保存 Word 文件", defaultextension=".docx", filetypes=[("Word Document", "*.docx")])
if output_path:
doc.save(output_path)
self.task_queue.put(('task_done', "成功", f"成功生成 {total_labels_generated} 页!\nWord文件已保存。", "Word文件生成成功!"))
elif task_type == 'pdf':
pdf_path = filedialog.asksaveasfilename(title="保存 PDF 文件", defaultextension=".pdf", filetypes=[("PDF Document", "*.pdf")])
if pdf_path:
temp_docx_path = os.path.splitext(pdf_path)[0] + "_temp.docx"
try:
doc.save(temp_docx_path)
self.task_queue.put(('update_status', "正在调用Word程序转换PDF..."))
self.convert_to_pdf_single(temp_docx_path, pdf_path)
self.task_queue.put(('task_done', "成功", f"成功生成 {total_labels_generated} 页!\nPDF文件已保存。", "PDF文件生成成功!"))
finally:
if os.path.exists(temp_docx_path): os.remove(temp_docx_path)
def run_scheme2(self, df, task_type):
"""方案二:按总数生成"""
doc = Document()
section = doc.sections[0]
section.page_width = Cm(10); section.page_height = Cm(10)
section.left_margin = Cm(1.27); section.right_margin = Cm(1.27)
section.top_margin = Cm(1.27); section.bottom_margin = Cm(1.27)
self.task_queue.put(('update_status', "方案二:正在生成概览内容..."))
grand_total_cartons = df['箱数'].sum()
for i, (index, row) in enumerate(df.iterrows()):
pi = str(row['PI']); asin = str(row['ASIN']); sku = str(row['SKU'])
carton_count = row['箱数']
# 如果不是第一页,就添加一个分页符
if i > 0:
doc.add_page_break()
carton_qty_text = f"CARTON QTY: {carton_count}/{grand_total_cartons}"
self.add_formatted_paragraph(doc, "Customer ID: FD", FONT_SIZES_CN['二号'])
doc.add_paragraph().paragraph_format.space_after = Pt(0)
self.add_formatted_paragraph(doc, f"ASIN: {asin}", FONT_SIZES_CN['二号'], is_bold=True)
sku_len = len(sku)
if sku_len <= 9: sku_font_size = FONT_SIZES_CN['一号']
elif sku_len <= 10: sku_font_size = FONT_SIZES_CN['二号']
elif sku_len <= 14: sku_font_size = FONT_SIZES_CN['小二']
elif sku_len <= 18: sku_font_size = FONT_SIZES_CN['三号']
else: sku_font_size = FONT_SIZES_CN['小三']
self.add_formatted_paragraph(doc, f"SKU: {sku}", sku_font_size, is_bold=False)
pi_len = len(pi)
if pi_len <= 12: pi_font_size = 28
elif pi_len <= 15: pi_font_size = 22
elif pi_len <= 19: pi_font_size = 18
elif pi_len <= 24: pi_font_size = 14
else: pi_font_size = 12
self.add_formatted_paragraph(doc, f"PI: {pi}", pi_font_size, is_bold=True)
self.add_formatted_paragraph(doc, carton_qty_text, 20)
self.add_formatted_paragraph(doc, "MADE IN CHINA", FONT_SIZES_CN['二号'])
total_pages = len(df)
if task_type == 'word':
output_path = filedialog.asksaveasfilename(title="保存 Word 文件", defaultextension=".docx", filetypes=[("Word Document", "*.docx")])
if output_path:
doc.save(output_path)
self.task_queue.put(('task_done', "成功", f"成功生成 {total_pages} 页!\nWord文件已保存。", "Word文件生成成功!"))
elif task_type == 'pdf':
pdf_path = filedialog.asksaveasfilename(title="保存 PDF 文件", defaultextension=".pdf", filetypes=[("PDF Document", "*.pdf")])
if pdf_path:
temp_docx_path = os.path.splitext(pdf_path)[0] + "_temp.docx"
try:
doc.save(temp_docx_path)
self.task_queue.put(('update_status', "正在调用Word程序转换PDF..."))
self.convert_to_pdf_single(temp_docx_path, pdf_path)
self.task_queue.put(('task_done', "成功", f"成功生成 {total_pages} 页!\nPDF文件已保存。", "PDF文件生成成功!"))
finally:
if os.path.exists(temp_docx_path): os.remove(temp_docx_path)
if __name__ == "__main__":
# 检查依赖是否成功导入
try:
import win32com.client
import pythoncom
except ImportError:
root = tk.Tk()
root.withdraw() # 隐藏主窗口
messagebox.showerror("缺少关键依赖", "启动失败!\n\n缺少 pywin32 库。\n请先在命令行运行: pip install pywin32")
else:
root = tk.Tk()
app = LabelGeneratorApp(root)
root.mainloop()