仿everything制作搞笑检索设计(是的,我没有打错字)

everything的确是一个很好的工具,所以仿照开发一个

# -*- coding: utf-8 -*-

import sys
import os
import sqlite3
import threading
from pathlib import Path
from PyQt5.QtWidgets import (
    QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
    QPushButton, QLineEdit, QListWidget, QLabel, QCheckBox, QProgressBar,
    QMessageBox, QGridLayout, QGroupBox
)
from PyQt5.QtCore import Qt, pyqtSignal, QObject

class IndexerSignals(QObject):
    count_update = pyqtSignal(int)       
    status = pyqtSignal(str)             
    current_file = pyqtSignal(str)      
    finished = pyqtSignal(int)          
    error = pyqtSignal(str)

class FileIndexer(QObject):
    def __init__(self):
        super().__init__()
        self.signals = IndexerSignals()
        self.stop_requested = False

    def stop(self):
        self.stop_requested = True

    def build_index(self, roots):
        db_path = Path.home() / ".file_searcher_db.sqlite"
        
        # 删除旧库,确保干净重建
        if db_path.exists():
            try:
                os.remove(db_path)
            except:
                pass

        try:
            conn = sqlite3.connect(str(db_path))
            cur = conn.cursor()
            
          
            cur.execute("""
                CREATE VIRTUAL TABLE IF NOT EXISTS files USING fts5(
                    name, 
                    path UNINDEXED,  -- UNINDEXED 表示该字段存储但不参与全文索引(节省空间且避免误搜)
                    is_dir UNINDEXED,
                    tokenize = 'unicode61'
                );
            """)
            conn.commit()

            processed_count = 0
            batch = []
            
            self.signals.status.emit("正在极速扫描中...")
            

            for root in roots:
                if self.stop_requested:
                    break
                

                for dirpath, dirnames, filenames in os.walk(root):
                    if self.stop_requested:
                        break
                    
                    # 1. 处理文件夹
                    for dname in dirnames:
                        full_path = os.path.join(dirpath, dname)
                        batch.append((dname, full_path, 1)) # 1 代表文件夹
                        processed_count += 1
                    
                    # 2. 处理文件
                    for fname in filenames:
                        full_path = os.path.join(dirpath, fname)
                        batch.append((fname, full_path, 0)) # 0 代表文件
                        processed_count += 1

                    if len(batch) >= 2000:
                        cur.executemany("INSERT INTO files(name, path, is_dir) VALUES (?, ?, ?)", batch)
                        conn.commit()
                        batch.clear()
                        
                        # 发送信号更新界面
                        self.signals.count_update.emit(processed_count)
                        self.signals.current_file.emit(dirpath) # 显示当前扫描到的目录即可

            # 写入剩余的数据
            if batch:
                cur.executemany("INSERT INTO files(name, path, is_dir) VALUES (?, ?, ?)", batch)
                conn.commit()

            conn.close()
            
            if not self.stop_requested:
                self.signals.finished.emit(processed_count)
            else:
                self.signals.status.emit("索引已停止")

        except Exception as e:
            self.signals.error.emit(str(e))

class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("极速文件搜索器 v3.0 (逻辑修正版)")
        self.setGeometry(200, 100, 950, 700)
        self.db_path = Path.home() / ".file_searcher_db.sqlite"
        self.indexer = FileIndexer()
        self.thread = None
        self.init_ui()

    def init_ui(self):
        central = QWidget()
        self.setCentralWidget(central)
        layout = QVBoxLayout(central)

        # === 1. 盘符选择 ===
        group_disk = QGroupBox("1. 索引设置")
        h_disk = QHBoxLayout()
        
        self.drives_layout = QGridLayout()
        self.checkboxes = {}
        self.refresh_drives() # 初始化盘符
        
        btn_refresh = QPushButton("刷新盘符")
        btn_refresh.clicked.connect(self.refresh_drives)
        
        h_disk.addLayout(self.drives_layout)
        h_disk.addWidget(btn_refresh)
        group_disk.setLayout(h_disk)

        # === 2. 控制台与进度 ===
        group_ctrl = QGroupBox("2. 索引控制")
        v_ctrl = QVBoxLayout()
        
        h_btns = QHBoxLayout()
        self.btn_update = QPushButton("开始建立索引")
        self.btn_update.setStyleSheet("background-color: #2E7D32; color: white; font-weight: bold; padding: 8px;")
        self.btn_update.clicked.connect(self.start_indexing)
        
        self.btn_stop = QPushButton("停止")
        self.btn_stop.setStyleSheet("background-color: #C62828; color: white; padding: 8px;")
        self.btn_stop.clicked.connect(self.stop_indexing)
        self.btn_stop.setEnabled(False)
        
        h_btns.addWidget(self.btn_update)
        h_btns.addWidget(self.btn_stop)
        
        self.lbl_status = QLabel("准备就绪")
        self.progress_bar = QProgressBar()
        self.progress_bar.setTextVisible(False) # 不显示百分比,因为没有总数
        self.progress_bar.setRange(0, 0)        
        self.progress_bar.hide()
        
        self.lbl_count = QLabel("已收录: 0")
        self.lbl_current = QLabel("...")
        self.lbl_current.setStyleSheet("color: gray; font-size: 10px;")
        
        v_ctrl.addLayout(h_btns)
        v_ctrl.addWidget(self.lbl_status)
        v_ctrl.addWidget(self.progress_bar)
        
        h_info = QHBoxLayout()
        h_info.addWidget(self.lbl_count)
        h_info.addWidget(self.lbl_current)
        v_ctrl.addLayout(h_info)
        
        group_ctrl.setLayout(v_ctrl)

        # === 3. 搜索区域 ===
        group_search = QGroupBox("3. 极速搜索")
        v_search = QVBoxLayout()
        
       
        h_filter = QHBoxLayout()
        self.chk_search_file = QCheckBox("搜文件")
        self.chk_search_file.setChecked(True)
        self.chk_search_file.stateChanged.connect(lambda: self.on_search(self.edit_search.text()))
        
        self.chk_search_dir = QCheckBox("搜文件夹")
        self.chk_search_dir.setChecked(True)
        self.chk_search_dir.stateChanged.connect(lambda: self.on_search(self.edit_search.text()))
        
        h_filter.addWidget(QLabel("过滤类型:"))
        h_filter.addWidget(self.chk_search_file)
        h_filter.addWidget(self.chk_search_dir)
        h_filter.addStretch()
        
        self.edit_search = QLineEdit()
        self.edit_search.setPlaceholderText("输入文件名关键词(空格隔开表示“与”,例如:合同 2024)...")
        self.edit_search.setStyleSheet("font-size: 14pt; padding: 6px;")
        self.edit_search.textChanged.connect(self.on_search)
        
        self.list_result = QListWidget()
        self.list_result.setStyleSheet("font-size: 11pt;")
        self.list_result.itemDoubleClicked.connect(self.open_item)
        
        v_search.addLayout(h_filter)
        v_search.addWidget(self.edit_search)
        v_search.addWidget(self.list_result)
        group_search.setLayout(v_search)

        # 添加到主布局
        layout.addWidget(group_disk)
        layout.addWidget(group_ctrl)
        layout.addWidget(group_search)

    def get_drives(self):
        drives = []
        for letter in "ABCDEFGHIJKLMNOPQRSTUVWXYZ":
            d = f"{letter}:\\"
            if os.path.exists(d):
                drives.append(d)
        return drives

    def refresh_drives(self):
        # 清除旧的
        for i in reversed(range(self.drives_layout.count())): 
            self.drives_layout.itemAt(i).widget().setParent(None)
        self.checkboxes.clear()
        
        drives = self.get_drives()
        for i, d in enumerate(drives):
            cb = QCheckBox(d)
            # 默认只勾选 C 和 D,避免全部勾选太慢
            if d.startswith("C") or d.startswith("D"):
                cb.setChecked(True)
            self.checkboxes[d] = cb
            self.drives_layout.addWidget(cb, i // 6, i % 6)

    def start_indexing(self):
        roots = [d for d, cb in self.checkboxes.items() if cb.isChecked()]
        if not roots:
            QMessageBox.warning(self, "提示", "请至少选择一个盘符!")
            return

        self.btn_update.setEnabled(False)
        self.btn_stop.setEnabled(True)
        self.progress_bar.show()
        self.list_result.clear()
        self.list_result.addItem("正在建立索引,建立过程中也可以尝试搜索...")
        
        self.indexer = FileIndexer()
        self.thread = threading.Thread(target=self.indexer.build_index, args=(roots,), daemon=True)
        
        self.indexer.signals.count_update.connect(lambda n: self.lbl_count.setText(f"已收录: {n:,}"))
        self.indexer.signals.status.connect(self.lbl_status.setText)
        self.indexer.signals.current_file.connect(self.lbl_current.setText)
        self.indexer.signals.finished.connect(self.on_finished)
        
        self.thread.start()

    def stop_indexing(self):
        self.indexer.stop()
        self.btn_stop.setEnabled(False)
        self.lbl_status.setText("正在停止...")

    def on_finished(self, total):
        self.progress_bar.hide()
        self.btn_update.setEnabled(True)
        self.btn_stop.setEnabled(False)
        self.lbl_status.setText("索引完成")
        self.lbl_current.setText("")
        QMessageBox.information(self, "完成", f"索引更新完毕!\n本次共收录 {total:,} 个项目。")
        self.list_result.clear()

    def on_search(self, text):
        text = text.strip()
        if not self.db_path.exists():
            return

        # 检查过滤条件
        show_files = self.chk_search_file.isChecked()
        show_dirs = self.chk_search_dir.isChecked()
        
        if not text:
            self.list_result.clear()
            return
            
        if not show_files and not show_dirs:
            self.list_result.clear()
            self.list_result.addItem("请至少勾选一种类型(文件或文件夹)")
            return

        tokens = text.split()
        fts_query_parts = []
        for t in tokens:
            # 加上 * 使得搜索 "con" 能匹配 "config"
            # 语法:name : "keyword*"
            fts_query_parts.append(f'name : "{t}*"')
        
        fts_query = " AND ".join(fts_query_parts)

        # 构建类型过滤 SQL
        type_filters = []
        if show_files: type_filters.append(0)
        if show_dirs: type_filters.append(1)
        type_sql = ",".join(map(str, type_filters))

        try:
            conn = sqlite3.connect(str(self.db_path))
            cur = conn.cursor()
            
            # 核心查询语句
            sql = f"""
                SELECT name, path, is_dir FROM files 
                WHERE files MATCH ? 
                AND is_dir IN ({type_sql})
                ORDER BY rank 
                LIMIT 500
            """
            
            cur.execute(sql, (fts_query,))
            rows = cur.fetchall()
            conn.close()

            self.list_result.clear()
            if not rows:
                self.list_result.addItem("未找到匹配项")
                return

            self.list_result.addItem(f"--- 找到 {len(rows)} 个结果 ---")
            for name, path, is_dir in rows:
                icon = "📁" if is_dir else "📄"
                item_text = f"{icon} {name}  →  {path}"
                self.list_result.addItem(item_text)
                
        except Exception as e:
            # 输入特殊字符时可能导致 FTS 语法错误,忽略即可
            pass

    def open_item(self, item):
        text = item.text()
        if "→" in text:
            path = text.split("→", 1)[1].strip()
            try:
                os.startfile(path)
            except Exception as e:
                QMessageBox.warning(self, "错误", f"无法打开路径:\n{path}\n错误信息:{e}")

if __name__ == "__main__":
    app = QApplication(sys.argv)
    win = MainWindow()
    win.show()
    sys.exit(app.exec_())

有什么改进思路希望多多指出

posted @ 2025-12-09 20:24  北风渡  阅读(0)  评论(0)    收藏  举报