import
sys
import
os
import
time
from
PyQt6.QtWidgets
import
(
QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QGridLayout,
QLabel, QLineEdit, QPushButton, QTreeWidget, QTreeWidgetItem,
QListWidget, QTextEdit, QScrollArea, QFileDialog, QMessageBox
)
from
PyQt6.QtCore
import
Qt, QThread, pyqtSignal, QMimeData, QSize
from
PyQt6.QtGui
import
QDragEnterEvent, QDropEvent
class
DraggableLineEdit(QLineEdit):
def
__init__(
self
, parent
=
None
):
super
().__init__(parent)
self
.setAcceptDrops(
True
)
def
dragEnterEvent(
self
, event: QDragEnterEvent):
if
event.mimeData().hasUrls():
event.acceptProposedAction()
def
dropEvent(
self
, event: QDropEvent):
urls
=
event.mimeData().urls()
if
urls:
path
=
urls[
0
].toLocalFile()
if
os.path.isdir(path)
or
os.path.isfile(path):
self
.setText(path)
class
SearchWorker(QThread):
update_file
=
pyqtSignal(
dict
)
finished
=
pyqtSignal()
def
__init__(
self
, folder, extensions, keyword):
super
().__init__()
self
.folder
=
folder
self
.extensions
=
extensions
self
.keyword
=
keyword
self
.running
=
True
def
run(
self
):
for
root, _, files
in
os.walk(
self
.folder):
if
not
self
.running:
break
for
file
in
files:
if
any
(
file
.endswith(ext)
for
ext
in
self
.extensions):
path
=
os.path.join(root,
file
)
if
self
.file_contains_keyword(path,
self
.keyword):
size
=
os.path.getsize(path)
self
.update_file.emit({
"name"
:
file
,
"size"
:
self
.format_size(size),
"path"
: path
})
self
.finished.emit()
def
file_contains_keyword(
self
, path, keyword):
for
encoding
in
[
'utf-8'
,
'gbk'
,
'latin-1'
]:
try
:
with
open
(path,
'r'
, encoding
=
encoding) as f:
return
any
(keyword
in
line
for
line
in
f)
except
(UnicodeDecodeError, Exception):
continue
return
False
def
format_size(
self
, size):
if
size <
1024
:
return
f
"{size} B"
elif
size <
1024
*
1024
:
return
f
"{size/1024:.1f} KB"
else
:
return
f
"{size/(1024 * 1024):.1f} MB"
class
FileReader(QThread):
update_line
=
pyqtSignal(
str
)
finished
=
pyqtSignal()
def
__init__(
self
, path, keyword):
super
().__init__()
self
.path
=
path
self
.keyword
=
keyword
self
.results
=
[]
def
run(
self
):
self
.results
=
[]
for
encoding
in
[
'utf-8'
,
'gbk'
,
'latin-1'
]:
try
:
with
open
(
self
.path,
'r'
, encoding
=
encoding) as f:
for
i, line
in
enumerate
(f,
1
):
if
self
.keyword
in
line:
text
=
f
"Line {i}: {line.strip()[:50]}"
self
.update_line.emit(text)
self
.results.append(line.strip())
break
except
(UnicodeDecodeError, Exception):
continue
self
.finished.emit()
class
AllFilesReader(QThread):
update_line
=
pyqtSignal(
str
)
finished
=
pyqtSignal()
def
__init__(
self
, paths, keyword):
super
().__init__()
self
.paths
=
paths
self
.keyword
=
keyword
def
run(
self
):
for
path
in
self
.paths:
if
not
os.path.isfile(path):
continue
reader
=
FileReader(path,
self
.keyword)
reader.update_line.connect(
self
.update_line.emit)
reader.start()
reader.wait()
self
.finished.emit()
class
MainWindow(QMainWindow):
def
__init__(
self
):
super
().__init__()
self
.current_matches
=
[]
self
.search_thread
=
None
self
.file_reader
=
None
self
.start_time
=
0
self
.init_ui()
def
init_ui(
self
):
self
.setWindowTitle(
"PyQt6文件搜索工具"
)
self
.setGeometry(
100
,
100
,
1200
,
700
)
main_widget
=
QWidget()
self
.setCentralWidget(main_widget)
main_layout
=
QVBoxLayout(main_widget)
top_panel
=
QWidget()
top_layout
=
QGridLayout(top_panel)
self
.folder_input
=
DraggableLineEdit()
self
.ext_input
=
QLineEdit(
".txt"
)
self
.keyword_input
=
QLineEdit()
self
.btn_search
=
QPushButton(
"开始搜索"
)
self
.btn_search.clicked.connect(
self
.start_search)
top_layout.addWidget(QLabel(
"目标文件夹:"
),
0
,
0
)
top_layout.addWidget(
self
.folder_input,
0
,
1
)
top_layout.addWidget(
self
.create_browse_btn(),
0
,
2
)
top_layout.addWidget(QLabel(
"文件后缀:"
),
1
,
0
)
top_layout.addWidget(
self
.ext_input,
1
,
1
)
top_layout.addWidget(QLabel(
"搜索内容:"
),
2
,
0
)
top_layout.addWidget(
self
.keyword_input,
2
,
1
)
top_layout.addWidget(
self
.btn_search,
0
,
3
,
3
,
1
)
stats_panel
=
QWidget()
stats_layout
=
QHBoxLayout(stats_panel)
self
.file_count
=
QLabel(
"文件总数: 0"
)
self
.line_count
=
QLabel(
"匹配行数: 0"
)
self
.time_label
=
QLabel(
"耗时: 0.00秒"
)
stats_layout.addWidget(
self
.file_count)
stats_layout.addWidget(
self
.line_count)
stats_layout.addWidget(
self
.time_label)
content_panel
=
QWidget()
content_layout
=
QHBoxLayout(content_panel)
self
.tree
=
QTreeWidget()
self
.tree.setHeaderLabels([
"文件名"
,
"大小"
,
"路径"
])
self
.tree.setColumnWidth(
0
,
250
)
self
.tree.setColumnWidth(
1
,
100
)
self
.tree.doubleClicked.connect(
self
.on_tree_double_click)
tree_scroll
=
QScrollArea()
tree_scroll.setWidgetResizable(
True
)
tree_scroll.setWidget(
self
.tree)
right_panel
=
QWidget()
right_layout
=
QVBoxLayout(right_panel)
btn_panel
=
QWidget()
btn_layout
=
QHBoxLayout(btn_panel)
self
.btn_search_all
=
QPushButton(
"搜索所有文件"
)
self
.btn_export_matches
=
QPushButton(
"导出结果"
)
self
.btn_export_tree
=
QPushButton(
"导出树信息"
)
self
.btn_import_tree
=
QPushButton(
"导入树信息"
)
btn_layout.addWidget(
self
.btn_search_all)
btn_layout.addWidget(
self
.btn_export_matches)
btn_layout.addWidget(
self
.btn_export_tree)
btn_layout.addWidget(
self
.btn_import_tree)
single_panel
=
QWidget()
single_layout
=
QHBoxLayout(single_panel)
self
.single_input
=
DraggableLineEdit()
btn_single
=
QPushButton(
"搜索"
)
btn_single.clicked.connect(
self
.single_file_search)
single_layout.addWidget(QLabel(
"单文件搜索:"
))
single_layout.addWidget(
self
.single_input)
single_layout.addWidget(btn_single)
self
.match_list
=
QListWidget()
self
.match_list.doubleClicked.connect(
self
.on_list_double_click)
list_scroll
=
QScrollArea()
list_scroll.setWidgetResizable(
True
)
list_scroll.setWidget(
self
.match_list)
self
.detail_text
=
QTextEdit()
self
.detail_text.setReadOnly(
True
)
right_layout.addWidget(single_panel)
right_layout.addWidget(btn_panel)
right_layout.addWidget(list_scroll)
right_layout.addWidget(
self
.detail_text)
content_layout.addWidget(tree_scroll)
content_layout.addWidget(right_panel)
main_layout.addWidget(top_panel)
main_layout.addWidget(stats_panel)
main_layout.addWidget(content_panel)
self
.btn_search_all.clicked.connect(
self
.search_all_files)
self
.btn_export_matches.clicked.connect(
self
.export_match_list)
self
.btn_export_tree.clicked.connect(
self
.export_tree_info)
self
.btn_import_tree.clicked.connect(
self
.import_tree_info)
def
create_browse_btn(
self
):
btn
=
QPushButton(
"浏览"
)
btn.clicked.connect(
self
.browse_folder)
btn.setFixedSize(QSize(
80
,
30
))
return
btn
def
browse_folder(
self
):
path
=
QFileDialog.getExistingDirectory(
self
,
"选择文件夹"
)
if
path:
self
.folder_input.setText(path)
def
start_search(
self
):
if
self
.search_thread
and
self
.search_thread.isRunning():
return
folder
=
self
.folder_input.text()
exts
=
self
.ext_input.text().strip().split(
";"
)
keyword
=
self
.keyword_input.text().strip()
if
not
all
([folder, exts, keyword]):
QMessageBox.critical(
self
,
"错误"
,
"请填写所有搜索条件"
)
return
self
.tree.clear()
self
.match_list.clear()
self
.current_matches
=
[]
self
.update_counts()
self
.start_time
=
time.time()
self
.search_thread
=
SearchWorker(folder, exts, keyword)
self
.search_thread.update_file.connect(
self
.add_file_result)
self
.search_thread.finished.connect(
self
.on_search_finished)
self
.search_thread.start()
self
.btn_search.setEnabled(
False
)
def
add_file_result(
self
, data):
item
=
QTreeWidgetItem()
item.setText(
0
, data[
"name"
])
item.setText(
1
, data[
"size"
])
item.setText(
2
, data[
"path"
])
self
.tree.addTopLevelItem(item)
self
.file_count.setText(f
"文件总数: {self.tree.topLevelItemCount()}"
)
def
on_search_finished(
self
):
self
.btn_search.setEnabled(
True
)
elapsed
=
time.time()
-
self
.start_time
self
.time_label.setText(f
"耗时: {elapsed:.2f}秒"
)
def
single_file_search(
self
):
path
=
self
.single_input.text()
keyword
=
self
.keyword_input.text().strip()
if
not
os.path.isfile(path):
QMessageBox.critical(
self
,
"错误"
,
"无效的文件路径"
)
return
self
.match_list.clear()
self
.current_matches
=
[]
self
.file_reader
=
FileReader(path, keyword)
self
.file_reader.update_line.connect(
self
.match_list.addItem)
self
.file_reader.finished.connect(
lambda
: (
self
.line_count.setText(f
"匹配行数: {self.match_list.count()}"
),
self
.current_matches.extend(
self
.file_reader.results)
))
self
.file_reader.start()
def
on_tree_double_click(
self
):
item
=
self
.tree.currentItem()
if
not
item:
return
path
=
item.text(
2
)
keyword
=
self
.keyword_input.text().strip()
self
.match_list.clear()
self
.current_matches
=
[]
self
.file_reader
=
FileReader(path, keyword)
self
.file_reader.update_line.connect(
self
.match_list.addItem)
self
.file_reader.finished.connect(
lambda
: (
self
.line_count.setText(f
"匹配行数: {self.match_list.count()}"
),
self
.current_matches.extend(
self
.file_reader.results)
))
self
.file_reader.start()
def
on_list_double_click(
self
):
index
=
self
.match_list.currentRow()
if
0
<
=
index <
len
(
self
.current_matches):
self
.detail_text.setPlainText(
self
.current_matches[index])
def
search_all_files(
self
):
paths
=
[]
root
=
self
.tree.invisibleRootItem()
for
i
in
range
(root.childCount()):
item
=
root.child(i)
paths.append(item.text(
2
))
keyword
=
self
.keyword_input.text().strip()
if
not
keyword:
QMessageBox.critical(
self
,
"错误"
,
"请输入搜索关键字"
)
return
self
.match_list.clear()
self
.current_matches
=
[]
self
.all_files_reader
=
AllFilesReader(paths, keyword)
self
.all_files_reader.update_line.connect(
self
.match_list.addItem)
self
.all_files_reader.finished.connect(
lambda
: (
self
.line_count.setText(f
"匹配行数: {self.match_list.count()}"
),
self
.current_matches.extend(
self
.file_reader.results)
if
self
.file_reader
else
None
))
self
.all_files_reader.start()
def
export_match_list(
self
):
keyword
=
self
.keyword_input.text().strip()
or
"search"
timestamp
=
time.strftime(
"%Y%m%d_%H%M%S"
)
filename
=
f
"{keyword}_{timestamp}.txt"
desktop
=
f
"D:/桌面/"
path
=
os.path.join(desktop, filename)
with
open
(path,
'w'
, encoding
=
'utf-8'
) as f:
for
i
in
range
(
self
.match_list.count()):
f.write(
self
.match_list.item(i).text()
+
"\n"
)
QMessageBox.information(
self
,
"导出完成"
, f
"文件已保存到:{path}"
)
def
export_tree_info(
self
):
items
=
[]
root
=
self
.tree.invisibleRootItem()
for
i
in
range
(root.childCount()):
item
=
root.child(i)
items.append(
"\t"
.join([
item.text(
0
),
item.text(
1
),
item.text(
2
)
]))
timestamp
=
time.strftime(
"%Y%m%d_%H%M%S"
)
filename
=
f
"tree_export_{timestamp}.txt"
desktop
=
f
"D:/桌面/"
path
=
os.path.join(desktop, filename)
with
open
(path,
'w'
, encoding
=
'utf-8'
) as f:
f.write(
"\n"
.join(items))
QMessageBox.information(
self
,
"导出完成"
, f
"树结构已保存到:{path}"
)
def
import_tree_info(
self
):
path, _
=
QFileDialog.getOpenFileName(
self
,
"选择导入文件"
, "
", "
文本文件 (
*
.txt)")
if
not
path:
return
self
.tree.clear()
with
open
(path,
'r'
, encoding
=
'utf-8'
) as f:
for
line
in
f:
parts
=
line.strip().split(
'\t'
)
if
len
(parts) !
=
3
:
continue
item
=
QTreeWidgetItem()
item.setText(
0
, parts[
0
])
item.setText(
1
, parts[
1
])
item.setText(
2
, parts[
2
])
self
.tree.addTopLevelItem(item)
self
.file_count.setText(f
"文件总数: {self.tree.topLevelItemCount()}"
)
def
update_counts(
self
):
self
.file_count.setText(f
"文件总数: {self.tree.topLevelItemCount()}"
)
self
.line_count.setText(f
"匹配行数: {self.match_list.count()}"
)
if
__name__
=
=
"__main__"
:
app
=
QApplication(sys.argv)
window
=
MainWindow()
window.show()
sys.exit(app.
exec
())