import os import comtypes.client import random import datetime from PyQt5.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton, QFrame, QListWidget, QListWidgetItem, QFileDialog, QProgressBar, QMessageBox, QSpacerItem) from PyQt5.QtCore import Qt, QThread, pyqtSignal, QSize, QTimer from PyQt5.QtGui import QPixmap, QIcon, QDragEnterEvent, QDropEvent, QDragLeaveEvent def generate_unique_filename(original_path): """生成唯一的文件名格式:原名称_日期_随机数.pdf""" base_name = os.path.basename(original_path) name_without_ext = os.path.splitext(base_name)[0] # 获取当前日期 current_date = datetime.datetime.now().strftime("%Y%m%d") # 生成4位随机数 random_num = random.randint(1000, 9999) # 组合成新文件名 new_filename = f"{name_without_ext}_{current_date}_{random_num}.pdf" return os.path.join(os.path.dirname(original_path), new_filename) class ConversionThread(QThread): progress_updated = pyqtSignal(int, int, str) # current, total, filename conversion_done = pyqtSignal(str, bool, str) # filename, success, message finished_all = pyqtSignal() def __init__(self, files): super().__init__() self.files = files def run(self): word = None try: word = comtypes.client.CreateObject('Word.Application') word.Visible = False total = len(self.files) for i, file_path in enumerate(self.files): self.progress_updated.emit(i + 1, total, os.path.basename(file_path)) try: # 使用新的文件名生成逻辑 pdf_path = generate_unique_filename(file_path) doc = word.Documents.Open(os.path.abspath(file_path)) doc.SaveAs(pdf_path, FileFormat=17) # 17 = PDF doc.Close() self.conversion_done.emit(file_path, True, pdf_path) # 传递新文件路径 except Exception as e: self.conversion_done.emit(file_path, False, str(e)) self.finished_all.emit() finally: if word: word.Quit() class WordToPDFPage(QWidget): def __init__(self): super().__init__() self.files = [] self.converted_files = {} self.conversion_thread = None self.init_ui() self.setStyleSheet(self.get_page_style()) def init_ui(self): self.setObjectName("wordToPdfPage") self.setAcceptDrops(True) layout = QHBoxLayout() layout.setContentsMargins(20, 20, 20, 20) layout.setSpacing(20) # 左侧面板 - 文件上传 left_panel = QWidget() left_layout = QVBoxLayout(left_panel) left_layout.setContentsMargins(0, 0, 0, 0) left_layout.setSpacing(15) # 上传区域(增强拖拽效果) upload_area = QFrame() upload_area.setObjectName("uploadArea") upload_area.setFixedHeight(200) upload_area.setAcceptDrops(True) upload_layout = QVBoxLayout(upload_area) upload_layout.setAlignment(Qt.AlignCenter) upload_icon = QLabel() upload_icon.setPixmap(QPixmap(":/icons/upload.png").scaled(64, 64, Qt.KeepAspectRatio, Qt.SmoothTransformation)) upload_icon.setAlignment(Qt.AlignCenter) self.upload_text = QLabel("拖拽文件或文件夹到此处\n或点击上传") self.upload_text.setAlignment(Qt.AlignCenter) self.upload_text.setWordWrap(True) upload_btn = QPushButton("选择文件或文件夹") upload_btn.setObjectName("uploadBtn") upload_btn.clicked.connect(self.handle_upload) upload_layout.addWidget(upload_icon) upload_layout.addWidget(self.upload_text) upload_layout.addWidget(upload_btn) # 文件列表 file_list_frame = QFrame() file_list_frame.setObjectName("fileListFrame") file_list_layout = QVBoxLayout(file_list_frame) file_list_layout.setContentsMargins(0, 0, 0, 0) file_list_layout.setSpacing(5) file_title = QLabel("待转换文件") file_title.setObjectName("sectionTitle") self.file_list = QListWidget() self.file_list.setObjectName("fileList") self.file_list.setSelectionMode(QListWidget.ExtendedSelection) file_list_layout.addWidget(file_title) file_list_layout.addWidget(self.file_list) # 操作按钮 btn_layout = QHBoxLayout() btn_layout.setSpacing(10) self.convert_btn = QPushButton("开始转换") self.convert_btn.setObjectName("convertBtn") self.convert_btn.clicked.connect(self.start_conversion) delete_btn = QPushButton("删除选中") delete_btn.setObjectName("deleteBtn") delete_btn.clicked.connect(self.delete_selected) btn_layout.addWidget(self.convert_btn) btn_layout.addWidget(delete_btn) # 组装左侧面板 left_layout.addWidget(upload_area) left_layout.addWidget(file_list_frame) left_layout.addLayout(btn_layout) # 右侧面板 - 转换结果 right_panel = QWidget() right_layout = QVBoxLayout(right_panel) right_layout.setContentsMargins(0, 0, 0, 0) right_layout.setSpacing(15) # 进度区域 progress_frame = QFrame() progress_frame.setObjectName("progressFrame") progress_layout = QVBoxLayout(progress_frame) progress_layout.setContentsMargins(15, 15, 15, 15) progress_layout.setSpacing(10) progress_title = QLabel("转换进度") progress_title.setObjectName("sectionTitle") # 总体进度 total_layout = QHBoxLayout() total_layout.setSpacing(10) total_label = QLabel("总体进度:") total_label.setObjectName("progressLabel") self.total_progress = QProgressBar() self.total_progress.setObjectName("totalProgress") self.total_progress.setTextVisible(False) total_layout.addWidget(total_label) total_layout.addWidget(self.total_progress) # 文件进度 file_layout = QHBoxLayout() file_layout.setSpacing(10) file_label = QLabel("当前文件:") file_label.setObjectName("progressLabel") self.file_progress = QProgressBar() self.file_progress.setObjectName("fileProgress") self.file_progress.setTextVisible(False) file_layout.addWidget(file_label) file_layout.addWidget(self.file_progress) # 状态标签 self.status_label = QLabel("就绪") self.status_label.setObjectName("statusLabel") self.status_label.setAlignment(Qt.AlignCenter) progress_layout.addWidget(progress_title) progress_layout.addLayout(total_layout) progress_layout.addLayout(file_layout) progress_layout.addWidget(self.status_label) # 结果列表 result_frame = QFrame() result_frame.setObjectName("resultFrame") result_layout = QVBoxLayout(result_frame) result_layout.setContentsMargins(0, 0, 0, 0) result_layout.setSpacing(5) result_title = QLabel("转换结果") result_title.setObjectName("sectionTitle") self.result_list = QListWidget() self.result_list.setObjectName("resultList") self.result_list.setSelectionMode(QListWidget.ExtendedSelection) result_layout.addWidget(result_title) result_layout.addWidget(self.result_list) # 下载按钮 self.download_btn = QPushButton("全部下载") self.download_btn.setObjectName("downloadBtn") self.download_btn.setEnabled(False) self.download_btn.clicked.connect(self.download_all) # 日志区域 log_frame = QFrame() log_frame.setObjectName("logFrame") log_layout = QVBoxLayout(log_frame) log_layout.setContentsMargins(0, 0, 0, 0) log_layout.setSpacing(5) log_title = QLabel("转换日志") log_title.setObjectName("sectionTitle") self.log_content = QListWidget() self.log_content.setObjectName("logContent") self.log_content.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOn) log_layout.addWidget(log_title) log_layout.addWidget(self.log_content) # 组装右侧面板 right_layout.addWidget(progress_frame) right_layout.addWidget(result_frame) right_layout.addWidget(self.download_btn) right_layout.addWidget(log_frame) # 主布局 layout.addWidget(left_panel) layout.addWidget(right_panel) self.setLayout(layout) def get_page_style(self): return """ /* ===== Word转PDF页面样式 ===== */ #wordToPdfPage { background-color: #1a202c; } /* 卡片样式 */ #uploadArea, #fileListFrame, #progressFrame, #resultFrame, #logFrame { background-color: #2d3748; border: 1px solid #4a5568; border-radius: 8px; padding: 15px; } /* 上传区域 */ #uploadArea { border: 2px dashed #4a5568; background-color: rgba(45, 55, 72, 0.5); transition: all 0.3s ease; } #uploadArea:hover { border-color: #818cf8; background-color: rgba(129, 140, 248, 0.1); } #uploadArea[dragActive="true"] { border-color: #4ade80; background-color: rgba(74, 222, 128, 0.1); } /* 标题样式 */ #sectionTitle { font-size: 16px; font-weight: 600; color: #e2e8f0; margin-bottom: 10px; } /* 文件列表 */ #fileList, #resultList, #logContent { background-color: rgba(26, 32, 44, 0.3); border: 1px solid #4a5568; border-radius: 6px; min-height: 200px; max-height: 250px; font-size: 13px; } #fileList::item, #resultList::item, #logContent::item { padding: 8px 12px; border-bottom: 1px solid rgba(74, 85, 104, 0.5); } #fileList::item:selected, #resultList::item:selected, #logContent::item:selected { background-color: rgba(129, 140, 248, 0.15); } /* 进度条 */ #totalProgress, #fileProgress { height: 8px; border-radius: 4px; background-color: #2d3748; } #totalProgress::chunk { background-color: #818cf8; border-radius: 4px; } #fileProgress::chunk { background-color: #4ade80; border-radius: 4px; } #progressLabel { color: #a0aec0; font-size: 13px; min-width: 70px; } #statusLabel { font-size: 14px; color: #e2e8f0; margin-top: 10px; padding: 8px; border-radius: 6px; background-color: rgba(26, 32, 44, 0.3); } /* 按钮样式 */ #uploadBtn, #convertBtn, #deleteBtn, #downloadBtn { min-height: 40px; font-weight: 500; } #uploadBtn { min-width: 180px; } #convertBtn { background-color: #4f46e5; } #convertBtn:disabled { background-color: #4a5568; } #downloadBtn { background-color: #10b981; } #downloadBtn:hover { background-color: #059669; } #deleteBtn { background-color: #ef4444; } #deleteBtn:hover { background-color: #dc2626; } /* 日志样式 */ #logContent::item[type="info"] { color: #a0aec0; } #logContent::item[type="success"] { color: #34d399; } #logContent::item[type="warning"] { color: #fbbf24; } #logContent::item[type="error"] { color: #f87171; } """ def dragEnterEvent(self, event: QDragEnterEvent): if event.mimeData().hasUrls(): event.acceptProposedAction() # 高亮显示拖拽区域 self.findChild(QFrame, "uploadArea").setProperty("dragActive", True) self.findChild(QFrame, "uploadArea").style().polish(self.findChild(QFrame, "uploadArea")) self.upload_text.setText("释放文件以添加") def dragLeaveEvent(self, event: QDragLeaveEvent): # 恢复拖拽区域样式 self.findChild(QFrame, "uploadArea").setProperty("dragActive", False) self.findChild(QFrame, "uploadArea").style().polish(self.findChild(QFrame, "uploadArea")) self.upload_text.setText("拖拽文件或文件夹到此处\n或点击上传") def dropEvent(self, event: QDropEvent): # 恢复拖拽区域样式 self.findChild(QFrame, "uploadArea").setProperty("dragActive", False) self.findChild(QFrame, "uploadArea").style().polish(self.findChild(QFrame, "uploadArea")) self.upload_text.setText("拖拽文件或文件夹到此处\n或点击上传") urls = event.mimeData().urls() files = [] added_count = 0 for url in urls: path = url.toLocalFile() if os.path.isdir(path): # 处理文件夹 for root, _, filenames in os.walk(path): for f in filenames: if f.lower().endswith(('.doc', '.docx')): files.append(os.path.join(root, f)) elif os.path.isfile(path) and path.lower().endswith(('.doc', '.docx')): files.append(path) if files: added_count = self.add_files(files) self.add_log(f"添加了 {added_count} 个文件", "success") else: self.add_log("未找到有效的Word文件", "warning") def handle_upload(self): """处理文件或文件夹上传""" options = QFileDialog.Options() files, _ = QFileDialog.getOpenFileNames( self, "选择Word文件", "", "Word Files (*.doc *.docx);;All Files (*)", options=options ) if files: added_count = self.add_files(files) self.add_log(f"添加了 {added_count} 个文件", "success") else: folder = QFileDialog.getExistingDirectory(self, "选择包含Word文件的文件夹") if folder: files = [] for root, _, filenames in os.walk(folder): for f in filenames: if f.lower().endswith(('.doc', '.docx')): files.append(os.path.join(root, f)) if files: added_count = self.add_files(files) self.add_log(f"从文件夹添加了 {added_count} 个文件", "success") else: self.add_log("文件夹中没有找到Word文件", "warning") def add_files(self, files): """添加文件到列表,避免重复""" added_count = 0 for file in files: if file not in self.files and os.path.exists(file): self.files.append(file) item = QListWidgetItem(os.path.basename(file)) item.setData(Qt.UserRole, file) self.file_list.addItem(item) added_count += 1 return added_count def delete_selected(self): """删除选中的文件""" selected_items = self.file_list.selectedItems() if not selected_items: self.add_log("请先选择要删除的文件", "warning") return for item in selected_items: self.files.remove(item.data(Qt.UserRole)) self.file_list.takeItem(self.file_list.row(item)) self.add_log(f"删除了 {len(selected_items)} 个文件", "info") def start_conversion(self): """开始转换文件""" if not self.files: self.add_log("请先添加要转换的文件!", "warning") return # 重置状态 self.converted_files = {} self.result_list.clear() self.log_content.clear() self.download_btn.setEnabled(False) self.convert_btn.setEnabled(False) # 重置进度条 self.total_progress.setValue(0) self.file_progress.setValue(0) self.status_label.setText("正在准备转换...") # 添加开始日志 self.add_log("开始转换任务", "info") self.add_log(f"共 {len(self.files)} 个文件待处理", "info") # 创建并启动转换线程 self.conversion_thread = ConversionThread(self.files.copy()) self.conversion_thread.progress_updated.connect(self.update_progress) self.conversion_thread.conversion_done.connect(self.handle_conversion_done) self.conversion_thread.finished_all.connect(self.handle_finished_all) self.conversion_thread.start() def update_progress(self, current, total, filename): """更新进度条""" self.total_progress.setMaximum(total) self.total_progress.setValue(current) # 重置文件进度为0 self.file_progress.setValue(0) self.file_progress.setMaximum(100) # 更新状态 self.status_label.setText(f"正在转换: {os.path.basename(filename)}") self.add_log(f"开始转换: {os.path.basename(filename)} ({current}/{total})", "info") # 模拟文件进度动画 self.animate_file_progress() def animate_file_progress(self): """模拟文件转换进度动画(实际应用中应由实际进度驱动)""" self.file_progress_value = 0 def update_progress(): if self.file_progress_value < 100: self.file_progress_value += 2 self.file_progress.setValue(self.file_progress_value) QTimer.singleShot(50, update_progress) update_progress() def handle_conversion_done(self, file_path, success, message): """处理单个文件转换完成""" basename = os.path.basename(file_path) if success: # 成功时,message包含新的PDF路径 pdf_path = message self.converted_files[file_path] = pdf_path # 从待转换列表移除 for i in range(self.file_list.count()): item = self.file_list.item(i) if item.data(Qt.UserRole) == file_path: self.file_list.takeItem(i) break # 添加到结果列表 item = QListWidgetItem(f"{basename} → {os.path.basename(pdf_path)}") item.setData(Qt.UserRole, pdf_path) self.result_list.addItem(item) self.add_log(f"✅ 转换成功: {basename} -> {os.path.basename(pdf_path)}", "success") else: # 失败时显示错误信息 self.add_log(f"❌ 转换失败: {basename} - {message}", "error") def handle_finished_all(self): """所有文件转换完成""" success_count = len(self.converted_files) fail_count = len(self.files) - success_count # 更新状态 self.status_label.setText(f"转换完成! 成功: {success_count}, 失败: {fail_count}") self.total_progress.setValue(self.total_progress.maximum()) self.file_progress.setValue(100) self.convert_btn.setEnabled(True) self.download_btn.setEnabled(bool(self.converted_files)) # 添加完成日志 if success_count: self.add_log(f"🎉 全部转换完成! 成功转换 {success_count} 个文件", "success") if fail_count: self.add_log(f"⚠️ 有 {fail_count} 个文件转换失败", "warning") if self.converted_files: self.add_log("点击'全部下载'按钮保存转换后的文件", "info") def download_all(self): """下载所有转换后的文件""" if not self.converted_files: self.add_log("没有可下载的文件", "warning") return folder = QFileDialog.getExistingDirectory(self, "选择保存位置") if not folder: self.add_log("下载已取消", "info") return success = 0 errors = [] # 开始下载 self.status_label.setText("正在下载文件...") self.add_log(f"开始下载 {len(self.converted_files)} 个文件到 {folder}", "info") for pdf_path in self.converted_files.values(): try: file_name = os.path.basename(pdf_path) destination = os.path.join(folder, file_name) # 复制文件 with open(pdf_path, 'rb') as src_file: with open(destination, 'wb') as dest_file: dest_file.write(src_file.read()) success += 1 self.add_log(f"已下载: {file_name}", "success") except Exception as e: errors.append(f"{os.path.basename(pdf_path)}: {str(e)}") self.add_log(f"下载失败: {os.path.basename(pdf_path)} - {str(e)}", "error") # 更新状态 self.status_label.setText(f"下载完成! 成功: {success}, 失败: {len(errors)}") # 显示结果 if errors: self.add_log(f"⚠️ 部分文件下载失败 ({len(errors)} 个)", "warning") else: self.add_log(f"✅ 全部文件下载成功!", "success") self.add_log(f"文件已保存到: {folder}", "info") def add_log(self, message, log_type="info"): """添加日志项""" item = QListWidgetItem(f"[{datetime.datetime.now().strftime('%H:%M:%S')}] {message}") item.setData(Qt.UserRole + 1, log_type) # 存储日志类型用于样式 self.log_content.addItem(item) self.log_content.scrollToBottom() def paintEvent(self, event): """为拖拽区域添加悬停效果""" if self.underMouse(): self.findChild(QFrame, "uploadArea").setProperty("hover", True) self.findChild(QFrame, "uploadArea").style().polish(self.findChild(QFrame, "uploadArea")) else: self.findChild(QFrame, "uploadArea").setProperty("hover", False) self.findChild(QFrame, "uploadArea").style().polish(self.findChild(QFrame, "uploadArea")) super().paintEvent(event)