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

1. 主窗口与界面设计
python
"""
文件扫描器主程序
功能:选择目录、扫描文件、树形展示、导出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 *
class FileScannerApp(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;
}
""")
def select_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("目录已选择,点击开始扫描")
def start_scan(self):
"""开始扫描文件"""
if not self.current_dir or not 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("正在扫描文件...")
def stop_scan(self):
"""停止扫描"""
if self.scanner and self.scanner.isRunning():
self.scanner.stop()
self.scanner.wait()
self.reset_controls()
self.status_label.setText("扫描已停止")
def update_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) < 40 else f"{filename[:37]}..."
self.status_label.setText(f"扫描中: {current}/{total} - {display_name}")
def on_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)} 个文件")
def on_scan_error(self, error_msg):
"""扫描错误处理"""
QMessageBox.critical(self, "错误", f"扫描失败: {error_msg}")
self.reset_controls()
self.status_label.setText(f"错误: {error_msg}")
def reset_controls(self):
"""重置控件状态"""
self.scan_btn.setEnabled(True)
self.browse_btn.setEnabled(True)
self.stop_btn.setEnabled(False)
self.progress_bar.setVisible(False)
def clear_file_tree(self):
"""清空文件树"""
self.file_tree.clear()
def build_file_tree(self):
"""构建文件树结构"""
if not 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)
def add_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
if not 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
def export_files(self):
"""导出文件列表到TXT"""
if not 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)}")
2. 文件扫描线程
python
class FileScanner(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
def run(self):
"""线程执行函数"""
try:
if not 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):
if not self._running:
return
total_files += len(files)
processed = 0
for root, dirs, files in os.walk(self.root_dir):
if not self._running:
return
for file in files:
if not 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))
def stop(self):
"""停止扫描"""
self._running = False
3. 应用程序入口
python
def main():
"""应用程序主函数"""
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 进行垂直和水平布局,包含以下核心区域:
- 标题区域:显示应用程序名称
- 目录选择区:显示当前选择的目录路径和浏览按钮
- 控制按钮区:开始扫描、停止、导出三个主要功能按钮
- 进度显示区:显示扫描进度
- 文件树展示区:以树形结构展示目录和文件
- 状态栏 :显示当前状态信息

树形结构实现原理
文件树的构建采用递归算法,但通过迭代方式实现以避免递归深度限制。对于每个文件的相对路径,按路径分隔符分割,逐级检查并创建树节点:
python
def add_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
# 创建新节点
if not 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 实现线程间通信:
- 进度信号:实时更新扫描进度
- 完成信号:扫描完成后传输文件列表
- 错误信号:传递扫描过程中的异常信息
线程安全的停止机制通过标志变量控制:
python
def stop(self):
"""安全的线程停止方法"""
self._running = False
def run(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
...
扩展功能建议
1. 添加文件筛选功能
python
class FilterDialog(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)
2. 添加右键菜单功能
python
class CustomTreeWidget(QTreeWidget):
"""自定义树控件,添加右键菜单"""
def __init__(self, parent=None):
super().__init__(parent)
self.setContextMenuPolicy(Qt.CustomContextMenu)
self.customContextMenuRequested.connect(self.show_context_menu)
def show_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))
3. 添加统计信息面板
python
class StatsPanel(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)
def update_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")
运行说明
安装依赖
bash
pip install PyQt5
运行程序
bash
python file_scanner.py
使用步骤
- 点击"选择目录"按钮,选择要扫描的目录
- 点击"开始扫描"按钮,开始扫描文件
- 在树形结构中浏览扫描结果
- 点击"导出TXT"按钮,保存文件列表
- 可随时点击"停止"按钮中断扫描
注意事项
- 权限问题:某些系统目录可能需要管理员权限才能访问
- 符号链接:当前版本不处理符号链接,避免无限循环
- 大目录处理:扫描非常大的目录时可能需要较长时间
- 内存使用:扫描结果完全存储在内存中,超大型目录需注意内存限制
- 编码问题:确保系统支持文件的字符编码
总结
本文实现了一个功能完整的目录文件扫描器,具有以下特点:
- 直观的图形界面:基于 PyQt5 的现代化界面
- 高效的扫描机制:多线程后台扫描,不阻塞界面
- 清晰的树形展示:层次化的文件结构展示
- 完整的导出功能:标准化的 TXT 文件输出
- 良好的用户体验:进度显示、状态反馈、错误处理
该工具适用于文件整理、项目分析、数据备份等多种场景,可有效提高文件管理效率。通过模块化设计和清晰的代码结构,便于进一步的功能扩展和维护。