重磅推出!带有人脸识别的锁屏软件。

你可曾因出去上厕所惨遭机房惨案?你可曾因密码被别人偷看而 AKIOI?你可曾在一顿饭的功夫做出 +∞+\infty 道黑题?

不可抗因素背景
  • 本机房因为极域和系统锁屏的冲突,系统锁屏被物理禁止了。

  • 系统自带的人像识别需要支持 Windows Hello 的红外摄像头,很多电脑都没有。

这是一款开源、免费且实用的工具,支持密码与刷脸双重识别,同时提供背景自定义及透明设计,并可在锁屏界面上显示时间。有需要的朋友,欢迎领取。

Github 地址。

注意:face_detection_yunet_2023mar.onnx 和 face_recognition_sface_2021dec.onnx 这两个库需要自己下,文件有点大,放不进 Github。


蓝奏云下载链接,由于我没有 VIP,就分三个放了:第一个,第二个,第三个,如果觉得好用记得点赞收藏。

有 Bug 记得私信,很多 Bug 可以解决,这里 会放常见问题。

等之后更新了可以考虑做安装包。


Tips

  • 这是开源项目,代码如果不符合你的喜好,可以自己改(显然可以用 AI)。

  • 人像识别是检测到你点头就解锁,如果你觉得不准确,可以考虑好好拍照(可以多拍点),实在不行把那个相似度的线降低(recognize2.exe 就是这样的,但是我还没放)。


制作耗时不知道多少小时,点个赞吧~。

不知道是不是洛谷首发。

项目借助 DeepSeek 生成。

1. get.py – 人脸照片采集器

  • 使用 YuNet 检测人脸,实时显示摄像头画面。

  • 手动(空格)或自动(每 2 秒)保存检测到的人脸区域,存入 ref_images/ 文件夹。

  • 用于提前录入用户的多角度、多表情人脸样本,供后续识别时生成平均特征。

2. recognize.py – 人脸识别与活体检测(点头)

  • 加载 YuNet 检测器和 SFace 特征提取器,计算 ref_images 中所有人脸的平均特征(归一化后作为参考)。
  • 主程序后台运行,通过 Windows 钩子监控鼠标移动:一旦鼠标被移动,立即启动 30 秒识别窗口。
  • 识别流程:摄像头持续检测人脸,同时分析头部 俯仰角(pitch) 变化来判断是否“点头”(要求俯仰差 ≥10°≥10° 且宽高比变化小)。
  • 点头成功后再连续 44 帧验证人脸特征余弦相似度 ≥0.7≥0.7,全部通过后执行:
    • 调用 terminate_process_by_name 杀死 lockscreen.exe(解锁)。
    • 切换输入法至搜狗/微软拼音(用于恢复输入环境)。

3. main.py – 配置界面(系统托盘程序)

  • 基于 customtkinter 的图形界面,提供多个选项卡:
    • 仪表板:显示系统状态,一键锁屏。
    • 快捷键:自定义快速锁屏的组合键(默认 Ctrl+Alt+L)。
    • 密码管理:修改锁屏密码(SHA256 加密存储)。
    • 外观设置:选择锁屏壁纸、开启透明背景、是否显示时间。
    • 高级设置:启用人脸识别联动。
    • 关于:版本信息。
  • 设置保存为 log/settings.json,支持浅色/深色/自动主题。
  • 程序启动后在系统托盘驻留,提供“立即锁屏”“显示主窗口”“退出程序”菜单。

4. lockscreen.py – 锁屏执行程序

  • 以管理员权限运行,创建全屏置顶窗口,拦截所有键盘鼠标输入(低层钩子)。
  • 支持自定义壁纸、透明背景、锁屏显示当前时间。
  • 按下 ESC 键弹出密码输入对话框(输入法强制为美式英语,禁止右键菜单)。
  • 密码验证成功则退出锁屏;失败时对话框抖动并清空输入。
  • 同时每隔 30 秒重载 settings.json,实现壁纸、时间、透明模式的动态更新。
  • 钩子线程具备自动重连机制,保证长时间运行不失效。

整体工作流

  1. 管理员运行 main.py(托盘程序)并启用人脸识别联动。
  2. 按下全局快捷键(或点击托盘菜单)→ 启动锁屏(lockscreen.py)和识别程序(recognize.py)。
  3. 锁屏界面出现,鼠标键盘被拦截;识别程序后台等待鼠标移动。
  4. 用户移动鼠标 → 识别程序打开摄像头,要求点头 + 人脸比对。
  5. 识别成功 → 自动终止 lockscreen.exe 并切换输入法,完成解锁。
  6. 若识别失败,30 秒后自动关闭摄像头,继续等待下一次鼠标移动。若使用密码,按 ESC 弹出密码框,输入正确密码同样可解锁。

目前暂时没放编译好的文件,下面是代码,可以随便看看,等我找到合适的云盘就把编译好的 exe 放进去。

下面的代码可供爱好者参考。

get.py
Python
import cv2
import os
import time
import sys

save_dir = "ref_images"
os.makedirs(save_dir, exist_ok=True)

cap = cv2.VideoCapture(0, cv2.CAP_DSHOW)
if not cap.isOpened():
    print("[ERROR] 无法打开摄像头")
    sys.exit(1)

# ------------------------ 加载 YuNet 人脸检测模型 ------------------------
YUNET_MODEL = "model/face_detection_yunet_2023mar.onnx"
if not os.path.exists(YUNET_MODEL):
    print(f"[ERROR] 未找到 YuNet 模型: {YUNET_MODEL}")
    print("请确保 model/face_detection_yunet_2023mar.onnx 存在")
    sys.exit(1)

detector = cv2.FaceDetectorYN.create(YUNET_MODEL, "", (320, 320))
print("[INFO] YuNet 人脸检测器加载成功")

print("=" * 50)
print("【智能训练照片采集器】")
print("功能:自动检测人脸并保存,支持手动/自动抓拍")
print("操作:")
print("  - 保持人脸在绿色框内")
print("  - 按 SPACE 手动抓拍")
print("  - 自动模式:每2秒保存一张(需检测到人脸)")
print("  - 按 ESC 退出")
print("提示:请采集不同微角度(左/右偏<30°)、不同表情(微笑/严肃)")
print("      确保光线均匀,面部清晰,背景简单")
print("=" * 50)

count = 0
last_cap_time = 0

while True:
    ret, frame = cap.read()
    if not ret:
        continue

    h, w = frame.shape[:2]
    detector.setInputSize((w, h))
    result = detector.detect(frame)

    faces = []
    if result[1] is not None:
        # 将 YuNet 输出的 face 转成 (x, y, w, h) 矩形框
        for face in result[1]:
            x, y, width, height = face[0:4].astype(int)
            faces.append((x, y, width, height))
            cv2.rectangle(frame, (x, y), (x+width, y+height), (0, 255, 0), 2)

    # 显示信息
    cv2.putText(frame, f"Saved: {count}", (10, 30),
                cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0, 255, 0), 2)
    cv2.putText(frame, "SPACE: save  |  ESC: quit", (10, 65),
                cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 255, 0), 2)

    cv2.imshow("Train Image Collector", frame)

    key = cv2.waitKey(1) & 0xFF
    now = time.time()

    if len(faces) > 0:
        # 手动抓拍或自动抓拍
        if key == 32 or (now - last_cap_time >= 2):
            # 取面积最大的人脸
            x, y, w, h = max(faces, key=lambda r: r[2]*r[3])
            face_img = frame[y:y+h, x:x+w]
            # 保存
            filename = os.path.join(save_dir, f"me_{count:04d}.jpg")
            cv2.imwrite(filename, face_img)
            print(f"已保存: {filename}")
            count += 1
            last_cap_time = now

    if key == 27:  # ESC
        break

cap.release()
cv2.destroyAllWindows()
print(f"\n共采集 {count} 张照片,保存在 '{save_dir}' 文件夹。")
print("现在请重新运行识别程序。")

lockscreen.py
Python
"""
锁屏程序 - 普通版(完整鼠标拦截,强制英文,允许 Shift)
彻底重构钩子与日志,解决卡顿、钩子失效、窗口抖动偏移问题
功能:
- 全屏置顶,拦截键盘鼠标,仅ESC唤出密码框
- 自动切换至美式英语键盘布局,杜绝中文输入
- 允许 Shift 键(大小写切换)
- 支持自定义壁纸、透明背景、时间显示
- 密码错误抖动
- 调试模式(10秒自动解锁)
- 解锁后恢复原始键盘布局
- **精确鼠标拦截**:密码框显示时只能点击密码框内控件,无法点击其他区域
- 彻底禁用右键菜单
- 低CPU占用,无频繁磁盘I/O
- 【已删除托盘图标功能】
- **钩子永不失效**:ESC直接触发主线程回调,无需鼠标干预
"""
import os
import sys
import json
import ctypes
import ctypes.wintypes
import hashlib
import threading
import subprocess
import tkinter as tk
from tkinter import messagebox
from PIL import Image, ImageTk
import time
import atexit
import signal
import traceback
from datetime import datetime

# ---------- 加载 Windows API ----------
user32 = ctypes.WinDLL('user32', use_last_error=True)
kernel32 = ctypes.WinDLL('kernel32', use_last_error=True)

# 输入法相关
imm32 = ctypes.WinDLL('imm32')
ImmAssociateContext = imm32.ImmAssociateContext
ImmAssociateContext.argtypes = [ctypes.wintypes.HWND, ctypes.wintypes.HANDLE]
ImmAssociateContext.restype = ctypes.wintypes.HANDLE

# 键盘布局
LoadKeyboardLayoutW = user32.LoadKeyboardLayoutW
LoadKeyboardLayoutW.argtypes = [ctypes.c_wchar_p, ctypes.c_uint]
LoadKeyboardLayoutW.restype = ctypes.wintypes.HANDLE
ActivateKeyboardLayout = user32.ActivateKeyboardLayout
ActivateKeyboardLayout.argtypes = [ctypes.wintypes.HANDLE, ctypes.c_uint]
ActivateKeyboardLayout.restype = ctypes.wintypes.HANDLE

# 窗口句柄相关
WindowFromPoint = user32.WindowFromPoint
WindowFromPoint.argtypes = [ctypes.wintypes.POINT]
WindowFromPoint.restype = ctypes.wintypes.HWND
IsChild = user32.IsChild
IsChild.argtypes = [ctypes.wintypes.HWND, ctypes.wintypes.HWND]
IsChild.restype = ctypes.wintypes.BOOL

KLF_ACTIVATE = 1
KLF_SETFORPROCESS = 0x100
ENGLISH_US_LAYOUT = "00000409"  # 美式英语

LOG_DIR = "log"
BG_ALPHA = 0.0
PASSWORD_FILE = os.path.join(LOG_DIR, "pass")
SETTINGS_FILE = os.path.join(LOG_DIR, "settings.json")
ERROR_LOG_PATH = os.path.join(LOG_DIR, "crash.log")
LOCK_SIGNAL_FILE = os.path.join(LOG_DIR, "lock_signal.txt")

if not os.path.exists(LOG_DIR):
    os.makedirs(LOG_DIR)

def log_error(msg):
    """仅记录严重错误,不记录正常信息"""
    try:
        with open(ERROR_LOG_PATH, 'a', encoding='utf-8') as f:
            f.write(f"{time.strftime('%Y-%m-%d %H:%M:%S')} - {msg}\n")
            f.flush()
    except:
        pass

def load_settings():
    if os.path.exists(SETTINGS_FILE):
        try:
            with open(SETTINGS_FILE, 'r', encoding='utf-8') as f:
                return json.load(f)
        except:
            pass
    return {"wallpaper": "", "show_time": False, "transparent_bg": False}

# ---------- 钩子常量 ----------
WH_KEYBOARD_LL = 13
WH_MOUSE_LL = 14
WM_KEYDOWN = 0x0100
WM_KEYUP = 0x0101
WM_SYSKEYDOWN = 0x0104
WM_SYSKEYUP = 0x0105
WM_LBUTTONDOWN = 0x0201
WM_LBUTTONUP = 0x0202
WM_RBUTTONDOWN = 0x0204
WM_RBUTTONUP = 0x0205
WM_MBUTTONDOWN = 0x0207
WM_MBUTTONUP = 0x0208
WM_MOUSEWHEEL = 0x020A
WM_XBUTTONDOWN = 0x020B
WM_XBUTTONUP = 0x020C
WM_NCRBUTTONDOWN = 0x00A4
WM_NCRBUTTONUP = 0x00A5

VK_SHIFT = 0x10
VK_LSHIFT = 0xA0
VK_RSHIFT = 0xA1
VK_CTRL = 0x11
VK_ALT = 0x12
VK_LWIN = 0x5B
VK_RWIN = 0x5C
VK_ESC = 0x1B
VK_BACK = 0x08
VK_TAB = 0x09
VK_ENTER = 0x0D
VK_CAPSLOCK = 0x14
VK_DELETE = 0x2E
VK_HOME = 0x24
VK_END = 0x23
VK_LEFT = 0x25
VK_RIGHT = 0x27
VK_UP = 0x26
VK_DOWN = 0x28

class KBDLLHOOKSTRUCT(ctypes.Structure):
    _fields_ = [
        ('vkCode', ctypes.wintypes.DWORD),
        ('scanCode', ctypes.wintypes.DWORD),
        ('flags', ctypes.wintypes.DWORD),
        ('time', ctypes.wintypes.DWORD),
        ('dwExtraInfo', ctypes.POINTER(ctypes.c_ulong))
    ]

class MSLLHOOKSTRUCT(ctypes.Structure):
    _fields_ = [
        ('pt', ctypes.wintypes.POINT),
        ('mouseData', ctypes.wintypes.DWORD),
        ('flags', ctypes.wintypes.DWORD),
        ('time', ctypes.wintypes.DWORD),
        ('dwExtraInfo', ctypes.POINTER(ctypes.c_ulong))
    ]

HOOKPROC = ctypes.WINFUNCTYPE(ctypes.c_int, ctypes.c_int, ctypes.wintypes.WPARAM, ctypes.wintypes.LPARAM)

g_lock = None

# ---------- 密码管理 ----------
class PasswordManager:
    PASSWORD_FILE = PASSWORD_FILE
    SALT = b"lockscreen_secure_salt"

    @classmethod
    def _hash(cls, pwd):
        h = hashlib.sha256()
        h.update(cls.SALT)
        h.update(pwd.encode('utf-8'))
        return h.hexdigest()

    @classmethod
    def initialize(cls):
        if not os.path.exists(cls.PASSWORD_FILE):
            cls.save_password("123456")
            print("初始密码已设为 123456")

    @classmethod
    def save_password(cls, new_pwd):
        with open(cls.PASSWORD_FILE, "w") as f:
            f.write(cls._hash(new_pwd))

    @classmethod
    def verify(cls, pwd):
        if not os.path.exists(cls.PASSWORD_FILE):
            cls.initialize()
        try:
            with open(cls.PASSWORD_FILE, "r") as f:
                stored = f.read().strip()
            return stored == cls._hash(pwd)
        except:
            return False

# ---------- 钩子回调 ----------
@HOOKPROC
def keyboard_proc(nCode, wParam, lParam):
    try:
        if nCode >= 0:
            kb = ctypes.cast(lParam, ctypes.POINTER(KBDLLHOOKSTRUCT)).contents
            vkCode = kb.vkCode
            if wParam in (WM_KEYDOWN, WM_SYSKEYDOWN):
                if g_lock and g_lock.should_block_key(vkCode):
                    return 1
    except Exception:
        pass
    return user32.CallNextHookEx(None, nCode, wParam, lParam)

@HOOKPROC
def mouse_proc(nCode, wParam, lParam):
    try:
        if nCode >= 0 and g_lock and g_lock.is_locked:
            ms = ctypes.cast(lParam, ctypes.POINTER(MSLLHOOKSTRUCT)).contents
            is_click = wParam in (WM_LBUTTONDOWN, WM_LBUTTONUP,
                                   WM_RBUTTONDOWN, WM_RBUTTONUP,
                                   WM_MBUTTONDOWN, WM_MBUTTONUP,
                                   WM_XBUTTONDOWN, WM_XBUTTONUP,
                                   WM_MOUSEWHEEL,
                                   WM_NCRBUTTONDOWN, WM_NCRBUTTONUP)
            if not is_click:
                return 0

            if g_lock.dialog_shown:
                hwnd_clicked = WindowFromPoint(ms.pt)
                if hwnd_clicked and g_lock.password_dialog:
                    if hwnd_clicked == g_lock.password_dialog.winfo_id() or IsChild(g_lock.password_dialog.winfo_id(), hwnd_clicked):
                        return 0
                return 1
            else:
                return 1
    except Exception:
        pass
    return user32.CallNextHookEx(None, nCode, wParam, lParam)

# ---------- 主类 ----------
class LockScreen:
    def __init__(self, debug_mode=False):
        self._init(debug_mode)

    def _init(self, debug_mode):
        self.debug_mode = debug_mode
        self.is_locked = True
        self.dialog_shown = False
        self.password_dialog = None
        self.timer_id = None
        self.timeout_id = None
        self.keyboard_hook = None
        self.mouse_hook = None
        self.settings = load_settings()
        self.canvas = None
        self.time_text_id = None
        self.bg_image_id = None
        self.signal_check_id = None
        self.reload_id = None
        self.original_layout = None
        self.keepalive_id = None

        if not ctypes.windll.shell32.IsUserAnAdmin():
            messagebox.showerror("错误", "请以管理员身份运行!")
            sys.exit(1)

        # 保存原始键盘布局
        try:
            self.original_layout = ActivateKeyboardLayout(0, 0)
        except Exception:
            pass

        # 强制英文布局
        self._force_english_layout()

        PasswordManager.initialize()

        self.root = tk.Tk()
        self.root.title("")
        self.root.overrideredirect(True)
        self.root.attributes('-topmost', True)
        self.root.geometry(f"{self.root.winfo_screenwidth()}x{self.root.winfo_screenheight()}+0+0")
        self.root.focus_force()
        self.root.protocol("WM_DELETE_WINDOW", lambda: None)
        self.root.grab_set_global()

        self.canvas = tk.Canvas(
            self.root,
            width=self.root.winfo_screenwidth(),
            height=self.root.winfo_screenheight(),
            highlightthickness=0,
            bg='#000000'
        )
        self.canvas.pack()

        # 设置背景
        transparent_bg = self.settings.get("transparent_bg", False)
        if transparent_bg:
            self.root.attributes('-alpha', BG_ALPHA)
        else:
            wallpaper = self.settings.get("wallpaper", "")
            if wallpaper and os.path.isfile(wallpaper):
                self._set_wallpaper(wallpaper)
                self.root.attributes('-alpha', 1.0)
            else:
                self.root.attributes('-alpha', BG_ALPHA)

        if self.settings.get("show_time", False):
            self._create_time_display()

        self._check_lock_signal()
        if self.debug_mode:
            self.timer_id = self.root.after(10000, self.debug_unlock)

        self._reload_settings_periodic()

        global g_lock
        g_lock = self

        atexit.register(self.cleanup)
        signal.signal(signal.SIGTERM, self.signal_handler)
        signal.signal(signal.SIGINT, self.signal_handler)

        self.hook_thread = threading.Thread(target=self._hook_message_loop, daemon=True)
        self.hook_thread.start()

        # 保活机制:每5秒调用一次空函数,确保主线程消息循环活跃
        self._keepalive()

    def _keepalive(self):
        """空函数,仅用于唤醒主线程消息循环"""
        self.keepalive_id = self.root.after(5000, self._keepalive)

    def _force_english_layout(self):
        try:
            hkl = LoadKeyboardLayoutW(ENGLISH_US_LAYOUT, KLF_ACTIVATE | KLF_SETFORPROCESS)
            if hkl:
                ActivateKeyboardLayout(hkl, 0)
        except Exception:
            pass

    def _reload_settings_periodic(self):
        """每隔30秒重新加载设置,且仅在文件真正改变时才更新壁纸等,不写日志"""
        try:
            new_settings = load_settings()
            if new_settings != self.settings:
                # 设置发生变化,需要更新
                old_wall = self.settings.get("wallpaper")
                new_wall = new_settings.get("wallpaper")
                if new_wall != old_wall:
                    if new_wall and os.path.isfile(new_wall):
                        self._set_wallpaper(new_wall)
                    else:
                        # 清除壁纸
                        if self.bg_image_id:
                            self.canvas.delete(self.bg_image_id)
                            self.bg_image_id = None
                # 更新时间显示
                if new_settings.get("show_time") != self.settings.get("show_time"):
                    if new_settings.get("show_time"):
                        if not self.time_text_id:
                            self._create_time_display()
                    else:
                        if self.time_text_id:
                            self.canvas.delete(self.time_text_id)
                            self.time_text_id = None
                # 更新透明背景
                if new_settings.get("transparent_bg") != self.settings.get("transparent_bg"):
                    alpha = BG_ALPHA if new_settings.get("transparent_bg") else 1.0
                    self.root.attributes('-alpha', alpha)
                self.settings = new_settings
        except Exception:
            pass
        self.reload_id = self.root.after(30000, self._reload_settings_periodic)

    def _set_wallpaper(self, path):
        try:
            img = Image.open(path)
            sw = self.root.winfo_screenwidth()
            sh = self.root.winfo_screenheight()
            img = img.resize((sw, sh), Image.Resampling.LANCZOS)
            self.bg_photo = ImageTk.PhotoImage(img)
            if self.bg_image_id:
                self.canvas.delete(self.bg_image_id)
            self.bg_image_id = self.canvas.create_image(0, 0, image=self.bg_photo, anchor='nw')
            self.canvas.tag_lower(self.bg_image_id)
        except Exception:
            pass

    def _create_time_display(self):
        self.time_text_id = self.canvas.create_text(
            50, self.root.winfo_screenheight() - 50,
            text='', fill='white', font=('Segoe UI', 60, 'normal'), anchor='sw'
        )
        self._update_time()

    def _update_time(self):
        if self.time_text_id:
            now = datetime.now()
            self.canvas.itemconfig(self.time_text_id, text=f"{now.strftime('%H:%M')}\n{now.strftime('%Y-%m-%d')}")
            self.root.after(1000, self._update_time)

    def _check_lock_signal(self):
        try:
            if os.path.exists(LOCK_SIGNAL_FILE):
                self.show_password_dialog()
                os.remove(LOCK_SIGNAL_FILE)
        except Exception:
            pass
        self.signal_check_id = self.root.after(500, self._check_lock_signal)

    def signal_handler(self, signum, frame):
        self.cleanup()
        sys.exit(0)

    def cleanup(self):
        try:
            if self.keyboard_hook:
                user32.UnhookWindowsHookEx(self.keyboard_hook)
            if self.mouse_hook:
                user32.UnhookWindowsHookEx(self.mouse_hook)
            if self.signal_check_id:
                self.root.after_cancel(self.signal_check_id)
            if self.reload_id:
                self.root.after_cancel(self.reload_id)
            if self.keepalive_id:
                self.root.after_cancel(self.keepalive_id)
        except Exception:
            pass

    def _hook_message_loop(self):
        """钩子线程,无限重连"""
        while True:
            try:
                self.keyboard_hook = user32.SetWindowsHookExW(
                    WH_KEYBOARD_LL, keyboard_proc,
                    kernel32.GetModuleHandleW(None), 0
                )
                if not self.keyboard_hook:
                    raise Exception("Keyboard hook failed")

                self.mouse_hook = user32.SetWindowsHookExW(
                    WH_MOUSE_LL, mouse_proc,
                    kernel32.GetModuleHandleW(None), 0
                )
                if not self.mouse_hook:
                    raise Exception("Mouse hook failed")

                msg = ctypes.wintypes.MSG()
                while user32.GetMessageW(ctypes.byref(msg), None, 0, 0) != 0:
                    user32.TranslateMessage(ctypes.byref(msg))
                    user32.DispatchMessageW(ctypes.byref(msg))
                # 正常退出WM_QUIT - 重启线程
            except Exception:
                pass
            finally:
                if self.keyboard_hook:
                    user32.UnhookWindowsHookEx(self.keyboard_hook)
                    self.keyboard_hook = None
                if self.mouse_hook:
                    user32.UnhookWindowsHookEx(self.mouse_hook)
                    self.mouse_hook = None
                time.sleep(1)  # 等待后重试

    def should_block_key(self, vk):
        """仅做逻辑判断,不调用任何Tk方法,避免跨线程问题"""
        if not self.is_locked:
            return False
        if self.dialog_shown:
            # 密码输入模式:放行字母、数字、Shift、功能键等
            if 0x30 <= vk <= 0x39 or 0x41 <= vk <= 0x5A or 0x60 <= vk <= 0x69:
                return False
            if vk in (VK_SHIFT, VK_LSHIFT, VK_RSHIFT):
                return False
            allowed = {VK_BACK, VK_TAB, VK_ENTER, VK_DELETE, VK_HOME, VK_END,
                       VK_LEFT, VK_RIGHT, VK_UP, VK_DOWN, VK_CAPSLOCK}
            if vk in allowed:
                return False
            if vk in (VK_LWIN, VK_RWIN):
                return True
            if vk == VK_ESC:
                # 直接通过 after 调用主线程方法(线程安全)
                self.root.after(0, self.show_password_dialog)
                return True
            return True
        else:
            # 锁屏模式:仅ESC有效
            if vk in (VK_LWIN, VK_RWIN):
                return True
            if vk == VK_ESC:
                self.root.after(0, self.show_password_dialog)
                return True
            return True

    def show_password_dialog(self):
        if self.dialog_shown:
            return
        try:
            self.dialog_shown = True
            dlg = tk.Toplevel(self.root)
            dlg.title("")
            dlg.overrideredirect(True)
            dlg.attributes('-topmost', True)
            dlg.transient(self.root)
            dlg.grab_set_global()
            dlg.configure(bg='#1e1e1e')
            dlg.attributes('-alpha', 0.95)

            w, h = 380, 200
            sw, sh = self.root.winfo_screenwidth(), self.root.winfo_screenheight()
            x = sw // 2 - w // 2
            y = sh // 2 - h // 2
            dlg.geometry(f"{w}x{h}+{x}+{y}")

            # 保存原始坐标供抖动使用
            self.dialog_orig_x = x
            self.dialog_orig_y = y

            canvas = tk.Canvas(dlg, width=w, height=h, bg='#1e1e1e', highlightthickness=0)
            canvas.place(x=0, y=0)
            canvas.create_round_rect(2, 2, w-2, h-2, radius=25, fill='#2d2d2d', outline='')
            canvas.create_text(w//2, 35, text="解锁屏幕", fill='#e0e0e0', font=('Segoe UI', 16, 'bold'))

            ex, ey = w//2-120, 70
            canvas.create_round_rect(ex, ey, ex+240, ey+40, radius=10, fill='#3c3c3c', outline='')

            def validate_input(char, action):
                if action == '0':
                    return True
                if len(char) == 0:
                    return True
                return 32 <= ord(char) <= 126

            vcmd = (self.root.register(validate_input), '%S', '%d')
            entry = tk.Entry(canvas, show='●', font=('Segoe UI', 14), bg='#3c3c3c',
                             fg='white', insertbackground='#6ab04c', bd=0, highlightthickness=0,
                             validate='key', validatecommand=vcmd)
            entry.place(x=ex+10, y=ey+8, width=220, height=24)

            hwnd = entry.winfo_id()
            ImmAssociateContext(hwnd, None)

            entry.bind('<Button-3>', lambda e: 'break')
            canvas.bind('<Button-3>', lambda e: 'break')
            dlg.bind('<Button-3>', lambda e: 'break')

            def on_focus_in(event):
                self._force_english_layout()
                ImmAssociateContext(entry.winfo_id(), None)
            entry.bind('<FocusIn>', on_focus_in)

            btn_y = 130
            unlock = tk.Button(canvas, text="解锁", command=lambda: self.check_password(entry),
                               bg='#6ab04c', fg='white', font=('Segoe UI', 11),
                               relief='flat', bd=0, cursor='hand2')
            unlock.place(x=w//2-110, y=btn_y, width=100, height=35)
            cancel = tk.Button(canvas, text="取消", command=self.hide_password_dialog,
                               bg='#d63031', fg='white', font=('Segoe UI', 11),
                               relief='flat', bd=0, cursor='hand2')
            cancel.place(x=w//2+10, y=btn_y, width=100, height=35)

            close_btn = tk.Button(canvas, text="✕", bg='#2d2d2d', fg='#a0a0a0',
                                  font=('Segoe UI', 10), bd=0, command=self.hide_password_dialog,
                                  activebackground='#c42b1c')
            close_btn.place(x=w-35, y=5, width=25, height=25)

            dlg.lift()
            dlg.focus_force()
            dlg.update()
            dlg.lift()
            dlg.focus_force()
            entry.focus_set()

            def reset_timeout():
                if self.timeout_id:
                    dlg.after_cancel(self.timeout_id)
                self.timeout_id = dlg.after(3000, self.hide_password_dialog)
            entry.bind('<Key>', lambda e: reset_timeout())
            reset_timeout()
            entry.bind('<Return>', lambda e: self.check_password(entry))

            self.password_dialog = dlg
            self.password_entry = entry
        except Exception as e:
            log_error(f"show_password_dialog error: {traceback.format_exc()}")
            self.dialog_shown = False

    def hide_password_dialog(self):
        try:
            if self.password_dialog:
                if self.timeout_id:
                    self.password_dialog.after_cancel(self.timeout_id)
                    self.timeout_id = None
                self.password_dialog.grab_release()
                self.password_dialog.destroy()
                self.password_dialog = None
            self.dialog_shown = False
        except Exception:
            pass

    def check_password(self, entry):
        try:
            if PasswordManager.verify(entry.get()):
                self.unlock()
            else:
                self.shake_window()
                entry.delete(0, tk.END)
                if self.password_dialog:
                    self.password_dialog.lift()
                    self.password_dialog.focus_force()
                    entry.focus_set()
                    self._force_english_layout()
                    ImmAssociateContext(entry.winfo_id(), None)
        except Exception as e:
            log_error(f"check_password error: {e}")

    def shake_window(self):
        if not self.password_dialog:
            return
        dlg = self.password_dialog
        orig_x = getattr(self, 'dialog_orig_x', None)
        orig_y = getattr(self, 'dialog_orig_y', None)
        if orig_x is None or orig_y is None:
            orig_x = dlg.winfo_x()
            orig_y = dlg.winfo_y()
        offsets = [5, -5, 5, -5, 3, -3, 2, -2, 0]
        idx = 0
        def step():
            nonlocal idx
            if idx >= len(offsets):
                dlg.geometry(f"+{orig_x}+{orig_y}")
                return
            off = offsets[idx]
            dlg.geometry(f"+{orig_x + off}+{orig_y}")
            idx += 1
            dlg.after(30, step)
        step()

    def unlock(self):
        self.is_locked = False
        if self.timer_id:
            self.root.after_cancel(self.timer_id)
        self.cleanup()
        if hasattr(self, 'original_layout') and self.original_layout:
            try:
                ActivateKeyboardLayout(self.original_layout, 0)
            except Exception:
                pass
        self.root.quit()
        self.root.destroy()
        os._exit(0)

    def debug_unlock(self):
        if self.is_locked:
            print("调试模式自动解锁")
            self.unlock()

    def open_config(self):
        try:
            base = os.path.dirname(os.path.abspath(sys.argv[0]))
            exe = os.path.join(base, 'config.exe')
            py = os.path.join(base, 'config.py')
            if getattr(sys, 'frozen', False):
                subprocess.Popen([exe] if os.path.exists(exe) else [py])
            else:
                subprocess.Popen([sys.executable, py] if os.path.exists(py) else [exe])
        except Exception:
            pass

    def run(self):
        try:
            self.root.mainloop()
        except Exception as e:
            log_error(f"run error: {traceback.format_exc()}")
        finally:
            self.cleanup()

def _create_round_rect(self, x1, y1, x2, y2, radius=20, **kwargs):
    points = [x1+radius, y1,
              x2-radius, y1,
              x2, y1,
              x2, y1+radius,
              x2, y2-radius,
              x2, y2,
              x2-radius, y2,
              x1+radius, y2,
              x1, y2,
              x1, y2-radius,
              x1, y1+radius,
              x1, y1]
    return self.create_polygon(points, smooth=True, **kwargs)

tk.Canvas.create_round_rect = _create_round_rect

if __name__ == "__main__":
    try:
        print("锁屏程序启动(普通版,无托盘图标,钩子直接回调)")
        lock = LockScreen(debug_mode=False)
        lock.run()
    except Exception as e:
        log_error(traceback.format_exc())

main.py
Python
# main.py - 快捷键稳定版 | CPU优化 | 无鼠标转圈 | @Zhl2010
import os
import sys
import json
import hashlib
import subprocess
import threading
import customtkinter as ctk
from tkinter import filedialog
from PIL import Image, ImageDraw
import ctypes
from ctypes import wintypes
import time
import gc
import logging

# 使用 keyboard 库(稳定且 CPU 占用低)
try:
    import keyboard
except ImportError:
    print("请先安装 keyboard: pip install keyboard")
    sys.exit(1)

# 确保 pystray 已安装
try:
    import pystray
except ImportError:
    print("请先安装 pystray: pip install pystray")
    sys.exit(1)

# 配置简单日志,便于排查热键问题
logging.basicConfig(level=logging.INFO, format='[%(levelname)s] %(message)s')

# ------------------------------- 路径与目录 -------------------------------
LOG_DIR = "log"
PASSWORD_FILE = os.path.join(LOG_DIR, "pass")
SETTINGS_FILE = os.path.join(LOG_DIR, "settings.json")
ICON_FILE = os.path.join(LOG_DIR, "icon.png")

if not os.path.exists(LOG_DIR):
    os.makedirs(LOG_DIR)

# ------------------------------- 密码管理器 -------------------------------
class PasswordManager:
    PASSWORD_FILE = PASSWORD_FILE
    SALT = b"lockscreen_secure_salt"

    @classmethod
    def _hash(cls, pwd):
        h = hashlib.sha256()
        h.update(cls.SALT)
        h.update(pwd.encode('utf-8'))
        return h.hexdigest()

    @classmethod
    def initialize(cls):
        if not os.path.exists(cls.PASSWORD_FILE):
            cls.save_password("123456")
            print("初始密码已设为 123456")

    @classmethod
    def save_password(cls, new_pwd):
        with open(cls.PASSWORD_FILE, "w") as f:
            f.write(cls._hash(new_pwd))

    @classmethod
    def verify(cls, pwd):
        if not os.path.exists(cls.PASSWORD_FILE):
            cls.initialize()
        try:
            with open(cls.PASSWORD_FILE, "r") as f:
                stored = f.read().strip()
            return stored == cls._hash(pwd)
        except:
            return False

# ------------------------------- 设置管理器 -------------------------------
class SettingsManager:
    SETTINGS_FILE = SETTINGS_FILE

    @classmethod
    def load(cls):
        if os.path.exists(cls.SETTINGS_FILE):
            try:
                with open(cls.SETTINGS_FILE, 'r', encoding='utf-8') as f:
                    return json.load(f)
            except:
                pass
        return {
            "wallpaper": "",
            "show_time": False,
            "transparent_bg": False,
            "theme": "auto",
            "hotkey": "ctrl+alt+l",
            "enable_face_recognition": False,
            "opacity": 0.96
        }

    @classmethod
    def save(cls, settings):
        with open(cls.SETTINGS_FILE, 'w', encoding='utf-8') as f:
            json.dump(settings, f, indent=4, ensure_ascii=False)

# ------------------------------- 快捷键管理器(修复版:自动恢复 + 异步回调)------------------------------
class HotkeyManager:
    def __init__(self, callback):
        self.callback = callback
        self.current_hotkey = None
        self.listening = False
        self._lock = threading.Lock()
        self._health_thread = None
        self._stop_health = threading.Event()
        self._last_trigger = 0  # 最后触发时间戳,用于健康检查

    def _safe_callback(self):
        """在独立线程中执行回调,避免阻塞钩子线程"""
        self._last_trigger = time.time()
        # 启动新线程执行用户回调(启动锁屏等)
        threading.Thread(target=self.callback, daemon=True).start()

    def start(self, hotkey_str):
        with self._lock:
            if self.listening:
                self.stop()
            try:
                # 移除可能存在的旧钩子
                keyboard.unhook_all()
                # 注册新热键,回调使用安全包装
                keyboard.add_hotkey(hotkey_str, self._safe_callback)
                self.current_hotkey = hotkey_str
                self.listening = True
                logging.info(f"快捷键 {hotkey_str} 已注册")
                # 启动健康检查线程
                self._start_health_check()
            except Exception as e:
                logging.error(f"注册快捷键失败: {e}")
                self.listening = False
                raise

    def stop(self):
        with self._lock:
            if not self.listening:
                return
            self._stop_health.set()
            if self._health_thread and self._health_thread.is_alive():
                self._health_thread.join(timeout=1)
            try:
                keyboard.unhook_all()
                logging.info(f"快捷键 {self.current_hotkey} 已移除")
            except Exception as e:
                logging.warning(f"移除快捷键时出错: {e}")
            self.listening = False
            self.current_hotkey = None
            self._last_trigger = 0

    def change_hotkey(self, new_hotkey):
        self.stop()
        self.start(new_hotkey)

    def _start_health_check(self):
        """启动健康检查线程,定期验证热键是否有效"""
        if self._health_thread and self._health_thread.is_alive():
            self._stop_health.set()
            self._health_thread.join(timeout=0.5)
        self._stop_health.clear()
        self._health_thread = threading.Thread(target=self._health_loop, daemon=True)
        self._health_thread.start()

    def _health_loop(self):
        """每隔10秒检查热键是否工作,若长时间未触发则尝试重新注册"""
        CHECK_INTERVAL = 10      # 检查间隔(秒)
        INACTIVE_TIMEOUT = 60    # 超过60秒无任何触发则认为可能失效
        while not self._stop_health.wait(CHECK_INTERVAL):
            if not self.listening:
                break
            now = time.time()
            # 如果从未触发过,或者距离上次触发超过 INACTIVE_TIMEOUT
            if self._last_trigger == 0 or (now - self._last_trigger) > INACTIVE_TIMEOUT:
                logging.warning("热键可能失效,尝试重新注册...")
                with self._lock:
                    if not self.listening:
                        break
                    hotkey = self.current_hotkey
                    if hotkey:
                        try:
                            keyboard.unhook_all()
                            keyboard.add_hotkey(hotkey, self._safe_callback)
                            logging.info(f"热键 {hotkey} 已重新注册")
                        except Exception as e:
                            logging.error(f"重新注册失败: {e}")

# ------------------------------- 主界面 -------------------------------
class MainApp(ctk.CTk):
    def __init__(self):
        super().__init__()
        self.title("LockScreen 控制中心")
        self.geometry("1200x780")
        self.minsize(1100, 720)
        self.center_window()

        PasswordManager.initialize()
        self.settings = SettingsManager.load()
        opacity = self.settings.get("opacity", 0.96)
        self.attributes('-alpha', opacity)

        ctk.set_default_color_theme("blue")
        self._apply_theme(self.settings.get("theme", "auto"))

        self._notify_after_id = None
        self.hotkey_manager = None
        self._idle_after_id = None

        # 主布局
        self.nav_frame = ctk.CTkFrame(self, width=280, corner_radius=0, fg_color=("#f8f9fa", "#0f1419"))
        self.nav_frame.pack(side="left", fill="y")
        self.nav_frame.pack_propagate(False)

        self.content_frame = ctk.CTkFrame(self, corner_radius=0, fg_color="transparent")
        self.content_frame.pack(side="right", fill="both", expand=True)

        self._build_navbar()
        self._build_content_area()

        self.notify_label = ctk.CTkLabel(
            self, text="", font=ctk.CTkFont(size=12),
            corner_radius=12, padx=20, pady=10
        )
        self.notify_label.place(relx=0.5, y=self.winfo_height()-50, anchor="s")
        self.notify_label.lower()

        self._start_hotkey_listener()
        self.after(200, self._start_tray_thread)

        self.protocol("WM_DELETE_WINDOW", self.hide_window)
        self.after(50, self._bring_to_front)

        # 启动长间隔空闲循环(降低 CPU)
        self._start_idle_loop()

    def center_window(self):
        self.update_idletasks()
        sw, sh = self.winfo_screenwidth(), self.winfo_screenheight()
        self.geometry(f"1200x780+{sw//2-600}+{sh//2-390}")

    def _start_idle_loop(self):
        """长间隔空闲循环,减少主循环唤醒频率"""
        if self._idle_after_id:
            self.after_cancel(self._idle_after_id)
        # 每 30 秒执行一次,仅为维持 after 队列,不做事
        self._idle_after_id = self.after(30000, self._start_idle_loop)

    def _build_navbar(self):
        logo_frame = ctk.CTkFrame(self.nav_frame, fg_color="transparent", height=100)
        logo_frame.pack(fill="x", pady=(40, 20))
        ctk.CTkLabel(logo_frame, text="LockScreen", font=ctk.CTkFont(size=28, weight="bold"), text_color="#F59E0B").pack()
        ctk.CTkLabel(logo_frame, text="Control Center", font=ctk.CTkFont(size=12), text_color="#64748B").pack()
        ctk.CTkFrame(self.nav_frame, height=1, fg_color="#E2E8F0").pack(fill="x", padx=24, pady=15)

        self.nav_btns = {}
        nav_items = [
            ("[家] 仪表板", "home"), ("[键] 快捷键", "hotkey"), ("[锁] 密码管理", "password"),
            ("[笔] 外观设置", "appearance"), ("[齿] 高级设置", "settings"), ("[i] 关于", "about")
        ]
        for text, key in nav_items:
            btn = ctk.CTkButton(
                self.nav_frame, text=text, font=ctk.CTkFont(size=14, weight="bold"),
                fg_color="transparent", text_color="#475569", hover_color="#E2E8F0",
                anchor="w", corner_radius=12, height=45,
                command=lambda k=key: self._switch_content(k)
            )
            btn.pack(fill="x", padx=16, pady=4)
            self.nav_btns[key] = btn

        bottom_frame = ctk.CTkFrame(self.nav_frame, fg_color="transparent")
        bottom_frame.pack(side="bottom", fill="x", pady=25)

        ctk.CTkLabel(bottom_frame, text="主题", font=ctk.CTkFont(size=12), text_color="#64748B").pack(anchor="w", padx=24, pady=(0,5))
        self.theme_var = ctk.StringVar(value=self._theme_to_display(self.settings.get("theme", "auto")))
        theme_menu = ctk.CTkOptionMenu(
            bottom_frame, values=["浅色", "深色", "自动"], variable=self.theme_var,
            command=self._change_theme, font=ctk.CTkFont(size=13), corner_radius=10
        )
        theme_menu.pack(padx=20, pady=(0, 15), fill="x")

        ctk.CTkLabel(bottom_frame, text="透明度", font=ctk.CTkFont(size=12), text_color="#64748B").pack(anchor="w", padx=24, pady=(0,5))
        self.opacity_slider = ctk.CTkSlider(
            bottom_frame, from_=0.7, to=1.0, number_of_steps=30,
            command=self._change_opacity, button_color="#F59E0B", progress_color="#F59E0B"
        )
        self.opacity_slider.set(self.settings.get("opacity", 0.96))
        self.opacity_slider.pack(padx=20, pady=(0, 15), fill="x")

        hotkey_text = self.settings.get("hotkey", "ctrl+alt+l")
        self.nav_hotkey_label = ctk.CTkLabel(bottom_frame, text=f"快捷键: {hotkey_text}", font=ctk.CTkFont(size=11), text_color="#10B981")
        self.nav_hotkey_label.pack(pady=(0, 15))

    def _build_content_area(self):
        self.content_cards = {}
        for key in ["home", "hotkey", "password", "appearance", "settings", "about"]:
            card = ctk.CTkFrame(self.content_frame, corner_radius=28, fg_color=("white", "#1E293B"))
            card.pack(fill="both", expand=True, padx=28, pady=28)
            card.pack_forget()
            self.content_cards[key] = card

        self._build_home_ui(self.content_cards["home"])
        self._build_hotkey_ui(self.content_cards["hotkey"])
        self._build_password_ui(self.content_cards["password"])
        self._build_appearance_ui(self.content_cards["appearance"])
        self._build_settings_ui(self.content_cards["settings"])
        self._build_about_ui(self.content_cards["about"])
        self._switch_content("home")

    def _build_home_ui(self, parent):
        welcome_frame = ctk.CTkFrame(parent, fg_color="transparent")
        welcome_frame.pack(fill="x", padx=36, pady=(32, 16))
        ctk.CTkLabel(welcome_frame, text="欢迎回来", font=ctk.CTkFont(size=32, weight="bold"), text_color="#F59E0B").pack(anchor="w")
        ctk.CTkLabel(welcome_frame, text="LockScreen 始终守护您的隐私", font=ctk.CTkFont(size=14), text_color="#64748B").pack(anchor="w")

        grid_frame = ctk.CTkFrame(parent, fg_color="transparent")
        grid_frame.pack(fill="both", expand=True, padx=36, pady=16)

        lock_card = self._create_glass_card(grid_frame, "[锁]", "立即锁屏", "一键保护屏幕", "#EF4444")
        lock_card.pack(side="left", fill="both", expand=True, padx=12, pady=12)
        ctk.CTkButton(lock_card, text="启动", command=self._launch_lockscreen, fg_color="#EF4444", hover_color="#DC2626",
                      corner_radius=16, height=48, width=160, font=ctk.CTkFont(size=15, weight="bold")).pack(pady=(20, 28))

        face_card = self._create_glass_card(grid_frame, "[人]", "人脸识别", "联动锁屏程序", "#3B82F6")
        face_card.pack(side="left", fill="both", expand=True, padx=12, pady=12)
        self.face_status = ctk.CTkLabel(face_card, text="已禁用" if not self.settings.get("enable_face_recognition") else "已启用",
                                        font=ctk.CTkFont(size=13), text_color="#F59E0B" if self.settings.get("enable_face_recognition") else "#94A3B8")
        self.face_status.pack(pady=5)
        ctk.CTkButton(face_card, text="前往设置", command=lambda: self._switch_content("settings"), fg_color="#3B82F6",
                      hover_color="#2563EB", corner_radius=16, height=48, width=160, font=ctk.CTkFont(size=15, weight="bold")).pack(pady=(20, 28))

        hotkey_card = self._create_glass_card(grid_frame, "[键]", "当前快捷键", "可自定义修改", "#8B5CF6")
        hotkey_card.pack(side="left", fill="both", expand=True, padx=12, pady=12)
        self.home_hotkey_label = ctk.CTkLabel(hotkey_card, text=self.settings.get("hotkey", "ctrl+alt+l"),
                                              font=ctk.CTkFont(size=16, weight="bold"), text_color="#10B981")
        self.home_hotkey_label.pack()
        ctk.CTkButton(hotkey_card, text="修改", command=lambda: self._switch_content("hotkey"), fg_color="#8B5CF6",
                      hover_color="#7C3AED", corner_radius=16, height=48, width=160, font=ctk.CTkFont(size=15, weight="bold")).pack(pady=(20, 28))

        info_frame = ctk.CTkFrame(parent, fg_color="transparent", corner_radius=24)
        info_frame.pack(fill="x", padx=36, pady=(20, 32))
        left_info = ctk.CTkFrame(info_frame, fg_color="transparent")
        left_info.pack(side="left", fill="both", expand=True, padx=20)
        right_info = ctk.CTkFrame(info_frame, fg_color="transparent")
        right_info.pack(side="right", fill="both", expand=True, padx=20)

        ctk.CTkLabel(left_info, text="系统状态", font=ctk.CTkFont(size=16, weight="bold"), text_color="#475569").pack(anchor="w", pady=(0,12))
        listening_status = "● 激活" if self.hotkey_manager and self.hotkey_manager.listening else "○ 未激活"
        face_status_text = "● 开启" if self.settings.get("enable_face_recognition") else "○ 关闭"
        lock_available = "● 可用" if self._find_lockscreen_path() else "○ 缺失"
        ctk.CTkLabel(left_info, text=f"快捷键监听: {listening_status}", font=ctk.CTkFont(size=13)).pack(anchor="w", pady=5)
        ctk.CTkLabel(left_info, text=f"人脸识别联动: {face_status_text}", font=ctk.CTkFont(size=13)).pack(anchor="w", pady=5)
        ctk.CTkLabel(left_info, text=f"锁屏程序: {lock_available}", font=ctk.CTkFont(size=13)).pack(anchor="w", pady=5)

        ctk.CTkLabel(right_info, text="使用提示", font=ctk.CTkFont(size=16, weight="bold"), text_color="#475569").pack(anchor="w", pady=(0,12))
        tips = ["• 快捷键全局生效,支持组合键", "• 启用人脸识别后锁屏同时启动识别", "• 默认密码: 123456,请及时修改", "• 右键系统托盘图标可快速操作"]
        for tip in tips:
            ctk.CTkLabel(right_info, text=tip, font=ctk.CTkFont(size=13), text_color="#64748B").pack(anchor="w", pady=3)

    def _create_glass_card(self, parent, icon, title, subtitle, accent_color):
        card = ctk.CTkFrame(parent, corner_radius=24, fg_color=("white", "#1E293B"), border_width=1,
                            border_color=("#E2E8F0", "#2D3A5E"))
        ctk.CTkLabel(card, text=icon, font=ctk.CTkFont(size=48)).pack(pady=(28, 8))
        ctk.CTkLabel(card, text=title, font=ctk.CTkFont(size=20, weight="bold")).pack()
        ctk.CTkLabel(card, text=subtitle, font=ctk.CTkFont(size=12), text_color="#64748B").pack()
        def on_enter(e): card.configure(border_color=accent_color, border_width=2)
        def on_leave(e): card.configure(border_color=("#E2E8F0", "#2D3A5E"), border_width=1)
        card.bind("<Enter>", on_enter); card.bind("<Leave>", on_leave)
        return card

    def _build_hotkey_ui(self, parent):
        self._build_simple_setting_page(parent, "快捷键配置", "自定义快速锁屏的组合键",
            [("快捷键组合:", "hotkey_entry", "例: ctrl+alt+l")], self._apply_hotkey, "应用")

    def _build_password_ui(self, parent):
        fields = [("原密码:", "old_pwd", ""), ("新密码:", "new_pwd", ""), ("确认新密码:", "confirm_pwd", "")]
        self.pwd_entries = {}
        header = ctk.CTkFrame(parent, fg_color="transparent")
        header.pack(fill="x", padx=36, pady=(36, 20))
        ctk.CTkLabel(header, text="密码管理", font=ctk.CTkFont(size=26, weight="bold"), text_color="#F59E0B").pack(anchor="w")
        ctk.CTkLabel(header, text="修改锁屏密码,保护隐私安全", font=ctk.CTkFont(size=13), text_color="#64748B").pack(anchor="w")
        card = ctk.CTkFrame(parent, corner_radius=24, fg_color=("white", "#1E293B"))
        card.pack(fill="x", padx=36, pady=16)
        for label, key, _ in fields:
            frame = ctk.CTkFrame(card, fg_color="transparent")
            frame.pack(fill="x", padx=28, pady=15)
            ctk.CTkLabel(frame, text=label, font=ctk.CTkFont(size=14, weight="bold"), width=100, anchor="e").pack(side="left", padx=(0,16))
            entry = ctk.CTkEntry(frame, show="●", font=ctk.CTkFont(size=14), corner_radius=12, height=44)
            entry.pack(side="left", fill="x", expand=True)
            self.pwd_entries[key] = entry
        btn_frame = ctk.CTkFrame(card, fg_color="transparent")
        btn_frame.pack(fill="x", padx=28, pady=(20, 32))
        ctk.CTkButton(btn_frame, text="修改密码", command=self._change_password, fg_color="#2563EB", hover_color="#1D4ED8", width=140, font=ctk.CTkFont(size=14, weight="bold"), height=44, corner_radius=14).pack(side="left", padx=(0, 16))
        ctk.CTkButton(btn_frame, text="清空", command=self._clear_password_fields, fg_color="#475569", hover_color="#334155", width=110, font=ctk.CTkFont(size=14), height=44, corner_radius=14).pack(side="left")
        tip_frame = ctk.CTkFrame(card, fg_color="transparent")
        tip_frame.pack(fill="x", padx=28, pady=(0, 24))
        ctk.CTkLabel(tip_frame, text="提示: 密码采用 SHA256 加密存储,请务必牢记。", font=ctk.CTkFont(size=11), text_color="#64748B").pack(anchor="w")

    def _build_appearance_ui(self, parent):
        header = ctk.CTkFrame(parent, fg_color="transparent")
        header.pack(fill="x", padx=36, pady=(36, 20))
        ctk.CTkLabel(header, text="外观设置", font=ctk.CTkFont(size=26, weight="bold"), text_color="#F59E0B").pack(anchor="w")
        ctk.CTkLabel(header, text="自定义锁屏界面样式", font=ctk.CTkFont(size=13), text_color="#64748B").pack(anchor="w")
        scroll = ctk.CTkScrollableFrame(parent, fg_color="transparent", corner_radius=0)
        scroll.pack(fill="both", expand=True, padx=36, pady=16)
        self.trans_var = ctk.BooleanVar(value=self.settings.get("transparent_bg", False))
        trans_sw = ctk.CTkSwitch(scroll, text="透明背景模式 (忽略壁纸)", variable=self.trans_var, command=self._on_transparent_toggle, font=ctk.CTkFont(size=14), switch_width=50)
        trans_sw.pack(anchor='w', pady=16, padx=20)
        self.wall_group = ctk.CTkFrame(scroll, fg_color="transparent")
        self.wall_group.pack(fill='x', pady=10, padx=20)
        path_frame = ctk.CTkFrame(self.wall_group, fg_color="transparent")
        path_frame.pack(fill='x', pady=6)
        self.wall_entry = ctk.CTkEntry(path_frame, placeholder_text="选择壁纸图片", width=500, corner_radius=14)
        self.wall_entry.pack(side='left', padx=(0, 12), fill='x', expand=True)
        self.wall_entry.insert(0, self.settings.get("wallpaper", ""))
        self.browse_btn = ctk.CTkButton(path_frame, text="浏览", command=self._browse_wallpaper, width=90, corner_radius=14)
        self.browse_btn.pack(side='left')
        self.preview_img = ctk.CTkLabel(self.wall_group, text="", width=680, height=180, corner_radius=20, fg_color=("#EFF3F8", "#2D2D3F"))
        self.preview_img.pack(pady=16)
        if not self.trans_var.get() and self.settings.get("wallpaper"):
            self._update_preview(self.settings["wallpaper"])
        else:
            self.preview_img.configure(text="(暂无预览)")
        self.time_var = ctk.BooleanVar(value=self.settings.get("show_time", False))
        time_sw = ctk.CTkSwitch(scroll, text="锁屏时显示当前时间 (左下角)", variable=self.time_var, command=self._on_time_toggle, font=ctk.CTkFont(size=14), switch_width=50)
        time_sw.pack(anchor='w', pady=20, padx=20)
        self._update_wallpaper_access()

    def _build_settings_ui(self, parent):
        self._build_simple_setting_page(parent, "高级设置", "扩展功能与联动选项",
            [("启用人脸识别联动", "face_toggle", "")], None, None, is_switch=True)

    def _build_about_ui(self, parent):
        header = ctk.CTkFrame(parent, fg_color="transparent")
        header.pack(fill="x", padx=36, pady=(36, 20))
        ctk.CTkLabel(header, text="关于 LockScreen", font=ctk.CTkFont(size=26, weight="bold"), text_color="#F59E0B").pack(anchor="w")
        card = ctk.CTkFrame(parent, corner_radius=24, fg_color=("white", "#1E293B"))
        card.pack(fill="both", expand=True, padx=36, pady=16)
        logo_area = ctk.CTkFrame(card, fg_color="transparent")
        logo_area.pack(pady=(40, 20))
        ctk.CTkLabel(logo_area, text="LS", font=ctk.CTkFont(size=56, weight="bold"), text_color="#F59E0B").pack()
        ctk.CTkLabel(logo_area, text="LockScreen Control Center", font=ctk.CTkFont(size=18, weight="bold"), text_color="#F59E0B").pack()
        features = ["全屏锁屏保护", "自定义快捷键", "人脸识别联动", "SHA256 加密密码", "系统托盘集成"]
        features_text = "功能特性:\n" + "\n".join(f"• {f}" for f in features)
        ctk.CTkLabel(card, text=features_text, justify="left", font=ctk.CTkFont(size=13), text_color="#475569").pack(anchor="center", pady=10)
        dev_frame = ctk.CTkFrame(card, fg_color="transparent")
        dev_frame.pack(pady=(10, 30))
        ctk.CTkLabel(dev_frame, text="开发者", font=ctk.CTkFont(size=14, weight="bold")).pack()
        ctk.CTkLabel(dev_frame, text="@Zhl2010", font=ctk.CTkFont(size=24, weight="bold"), text_color="#F59E0B").pack()
        ctk.CTkLabel(card, text="版本 2.0 Enhanced", font=ctk.CTkFont(size=11), text_color="#64748B").pack(pady=(0, 20))

    def _build_simple_setting_page(self, parent, title, subtitle, fields, apply_cmd, apply_text, is_switch=False):
        header = ctk.CTkFrame(parent, fg_color="transparent")
        header.pack(fill="x", padx=36, pady=(36, 20))
        ctk.CTkLabel(header, text=title, font=ctk.CTkFont(size=26, weight="bold"), text_color="#F59E0B").pack(anchor="w")
        ctk.CTkLabel(header, text=subtitle, font=ctk.CTkFont(size=13), text_color="#64748B").pack(anchor="w")
        card = ctk.CTkFrame(parent, corner_radius=24, fg_color=("white", "#1E293B"))
        card.pack(fill="x", padx=36, pady=16)
        if is_switch:
            self.face_var = ctk.BooleanVar(value=self.settings.get("enable_face_recognition", False))
            sw = ctk.CTkSwitch(card, text="启用人脸识别联动", variable=self.face_var, command=self._on_face_toggle,
                               font=ctk.CTkFont(size=15, weight="bold"), progress_color="#4CAF50", button_color="#2563EB")
            sw.pack(anchor='w', padx=28, pady=20)
            info_container = ctk.CTkFrame(card, corner_radius=16, border_width=1, fg_color="transparent")
            info_container.pack(fill="x", padx=28, pady=(10, 28))
            ctk.CTkLabel(info_container, text="功能说明", font=ctk.CTkFont(size=14, weight="bold")).pack(anchor="w", padx=20, pady=(16, 8))
            ctk.CTkLabel(info_container, text="• 人脸识别需要单独的 recognize.exe 程序放在同一目录下\n• 识别成功后会自动解锁屏幕\n• 若未找到 recognize.exe,仅锁屏功能正常",
                         justify="left", font=ctk.CTkFont(size=12), text_color="#64748B").pack(anchor="w", padx=20, pady=(0, 16))
        else:
            input_frame = ctk.CTkFrame(card, fg_color="transparent")
            input_frame.pack(fill="x", padx=28, pady=20)
            ctk.CTkLabel(input_frame, text=fields[0][0], font=ctk.CTkFont(size=14, weight="bold"), width=120, anchor="e").pack(side="left", padx=(0,16))
            entry = ctk.CTkEntry(input_frame, placeholder_text=fields[0][2], font=ctk.CTkFont(size=14), width=280, corner_radius=12, height=44)
            entry.pack(side="left", padx=(0,16))
            entry.insert(0, self.settings.get("hotkey", "ctrl+alt+l"))
            self.hotkey_entry = entry
            ctk.CTkButton(input_frame, text=apply_text, command=apply_cmd, fg_color="#4CAF50", hover_color="#388E3C", width=110,
                          font=ctk.CTkFont(size=14, weight="bold"), height=44, corner_radius=12).pack(side="left")
            ctk.CTkLabel(card, text="支持组合键: Ctrl / Shift / Alt / Win + 字母/数字/功能键", font=ctk.CTkFont(size=12), text_color="#64748B").pack(anchor="w", padx=28, pady=(0, 28))

    # ---------- 核心功能 ----------
    def _apply_theme(self, mode):
        if mode == "light":
            ctk.set_appearance_mode("light")
        elif mode == "dark":
            ctk.set_appearance_mode("dark")
        else:
            ctk.set_appearance_mode("system")

    def _theme_to_display(self, mode):
        return {"light": "浅色", "dark": "深色", "system": "自动"}.get(mode, "自动")

    def _change_theme(self, choice):
        mode = {"浅色": "light", "深色": "dark", "自动": "system"}[choice]
        self._apply_theme(mode)
        self.settings["theme"] = mode
        SettingsManager.save(self.settings)
        self._show_notification("主题已切换")

    def _change_opacity(self, value):
        opacity = float(value)
        self.attributes('-alpha', opacity)
        self.settings["opacity"] = opacity
        SettingsManager.save(self.settings)

    def _switch_content(self, key):
        for card in self.content_cards.values():
            card.pack_forget()
        self.content_cards[key].pack(fill="both", expand=True, padx=28, pady=28)
        for k, btn in self.nav_btns.items():
            btn.configure(fg_color="transparent", text_color="#475569")
        if key in self.nav_btns:
            self.nav_btns[key].configure(fg_color="#E2E8F0", text_color="#0F1419")

    def _show_notification(self, msg, is_err=False):
        if self._notify_after_id:
            self.after_cancel(self._notify_after_id)
        color = "#DC2626" if is_err else "#10B981"
        self.notify_label.configure(text=msg, fg_color=color, text_color="white")
        self.notify_label.place(relx=0.5, y=self.winfo_height()-50, anchor="s")
        self._notify_after_id = self.after(2500, self._hide_notification)

    def _hide_notification(self):
        self.notify_label.place_forget()
        self._notify_after_id = None

    def _find_lockscreen_path(self):
        base = os.path.dirname(os.path.abspath(sys.argv[0]))
        for name in ['lockscreen.exe', 'lockscreen_enhanced.py', 'lockscreen.py']:
            path = os.path.join(base, name)
            if os.path.exists(path):
                return path
        return None

    def _launch_lockscreen(self):
        """启动锁屏,无鼠标转圈优化"""
        path = self._find_lockscreen_path()
        if not path:
            self._show_notification("未找到锁屏程序", is_err=True)
            return
        try:
            # 隐藏控制台窗口,避免鼠标转圈
            startupinfo = subprocess.STARTUPINFO()
            startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
            startupinfo.wShowWindow = subprocess.SW_HIDE
            if path.endswith('.exe'):
                subprocess.Popen([path], creationflags=subprocess.CREATE_NO_WINDOW, startupinfo=startupinfo)
            else:
                subprocess.Popen([sys.executable, path], creationflags=subprocess.CREATE_NO_WINDOW, startupinfo=startupinfo)

            if self.settings.get("enable_face_recognition", False):
                base = os.path.dirname(os.path.abspath(sys.argv[0]))
                recog_path = os.path.join(base, "recognize.exe")
                if os.path.exists(recog_path):
                    subprocess.Popen([recog_path], creationflags=subprocess.CREATE_NO_WINDOW, startupinfo=startupinfo)
        except Exception as e:
            self._show_notification(f"启动失败: {str(e)}", is_err=True)

    def _apply_hotkey(self):
        new_hotkey = self.hotkey_entry.get().strip().lower()
        if not new_hotkey:
            self._show_notification("快捷键不能为空", is_err=True)
            return
        try:
            self.hotkey_manager.change_hotkey(new_hotkey)
            self.settings["hotkey"] = new_hotkey
            SettingsManager.save(self.settings)
            self.nav_hotkey_label.configure(text=f"快捷键: {new_hotkey}")
            self.home_hotkey_label.configure(text=new_hotkey)
            self._show_notification(f"快捷键已改为 {new_hotkey}")
        except Exception as e:
            self._show_notification(f"设置失败: {str(e)}", is_err=True)

    def _change_password(self):
        old = self.pwd_entries["old_pwd"].get()
        new = self.pwd_entries["new_pwd"].get()
        cf = self.pwd_entries["confirm_pwd"].get()
        if not old:
            self._show_notification("请输入原密码", is_err=True)
            return
        if not PasswordManager.verify(old):
            self._show_notification("原密码错误", is_err=True)
            return
        if not new:
            self._show_notification("请输入新密码", is_err=True)
            return
        if new != cf:
            self._show_notification("两次密码不一致", is_err=True)
            return
        PasswordManager.save_password(new)
        self._show_notification("密码修改成功")
        self._clear_password_fields()

    def _clear_password_fields(self):
        for entry in self.pwd_entries.values():
            entry.delete(0, 'end')

    def _on_face_toggle(self):
        enabled = self.face_var.get()
        self.settings["enable_face_recognition"] = enabled
        SettingsManager.save(self.settings)
        self.face_status.configure(text="已启用" if enabled else "已禁用")
        self.face_status.configure(text_color="#F59E0B" if enabled else "#94A3B8")
        self._show_notification(f"人脸识别联动 {'已开启' if enabled else '已关闭'}")

    def _update_wallpaper_access(self):
        if self.trans_var.get():
            self.wall_entry.configure(state="disabled")
            self.browse_btn.configure(state="disabled")
            self.preview_img.configure(image=None, text="透明模式已开启,不显示壁纸")
        else:
            self.wall_entry.configure(state="normal")
            self.browse_btn.configure(state="normal")
            wp = self.settings.get("wallpaper", "")
            if wp:
                self._update_preview(wp)
            else:
                self.preview_img.configure(image=None, text="(未选择壁纸)")

    def _on_transparent_toggle(self):
        new_val = self.trans_var.get()
        self.settings["transparent_bg"] = new_val
        SettingsManager.save(self.settings)
        self._update_wallpaper_access()
        self._show_notification("透明模式已" + ("开启" if new_val else "关闭"))

    def _browse_wallpaper(self):
        if self.trans_var.get():
            return
        path = filedialog.askopenfilename(filetypes=[("图片", "*.png *.jpg *.jpeg *.bmp *.gif")])
        if path:
            self.wall_entry.delete(0, 'end')
            self.wall_entry.insert(0, path)
            self._save_wallpaper_path(path)

    def _save_wallpaper_path(self, path):
        if not path or not os.path.isfile(path):
            self._show_notification("文件不存在", is_err=True)
            return
        self.settings["wallpaper"] = path
        if self.settings["transparent_bg"]:
            self.settings["transparent_bg"] = False
            self.trans_var.set(False)
            self._update_wallpaper_access()
        SettingsManager.save(self.settings)
        self._update_preview(path)
        self._show_notification("壁纸已保存")

    def _update_preview(self, path):
        try:
            img = Image.open(path)
            img.thumbnail((660, 160), Image.Resampling.LANCZOS)
            photo = ctk.CTkImage(img, size=(img.width, img.height))
            self.preview_img.configure(image=photo, text="")
            self.preview_img.image = photo
        except Exception:
            self.preview_img.configure(image=None, text="预览失败")

    def _on_time_toggle(self):
        self.settings["show_time"] = self.time_var.get()
        SettingsManager.save(self.settings)
        self._show_notification("时间显示" + ("已开启" if self.time_var.get() else "已关闭"))

    def _start_hotkey_listener(self):
        hotkey_str = self.settings.get("hotkey", "ctrl+alt+l")
        self.hotkey_manager = HotkeyManager(self._on_hotkey_pressed)
        try:
            self.hotkey_manager.start(hotkey_str)
        except Exception as e:
            print(f"[ERROR] 快捷键初始化失败: {e}")

    def _on_hotkey_pressed(self):
        self._launch_lockscreen()

    def _start_tray_thread(self):
        self.tray_thread = threading.Thread(target=self._setup_tray, daemon=True)
        self.tray_thread.start()

    def _create_tray_icon(self):
        size = 64
        img = Image.new('RGBA', (size, size), (0, 0, 0, 0))
        draw = ImageDraw.Draw(img)
        draw.arc((20, 12, 44, 36), start=0, end=180, fill=(100, 150, 100), width=5)
        draw.rounded_rectangle((14, 30, 50, 58), radius=10, fill=(120, 180, 120), outline=(60, 100, 60))
        draw.ellipse((28, 42, 36, 50), fill=(40, 70, 40))
        draw.rectangle((30, 48, 34, 55), fill=(40, 70, 40))
        return img

    def _setup_tray(self):
        if os.path.exists(ICON_FILE):
            try:
                icon_img = Image.open(ICON_FILE).resize((64, 64), Image.Resampling.LANCZOS)
            except:
                icon_img = self._create_tray_icon()
        else:
            icon_img = self._create_tray_icon()
        menu = pystray.Menu(
            pystray.MenuItem("立即锁屏", lambda: self._launch_lockscreen()),
            pystray.MenuItem("显示主窗口", lambda: self.show_window()),
            pystray.MenuItem("退出程序", self._quit_app)
        )
        icon = pystray.Icon("lockscreen_main", icon_img, "LockScreen", menu)
        icon.run()

    def hide_window(self):
        self.withdraw()
        gc.collect()

    def show_window(self):
        self.deiconify()
        self.lift()
        self.focus_force()
        self._bring_to_front()

    def _bring_to_front(self):
        self.lift()
        self.focus_force()
        self.attributes('-topmost', True)
        self.after(300, lambda: self.attributes('-topmost', False))

    def _quit_app(self):
        if self.hotkey_manager:
            self.hotkey_manager.stop()
        self.quit()
        os._exit(0)

if __name__ == "__main__":
    app = MainApp()
    app.mainloop()

recognize.py
Python
# recognize.py - 人脸识别 + 可靠结束进程(纯 ctypes,无 taskkill)
import cv2
import numpy as np
import time
import os
import sys
import ctypes
from ctypes import wintypes
from collections import deque

# ======================== 模型路径 ========================
YUNET_MODEL = "model/face_detection_yunet_2023mar.onnx"
SFACE_MODEL = "model/face_recognition_sface_2021dec.onnx"

if not os.path.exists(YUNET_MODEL):
    print(f"[ERROR] 缺少 Yunet 模型: {YUNET_MODEL}")
    sys.exit(1)
if not os.path.exists(SFACE_MODEL):
    print(f"[ERROR] 缺少 SFace 模型: {SFACE_MODEL}")
    sys.exit(1)

detector = cv2.FaceDetectorYN.create(YUNET_MODEL, "", (320, 320))
feature_extractor = cv2.FaceRecognizerSF.create(SFACE_MODEL, "")
print("[INFO] 模型加载成功")

# ======================== 人脸对齐 ========================
def align_face(img, face):
    std_points = np.array([
        [30.2946, 51.6963], [65.5318, 51.6963], [48.0252, 71.7366],
        [33.5493, 92.3655], [62.7299, 92.3655]
    ], dtype=np.float32)
    landmarks = face[4:14].reshape(5, 2).astype(np.float32)
    src_points = np.array([landmarks[0], landmarks[1], landmarks[2], landmarks[4], landmarks[3]])
    M, _ = cv2.estimateAffine2D(src_points, std_points)
    return cv2.warpAffine(img, M, (112, 112))

# ======================== 加载参考特征 ========================
def get_ref_feat():
    features = []
    if os.path.exists("my_photo.jpg"):
        img = cv2.imread("my_photo.jpg")
        if img is not None:
            h, w = img.shape[:2]
            detector.setInputSize((w, h))
            result = detector.detect(img)
            if result[1] is not None and result[1].shape[0] > 0:
                aligned = align_face(img, result[1][0])
                features.append(feature_extractor.feature(aligned).ravel())
                print("[INFO] 已添加 my_photo.jpg 特征")
    ref_dir = "ref_images"
    if os.path.exists(ref_dir):
        for f in sorted(os.listdir(ref_dir)):
            if f.lower().endswith(('.jpg', '.jpeg', '.png')):
                img = cv2.imread(os.path.join(ref_dir, f))
                if img is None: continue
                h, w = img.shape[:2]
                detector.setInputSize((w, h))
                result = detector.detect(img)
                if result[1] is not None and result[1].shape[0] > 0:
                    aligned = align_face(img, result[1][0])
                    features.append(feature_extractor.feature(aligned).ravel())
    if not features:
        return None
    avg_feat = np.mean(features, axis=0)
    norm = np.linalg.norm(avg_feat)
    if norm > 0:
        avg_feat /= norm
    print(f"[INFO] 使用 {len(features)} 张照片的平均特征")
    return avg_feat.reshape(1, -1)

ref_feat = get_ref_feat()
if ref_feat is None:
    print("[ERROR] 未找到参考人脸")
    sys.exit(1)

THRESHOLD = 0.7
NOD_WINDOW_SEC = 2.0
NOD_DIFF_THRESH = 10.0
ASPECT_RATIO_CHANGE_MAX = 0.1

# ======================== 鼠标移动检测 ========================
user32 = ctypes.windll.user32
point = wintypes.POINT()
last_x, last_y = -1, -1

def mouse_moved():
    global last_x, last_y
    user32.GetCursorPos(ctypes.byref(point))
    if last_x == -1 and last_y == -1:
        last_x, last_y = point.x, point.y
        return False
    moved = (point.x != last_x) or (point.y != last_y)
    last_x, last_y = point.x, point.y
    return moved

# ======================== 强力进程终止(纯 ctypes,带重试和确认)=======================
def terminate_process_by_name(process_name):
    """
    终止所有匹配进程名(不区分大小写)的进程。
    返回 True 表示至少成功终止一个。
    """
    kernel32 = ctypes.windll.kernel32
    PROCESS_TERMINATE = 0x0001
    PROCESS_QUERY_INFORMATION = 0x0400
    TH32CS_SNAPPROCESS = 0x00000002
    INVALID_HANDLE_VALUE = -1

    class PROCESSENTRY32W(ctypes.Structure):
        _fields_ = [
            ("dwSize", wintypes.DWORD),
            ("cntUsage", wintypes.DWORD),
            ("th32ProcessID", wintypes.DWORD),
            ("th32DefaultHeapID", ctypes.POINTER(wintypes.ULONG)),
            ("th32ModuleID", wintypes.DWORD),
            ("cntThreads", wintypes.DWORD),
            ("th32ParentProcessID", wintypes.DWORD),
            ("pcPriClassBase", wintypes.LONG),
            ("dwFlags", wintypes.DWORD),
            ("szExeFile", ctypes.c_wchar * 260)
        ]

    killed = False
    # 最多尝试 3 次,每次间隔 0.2 秒
    for attempt in range(3):
        hSnapshot = kernel32.CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0)
        if hSnapshot == INVALID_HANDLE_VALUE:
            continue

        pe = PROCESSENTRY32W()
        pe.dwSize = ctypes.sizeof(PROCESSENTRY32W)
        if kernel32.Process32FirstW(hSnapshot, ctypes.byref(pe)):
            while True:
                if pe.szExeFile.lower() == process_name.lower():
                    hProcess = kernel32.OpenProcess(PROCESS_TERMINATE | PROCESS_QUERY_INFORMATION,
                                                    False, pe.th32ProcessID)
                    if hProcess:
                        if kernel32.TerminateProcess(hProcess, 0):
                            print(f"[INFO] 已终止进程 {process_name} (PID: {pe.th32ProcessID})")
                            killed = True
                        else:
                            print(f"[WARN] 无法终止进程 {process_name} (PID: {pe.th32ProcessID}),可能权限不足")
                        kernel32.CloseHandle(hProcess)
                    else:
                        print(f"[WARN] 无法打开进程 {process_name} (PID: {pe.th32ProcessID}),尝试以管理员身份运行")
                if not kernel32.Process32NextW(hSnapshot, ctypes.byref(pe)):
                    break
        kernel32.CloseHandle(hSnapshot)

        if killed:
            # 等待并确认进程是否真的消失
            time.sleep(0.1)
            if not is_process_running(process_name):
                return True
            else:
                print(f"[INFO] 进程 {process_name} 仍存在,重试...")
        else:
            print(f"[INFO] 未找到进程 {process_name} (尝试 {attempt+1}/3)")
        time.sleep(0.2)
    return killed

def is_process_running(process_name):
    """检查是否存在至少一个指定名称的进程"""
    kernel32 = ctypes.windll.kernel32
    TH32CS_SNAPPROCESS = 0x00000002
    hSnapshot = kernel32.CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0)
    if hSnapshot == -1:
        return False
    class PROCESSENTRY32W(ctypes.Structure):
        _fields_ = [("dwSize", wintypes.DWORD), ("szExeFile", ctypes.c_wchar * 260)]
    pe = PROCESSENTRY32W()
    pe.dwSize = ctypes.sizeof(PROCESSENTRY32W)
    found = False
    if kernel32.Process32FirstW(hSnapshot, ctypes.byref(pe)):
        while True:
            if pe.szExeFile.lower() == process_name.lower():
                found = True
                break
            if not kernel32.Process32NextW(hSnapshot, ctypes.byref(pe)):
                break
    kernel32.CloseHandle(hSnapshot)
    return found

# ======================== 输入法切换 ========================
def switch_to_sogou_or_ms_pinyin():
    def enum_keyboard_layouts():
        num = user32.GetKeyboardLayoutList(0, None)
        if num == 0:
            return []
        hkls = (wintypes.HKL * num)()
        user32.GetKeyboardLayoutList(num, hkls)
        return list(hkls)

    for hkl in enum_keyboard_layouts():
        name_buf = ctypes.create_unicode_buffer(9)
        if user32.GetKeyboardLayoutNameW(name_buf):
            if "sogou" in name_buf.value.lower():
                user32.ActivateKeyboardLayout(hkl, 0)
                return True
    ms_hkl = user32.LoadKeyboardLayoutW("00000804", 1)
    if ms_hkl:
        user32.ActivateKeyboardLayout(ms_hkl, 0)
        return True
    return False

# ======================== 30秒识别窗口 ========================
def run_recognition_window():
    cap = cv2.VideoCapture(0, cv2.CAP_DSHOW)
    if not cap.isOpened():
        print("[ERROR] 无法打开摄像头")
        return False

    end_time = time.time() + 30
    print("[INFO] 识别窗口开启30秒,请面对摄像头并点头")
    history = deque(maxlen=300)
    verify_frames_left = 0

    while time.time() < end_time:
        ret, frame = cap.read()
        if not ret:
            time.sleep(0.01)
            continue

        h, w = frame.shape[:2]
        detector.setInputSize((w, h))
        _, faces = detector.detect(frame)

        current_time = time.time()
        face = None
        if faces is not None:
            face = faces[0]
            bbox = face[:4].astype(int)
            aspect_ratio = bbox[2] / bbox[3]
            pitch = face[5]
            history.append((current_time, pitch, aspect_ratio))

        while history and history[0][0] < current_time - NOD_WINDOW_SEC:
            history.popleft()

        if verify_frames_left == 0 and len(history) >= 5:
            pitches = [p for _, p, _ in history]
            aspects = [a for _, _, a in history]
            if (max(pitches) - min(pitches) >= NOD_DIFF_THRESH and
                max(aspects) - min(aspects) <= ASPECT_RATIO_CHANGE_MAX):
                verify_frames_left = 4

        if verify_frames_left > 0 and face is not None:
            aligned = align_face(frame, face)
            feat = feature_extractor.feature(aligned)
            sim = feature_extractor.match(ref_feat, feat, cv2.FaceRecognizerSF_FR_COSINE)
            if sim >= THRESHOLD:
                cap.release()
                return True
            verify_frames_left -= 1
        elif verify_frames_left > 0:
            verify_frames_left -= 1

        time.sleep(0.01)

    cap.release()
    return False

# ======================== 主循环 ========================
def main():
    print("[INFO] 程序已启动,移动鼠标触发30秒人脸识别")
    print("[INFO] 识别成功后立即结束 lockscreen.exe,然后切换输入法并退出")
    while True:
        if mouse_moved():
            print(f"[{time.strftime('%H:%M:%S')}] 检测到鼠标移动,启动识别")
            if run_recognition_window():
                print(f"[{time.strftime('%H:%M:%S')}] 识别成功!正在终止 lockscreen.exe...")
                success = terminate_process_by_name("lockscreen.exe")
                if success:
                    print("[INFO] lockscreen.exe 已成功终止")
                else:
                    print("[ERROR] 未能终止 lockscreen.exe,请以管理员身份运行此程序")
                switch_to_sogou_or_ms_pinyin()
                print("[INFO] 任务完成,退出")
                break
            else:
                print(f"[{time.strftime('%H:%M:%S')}] 30秒内未识别,继续等待")
        else:
            time.sleep(0.02)

if __name__ == "__main__":
    try:
        main()
    except KeyboardInterrupt:
        print("\n[INFO] 用户中断")
    finally:
        cv2.destroyAllWindows()

未经允许禁止转载、套用。


原版。