# -*- coding: utf-8 -*-import sysfrom pathlib import Pathfrom datetime import datetimefrom typing import List, Dict, Anyfrom PyQt6.QtWidgets import ( QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QPushButton, QLabel, QLineEdit, QTextEdit, QFileDialog, QGroupBox, QGridLayout, QCheckBox, QSpinBox, QDoubleSpinBox, QProgressBar, QTableWidget, QTableWidgetItem, QHeaderView, QTabWidget, QSplitter, QMessageBox, QFrame)from PyQt6.QtCore import Qt, QThread, pyqtSignalfrom PyQt6.QtGui import QFont, QTextCursor, QColorclass WorkerThread(QThread): """模拟后台工作线程""" log_signal = pyqtSignal(str) progress_signal = pyqtSignal(int, int) finished_signal = pyqtSignal(list) error_signal = pyqtSignal(str) def __init__(self, config: Dict[str, Any]): super().__init__() self.config = config self.is_running = True def run(self): """模拟执行处理任务""" try: self.log_signal.emit("[INFO] 开始模拟处理...") # 模拟获取文件列表 source_dir = Path(self.config.get('source_dir', '')) if source_dir.exists(): dwg_files = [f for f in source_dir.iterdir() if f.suffix.lower() == '.dwg'] total = len(dwg_files) self.log_signal.emit(f"[INFO] 找到 {total} 个DWG文件") else: total = 5 self.log_signal.emit("[WARN] 源文件夹不存在,使用模拟数据") # 模拟处理过程 result_rows = [] for i in range(total): if not self.is_running: self.log_signal.emit("[INFO] 用户停止处理") break # 模拟处理进度 self.progress_signal.emit(i + 1, total) self.log_signal.emit(f"[PROCESS] 正在处理文件 {i+1}/{total}...") # 模拟处理结果 result_rows.append({ "文件名": f"图纸_{i+1:03d}.dwg", "处理方式": "模拟模式", "结果": "成功" if i % 3 != 2 else "失败", "替换图框数": i + 1, "输出文件": f"C:/Output/图纸_{i+1:03d}.dwg", "说明": "模拟处理" if i % 3 != 2 else "模拟失败" }) # 模拟处理耗时 self.msleep(500) self.log_signal.emit("[INFO] 模拟处理完成") self.finished_signal.emit(result_rows) except Exception as e: self.error_signal.emit(str(e)) def stop(self): """停止处理""" self.is_running = Falseclass CADBatchReplaceGUI(QMainWindow): """主窗口""" def __init__(self): super().__init__() self.worker = None self.result_rows = [] self.init_ui() def init_ui(self): """初始化界面""" self.setWindowTitle("CAD图块批量替换工具") self.setGeometry(100, 100, 1200, 800) # 中央部件 central_widget = QWidget() self.setCentralWidget(central_widget) main_layout = QVBoxLayout(central_widget) # 创建分割器 splitter = QSplitter(Qt.Orientation.Vertical) main_layout.addWidget(splitter) # 上部:配置区域 config_widget = QWidget() config_layout = QVBoxLayout(config_widget) # === 配置分组 === config_group = QGroupBox("📐 配置参数") config_grid = QGridLayout(config_group) # 第0行:源文件夹 config_grid.addWidget(QLabel("源文件夹:"), 0, 0) self.source_dir_edit = QLineEdit(r"C:\Temp\CAD_Source") config_grid.addWidget(self.source_dir_edit, 0, 1, 1, 2) btn_source = QPushButton("浏览...") btn_source.clicked.connect(self.browse_source_dir) config_grid.addWidget(btn_source, 0, 3) # 第1行:输出文件夹 config_grid.addWidget(QLabel("输出文件夹:"), 1, 0) self.output_dir_edit = QLineEdit(r"C:\Temp\CAD_Output") config_grid.addWidget(self.output_dir_edit, 1, 1, 1, 2) btn_output = QPushButton("浏览...") btn_output.clicked.connect(self.browse_output_dir) config_grid.addWidget(btn_output, 1, 3) # 第2行:A3图框文件 config_grid.addWidget(QLabel("A3图框文件:"), 2, 0) self.a3_path_edit = QLineEdit(r"C:\Temp\A3.dwg") config_grid.addWidget(self.a3_path_edit, 2, 1, 1, 2) btn_a3 = QPushButton("浏览...") btn_a3.clicked.connect(self.browse_a3_file) config_grid.addWidget(btn_a3, 2, 3) # 第3行:图块名称 config_grid.addWidget(QLabel("旧图块名称:"), 3, 0) self.old_block_edit = QLineEdit("A$C23BC78A5") config_grid.addWidget(self.old_block_edit, 3, 1) config_grid.addWidget(QLabel("新图块名称:"), 3, 2) self.new_block_edit = QLineEdit("A3") config_grid.addWidget(self.new_block_edit, 3, 3) # 第4行:缩放比例 config_grid.addWidget(QLabel("缩放比例:"), 4, 0) self.scale_spin = QDoubleSpinBox() self.scale_spin.setRange(0.01, 10.0) self.scale_spin.setSingleStep(0.1) self.scale_spin.setValue(0.5) config_grid.addWidget(self.scale_spin, 4, 1) # 第4行:并行数 config_grid.addWidget(QLabel("并行处理数:"), 4, 2) self.workers_spin = QSpinBox() self.workers_spin.setRange(1, 16) self.workers_spin.setValue(4) config_grid.addWidget(self.workers_spin, 4, 3) # 第5行:超时时间 config_grid.addWidget(QLabel("超时时间(秒):"), 5, 0) self.timeout_spin = QSpinBox() self.timeout_spin.setRange(30, 600) self.timeout_spin.setValue(180) config_grid.addWidget(self.timeout_spin, 5, 1) # 第5行:选项 self.overwrite_check = QCheckBox("覆盖已存在的输出文件") self.overwrite_check.setChecked(True) config_grid.addWidget(self.overwrite_check, 5, 2) self.core_console_check = QCheckBox("强制使用Core Console模式") self.core_console_check.setChecked(True) config_grid.addWidget(self.core_console_check, 5, 3) # 第6行:Core Console路径 config_grid.addWidget(QLabel("Core Console路径:"), 6, 0) self.accore_path_edit = QLineEdit(r"F:\Auto CAD\AutoCAD 2016\accoreconsole.exe") config_grid.addWidget(self.accore_path_edit, 6, 1, 1, 2) btn_accore = QPushButton("浏览...") btn_accore.clicked.connect(self.browse_accore_file) config_grid.addWidget(btn_accore, 6, 3) config_layout.addWidget(config_group) # === 控制按钮 === btn_layout = QHBoxLayout() self.btn_start = QPushButton("▶ 开始处理") self.btn_start.setStyleSheet(""" QPushButton { font-weight: bold; font-size: 14px; padding: 8px 20px; background-color: #4CAF50; color: white; border-radius: 5px; } QPushButton:hover { background-color: #45a049; } QPushButton:disabled { background-color: #cccccc; color: #666666; } """) self.btn_start.clicked.connect(self.start_processing) btn_layout.addWidget(self.btn_start) self.btn_stop = QPushButton("⏹ 停止") self.btn_stop.setStyleSheet(""" QPushButton { font-weight: bold; font-size: 14px; padding: 8px 20px; background-color: #f44336; color: white; border-radius: 5px; } QPushButton:hover { background-color: #d32f2f; } QPushButton:disabled { background-color: #cccccc; color: #666666; } """) self.btn_stop.setEnabled(False) self.btn_stop.clicked.connect(self.stop_processing) btn_layout.addWidget(self.btn_stop) btn_layout.addStretch() self.btn_clear_log = QPushButton("🗑 清空日志") self.btn_clear_log.clicked.connect(self.clear_log) btn_layout.addWidget(self.btn_clear_log) self.btn_export = QPushButton("📊 导出结果") self.btn_export.clicked.connect(self.export_results) btn_layout.addWidget(self.btn_export) config_layout.addLayout(btn_layout) # === 进度条 === self.progress_bar = QProgressBar() self.progress_bar.setVisible(False) self.progress_bar.setStyleSheet(""" QProgressBar { border: 1px solid #ccc; border-radius: 5px; text-align: center; height: 25px; } QProgressBar::chunk { background-color: #4CAF50; border-radius: 5px; } """) config_layout.addWidget(self.progress_bar) splitter.addWidget(config_widget) # === 底部:日志和结果 === bottom_widget = QWidget() bottom_layout = QVBoxLayout(bottom_widget) tab_widget = QTabWidget() tab_widget.setStyleSheet(""" QTabWidget::pane { border: 1px solid #ccc; border-radius: 5px; } QTabBar::tab { padding: 8px 16px; margin-right: 2px; } QTabBar::tab:selected { background-color: #e0e0e0; font-weight: bold; } """) # === 日志标签页 === log_widget = QWidget() log_layout = QVBoxLayout(log_widget) self.log_text = QTextEdit() self.log_text.setFont(QFont("Consolas", 10)) self.log_text.setReadOnly(True) self.log_text.setStyleSheet(""" QTextEdit { background-color: #1e1e1e; color: #d4d4d4; border: none; } """) log_layout.addWidget(self.log_text) tab_widget.addTab(log_widget, "📋 运行日志") # === 结果标签页 === result_widget = QWidget() result_layout = QVBoxLayout(result_widget) self.result_table = QTableWidget() self.result_table.setColumnCount(6) self.result_table.setHorizontalHeaderLabels(["文件名", "处理方式", "结果", "替换图框数", "输出文件", "说明"]) self.result_table.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeMode.ResizeToContents) self.result_table.horizontalHeader().setSectionResizeMode(0, QHeaderView.ResizeMode.Stretch) self.result_table.horizontalHeader().setSectionResizeMode(4, QHeaderView.ResizeMode.Stretch) self.result_table.setAlternatingRowColors(True) self.result_table.setStyleSheet(""" QTableWidget { gridline-color: #d0d0d0; } QTableWidget::item:selected { background-color: #4CAF50; color: white; } """) result_layout.addWidget(self.result_table) tab_widget.addTab(result_widget, "📊 处理结果") bottom_layout.addWidget(tab_widget) splitter.addWidget(bottom_widget) # 设置分割比例 splitter.setSizes([400, 400]) # === 初始日志 === self.log("=" * 70) self.log(" CAD图块批量替换工具 v2.0") self.log(" 界面加载完成,等待用户操作") self.log("=" * 70) self.log("📁 工作目录: " + str(Path(__file__).resolve().parent)) self.log("💡 提示: 点击「开始处理」执行模拟任务") self.log("=" * 70) # ==================== 日志方法 ==================== def log(self, msg: str): """添加日志""" timestamp = datetime.now().strftime("%H:%M:%S") self.log_text.append(f"[{timestamp}] {msg}") self.log_text.moveCursor(QTextCursor.MoveOperation.End) # ==================== 浏览方法 ==================== def browse_source_dir(self): """浏览源文件夹""" dir_path = QFileDialog.getExistingDirectory( self, "选择源文件夹", self.source_dir_edit.text() ) if dir_path: self.source_dir_edit.setText(dir_path) def browse_output_dir(self): """浏览输出文件夹""" dir_path = QFileDialog.getExistingDirectory( self, "选择输出文件夹", self.output_dir_edit.text() ) if dir_path: self.output_dir_edit.setText(dir_path) def browse_a3_file(self): """浏览A3图框文件""" file_path, _ = QFileDialog.getOpenFileName( self, "选择A3图框文件", self.a3_path_edit.text(), "DWG Files (*.dwg)" ) if file_path: self.a3_path_edit.setText(file_path) def browse_accore_file(self): """浏览accoreconsole.exe""" file_path, _ = QFileDialog.getOpenFileName( self, "选择accoreconsole.exe", "", "Executable Files (*.exe)" ) if file_path: self.accore_path_edit.setText(file_path) # ==================== 处理方法 ==================== def start_processing(self): """开始处理""" # 收集配置 config = { 'source_dir': self.source_dir_edit.text(), 'output_dir': self.output_dir_edit.text(), 'a3_path': self.a3_path_edit.text(), 'old_block': self.old_block_edit.text(), 'new_block': self.new_block_edit.text(), 'scale_ratio': self.scale_spin.value(), 'overwrite': self.overwrite_check.isChecked(), 'force_core_console': self.core_console_check.isChecked(), 'accore_path': self.accore_path_edit.text(), 'workers': self.workers_spin.value(), 'timeout': self.timeout_spin.value(), } # 清空旧结果 self.result_table.setRowCount(0) self.result_rows = [] # 禁用按钮 self.btn_start.setEnabled(False) self.btn_stop.setEnabled(True) self.progress_bar.setVisible(True) self.progress_bar.setValue(0) self.log("\n" + "=" * 70) self.log("▶ 开始执行处理任务") self.log(f"📂 源文件夹: {config['source_dir']}") self.log(f"📂 输出文件夹: {config['output_dir']}") self.log(f"🔄 替换: {config['old_block']} → {config['new_block']}") self.log("=" * 70) # 启动工作线程 self.worker = WorkerThread(config) self.worker.log_signal.connect(self.log) self.worker.progress_signal.connect(self.update_progress) self.worker.finished_signal.connect(self.on_finished) self.worker.error_signal.connect(self.on_error) self.worker.start() def stop_processing(self): """停止处理""" if self.worker and self.worker.isRunning(): self.worker.stop() self.log("⏹ 用户请求停止处理...") self.btn_stop.setEnabled(False) def update_progress(self, current: int, total: int): """更新进度""" if total > 0: percent = int(current / total * 100) self.progress_bar.setValue(percent) self.progress_bar.setFormat(f"{current}/{total} ({percent}%)") def on_finished(self, result_rows: list): """处理完成""" self.result_rows = result_rows self.update_result_table(result_rows) # 统计信息 success = sum(1 for row in result_rows if row["结果"] == "成功") fail = sum(1 for row in result_rows if row["结果"] == "失败") skipped = sum(1 for row in result_rows if row["结果"] == "跳过") replaced = sum(int(row["替换图框数"] or 0) for row in result_rows) self.log("\n" + "=" * 70) self.log("✅ 处理完成!") self.log(f"📊 统计: 成功 {success} | 失败 {fail} | 跳过 {skipped}") self.log(f"🔄 替换图框总数: {replaced}") self.log("=" * 70) # 恢复按钮 self.btn_start.setEnabled(True) self.btn_stop.setEnabled(False) self.progress_bar.setVisible(False) # 显示提示 QMessageBox.information( self, "处理完成", f"✅ 处理完成!\n\n" f"📊 成功: {success}\n" f"❌ 失败: {fail}\n" f"⏭ 跳过: {skipped}\n" f"🔄 替换图框: {replaced}" ) def on_error(self, error_msg: str): """错误处理""" self.log(f"❌ [ERROR] {error_msg}") QMessageBox.critical(self, "错误", f"处理过程中发生错误:\n\n{error_msg}") self.btn_start.setEnabled(True) self.btn_stop.setEnabled(False) self.progress_bar.setVisible(False) # ==================== 表格方法 ==================== def update_result_table(self, rows: list): """更新结果表格""" self.result_table.setRowCount(len(rows)) for i, row in enumerate(rows): # 文件名 self.result_table.setItem(i, 0, QTableWidgetItem(row["文件名"])) # 处理方式 self.result_table.setItem(i, 1, QTableWidgetItem(row["处理方式"])) # 结果(带颜色) status_item = QTableWidgetItem(row["结果"]) if row["结果"] == "成功": status_item.setBackground(QColor(200, 255, 200)) status_item.setForeground(QColor(0, 128, 0)) elif row["结果"] == "失败": status_item.setBackground(QColor(255, 200, 200)) status_item.setForeground(QColor(255, 0, 0)) else: status_item.setBackground(QColor(255, 255, 200)) status_item.setForeground(QColor(128, 128, 0)) self.result_table.setItem(i, 2, status_item) # 替换图框数 self.result_table.setItem(i, 3, QTableWidgetItem(str(row["替换图框数"]))) # 输出文件 output_item = QTableWidgetItem(str(row["输出文件"])) output_item.setToolTip(str(row["输出文件"])) self.result_table.setItem(i, 4, output_item) # 说明 self.result_table.setItem(i, 5, QTableWidgetItem(row["说明"])) # 调整列宽 self.result_table.resizeColumnsToContents() # ==================== 其他方法 ==================== def clear_log(self): """清空日志""" self.log_text.clear() self.log("🗑 日志已清空") def export_results(self): """导出结果""" if not self.result_rows: QMessageBox.information(self, "提示", "没有结果可导出") return file_path, _ = QFileDialog.getSaveFileName( self, "导出结果", f"处理结果_{datetime.now().strftime('%Y%m%d_%H%M%S')}.csv", "CSV Files (*.csv)" ) if file_path: try: import csv with open(file_path, 'w', newline='', encoding='utf-8-sig') as f: writer = csv.DictWriter(f, fieldnames=self.result_rows[0].keys()) writer.writeheader() writer.writerows(self.result_rows) QMessageBox.information(self, "成功", f"✅ 结果已导出到:\n{file_path}") self.log(f"📊 结果已导出: {file_path}") except Exception as e: QMessageBox.critical(self, "错误", f"导出失败:\n{e}") self.log(f"❌ 导出失败: {e}")def main(): """主函数""" app = QApplication(sys.argv) app.setStyle('Fusion') # 设置应用图标(可选) app.setApplicationName("CAD图块批量替换工具") app.setApplicationDisplayName("CAD图块批量替换工具") window = CADBatchReplaceGUI() window.show() sys.exit(app.exec())if __name__ == "__main__": main()