import sys import os from PyQt6.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QLabel, QLineEdit, QPushButton, QProgressBar, QTextEdit, QFileDialog, QMessageBox, QFrame, QStyle, QGraphicsDropShadowEffect, QListWidget, QAbstractItemView, QSizePolicy, QDialog) from PyQt6.QtCore import Qt, QThread, pyqtSignal, QObject, QPoint from PyQt6.QtGui import QIcon, QFont, QAction, QColor, QPixmap def resource_path(relative_path): """ Get absolute path to resource, works for dev and for PyInstaller """ try: # PyInstaller creates a temp folder and stores path in _MEIPASS base_path = sys._MEIPASS except Exception: base_path = os.path.dirname(os.path.abspath(__file__)) return os.path.join(base_path, relative_path) class ScanWorker(QObject): finished = pyqtSignal(bool, str) progress = pyqtSignal(int, str) log = pyqtSignal(str) def __init__(self, target_paths, output_path): super().__init__() self.target_paths = target_paths # 这是一个列表 self.output_path = output_path self.scanned_count = 0 self.is_running = True self.last_emit_time = 0 # For throttling progress signals def run(self): # 延迟导入,优化启动速度 import concurrent.futures import time try: # 修正:信号发射使用 emit() 方法 self.log.emit(f"启动多线程扫描 (线程数: {min(32, (os.cpu_count() or 1) * 4)})...") with concurrent.futures.ThreadPoolExecutor(max_workers=min(32, (os.cpu_count() or 1) * 4)) as executor: if len(self.target_paths) == 1: # 单目录模式:直接扫描,不使用虚拟根节点 path = self.target_paths[0] self.log.emit(f"正在扫描: {path}") root_tree = self.build_tree(path, executor=executor) display_path = path else: # 多目录模式:使用虚拟根节点 root_tree = { "name": "扫描汇总", "path": "Multiple Roots", "type": "directory", "children": [], "size": 0, "file_count": 0, "dir_count": 0 } for path in self.target_paths: if not self.is_running: break self.log.emit(f"正在扫描: {path}") tree_data = self.build_tree(path, executor=executor) # 标记需要保留绝对路径,防止前端路径拼接错误 tree_data["_keep_path"] = True self._merge_child_stats(root_tree, tree_data) display_path = "Multi-Scan: " + "; ".join(self.target_paths) if not self.is_running: return self.log.emit(f"扫描完成,正在生成 HTML 报告...") self.generate_html(root_tree, display_path, self.output_path) self.finished.emit(True, self.output_path) except Exception as e: self.finished.emit(False, str(e)) def build_tree(self, path, executor=None, depth=0): if not self.is_running: return {} # Lazy import time inside method to avoid top-level import if not needed elsewhere? # Actually it's better to import at top of file or in run(), but build_tree is recursive. # We'll use the one imported in run() or standard library time if available globally. # But wait, run() imports are local. Let's rely on standard 'import time' being fast enough to put at top of file, # OR just use 'import time' inside build_tree or rely on it being available. # To be safe and clean, I will add 'import time' to top of file in next step or use it here. # For now, let's assume I can add it to top of file or use local import. import time name = os.path.basename(path) if name == "": name = path node = { "name": name, "path": path, "type": "directory", "children": [], "size": 0, "file_count": 0, "dir_count": 0 } try: with os.scandir(path) as it: entries = list(it) entries.sort(key=lambda x: (not x.is_dir(), x.name.lower())) futures = [] for entry in entries: if not self.is_running: break self.scanned_count += 1 # Throttle progress emission: emit every 100ms or every 1000 items current_time = time.time() if current_time - self.last_emit_time > 0.1: self.progress.emit(self.scanned_count, entry.path) self.last_emit_time = current_time if entry.is_dir(): if executor and depth < 2: future = executor.submit(self.build_tree, entry.path, executor, depth + 1) futures.append(future) else: child_node = self.build_tree(entry.path, executor, depth + 1) self._merge_child_stats(node, child_node) else: try: stat = entry.stat() size = stat.st_size except OSError: size = 0 node["children"].append({ "name": entry.name, "path": entry.path, "type": "file", "size": size }) node["size"] += size node["file_count"] += 1 for future in futures: if not self.is_running: break try: child_node = future.result() self._merge_child_stats(node, child_node) except Exception as e: # 修正:信号发射使用 emit() 方法 self.log.emit(f"线程执行错误: {e}") except PermissionError: self.log.emit(f"权限被拒绝: {path}") node["error"] = "Permission Denied" except Exception as e: self.log.emit(f"读取错误 {path}: {e}") node["error"] = str(e) return node def _merge_child_stats(self, parent, child): parent["children"].append(child) parent["size"] += child.get("size", 0) parent["file_count"] += child.get("file_count", 0) parent["dir_count"] += child.get("dir_count", 0) + 1 def _format_size(self, size): if size < 1024: return f"{size} B" elif size < 1024 * 1024: return f"{size/1024:.2f} KB" elif size < 1024 * 1024 * 1024: return f"{size/(1024*1024):.2f} MB" else: return f"{size/(1024*1024*1024):.2f} GB" def _get_icon(self, name, is_dir): if is_dir: return "📂" ext = os.path.splitext(name)[1].lower() icons = { # 图片 / 设计 '.jpg': '🖼️', '.jpeg': '🖼️', '.png': '🖼️', '.gif': '🖼️', '.bmp': '🖼️', '.svg': '🖼️', '.ico': '🖼️', '.webp': '🖼️', '.tiff': '🖼️', '.psd': '🎨', '.ai': '🎨', '.raw': '📷', '.heic': '📷', # 视频 '.mp4': '🎬', '.avi': '🎬', '.mkv': '🎬', '.mov': '🎬', '.wmv': '🎬', '.flv': '🎬', '.webm': '🎬', '.m4v': '🎬', '.3gp': '🎬', '.ts': '🎬', # 音频 '.mp3': '🎵', '.wav': '🎵', '.flac': '🎵', '.aac': '🎵', '.ogg': '🎵', '.wma': '🎵', '.m4a': '🎵', '.mid': '🎹', # 代码 - Web '.html': '🌐', '.htm': '🌐', '.css': '🎨', '.scss': '🎨', '.sass': '🎨', '.less': '🎨', '.js': '📜', '.jsx': '⚛️', '.ts': '📘', '.tsx': '⚛️', '.vue': '🟢', '.php': '🐘', '.asp': '🌐', '.aspx': '🌐', '.jsp': '☕', # 代码 - 后端/通用 '.py': '🐍', '.pyw': '🐍', '.pyc': '🐍', '.java': '☕', '.jar': '☕', '.class': '☕', '.c': '🇨', '.cpp': '🇨', '.cxx': '🇨', '.h': '🇨', '.hpp': '🇨', '.cs': '#️⃣', '.go': '🐹', '.rs': '🦀', '.rb': '💎', '.pl': '🐪', '.swift': '🐦', '.kt': '🎯', '.lua': '🌙', '.r': '📈', '.m': '🍎', '.sh': '💻', '.bat': '💻', '.ps1': '💻', '.cmd': '💻', '.vbs': '📜', # 配置文件 / 数据 '.json': '⚙️', '.xml': '⚙️', '.yaml': '⚙️', '.yml': '⚙️', '.toml': '⚙️', '.ini': '⚙️', '.cfg': '⚙️', '.conf': '⚙️', '.env': '🔒', '.gitignore': '🚫', '.dockerfile': '🐳', '.makefile': '🐘', '.sql': '🗄️', '.db': '🗄️', '.sqlite': '🗄️', '.mdb': '🗄️', '.csv': '📊', '.tsv': '📊', # 文档 / 办公 '.pdf': '📕', '.doc': '📘', '.docx': '📘', '.rtf': '📝', '.odt': '📘', '.xls': '📗', '.xlsx': '📗', '.ods': '📗', '.ppt': '📙', '.pptx': '📙', '.odp': '📙', '.txt': '📝', '.md': '📝', '.markdown': '📝', '.log': '📝', '.tex': '📜', '.epub': '📚', # 压缩包 / 镜像 '.zip': '📦', '.rar': '📦', '.7z': '📦', '.tar': '📦', '.gz': '📦', '.bz2': '📦', '.iso': '💿', '.dmg': '💿', '.img': '💿', # 系统 / 执行 '.exe': '🚀', '.msi': '📦', '.dll': '🔧', '.sys': '⚙️', '.apk': '📱', '.ipa': '📱', # 字体 '.ttf': '🔤', '.otf': '🔤', '.woff': '🔤', '.woff2': '🔤', '.eot': '🔤', # 其他 '.bak': '🔙', '.tmp': '⏱️', '.swp': '⏱️', '.lock': '🔒' } return icons.get(ext, "📄") def _optimize_data_for_json(self, node): """ 递归优化数据结构以减小 JSON 体积: 1. 移除 'path' (前端动态计算) 2. 缩短 key 名: name->n, size->s, type->t, children->c, error->e 3. 移除 file_count/dir_count (仅在根节点或按需计算,或者保留但缩短key) """ # Mapping: # n: name # t: type (0: file, 1: directory) - 使用整数进一步减小体积 # s: size # c: children # e: error is_dir = node["type"] == "directory" optimized = { "n": node["name"], "t": 1 if is_dir else 0 } # 显式保留路径(用于多根节点等特殊情况) if node.get("_keep_path"): optimized["p"] = node["path"] if "size" in node and node["size"] > 0: optimized["s"] = node["size"] if "error" in node: optimized["e"] = node["error"] if is_dir and "children" in node: optimized["c"] = [self._optimize_data_for_json(child) for child in node["children"]] return optimized def generate_html(self, tree_data, root_path, output_path): import json import datetime import html # 1. 准备统计数据 total_size = self._format_size(tree_data.get("size", 0)) total_files = tree_data.get("file_count", 0) total_dirs = tree_data.get("dir_count", 0) # 2. 优化树数据结构 (大幅减小体积) self.log.emit(f"正在优化数据结构以减小报告体积...") optimized_tree = self._optimize_data_for_json(tree_data) # 3. 序列化为 JSON self.log.emit(f"正在序列化 JSON 数据...") json_data = json.dumps(optimized_tree, ensure_ascii=False) self.log.emit(f"正在生成 HTML 文件...") html_content = f""" 目录扫描报告 - 结构可视化

正在构建可视化结构...

🗂️ 目录结构报告

{total_dirs}文件夹
{total_files}文件
{total_size}总大小
📍 扫描路径: {html.escape(root_path)} 🕒 {datetime.datetime.now().strftime('%Y-%m-%d %H:%M')}
🔍
    """ with open(output_path, 'w', encoding='utf-8') as f: f.write(html_content) class ModernMessageBox(QDialog): def __init__(self, title, message, parent=None, buttons=None): super().__init__(parent) if buttons is None: buttons = ["确定"] self.setWindowFlags(Qt.WindowType.FramelessWindowHint | Qt.WindowType.Dialog) self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground) self.result_text = None layout = QVBoxLayout(self) layout.setContentsMargins(10, 10, 10, 10) self.container = QWidget() self.container.setStyleSheet(""" QWidget { background-color: white; border-radius: 12px; border: 1px solid #e0e0e0; } """) shadow = QGraphicsDropShadowEffect(self) shadow.setBlurRadius(20) shadow.setXOffset(0) shadow.setYOffset(5) shadow.setColor(QColor(0, 0, 0, 40)) self.container.setGraphicsEffect(shadow) layout.addWidget(self.container) inner_layout = QVBoxLayout(self.container) inner_layout.setContentsMargins(24, 24, 24, 24) inner_layout.setSpacing(20) # Header title_lbl = QLabel(title) title_lbl.setStyleSheet("font-size: 18px; font-weight: 600; color: #222; border: none; background: transparent;") inner_layout.addWidget(title_lbl) # Message msg_lbl = QLabel(message) msg_lbl.setWordWrap(True) msg_lbl.setStyleSheet("font-size: 14px; color: #555; line-height: 1.4; border: none; background: transparent;") inner_layout.addWidget(msg_lbl) # Buttons btn_layout = QHBoxLayout() btn_layout.addStretch() btn_layout.setSpacing(10) for btn_text in buttons: btn = QPushButton(btn_text) btn.setCursor(Qt.CursorShape.PointingHandCursor) btn.setFixedHeight(36) # Primary button style if btn_text in ["确定", "是", "打开报告", "Yes", "OK"]: btn.setStyleSheet(""" QPushButton { background-color: #222; color: white; border-radius: 6px; padding: 0 20px; font-weight: 600; border: none; } QPushButton:hover { background-color: #444; } QPushButton:pressed { background-color: #000; } """) else: btn.setStyleSheet(""" QPushButton { background-color: white; color: #333; border-radius: 6px; padding: 0 20px; border: 1px solid #ddd; font-weight: 500; } QPushButton:hover { background-color: #f5f5f5; border-color: #ccc; } QPushButton:pressed { background-color: #eee; } """) btn.clicked.connect(lambda checked, t=btn_text: self.on_btn_clicked(t)) btn_layout.addWidget(btn) inner_layout.addLayout(btn_layout) def on_btn_clicked(self, text): self.result_text = text self.accept() def mousePressEvent(self, event): if event.button() == Qt.MouseButton.LeftButton: self.old_pos = event.globalPosition().toPoint() def mouseMoveEvent(self, event): if hasattr(self, 'old_pos') and self.old_pos: delta = event.globalPosition().toPoint() - self.old_pos self.move(self.pos() + delta) self.old_pos = event.globalPosition().toPoint() def mouseReleaseEvent(self, event): self.old_pos = None class DirectoryScannerWindow(QMainWindow): def __init__(self): super().__init__() self.setWindowTitle("目录扫描器") self.resize(520, 540) # 稍微加大一点以容纳阴影 # 无边框设置 self.setWindowFlags(Qt.WindowType.FramelessWindowHint) self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground) self.worker = None self.old_pos = None self.init_ui() def mousePressEvent(self, event): if event.button() == Qt.MouseButton.LeftButton: self.old_pos = event.globalPosition().toPoint() def mouseMoveEvent(self, event): if self.old_pos: delta = event.globalPosition().toPoint() - self.old_pos self.move(self.pos() + delta) self.old_pos = event.globalPosition().toPoint() def mouseReleaseEvent(self, event): self.old_pos = None def init_ui(self): # 外层容器(用于显示阴影) container = QWidget() self.setCentralWidget(container) container_layout = QVBoxLayout(container) container_layout.setContentsMargins(10, 10, 10, 10) # 定义样式表 self.STYLES = { "main_widget": """ QWidget#MainWidget { background-color: white; border-radius: 16px; border: 1px solid #f0f0f0; } """, "title_btn": """ QPushButton { border: none; border-radius: 16px; font-weight: bold; color: #888; background: transparent; } QPushButton:hover { background-color: #f5f5f5; color: #333; } """, "close_btn": """ QPushButton { border: none; border-radius: 16px; font-weight: bold; color: #888; background: transparent; } QPushButton:hover { background-color: #ff4d4d; color: white; } """, "list_widget": """ QListWidget { border: 1px solid #eaeaea; border-radius: 6px; background: #fdfdfd; padding: 5px; font-size: 13px; color: #333; } QListWidget:focus { border: 1px solid #ccc; background: white; } """, "progress_bar": """ QProgressBar { border: none; background: #f0f0f0; border-radius: 2px; } QProgressBar::chunk { background-color: #333; border-radius: 2px; } """, "scan_btn": """ QPushButton { background-color: #222; color: white; border-radius: 19px; font-weight: 600; font-size: 13px; border: none; } QPushButton:hover { background-color: #444; } QPushButton:pressed { background-color: #000; } QPushButton:disabled { background-color: #eee; color: #aaa; } """, "log_text": """ QTextEdit { color: #555; font-family: 'Consolas', monospace; font-size: 11px; background: #f9f9f9; padding: 12px; border-radius: 8px; } """ } # 主内容区域 self.main_widget = QWidget() self.main_widget.setObjectName("MainWidget") self.main_widget.setStyleSheet(self.STYLES["main_widget"]) # 添加阴影效果 shadow = QGraphicsDropShadowEffect(self) shadow.setBlurRadius(15) shadow.setXOffset(0) shadow.setYOffset(4) shadow.setColor(QColor(0, 0, 0, 30)) self.main_widget.setGraphicsEffect(shadow) container_layout.addWidget(self.main_widget) # 布局 layout = QVBoxLayout(self.main_widget) layout.setContentsMargins(30, 20, 30, 30) layout.setSpacing(20) # Custom Title Bar title_bar = QHBoxLayout() title_bar.setContentsMargins(0, 0, 0, 10) # Logo logo_path = resource_path("logo.png") if os.path.exists(logo_path): self.setWindowIcon(QIcon(logo_path)) logo_lbl = QLabel() pixmap = QPixmap(logo_path) scaled = pixmap.scaled(24, 24, Qt.AspectRatioMode.KeepAspectRatio, Qt.TransformationMode.SmoothTransformation) logo_lbl.setPixmap(scaled) logo_lbl.setStyleSheet("border: none; background: transparent; margin-right: 5px;") title_bar.addWidget(logo_lbl) title_label = QLabel("目录扫描器") title_label.setFont(QFont("Segoe UI", 14, QFont.Weight.Bold)) title_label.setStyleSheet("color: #333; border: none; background: transparent;") btn_min = QPushButton("─") btn_min.setFixedSize(32, 32) btn_min.setCursor(Qt.CursorShape.PointingHandCursor) btn_min.clicked.connect(self.showMinimized) btn_min.setStyleSheet(self.STYLES["title_btn"]) btn_close = QPushButton("✕") btn_close.setFixedSize(32, 32) btn_close.setCursor(Qt.CursorShape.PointingHandCursor) btn_close.clicked.connect(self.close) btn_close.setStyleSheet(self.STYLES["close_btn"]) title_bar.addWidget(title_label) title_bar.addStretch() title_bar.addWidget(btn_min) title_bar.addWidget(btn_close) layout.addLayout(title_bar) # Form form_layout = QVBoxLayout() form_layout.setSpacing(20) # Directory List self.dir_list = QListWidget() self.dir_list.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection) self.dir_list.setStyleSheet(self.STYLES["list_widget"]) self.dir_list.setFixedHeight(100) btn_add_dir = self.create_tool_button("➕", self.add_directory) btn_remove_dir = self.create_tool_button("➖", self.remove_directory) dir_btn_layout = QVBoxLayout() dir_btn_layout.addWidget(btn_add_dir) dir_btn_layout.addWidget(btn_remove_dir) dir_btn_layout.addStretch() dir_input_layout = QHBoxLayout() dir_input_layout.addWidget(self.dir_list) dir_input_layout.addLayout(dir_btn_layout) container = QVBoxLayout() container.setSpacing(6) label = QLabel("扫描目标 (支持多选)") label.setStyleSheet("color: #888; font-weight: 500; font-size: 12px; letter-spacing: 0.5px;") container.addWidget(label) container.addLayout(dir_input_layout) form_layout.addLayout(container) # Output self.entry_out = self.create_input("默认保存到当前目录...") btn_out = self.create_tool_button("💾", self.select_save_file) form_layout.addLayout(self.create_field("保存报告", self.entry_out, btn_out)) layout.addLayout(form_layout) # Progress & Action action_layout = QHBoxLayout() # Progress Section self.progress_container = QWidget() progress_layout = QVBoxLayout(self.progress_container) progress_layout.setContentsMargins(0, 0, 0, 0) progress_layout.setSpacing(5) self.progress_bar = QProgressBar() self.progress_bar.setTextVisible(False) self.progress_bar.setFixedHeight(4) self.progress_bar.setStyleSheet(self.STYLES["progress_bar"]) self.progress_label = QLabel("准备就绪") self.progress_label.setStyleSheet("color: #999; font-size: 12px;") # 关键:设置 SizePolicy 为 Ignored,防止文本过长撑开窗口 self.progress_label.setSizePolicy(QSizePolicy.Policy.Ignored, QSizePolicy.Policy.Preferred) progress_layout.addWidget(self.progress_bar) progress_layout.addWidget(self.progress_label) self.progress_container.setVisible(False) action_layout.addWidget(self.progress_container, stretch=1) # Scan Button self.btn_scan = QPushButton("开始扫描") self.btn_scan.setCursor(Qt.CursorShape.PointingHandCursor) self.btn_scan.setFixedSize(110, 38) self.btn_scan.setStyleSheet(self.STYLES["scan_btn"]) self.btn_scan.clicked.connect(self.start_scan) action_layout.addWidget(self.btn_scan) layout.addLayout(action_layout) # Log self.log_text = QTextEdit() self.log_text.setReadOnly(True) self.log_text.setFrameShape(QFrame.Shape.NoFrame) self.log_text.setPlaceholderText("等待任务启动...") self.log_text.setStyleSheet(self.STYLES["log_text"]) layout.addWidget(self.log_text, stretch=1) def create_input(self, placeholder): le = QLineEdit() le.setPlaceholderText(placeholder) le.setStyleSheet(""" QLineEdit { padding: 8px 12px; border: 1px solid #eaeaea; border-radius: 6px; background: #fdfdfd; font-size: 13px; color: #333; } QLineEdit:focus { border: 1px solid #ccc; background: white; } """) return le def create_tool_button(self, text, callback): btn = QPushButton(text) btn.setCursor(Qt.CursorShape.PointingHandCursor) btn.setFixedSize(36, 36) btn.clicked.connect(callback) btn.setStyleSheet(""" QPushButton { border: 1px solid #eaeaea; border-radius: 6px; background: white; font-size: 14px; } QPushButton:hover { background: #f5f5f5; border-color: #ddd; } """) return btn def create_field(self, label_text, widget, btn): container = QVBoxLayout() container.setSpacing(6) label = QLabel(label_text) label.setStyleSheet("color: #888; font-weight: 500; font-size: 12px; letter-spacing: 0.5px;") container.addWidget(label) input_layout = QHBoxLayout() input_layout.setSpacing(8) input_layout.addWidget(widget) input_layout.addWidget(btn) container.addLayout(input_layout) return container def add_directory(self): import datetime # Lazy import directory = QFileDialog.getExistingDirectory(self, "选择扫描目录") if directory: # 检查是否重复 items = [self.dir_list.item(i).text() for i in range(self.dir_list.count())] if directory not in items: self.dir_list.addItem(directory) # 自动设置默认输出路径 (如果还没设置) if not self.entry_out.text(): timestamp = datetime.datetime.now().strftime('%Y%m%d_%H%M%S') name_part = os.path.basename(directory) if self.dir_list.count() == 1 else "multi_dirs" default_name = f"scan_report_{name_part}_{timestamp}.html" self.entry_out.setText(os.path.join(os.getcwd(), default_name)) def remove_directory(self): for item in self.dir_list.selectedItems(): self.dir_list.takeItem(self.dir_list.row(item)) def select_save_file(self): file_path, _ = QFileDialog.getSaveFileName(self, "保存报告", "", "HTML Files (*.html);;All Files (*.*)") if file_path: self.entry_out.setText(file_path) def log(self, message): self.log_text.append(message) def start_scan(self): import datetime # Lazy import target_paths = [self.dir_list.item(i).text() for i in range(self.dir_list.count())] output_path = self.entry_out.text() if not target_paths: QMessageBox.critical(self, "错误", "请至少添加一个扫描目录!") return # Normalize paths target_paths = [os.path.normpath(p) for p in target_paths] if not output_path: timestamp = datetime.datetime.now().strftime('%Y%m%d_%H%M%S') name_part = os.path.basename(target_paths[0]) if len(target_paths) == 1 else "multi_dirs" default_name = f"scan_report_{name_part}_{timestamp}.html" output_path = os.path.join(os.getcwd(), default_name) self.entry_out.setText(output_path) self.btn_scan.setEnabled(False) self.log_text.clear() self.progress_container.setVisible(True) self.progress_bar.setRange(0, 0) # Indeterminate mode self.log(f"开始扫描 {len(target_paths)} 个目录...") self.thread = QThread() self.worker = ScanWorker(target_paths, output_path) self.worker.moveToThread(self.thread) self.thread.started.connect(self.worker.run) self.worker.finished.connect(self.scan_finished) self.worker.finished.connect(self.thread.quit) self.worker.finished.connect(self.worker.deleteLater) self.thread.finished.connect(self.thread.deleteLater) self.worker.progress.connect(self.update_progress) self.worker.log.connect(self.log) self.thread.start() def update_progress(self, count, path): # 智能截断路径,防止撑开窗口 # 获取文件名 name = os.path.basename(path) # 如果路径太长,优先显示文件名 if len(path) > 35: if len(name) > 30: display_path = name[:25] + "..." else: # 尝试显示部分父目录 + 文件名 display_path = "..." + path[-30:] else: display_path = path self.progress_label.setText(f"已扫描: {count} | 当前: {display_path}") def scan_finished(self, success, message): self.btn_scan.setEnabled(True) self.progress_bar.setRange(0, 100) self.progress_bar.setValue(100) if success: self.log(f"报告已生成: {message}") self.progress_label.setText("完成") msg_box = ModernMessageBox("扫描完成", f"扫描任务已完成!\n报告已保存至:\n{os.path.basename(message)}\n\n是否立即打开报告?", self, ["打开报告", "稍后"]) msg_box.exec() if msg_box.result_text == "打开报告": try: os.startfile(message) except Exception as e: QMessageBox.warning(self, "错误", f"无法打开文件: {e}") else: self.log(f"发生错误: {message}") self.progress_label.setText("发生错误") ModernMessageBox("错误", f"扫描过程中发生错误:\n{message}", self, ["确定"]).exec() if __name__ == "__main__": # 1. 设置 AppUserModelID,使 Windows 任务栏能正确识别独立图标 import ctypes myappid = 'mycompany.myproduct.subproduct.version' # 任意唯一的字符串 try: ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID(myappid) except Exception: pass app = QApplication(sys.argv) app.setStyle("Fusion") # Use Fusion style for better cross-platform look base # 2. 设置全局应用程序图标 logo_path = resource_path("logo.png") if os.path.exists(logo_path): app.setWindowIcon(QIcon(logo_path)) # Global Font font = QFont("Segoe UI", 9) app.setFont(font) window = DirectoryScannerWindow() window.show() sys.exit(app.exec())