[Python] 纯文本查看 复制代码
# -*- coding: utf-8 -*-
"""
Windows Driver Backup & Restore Tool v1.0
带图形界面的驱动备份与恢复工具
"""
import os
import sys
import subprocess
import threading
import shutil
import datetime
import platform
from pathlib import Path
try:
import tkinter as tk
from tkinter import ttk, messagebox, filedialog, scrolledtext
except ImportError:
print("错误: 需要 tkinter 库, 请确认 Python 环境完整")
sys.exit(1)
# ──────────────────────────────────────────────
# 全局常量
# ──────────────────────────────────────────────
WINDOW_TITLE = "Windows 驱动备份与恢复工具"
WINDOW_SIZE = "920x680"
def _default_export_dir():
"""优先 D:\\MyDrivers,D 盘不存在则用 C:\\MyDrivers"""
if os.path.isdir("D:\\"):
return r"D:\MyDrivers"
return r"C:\MyDrivers"
DEFAULT_EXPORT_DIR = _default_export_dir()
# ── 配色方案 (商务深蓝风格) ──
THEME_BG = "#1a2332"
THEME_SURFACE = "#2c3e50"
THEME_CARD = "#34495e"
THEME_PRIMARY = "#3498db"
THEME_SECONDARY= "#2980b9"
THEME_WARNING = "#f39c12"
THEME_ERROR = "#e74c3c"
THEME_TEXT = "#ecf0f1"
THEME_DIM = "#95a5a6"
THEME_COMMENT = "#7f8c8d"
THEME_BLUE = "#3498db"
THEME_LINE = "#34495e"
THEME_OVERLAY = "#7f8c8d"
# ── 图标 (Unicode 符号) ──
ICON_BACKUP = "\u2B07" # ⬇
ICON_RESTORE = "\u2B06" # ⬆
ICON_SCAN = "\u2316" # ⌖
ICON_FOLDER = "\U0001F4C2" # 📂
ICON_ADMIN = "\u26A0" # ⚠
ICON_OK = "\u2713" # ✓
ICON_FAIL = "\u2717" # ✗
class DriverTool:
"""驱动备份与恢复工具主类"""
def __init__(self, root):
self.root = root
self.root.title(WINDOW_TITLE)
self.root.geometry(WINDOW_SIZE)
self.root.minsize(780, 560)
self.root.configure(bg=THEME_BG)
self._setup_styles()
self._build_ui()
self._check_admin()
# ── 样式配置 ──────────────────────────────
def _setup_styles(self):
style = ttk.Style()
style.theme_use("clam")
# 全局默认
style.configure(".", background=THEME_BG, foreground=THEME_TEXT,
fieldbackground=THEME_SURFACE, font=("Microsoft YaHei UI", 10))
style.configure("TFrame", background=THEME_BG)
style.configure("TLabel", background=THEME_BG, foreground=THEME_TEXT)
# 标题
style.configure("Title.TLabel", font=("Microsoft YaHei UI", 18, "bold"),
foreground=THEME_PRIMARY, background=THEME_BG)
style.configure("Subtitle.TLabel", font=("Microsoft YaHei UI", 9),
foreground=THEME_DIM, background=THEME_BG)
# 子标题 / 状态
style.configure("Sub.TLabel", font=("Microsoft YaHei UI", 10),
foreground=THEME_DIM, background=THEME_BG)
style.configure("Status.TLabel", font=("Microsoft YaHei UI", 9),
foreground=THEME_SECONDARY, background=THEME_BG)
style.configure("Error.TLabel", font=("Microsoft YaHei UI", 9, "bold"),
foreground=THEME_ERROR, background=THEME_BG)
style.configure("Admin.TLabel", font=("Microsoft YaHei UI", 9, "bold"),
foreground=THEME_WARNING, background=THEME_BG)
# 卡片容器
style.configure("Card.TFrame", background=THEME_CARD)
style.configure("Card.TLabel", background=THEME_CARD, foreground=THEME_TEXT)
style.configure("CardSub.TLabel", background=THEME_CARD, foreground=THEME_DIM,
font=("Microsoft YaHei UI", 9, "bold"))
# 主操作按钮 (备份/恢复)
style.configure("Accent.TButton", font=("Microsoft YaHei UI", 11, "bold"),
padding=(20, 8))
style.map("Accent.TButton",
background=[("active", THEME_PRIMARY), ("!active", THEME_LINE), ("disabled", THEME_COMMENT)],
foreground=[("active", THEME_BG), ("!active", THEME_TEXT), ("disabled", THEME_DIM)])
# 次要按钮 (扫描/打开)
style.configure("Secondary.TButton", font=("Microsoft YaHei UI", 10),
padding=(16, 6))
style.map("Secondary.TButton",
background=[("active", THEME_SECONDARY), ("!active", THEME_SURFACE), ("disabled", THEME_COMMENT)],
foreground=[("active", THEME_BG), ("!active", THEME_TEXT), ("disabled", THEME_DIM)])
# 输入框
style.configure("TEntry", fieldbackground=THEME_SURFACE,
foreground=THEME_TEXT, insertcolor=THEME_TEXT,
bordercolor=THEME_LINE, focuscolor=THEME_BLUE)
# 小按钮 (浏览/清空)
style.configure("Browse.TButton", padding=(10, 4))
style.map("Browse.TButton",
background=[("active", THEME_LINE), ("!active", THEME_SURFACE)],
foreground=[("active", THEME_TEXT), ("!active", THEME_TEXT)])
# 单选按钮
style.configure("TRadiobutton", background=THEME_CARD, foreground=THEME_TEXT,
font=("Microsoft YaHei UI", 10))
style.map("TRadiobutton",
background=[("active", THEME_CARD)],
foreground=[("active", THEME_TEXT), ("!active", THEME_TEXT)])
# 分隔线
style.configure("TSeparator", background=THEME_LINE)
# 进度条
style.configure("TProgressbar",
troughcolor=THEME_SURFACE,
background=THEME_PRIMARY,
bordercolor=THEME_LINE,
lightcolor=THEME_PRIMARY,
darkcolor=THEME_PRIMARY)
# ── 界面构建 ──────────────────────────────
def _build_ui(self):
main = ttk.Frame(self.root, padding=(28, 18, 28, 14))
main.pack(fill="both", expand=True)
# ── 标题区 ──
title_frame = ttk.Frame(main)
title_frame.pack(fill="x", pady=(0, 4))
ttk.Label(title_frame, text=WINDOW_TITLE, style="Title.TLabel").pack(side="left")
ttk.Label(title_frame, text="v1.0", style="Subtitle.TLabel").pack(side="left", padx=(8, 0), pady=(8, 0))
# 管理员状态
self.admin_label = ttk.Label(main, text="", style="Status.TLabel")
self.admin_label.pack(anchor="w", pady=(0, 8))
# ── 卡片: 路径设置 ──
path_card = ttk.Frame(main, style="Card.TFrame", padding=(14, 10))
path_card.pack(fill="x", pady=(0, 8))
ttk.Label(path_card, text="驱动目录", style="CardSub.TLabel").pack(anchor="w", pady=(0, 6))
path_row = ttk.Frame(path_card, style="Card.TFrame")
path_row.pack(fill="x")
self.path_var = tk.StringVar(value=DEFAULT_EXPORT_DIR)
entry = ttk.Entry(path_row, textvariable=self.path_var, width=60)
entry.pack(side="left", padx=(0, 8), fill="x", expand=True)
ttk.Button(path_row, text="浏览...", style="Browse.TButton",
command=self._browse_dir).pack(side="left")
# ── 卡片: 安装模式 ──
mode_card = ttk.Frame(main, style="Card.TFrame", padding=(14, 10))
mode_card.pack(fill="x", pady=(0, 8))
ttk.Label(mode_card, text="安装模式", style="CardSub.TLabel").pack(anchor="w", pady=(0, 6))
mode_row = ttk.Frame(mode_card, style="Card.TFrame")
mode_row.pack(fill="x")
self.mode_var = tk.StringVar(value="full")
modes = [
("完整安装 (添加 + 安装)", "full"),
("仅添加到驱动存储", "add"),
("仅扫描硬件变更", "scan"),
]
for i, (text, val) in enumerate(modes):
rb = ttk.Radiobutton(mode_row, text=text, variable=self.mode_var, value=val)
rb.pack(side="left", padx=(0, 20) if i < len(modes) - 1 else (0, 0))
# ── 按钮区 ──
btn_frame = ttk.Frame(main)
btn_frame.pack(fill="x", pady=(4, 8))
self.btn_backup = ttk.Button(btn_frame, text=f" {ICON_BACKUP} 备份驱动 ",
style="Accent.TButton",
command=self._start_backup)
self.btn_backup.pack(side="left", padx=(0, 12))
self.btn_restore = ttk.Button(btn_frame, text=f" {ICON_RESTORE} 恢复驱动 ",
style="Accent.TButton",
command=self._start_restore)
self.btn_restore.pack(side="left", padx=(0, 12))
self.btn_scan = ttk.Button(btn_frame, text=f" {ICON_SCAN} 扫描硬件 ",
style="Secondary.TButton",
command=self._start_scan)
self.btn_scan.pack(side="left", padx=(0, 12))
self.btn_open = ttk.Button(btn_frame, text=f" {ICON_FOLDER} 打开目录 ",
style="Secondary.TButton",
command=self._open_dir)
self.btn_open.pack(side="left")
# ── 进度条 + 状态 ──
progress_frame = ttk.Frame(main)
progress_frame.pack(fill="x", pady=(0, 4))
self.progress = ttk.Progressbar(progress_frame, mode="determinate", length=400)
self.progress.pack(fill="x")
stat_frame = ttk.Frame(main)
stat_frame.pack(fill="x", pady=(2, 0))
self.stat_label = ttk.Label(stat_frame, text="就绪", style="Status.TLabel")
self.stat_label.pack(side="left")
# ── 日志区 ──
log_header = ttk.Frame(main)
log_header.pack(fill="x", pady=(10, 4))
ttk.Label(log_header, text="运行日志", style="Sub.TLabel").pack(side="left")
ttk.Button(log_header, text="清空", style="Browse.TButton",
command=self._clear_log).pack(side="right")
self.log = scrolledtext.ScrolledText(
main, height=10, wrap="word", state="disabled",
bg=THEME_SURFACE, fg=THEME_TEXT, insertbackground=THEME_TEXT,
font=("Cascadia Code", 9), relief="flat", padx=10, pady=8,
selectbackground=THEME_BLUE, selectforeground=THEME_BG,
borderwidth=0, highlightthickness=1,
highlightbackground=THEME_LINE, highlightcolor=THEME_BLUE
)
self.log.pack(fill="both", expand=True, pady=(0, 4))
# 日志颜色标签
self.log.tag_configure("ok", foreground=THEME_PRIMARY)
self.log.tag_configure("warn", foreground=THEME_WARNING)
self.log.tag_configure("error", foreground=THEME_ERROR)
self.log.tag_configure("info", foreground=THEME_SECONDARY)
self.log.tag_configure("dim", foreground=THEME_DIM)
self.log.tag_configure("header", foreground=THEME_OVERLAY,
font=("Cascadia Code", 9, "bold"))
# ── 底部统计栏 ──
ttk.Separator(main, orient="horizontal").pack(fill="x", pady=(4, 4))
bottom = ttk.Frame(main)
bottom.pack(fill="x")
self.count_label = ttk.Label(bottom, text="", style="Sub.TLabel")
self.count_label.pack(side="left")
# ── 辅助方法 ──────────────────────────────
def _check_admin(self):
"""检测管理员权限"""
try:
result = subprocess.run(
["net", "session"],
capture_output=True, timeout=5
)
if result.returncode == 0:
self.admin_label.configure(
text=f"{ICON_OK} 管理员权限已确认", style="Status.TLabel"
)
self._set_buttons(True)
else:
self.admin_label.configure(
text=f"{ICON_ADMIN} 未以管理员身份运行 - 备份/恢复功能不可用",
style="Error.TLabel"
)
self._set_buttons(False)
except Exception:
self.admin_label.configure(
text=f"{ICON_FAIL} 无法检测权限状态", style="Error.TLabel"
)
self._set_buttons(False)
def _set_buttons(self, enabled):
state = "normal" if enabled else "disabled"
for btn in (self.btn_backup, self.btn_restore, self.btn_scan):
btn.configure(state=state)
def _browse_dir(self):
chosen = filedialog.askdirectory(
title="选择驱动目录",
initialdir=self.path_var.get()
)
if chosen:
self.path_var.set(chosen)
def _open_dir(self):
path = self.path_var.get()
if not os.path.isdir(path):
messagebox.showwarning("提示", f"目录不存在:\n{path}")
return
os.startfile(path)
def _get_driver_dir(self):
path = self.path_var.get().strip()
if not path:
return None
if path.endswith(("\\", "/")):
path = path.rstrip("\\/")
return path
def _ui(self, func, *args):
"""将 UI 操作调度到主线程执行 (线程安全)"""
self.root.after(0, func, *args)
def _log(self, text, tag=None, end=True):
"""写入日志. end=False 时不追加换行符"""
self.log.configure(state="normal")
line = text if not end else text + "\n"
if tag:
self.log.insert("end", line, tag)
else:
self.log.insert("end", line)
self.log.see("end")
self.log.configure(state="disabled")
def _log_header(self, text):
self._log(f"{'=' * 60}", "dim")
self._log(text, "header")
self._log(f"{'=' * 60}", "dim")
def _clear_log(self):
self.log.configure(state="normal")
self.log.delete("1.0", "end")
self.log.configure(state="disabled")
def _set_progress(self, current, total):
if total > 0:
self.progress["value"] = (current / total) * 100
def _update_stat(self, text):
self.stat_label.configure(text=text)
def _update_count(self, ok=0, added=0, skip=0, fail=0):
text = (f"安装成功: {ok} | 已添加: {added} | "
f"跳过: {skip} | 失败: {fail}")
self.count_label.configure(text=text)
def _toggle_buttons(self, enabled):
state = "normal" if enabled else "disabled"
for btn in (self.btn_backup, self.btn_restore, self.btn_scan):
btn.configure(state=state)
def _find_inf_files(self, directory):
"""递归查找所有 .inf 文件"""
inf_list = []
for root_dir, dirs, files in os.walk(directory):
for f in files:
if f.lower().endswith(".inf"):
inf_list.append(os.path.join(root_dir, f))
return inf_list
def _get_system_info(self):
"""收集系统信息"""
info = {}
# 基本系统信息
info["操作系统"] = platform.platform()
info["系统版本"] = platform.version()
info["系统架构"] = platform.architecture()[0]
info["计算机名称"] = platform.node()
info["处理器"] = platform.processor()
# Windows 特定信息
if sys.platform == "win32":
try:
# 获取 Windows 版本详细信息
code, stdout, _ = self._run_cmd(["systeminfo"], timeout=30)
if code == 0:
for line in stdout.splitlines():
if "OS 名称:" in line:
info["OS 名称"] = line.split(":", 1)[1].strip()
elif "OS 版本:" in line:
info["OS 版本"] = line.split(":", 1)[1].strip()
elif "系统类型:" in line:
info["系统类型"] = line.split(":", 1)[1].strip()
elif "注册的所有人:" in line:
info["注册的所有人"] = line.split(":", 1)[1].strip()
except Exception:
pass
# 内存信息
try:
if sys.platform == "win32":
code, stdout, _ = self._run_cmd(["wmic", "OS", "get", "TotalVisibleMemorySize"], timeout=10)
if code == 0:
lines = stdout.strip().splitlines()
if len(lines) > 1:
try:
memory_kb = int(lines[1].strip())
info["内存"] = f"{memory_kb / 1024 / 1024:.2f} GB"
except:
pass
except Exception:
pass
return info
def _read_backup_info(self, directory):
"""读取并解析备份信息文件"""
info_file = os.path.join(directory, "backup_info.txt")
if not os.path.exists(info_file):
return None
info = {}
try:
with open(info_file, "r", encoding="utf-8") as f:
lines = f.readlines()
current_section = None
for line in lines:
line = line.strip()
if line.startswith("#"):
current_section = line[1:].strip()
elif line.startswith("="):
continue
elif ":" in line:
key, value = line.split(":", 1)
key = key.strip()
value = value.strip()
info[key] = value
except Exception:
return None
return info
# ── 运行命令 ──────────────────────────────
def _run_cmd(self, cmd, timeout=120):
"""运行系统命令并返回结果"""
try:
kwargs = dict(capture_output=True, text=True, timeout=timeout)
if sys.platform == "win32":
kwargs["creationflags"] = subprocess.CREATE_NO_WINDOW
result = subprocess.run(cmd, **kwargs)
return result.returncode, result.stdout, result.stderr
except subprocess.TimeoutExpired:
return -1, "", "命令执行超时"
except Exception as e:
return -1, "", str(e)
# ── 备份驱动 ──────────────────────────────
def _start_backup(self):
driver_dir = self._get_driver_dir()
if not driver_dir:
messagebox.showwarning("提示", "请先设置驱动导出目录")
return
def worker():
self._ui(self._toggle_buttons, False)
self._ui(self._clear_log)
self._ui(self._log_header, f"开始备份驱动 -> {driver_dir}")
self._ui(self._log, f"时间: {datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}", "info")
self._ui(self._log, "")
if not os.path.isdir(driver_dir):
os.makedirs(driver_dir, exist_ok=True)
self._ui(self._log, f"已创建目录: {driver_dir}", "info")
self._ui(self._update_stat, "正在备份驱动, 请稍候...")
self._ui(self._log, "执行: dism /online /export-driver ...", "dim")
self._ui(self._log, "")
code, stdout, stderr = self._run_cmd([
"dism", "/online", "/export-driver",
f"/destination:{driver_dir}"
], timeout=600)
if code == 0:
self._ui(self._log, "[OK] 驱动备份成功!", "ok")
if stdout:
for line in stdout.strip().splitlines():
if line.strip():
self._ui(self._log, f" {line}", "dim")
self._ui(self._update_stat, "备份完成")
inf_files = self._find_inf_files(driver_dir)
self._ui(self._log, f"\n目录中共有 {len(inf_files)} 个驱动文件 (.inf)", "info")
# 创建备份信息文件
info_file = os.path.join(driver_dir, "backup_info.txt")
try:
with open(info_file, "w", encoding="utf-8") as f:
f.write("# 驱动备份信息\n")
f.write("=" * 60 + "\n")
f.write(f"备份时间: {datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n")
f.write(f"备份目录: {driver_dir}\n")
f.write(f"驱动文件数量: {len(inf_files)}\n")
f.write(f"作者: 吾爱破解论坛 liuyang207\n")
f.write("\n# 系统信息\n")
f.write("=" * 60 + "\n")
system_info = self._get_system_info()
for key, value in system_info.items():
f.write(f"{key}: {value}\n")
f.write("\n# 备份命令\n")
f.write("=" * 60 + "\n")
f.write(f"dism /online /export-driver /destination:{driver_dir}\n")
self._ui(self._log, f"[OK] 已创建备份信息文件: {info_file}", "ok")
except Exception as e:
self._ui(self._log, f"[WARN] 创建备份信息文件失败: {str(e)}", "warn")
else:
self._ui(self._log, f"[FAIL] 驱动备份失败, 错误码: {code}", "error")
if stderr:
self._ui(self._log, f" {stderr.strip()}", "error")
self._ui(self._update_stat, "备份失败")
self._ui(messagebox.showerror, "错误", f"驱动备份失败:\n{stderr or f'错误码 {code}'}")
self._ui(self._set_progress, 100, 100)
self._ui(self._toggle_buttons, True)
threading.Thread(target=worker, daemon=True).start()
# ── 恢复驱动 ──────────────────────────────
def _start_restore(self):
driver_dir = self._get_driver_dir()
if not driver_dir:
messagebox.showwarning("提示", "请先设置驱动目录")
return
if not os.path.isdir(driver_dir):
messagebox.showwarning("提示", f"驱动目录不存在:\n{driver_dir}")
return
# 检查备份信息文件
backup_info = self._read_backup_info(driver_dir)
if backup_info:
# 显示自定义风格的备份信息确认对话框
dialog = tk.Toplevel(self.root)
dialog.title("备份信息确认")
dialog.geometry("500x400")
dialog.resizable(False, False)
dialog.transient(self.root)
dialog.grab_set()
dialog.attributes('-topmost', True)
# 计算对话框位置,使其在主窗口中央
root_x = self.root.winfo_x()
root_y = self.root.winfo_y()
root_width = self.root.winfo_width()
root_height = self.root.winfo_height()
dialog_width = 500
dialog_height = 400
x = root_x + (root_width - dialog_width) // 2
y = root_y + (root_height - dialog_height) // 2
# 确保对话框位置有效
x = max(0, x)
y = max(0, y)
dialog.geometry(f"{dialog_width}x{dialog_height}+{x}+{y}")
# 强制对话框显示在最前面
dialog.lift()
self.root.update()
dialog.focus_force()
# 设置对话框样式
dialog.configure(bg=THEME_BG)
# 标题
title_label = ttk.Label(
dialog,
text="备份信息确认",
style="Title.TLabel"
)
title_label.pack(pady=(18, 12))
# 信息框架
info_frame = ttk.Frame(dialog, style="Card.TFrame", padding=(16, 12))
info_frame.pack(fill="both", expand=True, padx=24, pady=(0, 12))
# 备份信息
info_text = []
if "备份时间" in backup_info:
info_text.append(f"备份时间: {backup_info['备份时间']}")
if "驱动文件数量" in backup_info:
info_text.append(f"驱动文件数量: {backup_info['驱动文件数量']}")
if "作者" in backup_info:
info_text.append(f"作者: {backup_info['作者']}")
if "操作系统" in backup_info:
info_text.append(f"操作系统: {backup_info['操作系统']}")
if "系统架构" in backup_info:
info_text.append(f"系统架构: {backup_info['系统架构']}")
for line in info_text:
ttk.Label(info_frame, text=line, style="Card.TLabel",
wraplength=430).pack(anchor="w", pady=3)
# 确认提示
confirm_label = ttk.Label(
dialog,
text="确认要恢复这些驱动吗?",
style="Status.TLabel"
)
confirm_label.pack(pady=(0, 12))
# 按钮框架
btn_frame = ttk.Frame(dialog)
btn_frame.pack(fill="x", padx=24, pady=(0, 20))
# 确认按钮
confirm_btn = ttk.Button(
btn_frame,
text=" 确认恢复 ",
style="Accent.TButton",
command=lambda: self._confirm_restore(dialog, True)
)
confirm_btn.pack(side="left", padx=(0, 12), fill="x", expand=True)
# 取消按钮
cancel_btn = ttk.Button(
btn_frame,
text=" 取 消 ",
style="Secondary.TButton",
command=lambda: self._confirm_restore(dialog, False)
)
cancel_btn.pack(side="left", fill="x", expand=True)
# 等待用户响应
self.restore_confirmed = None
dialog.wait_window()
if not self.restore_confirmed:
return
def worker():
self._ui(self._toggle_buttons, False)
self._ui(self._clear_log)
mode = self.mode_var.get()
mode_text = {"full": "完整安装", "add": "仅添加", "scan": "仅扫描"}
self._ui(self._log_header, f"恢复驱动 - 模式: {mode_text.get(mode, mode)}")
self._ui(self._log, f"驱动来源: {driver_dir}", "info")
self._ui(self._log, f"时间: {datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}", "info")
self._ui(self._log, "")
# scan 模式:仅扫描硬件变更,无需遍历 inf 文件
if mode == "scan":
self._ui(self._log, "正在扫描硬件变更...", "info")
self._ui(self._update_stat, "正在扫描硬件...")
code, stdout, stderr = self._run_cmd(
["pnputil", "/scan-devices"], timeout=120
)
if stdout:
for line in stdout.strip().splitlines():
if line.strip():
self._ui(self._log, f" {line}")
self._ui(self._log, "")
if code == 0:
self._ui(self._log, "[OK] 硬件扫描完成", "ok")
self._ui(self._log, "部分驱动可能需要重启后生效。", "warn")
else:
self._ui(self._log, f"[FAIL] 扫描失败, 错误码: {code}", "error")
if stderr:
self._ui(self._log, f" {stderr.strip()}", "error")
self._ui(self._set_progress, 100, 100)
self._ui(self._update_stat, "扫描完成")
self._ui(self._toggle_buttons, True)
return
inf_files = self._find_inf_files(driver_dir)
total = len(inf_files)
if total == 0:
self._ui(self._log, "[WARN] 未找到任何 .inf 驱动文件!", "warn")
self._ui(self._update_stat, "未找到驱动文件")
self._ui(self._toggle_buttons, True)
return
self._ui(self._log, f"共发现 {total} 个驱动文件\n", "info")
ok = added = skip = fail = 0
for i, inf_path in enumerate(inf_files, 1):
name = os.path.basename(inf_path)
self._ui(self._set_progress, i, total)
self._ui(self._update_stat, f"处理中 [{i}/{total}]: {name}")
self._ui(self._log, f"[{i}/{total}] {name} ... ", "dim", False)
if mode == "full":
# 先尝试 add + install
code, stdout, stderr = self._run_cmd(
["pnputil", "/add-driver", inf_path, "/install"],
timeout=60
)
if code == 0:
self._ui(self._log_append, "OK - installed", "ok")
ok += 1
continue
# 回退到仅添加
code2, _, _ = self._run_cmd(
["pnputil", "/add-driver", inf_path],
timeout=60
)
if code2 == 0:
self._ui(self._log_append, "OK - added to store", "ok")
added += 1
elif code2 == 258:
self._ui(self._log_append, "SKIP - already exists", "warn")
skip += 1
else:
self._ui(self._log_append, f"FAIL - error {code2}", "error")
fail += 1
elif mode == "add":
code, stdout, stderr = self._run_cmd(
["pnputil", "/add-driver", inf_path],
timeout=60
)
if code == 0:
self._ui(self._log_append, "OK - added", "ok")
added += 1
elif code == 258:
self._ui(self._log_append, "SKIP - already exists", "warn")
skip += 1
else:
self._ui(self._log_append, f"FAIL - error {code}", "error")
fail += 1
# 汇总
self._ui(self._log, "")
self._ui(self._log_header, "结果汇总")
self._ui(self._log, f" 安装成功: {ok}", "ok")
self._ui(self._log, f" 已添加: {added}", "info")
self._ui(self._log, f" 跳过: {skip}", "warn")
self._ui(self._log, f" 失败: {fail}", "error" if fail > 0 else "dim")
self._ui(self._log, f" 合计: {total}", "info")
self._ui(self._update_count, ok, added, skip, fail)
self._ui(self._set_progress, 100, 100)
# 安装成功后扫描硬件
if ok > 0 or added > 0:
self._ui(self._log, "")
self._ui(self._log, "正在扫描硬件变更...", "info")
code, stdout, stderr = self._run_cmd(
["pnputil", "/scan-devices"], timeout=120
)
self._ui(self._log, "硬件扫描完成。", "ok" if code == 0 else "error")
self._ui(self._log, "")
self._ui(self._log, "部分驱动可能需要重启后生效。", "warn")
self._ui(self._update_stat, "恢复完成")
self._ui(self._toggle_buttons, True)
# 显示带有倒计时的重启确认对话框
self._ui(self._show_restart_dialog, ok + added, skip, fail)
threading.Thread(target=worker, daemon=True).start()
def _start_scan(self):
"""仅扫描硬件变更"""
def worker():
self._ui(self._toggle_buttons, False)
self._ui(self._clear_log)
self._ui(self._log_header, "扫描硬件变更")
self._ui(self._log, "")
self._ui(self._update_stat, "正在扫描硬件...")
code, stdout, stderr = self._run_cmd(
["pnputil", "/scan-devices"], timeout=120
)
if stdout:
for line in stdout.strip().splitlines():
if line.strip():
self._ui(self._log, f" {line}")
self._ui(self._log, "")
if code == 0:
self._ui(self._log, "[OK] 硬件扫描完成", "ok")
else:
self._ui(self._log, f"[FAIL] 扫描失败, 错误码: {code}", "error")
self._ui(self._update_stat, "扫描完成")
self._ui(self._set_progress, 100, 100)
self._ui(self._toggle_buttons, True)
threading.Thread(target=worker, daemon=True).start()
# ── 日志辅助(同一行追加) ──────────────────
def _log_append(self, text, tag=None):
"""在当前行末尾追加内容并换行(用于显示处理结果)"""
self.log.configure(state="normal")
self.log.insert("end", text + "\n", tag)
self.log.see("end")
self.log.configure(state="disabled")
def _show_restart_dialog(self, success_count, skip_count, fail_count):
"""显示带有倒计时的重启确认对话框"""
dialog = tk.Toplevel(self.root)
dialog.title("驱动恢复完成")
dialog.geometry("500x300")
dialog.resizable(False, False)
dialog.transient(self.root)
dialog.grab_set()
dialog.attributes('-topmost', True)
# 计算对话框位置,使其在主窗口中央
root_x = self.root.winfo_x()
root_y = self.root.winfo_y()
root_width = self.root.winfo_width()
root_height = self.root.winfo_height()
dialog_width = 500
dialog_height = 300
x = root_x + (root_width - dialog_width) // 2
y = root_y + (root_height - dialog_height) // 2
# 确保对话框位置有效
x = max(0, x)
y = max(0, y)
dialog.geometry(f"{dialog_width}x{dialog_height}+{x}+{y}")
# 强制对话框显示在最前面
dialog.lift()
self.root.update()
dialog.focus_force()
# 设置对话框样式
dialog.configure(bg=THEME_BG)
# 标题
title_label = ttk.Label(
dialog,
text="驱动恢复完成",
style="Title.TLabel"
)
title_label.pack(pady=(20, 10))
# 结果信息
result_text = f"成功: {success_count} 跳过: {skip_count} 失败: {fail_count}"
result_label = ttk.Label(
dialog,
text=result_text,
style="Sub.TLabel"
)
result_label.pack(pady=(0, 12))
# 重启提示
restart_label = ttk.Label(
dialog,
text="部分驱动需要重启后生效",
style="Status.TLabel"
)
restart_label.pack(pady=(0, 8))
# 倒计时标签
self.countdown_var = tk.StringVar(value="10")
countdown_label = ttk.Label(
dialog,
textvariable=self.countdown_var,
font=("Microsoft YaHei UI", 20, "bold"),
foreground=THEME_PRIMARY,
background=THEME_BG
)
countdown_label.pack(pady=(0, 16))
# 按钮框架
btn_frame = ttk.Frame(dialog)
btn_frame.pack(fill="x", padx=24, pady=(0, 20))
# 立即重启按钮
restart_btn = ttk.Button(
btn_frame,
text=" 立即重启 ",
style="Accent.TButton",
command=lambda: self._restart_system(dialog)
)
restart_btn.pack(side="left", padx=(0, 12), fill="x", expand=True)
# 取消重启按钮
cancel_btn = ttk.Button(
btn_frame,
text=" 取消重启 ",
style="Secondary.TButton",
command=lambda: self._cancel_restart(dialog)
)
cancel_btn.pack(side="left", fill="x", expand=True)
# 开始倒计时
self.countdown_time = 10
self.countdown_id = None
self._update_countdown(dialog)
def _update_countdown(self, dialog):
"""更新倒计时"""
if self.countdown_time > 0:
# 检查对话框是否仍然存在
if not dialog.winfo_exists():
return
self.countdown_time -= 1
self.countdown_var.set(str(self.countdown_time))
self.countdown_id = self.root.after(1000, self._update_countdown, dialog)
else:
# 倒计时结束,自动重启
if dialog.winfo_exists():
self._restart_system(dialog)
def _cancel_restart(self, dialog):
"""取消重启"""
# 取消倒计时
if self.countdown_id:
self.root.after_cancel(self.countdown_id)
# 关闭对话框
dialog.destroy()
def _confirm_restore(self, dialog, confirmed):
"""处理恢复确认"""
self.restore_confirmed = confirmed
dialog.destroy()
def _restart_system(self, dialog):
"""重启系统"""
dialog.destroy()
try:
# 使用 shutdown 命令重启系统,/r 表示重启,/t 0 表示立即执行
subprocess.run(["shutdown", "/r", "/t", "0"], capture_output=True)
except Exception:
pass
def main():
root = tk.Tk()
# 尝试设置 DPI 感知 (Windows)
try:
from ctypes import windll
windll.shcore.SetProcessDpiAwareness(1)
except Exception:
pass
# 尝试设置窗口图标
try:
root.iconbitmap(default="")
except Exception:
pass
app = DriverTool(root)
root.mainloop()
if __name__ == "__main__":
main()