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"""
目录扫描报告 - 结构可视化
"""
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())