
本文将详细介绍如何使用 Python 和 PyQt5 创建一个功能完整的目录文件扫描器。该工具能够选择目录、递归扫描所有文件、以树形结构展示结果,并将文件列表导出为文本文件。

"""
文件扫描器主程序
功能:选择目录、扫描文件、树形展示、导出TXT
作者:智能助手
日期:2024年1月24日
"""
import os
import sys
from pathlib import Path
from datetime import datetime
from PyQt5.QtCore import Qt, QThread, pyqtSignal, QSize
from PyQt5.QtGui import QFont, QColor, QIcon, QPalette, QLinearGradient
from PyQt5.QtWidgets import *
classFileScannerApp(QMainWindow):
"""主应用程序窗口,负责界面布局和功能协调"""
def__init__(self):
super().__init__()
self.current_dir = ""
self.file_list = []
self.scanner = None
self._setup_ui()
self._setup_styles()
self.setWindowTitle("文件扫描器")
self.setGeometry(100, 100, 1000, 700)
def_setup_ui(self):
"""初始化用户界面"""
central_widget = QWidget()
self.setCentralWidget(central_widget)
main_layout = QVBoxLayout(central_widget)
main_layout.setSpacing(10)
main_layout.setContentsMargins(15, 15, 15, 15)
# 标题
title_label = QLabel("📁 目录文件扫描器")
title_label.setStyleSheet("font-size: 20px; font-weight: bold; color: #2c3e50;")
title_label.setAlignment(Qt.AlignCenter)
main_layout.addWidget(title_label)
# 目录选择区域
dir_group = QGroupBox("目录设置")
dir_layout = QHBoxLayout()
self.dir_label = QLabel("未选择目录")
self.dir_label.setStyleSheet("color: #7f8c8d; padding: 5px; border: 1px solid #ddd; border-radius: 3px;")
self.dir_label.setMinimumHeight(30)
self.browse_btn = QPushButton("选择目录")
self.browse_btn.setFixedWidth(100)
self.browse_btn.clicked.connect(self.select_directory)
dir_layout.addWidget(self.dir_label, 1)
dir_layout.addWidget(self.browse_btn)
dir_group.setLayout(dir_layout)
main_layout.addWidget(dir_group)
# 控制按钮区域
control_group = QGroupBox("操作控制")
control_layout = QHBoxLayout()
self.scan_btn = QPushButton("🔍 开始扫描")
self.scan_btn.setFixedWidth(120)
self.scan_btn.clicked.connect(self.start_scan)
self.scan_btn.setEnabled(False)
self.stop_btn = QPushButton("⏹ 停止")
self.stop_btn.setFixedWidth(100)
self.stop_btn.clicked.connect(self.stop_scan)
self.stop_btn.setEnabled(False)
self.export_btn = QPushButton("💾 导出TXT")
self.export_btn.setFixedWidth(120)
self.export_btn.clicked.connect(self.export_files)
self.export_btn.setEnabled(False)
control_layout.addWidget(self.scan_btn)
control_layout.addWidget(self.stop_btn)
control_layout.addWidget(self.export_btn)
control_layout.addStretch()
control_group.setLayout(control_layout)
main_layout.addWidget(control_group)
# 进度条
self.progress_bar = QProgressBar()
self.progress_bar.setVisible(False)
main_layout.addWidget(self.progress_bar)
# 文件树显示
tree_group = QGroupBox("文件列表")
tree_layout = QVBoxLayout()
self.file_tree = QTreeWidget()
self.file_tree.setHeaderLabels(["文件/目录", "路径"])
self.file_tree.setColumnWidth(0, 300)
self.file_tree.header().setSectionResizeMode(0, QHeaderView.Interactive)
self.file_tree.header().setSectionResizeMode(1, QHeaderView.Stretch)
tree_layout.addWidget(self.file_tree)
tree_group.setLayout(tree_layout)
main_layout.addWidget(tree_group, 1)
# 状态栏
self.status_bar = QStatusBar()
self.setStatusBar(self.status_bar)
self.status_label = QLabel("就绪")
self.status_bar.addWidget(self.status_label)
def_setup_styles(self):
"""设置界面样式"""
self.setStyleSheet("""
QMainWindow {
background-color: #f5f5f5;
}
QGroupBox {
font-weight: bold;
border: 1px solid #ccc;
border-radius: 5px;
margin-top: 10px;
padding-top: 10px;
}
QGroupBox::title {
subcontrol-origin: margin;
left: 10px;
padding: 0 5px 0 5px;
}
QPushButton {
background-color: #3498db;
color: white;
border: none;
padding: 8px 15px;
border-radius: 4px;
font-weight: bold;
}
QPushButton:hover {
background-color: #2980b9;
}
QPushButton:disabled {
background-color: #bdc3c7;
color: #7f8c8d;
}
QTreeWidget {
background-color: white;
border: 1px solid #ddd;
border-radius: 3px;
font-family: 'Microsoft YaHei', 'Segoe UI';
font-size: 12px;
}
QTreeWidget::item {
padding: 3px;
}
QTreeWidget::item:hover {
background-color: #ecf0f1;
}
QTreeWidget::item:selected {
background-color: #3498db;
color: white;
}
""")
defselect_directory(self):
"""选择目录"""
dir_path = QFileDialog.getExistingDirectory(
self,
"选择目录",
str(Path.home()),
QFileDialog.ShowDirsOnly
)
if dir_path:
self.current_dir = dir_path
self.dir_label.setText(f"已选择: {dir_path}")
self.scan_btn.setEnabled(True)
self.clear_file_tree()
self.status_label.setText("目录已选择,点击开始扫描")
defstart_scan(self):
"""开始扫描文件"""
ifnot self.current_dir ornot os.path.exists(self.current_dir):
QMessageBox.warning(self, "警告", "请先选择有效的目录")
return
self.clear_file_tree()
self.scan_btn.setEnabled(False)
self.browse_btn.setEnabled(False)
self.stop_btn.setEnabled(True)
self.export_btn.setEnabled(False)
self.progress_bar.setVisible(True)
self.progress_bar.setValue(0)
self.scanner = FileScanner(self.current_dir)
self.scanner.progress.connect(self.update_progress)
self.scanner.finished.connect(self.on_scan_finished)
self.scanner.error.connect(self.on_scan_error)
self.scanner.start()
self.status_label.setText("正在扫描文件...")
defstop_scan(self):
"""停止扫描"""
if self.scanner and self.scanner.isRunning():
self.scanner.stop()
self.scanner.wait()
self.reset_controls()
self.status_label.setText("扫描已停止")
defupdate_progress(self, current, total, filename):
"""更新扫描进度"""
if total > 0:
progress = int((current / total) * 100)
self.progress_bar.setMaximum(total)
self.progress_bar.setValue(current)
display_name = filename if len(filename) < 40elsef"{filename[:37]}..."
self.status_label.setText(f"扫描中: {current}/{total} - {display_name}")
defon_scan_finished(self, file_list):
"""扫描完成处理"""
self.file_list = sorted(file_list)
self.build_file_tree()
self.reset_controls()
self.export_btn.setEnabled(True)
self.status_label.setText(f"扫描完成!共找到 {len(self.file_list)} 个文件")
QMessageBox.information(self, "完成", f"扫描完成!\n找到 {len(self.file_list)} 个文件")
defon_scan_error(self, error_msg):
"""扫描错误处理"""
QMessageBox.critical(self, "错误", f"扫描失败: {error_msg}")
self.reset_controls()
self.status_label.setText(f"错误: {error_msg}")
defreset_controls(self):
"""重置控件状态"""
self.scan_btn.setEnabled(True)
self.browse_btn.setEnabled(True)
self.stop_btn.setEnabled(False)
self.progress_bar.setVisible(False)
defclear_file_tree(self):
"""清空文件树"""
self.file_tree.clear()
defbuild_file_tree(self):
"""构建文件树结构"""
ifnot self.file_list:
return
root_name = os.path.basename(self.current_dir) or self.current_dir
root_item = QTreeWidgetItem(self.file_tree, [root_name, self.current_dir])
root_item.setExpanded(True)
for file_path in self.file_list:
self.add_to_tree(root_item, file_path)
defadd_to_tree(self, parent_item, file_path):
"""添加文件到树中"""
parts = file_path.split(os.sep)
current_item = parent_item
for i, part in enumerate(parts):
found = False
for j in range(current_item.childCount()):
child = current_item.child(j)
if child.text(0) == part:
current_item = child
found = True
break
ifnot found:
full_path = os.path.join(self.current_dir, *parts[:i+1])
is_file = (i == len(parts) - 1)
new_item = QTreeWidgetItem(current_item, [part, full_path])
if is_file:
new_item.setForeground(0, QColor(0, 128, 0))
current_item = new_item
defexport_files(self):
"""导出文件列表到TXT"""
ifnot self.file_list:
QMessageBox.warning(self, "警告", "没有文件可导出")
return
default_name = f"文件列表_{datetime.now().strftime('%Y%m%d_%H%M%S')}.txt"
file_path, _ = QFileDialog.getSaveFileName(
self, "保存文件列表",
str(Path.home() / default_name),
"文本文件 (*.txt);;所有文件 (*)"
)
if file_path:
try:
with open(file_path, 'w', encoding='utf-8') as f:
f.write(f"目录: {self.current_dir}\n")
f.write(f"扫描时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n")
f.write(f"文件总数: {len(self.file_list)}\n")
f.write("=" * 50 + "\n\n")
for file in self.file_list:
f.write(file + "\n")
QMessageBox.information(self, "成功", f"文件已保存到:\n{file_path}")
self.status_label.setText(f"已保存: {os.path.basename(file_path)}")
except Exception as e:
QMessageBox.critical(self, "错误", f"保存失败: {str(e)}")
classFileScanner(QThread):
"""后台文件扫描线程,避免界面卡顿"""
progress = pyqtSignal(int, int, str) # 当前进度, 总数, 当前文件
finished = pyqtSignal(list) # 文件列表
error = pyqtSignal(str) # 错误信息
def__init__(self, root_dir):
super().__init__()
self.root_dir = root_dir
self._running = True
defrun(self):
"""线程执行函数"""
try:
ifnot os.path.exists(self.root_dir):
self.error.emit(f"目录不存在: {self.root_dir}")
return
file_list = []
total_files = 0
# 先统计总文件数
for root, dirs, files in os.walk(self.root_dir):
ifnot self._running:
return
total_files += len(files)
processed = 0
for root, dirs, files in os.walk(self.root_dir):
ifnot self._running:
return
for file in files:
ifnot self._running:
return
try:
full_path = os.path.join(root, file)
rel_path = os.path.relpath(full_path, self.root_dir)
file_list.append(rel_path)
processed += 1
if total_files > 0:
self.progress.emit(processed, total_files, rel_path)
except Exception:
continue
if self._running:
self.finished.emit(file_list)
except Exception as e:
self.error.emit(str(e))
defstop(self):
"""停止扫描"""
self._running = False
defmain():
"""应用程序主函数"""
app = QApplication(sys.argv)
app.setApplicationName("文件扫描器")
app.setFont(QFont("Microsoft YaHei", 10))
window = FileScannerApp()
window.show()
sys.exit(app.exec_())
if __name__ == "__main__":
main()
界面采用 QVBoxLayout 和 QHBoxLayout 进行垂直和水平布局,包含以下核心区域:

文件树的构建采用递归算法,但通过迭代方式实现以避免递归深度限制。对于每个文件的相对路径,按路径分隔符分割,逐级检查并创建树节点:
defadd_to_tree(parent_item, file_path):
"""添加文件到树中的算法实现"""
parts = file_path.split(os.sep) # 分割路径
current_item = parent_item
for i, part in enumerate(parts):
# 检查是否已存在该节点
found = False
for j in range(current_item.childCount()):
if current_item.child(j).text(0) == part:
current_item = current_item.child(j)
found = True
break
# 创建新节点
ifnot found:
is_file = (i == len(parts) - 1)
new_item = QTreeWidgetItem(current_item, [part, ""])
if is_file:
new_item.setForeground(0, QColor(0, 128, 0))
current_item = new_item
扫描过程在独立线程中执行,避免界面卡顿。使用 QThread 和 pyqtSignal 实现线程间通信:
线程安全的停止机制通过标志变量控制:
defstop(self):
"""安全的线程停止方法"""
self._running = False
defrun(self):
"""线程运行循环"""
while self._running and not_finished:
# 扫描逻辑...
生成的 TXT 文件包含标准化格式:
目录: /path/to/directory
扫描时间: 2024-01-24 10:30:00
文件总数: 1234
==================================================
folder1/file1.txt
folder1/file2.py
folder2/subfolder/document.docx
...
classFilterDialog(QDialog):
"""文件筛选对话框"""
def__init__(self, parent=None):
super().__init__(parent)
self.setWindowTitle("文件筛选")
layout = QVBoxLayout()
# 扩展名筛选
ext_layout = QHBoxLayout()
ext_label = QLabel("扩展名:")
self.ext_input = QLineEdit()
self.ext_input.setPlaceholderText("例如: txt,py,jpg (逗号分隔)")
ext_layout.addWidget(ext_label)
ext_layout.addWidget(self.ext_input, 1)
# 大小筛选
size_layout = QHBoxLayout()
size_label = QLabel("最小大小(KB):")
self.min_size = QSpinBox()
self.min_size.setRange(0, 999999)
size_layout.addWidget(size_label)
size_layout.addWidget(self.min_size)
layout.addLayout(ext_layout)
layout.addLayout(size_layout)
# 按钮
btn_layout = QHBoxLayout()
ok_btn = QPushButton("确定")
cancel_btn = QPushButton("取消")
ok_btn.clicked.connect(self.accept)
cancel_btn.clicked.connect(self.reject)
btn_layout.addWidget(ok_btn)
btn_layout.addWidget(cancel_btn)
layout.addLayout(btn_layout)
self.setLayout(layout)
classCustomTreeWidget(QTreeWidget):
"""自定义树控件,添加右键菜单"""
def__init__(self, parent=None):
super().__init__(parent)
self.setContextMenuPolicy(Qt.CustomContextMenu)
self.customContextMenuRequested.connect(self.show_context_menu)
defshow_context_menu(self, position):
"""显示右键菜单"""
menu = QMenu()
open_action = QAction("打开文件", self)
open_folder_action = QAction("打开所在文件夹", self)
copy_path_action = QAction("复制路径", self)
menu.addAction(open_action)
menu.addAction(open_folder_action)
menu.addSeparator()
menu.addAction(copy_path_action)
menu.exec_(self.viewport().mapToGlobal(position))
classStatsPanel(QWidget):
"""统计信息面板"""
def__init__(self, parent=None):
super().__init__(parent)
layout = QVBoxLayout()
# 文件类型统计
self.type_stats = {}
self.type_label = QLabel("文件类型: 0")
# 大小统计
self.total_size = 0
self.size_label = QLabel("总大小: 0 B")
# 时间统计
self.scan_time = 0
self.time_label = QLabel("扫描时间: 0s")
layout.addWidget(self.type_label)
layout.addWidget(self.size_label)
layout.addWidget(self.time_label)
self.setLayout(layout)
defupdate_stats(self, file_list, scan_time):
"""更新统计信息"""
# 计算文件类型分布
self.type_stats.clear()
for file in file_list:
ext = os.path.splitext(file)[1].lower()
self.type_stats[ext] = self.type_stats.get(ext, 0) + 1
# 计算总大小
self.total_size = 0
# 更新显示
self.type_label.setText(f"文件类型: {len(self.type_stats)} 种")
self.time_label.setText(f"扫描时间: {scan_time:.2f}s")
pip install PyQt5
python file_scanner.py
本文实现了一个功能完整的目录文件扫描器,具有以下特点:
该工具适用于文件整理、项目分析、数据备份等多种场景,可有效提高文件管理效率。通过模块化设计和清晰的代码结构,便于进一步的功能扩展和维护。


陪伴是最长情的告白
为你推送最实用的资讯

识别二维码 关注我们