镜像自地址
https://github.com/a13zhy/a13rollcall.git
已同步 2026-10-20 11:27:47 +08:00
795 行
34 KiB
Python
795 行
34 KiB
Python
import os
|
||
import sys
|
||
import subprocess
|
||
import configparser
|
||
import tkinter as tk
|
||
from tkinter import messagebox
|
||
from tkinter import Tk, Frame, Label
|
||
from tkinter.font import Font
|
||
|
||
# ========== 文件名配置区,可根据实际打包名称修改 ==========
|
||
APP_EXE = "A13课堂点名系统.exe"
|
||
WIZARD_INI = "wizard_record.ini"
|
||
STUDENT_FILE = "students.txt"
|
||
TEXT_FILE = "texts.txt"
|
||
MAIN_PY = "main.py"
|
||
# ========================================================
|
||
|
||
if getattr(sys, 'frozen', False):
|
||
BASE_DIR = os.path.dirname(sys.executable)
|
||
else:
|
||
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
||
|
||
INI_PATH = os.path.join(BASE_DIR, WIZARD_INI)
|
||
STUDENT_PATH = os.path.join(BASE_DIR, STUDENT_FILE)
|
||
TEXT_PATH = os.path.join(BASE_DIR, TEXT_FILE)
|
||
APP_EXE_PATH = os.path.join(BASE_DIR, APP_EXE)
|
||
MAIN_PY_PATH = os.path.join(BASE_DIR, MAIN_PY)
|
||
|
||
# 配色方案 - 现代蓝白风格
|
||
COLORS = {
|
||
"bg": "#f5f7fa",
|
||
"header_bg": "#2563eb",
|
||
"header_bg2": "#1d4ed8",
|
||
"card_bg": "#ffffff",
|
||
"text_primary": "#1e293b",
|
||
"text_secondary": "#64748b",
|
||
"text_hint": "#94a3b8",
|
||
"text_light": "#ffffff",
|
||
"primary": "#2563eb",
|
||
"primary_hover": "#1d4ed8",
|
||
"primary_pressed": "#1e40af",
|
||
"primary_light": "#dbeafe",
|
||
"success": "#10b981",
|
||
"success_hover": "#059669",
|
||
"success_light": "#d1fae5",
|
||
"warning": "#f59e0b",
|
||
"warning_light": "#fef3c7",
|
||
"danger": "#ef4444",
|
||
"danger_light": "#fee2e2",
|
||
"border": "#e2e8f0",
|
||
"step_active": "#2563eb",
|
||
"step_done": "#10b981",
|
||
"step_inactive": "#cbd5e1",
|
||
"btn_secondary": "#f1f5f9",
|
||
"btn_secondary_hover": "#e2e8f0",
|
||
"btn_secondary_text": "#475569",
|
||
}
|
||
|
||
|
||
def apply_round_corner(window, radius=16):
|
||
"""给Windows窗口设置圆角区域(仅overrideredirect窗口有效)"""
|
||
try:
|
||
import ctypes
|
||
hwnd = ctypes.windll.user32.GetParent(window.winfo_id())
|
||
if not hwnd:
|
||
hwnd = window.winfo_id()
|
||
window.update_idletasks()
|
||
w = window.winfo_width()
|
||
h = window.winfo_height()
|
||
if w <= 1 or h <= 1:
|
||
return
|
||
hrgn = ctypes.windll.gdi32.CreateRoundRectRgn(0, 0, w, h, radius * 2, radius * 2)
|
||
ctypes.windll.user32.SetWindowRgn(hwnd, hrgn, True)
|
||
except Exception:
|
||
pass
|
||
|
||
|
||
def check_file_status(filepath):
|
||
"""检测文件状态,返回 (是否存在, 是否有内容, 行数)"""
|
||
if not os.path.exists(filepath):
|
||
return False, False, 0
|
||
try:
|
||
with open(filepath, "r", encoding="utf-8") as f:
|
||
lines = [l.strip() for l in f.readlines() if l.strip()]
|
||
return True, len(lines) > 0, len(lines)
|
||
except Exception:
|
||
return True, False, 0
|
||
|
||
|
||
class RoundedButton(tk.Canvas):
|
||
"""自定义圆角按钮"""
|
||
|
||
def __init__(self, master, text="", width=120, height=38, radius=8,
|
||
bg=None, hover_bg=None, pressed_bg=None, fg=None, font=None,
|
||
command=None):
|
||
super().__init__(master, width=width, height=height, bg=master["bg"],
|
||
highlightthickness=0, bd=0)
|
||
self.master_bg = master["bg"]
|
||
self.text = text
|
||
self.width = width
|
||
self.height = height
|
||
self.radius = radius
|
||
self.command = command
|
||
self.disabled = False
|
||
self.normal_bg = bg or COLORS["primary"]
|
||
self.hover_bg = hover_bg or COLORS["primary_hover"]
|
||
self.pressed_bg = pressed_bg or COLORS["primary_pressed"]
|
||
self.fg = fg or COLORS["text_light"]
|
||
self.font = font or ("微软雅黑", 11, "normal")
|
||
self._draw(self.normal_bg)
|
||
self.bind("<Enter>", self._on_enter)
|
||
self.bind("<Leave>", self._on_leave)
|
||
self.bind("<Button-1>", self._on_click)
|
||
self.bind("<ButtonRelease-1>", self._on_release)
|
||
|
||
def _draw(self, color):
|
||
self.delete("all")
|
||
r = self.radius
|
||
w = self.width
|
||
h = self.height
|
||
self.create_arc((0, 0, 2 * r, 2 * r), start=90, extent=90, fill=color, outline=color)
|
||
self.create_arc((w - 2 * r, 0, w, 2 * r), start=0, extent=90, fill=color, outline=color)
|
||
self.create_arc((0, h - 2 * r, 2 * r, h), start=180, extent=90, fill=color, outline=color)
|
||
self.create_arc((w - 2 * r, h - 2 * r, w, h), start=270, extent=90, fill=color, outline=color)
|
||
self.create_rectangle((r, 0, w - r, h), fill=color, outline=color)
|
||
self.create_rectangle((0, r, w, h - r), fill=color, outline=color)
|
||
self.create_text(w / 2, h / 2, text=self.text, fill=self.fg, font=self.font)
|
||
|
||
def _on_enter(self, e):
|
||
if not self.disabled:
|
||
self._draw(self.hover_bg)
|
||
|
||
def _on_leave(self, e):
|
||
if not self.disabled:
|
||
self._draw(self.normal_bg)
|
||
|
||
def _on_click(self, e):
|
||
if not self.disabled:
|
||
self._draw(self.pressed_bg)
|
||
|
||
def _on_release(self, e):
|
||
if not self.disabled:
|
||
self._draw(self.hover_bg)
|
||
if self.command:
|
||
self.command()
|
||
|
||
def set_disabled(self, disabled):
|
||
self.disabled = disabled
|
||
if disabled:
|
||
self._draw("#cbd5e1")
|
||
else:
|
||
self._draw(self.normal_bg)
|
||
|
||
def set_text(self, text):
|
||
self.text = text
|
||
self._draw(self.normal_bg if not self.disabled else "#cbd5e1")
|
||
|
||
|
||
class StepIndicator(Frame):
|
||
"""步骤进度指示器"""
|
||
|
||
def __init__(self, master, total_steps, current_step=0):
|
||
super().__init__(master, bg=COLORS["card_bg"])
|
||
self.total_steps = total_steps
|
||
self.current_step = current_step
|
||
self.dots = []
|
||
self.labels = []
|
||
self._build()
|
||
|
||
def _build(self):
|
||
container = Frame(self, bg=COLORS["card_bg"])
|
||
container.pack(pady=8)
|
||
for i in range(self.total_steps):
|
||
dot_frame = Frame(container, bg=COLORS["card_bg"])
|
||
dot_frame.pack(side="left", padx=10)
|
||
dot = tk.Canvas(dot_frame, width=28, height=28, bg=COLORS["card_bg"],
|
||
highlightthickness=0, bd=0)
|
||
dot.pack()
|
||
self.dots.append(dot)
|
||
lbl = Label(dot_frame, text=f"步骤{i + 1}", bg=COLORS["card_bg"],
|
||
fg=COLORS["text_hint"], font=("微软雅黑", 9))
|
||
lbl.pack(pady=(2, 0))
|
||
self.labels.append(lbl)
|
||
if i < self.total_steps - 1:
|
||
line = Frame(container, bg=COLORS["border"], width=36, height=2)
|
||
line.pack(side="left", pady=(0, 20))
|
||
self._update()
|
||
|
||
def _update(self):
|
||
for i in range(self.total_steps):
|
||
dot = self.dots[i]
|
||
dot.delete("all")
|
||
if i < self.current_step:
|
||
color = COLORS["step_done"]
|
||
dot.create_oval(2, 2, 26, 26, fill=color, outline=color)
|
||
dot.create_text(14, 14, text="✓", fill="white", font=("微软雅黑", 10, "bold"))
|
||
self.labels[i].config(fg=COLORS["step_done"])
|
||
elif i == self.current_step:
|
||
color = COLORS["step_active"]
|
||
dot.create_oval(2, 2, 26, 26, fill=color, outline=color)
|
||
dot.create_text(14, 14, text=str(i + 1), fill="white", font=("微软雅黑", 11, "bold"))
|
||
self.labels[i].config(fg=COLORS["step_active"], font=("微软雅黑", 9, "bold"))
|
||
else:
|
||
color = COLORS["step_inactive"]
|
||
dot.create_oval(2, 2, 26, 26, fill=color, outline=color)
|
||
dot.create_text(14, 14, text=str(i + 1), fill="white", font=("微软雅黑", 11, "bold"))
|
||
self.labels[i].config(fg=COLORS["text_hint"])
|
||
|
||
def set_step(self, step):
|
||
self.current_step = step
|
||
self._update()
|
||
|
||
|
||
class ModernWizard(Tk):
|
||
def __init__(self):
|
||
super().__init__()
|
||
self.title("课堂点名抽背系统 - 首次使用向导")
|
||
self.geometry("880x980")
|
||
self.minsize(820, 620)
|
||
self.configure(bg=COLORS["bg"])
|
||
self.resizable(False, False)
|
||
self.overrideredirect(True)
|
||
|
||
self.current_page = 0
|
||
self.confirm_edit_done = False
|
||
|
||
self.title_font = Font(family="Microsoft YaHei", size=18, weight="bold")
|
||
self.subtitle_font = Font(family="Microsoft YaHei", size=11)
|
||
self.content_font = Font(family="Microsoft YaHei", size=11)
|
||
self.hint_font = Font(family="Microsoft YaHei", size=10)
|
||
self.small_font = Font(family="Microsoft YaHei", size=9)
|
||
|
||
self.pages_content = [
|
||
{
|
||
"icon": "👋",
|
||
"title": "欢迎使用 课堂点名抽背系统",
|
||
"subtitle": "专为课堂教学设计的随机点名与背诵抽查工具 · V6.2",
|
||
"text": """本软件用于课堂随机点名、抽查学生背诵,支持名单导入、课文分段抽查、点名记录保存与积分统计。
|
||
|
||
本向导将带你完成初始配置,只需简单几步即可开始使用:
|
||
|
||
① 了解数据源文件格式
|
||
② 编辑学生名单与课文库
|
||
③ 熟悉软件主要功能(含快捷抽取模式)
|
||
④ 完成配置,启动软件
|
||
|
||
📌 核心亮点:
|
||
• 第六代A13智能抽取引擎,支持动态权重
|
||
• 交互式新手引导,手把手教你每个按钮
|
||
• 快捷抽取模式,极简界面一键点名
|
||
• 积分排行榜 + 数据导出,课堂管理更高效
|
||
• 明暗双主题 + 粒子动效,视觉体验出众
|
||
|
||
点击右下角【下一步】按钮继续,或点击左下角【跳过向导】直接进入软件。""",
|
||
"need_confirm": False,
|
||
"show_skip": True,
|
||
},
|
||
{
|
||
"icon": "📋",
|
||
"title": "数据源文件格式说明",
|
||
"subtitle": "正确的文件格式是软件正常运行的基础",
|
||
"text": """【students.txt 学生名单】
|
||
• 每行写 1 位学生姓名,不要表头
|
||
• 支持带序号格式(如:1 张三),程序会自动去除序号
|
||
• 空行会被自动忽略
|
||
|
||
示例:
|
||
张三
|
||
李四
|
||
王五
|
||
|
||
【texts.txt 课文库】
|
||
• 使用【课文标题】标记新一篇课文,符号必须是中文方括号【】
|
||
• 空行作为段落分隔,两个空行之间的文字算作 1 个段落
|
||
• 连续多行文字会自动合并为同一个段落
|
||
|
||
格式样例:
|
||
【岳阳楼记】
|
||
第一段正文……
|
||
|
||
第二段正文……
|
||
【醉翁亭记】
|
||
第一段正文……
|
||
|
||
⚠ 重要提醒(请务必注意):
|
||
1. 文件必须为 UTF-8 编码的 .txt 文件,禁止使用 Word 文档
|
||
2. 修改完毕务必按 Ctrl+S 保存文件
|
||
3. 保存完成后再回到向导点击【我已编辑完成】
|
||
4. 软件运行中修改txt后,需点击「刷新名单」重载数据
|
||
|
||
💡 小技巧:可以先用记事本打开样例文件,直接在上面修改,格式不会出错!""",
|
||
"need_confirm": True,
|
||
"show_skip": False,
|
||
},
|
||
{
|
||
"icon": "🔧",
|
||
"title": "软件主要功能介绍",
|
||
"subtitle": "了解核心功能,快速上手课堂点名",
|
||
"text": """🎯 核心功能
|
||
|
||
1. 随机点名:点击中间画布或按空格键,启动随机抽取滚动
|
||
2. 状态标记:抽取完成后标记「已背过 / 未背熟 / 未背过」,自动记录积分
|
||
3. 分段抽查:课文库按空行拆分为多个段落,支持随机抽段、顺序推进
|
||
4. 数据统计:自动保存抽查记录,支持积分排行榜和数据导出到桌面
|
||
5. 丰富设置:支持动态权重、明暗主题、快捷键、粒子动效等功能
|
||
|
||
⚡ 快捷抽取模式(新增!)
|
||
|
||
主界面右下角有「⚡ 快捷抽取」按钮,点击后:
|
||
• 隐藏主界面,打开极简抽取窗口
|
||
• 只抽取学生名字,不抽取课文
|
||
• 有独立的设置按钮(置顶/动画/不重复等)
|
||
• 一键「返回主界面」恢复完整功能
|
||
• 记录为临时文件(日期+时间.txt),关闭后自动删除
|
||
|
||
⚠ 注意:快捷抽取的记录不会保存到主程序,关闭即删除,适合临时快速点名!
|
||
|
||
⌨️ 快捷键
|
||
|
||
空格键 — 开始 / 停止抽取
|
||
数字 1 — 标记为「已背过」(+10分)
|
||
数字 2 — 标记为「未背熟」(-5分)
|
||
数字 3 — 标记为「未背过」(-15分)
|
||
R 键 — 重置本轮抽取记录
|
||
|
||
💡 使用小提示
|
||
|
||
• 修改外部 txt 后,需在软件内点击「刷新名单」重载数据
|
||
• 不建议手动修改 records.txt、stats.txt,会破坏统计数据
|
||
• 底部状态栏实时显示名单状态、本轮进度与权重提示
|
||
• 首次进入主界面会有交互式引导,带你熟悉每个按钮
|
||
• 统计数据可一键导出为带时间戳的txt文件,自动保存到桌面""",
|
||
"need_confirm": False,
|
||
"show_skip": False,
|
||
},
|
||
{
|
||
"icon": "✅",
|
||
"title": "向导完成!",
|
||
"subtitle": "配置已就绪,即将启动课堂点名抽背系统",
|
||
"text": """全部引导介绍完毕!
|
||
|
||
向导记录已写入本地,下次启动将直接进入主程序,不再弹出向导。
|
||
|
||
你现在可以:
|
||
|
||
🚀 点击【完成并启动】按钮,立即开始使用
|
||
📖 首次进入主界面会弹出交互式操作引导,高亮每个按钮
|
||
⚙️ 随时可以在软件内点击「设置」修改各项配置
|
||
🌐 在「设置 → 关于」中可访问产品官网及定制班级专属版本
|
||
⚡ 主界面右下角「快捷抽取」按钮,体验极简点名模式
|
||
|
||
📌 温馨提示:
|
||
• 如果数据源文件为空或损坏,下次启动会自动重新打开向导
|
||
• 重置数据后,ini标记会被修改,可重新运行向导和界面教学
|
||
• 统计导出文件会自动加上时间戳,并保存到桌面,方便查找
|
||
|
||
祝你使用愉快,课堂点名高效顺畅!🎓""",
|
||
"need_confirm": False,
|
||
"show_skip": False,
|
||
},
|
||
]
|
||
|
||
self._build_ui()
|
||
self._update_page()
|
||
self._center_window()
|
||
self.after(80, lambda: apply_round_corner(self, 18))
|
||
|
||
def _center_window(self):
|
||
self.update_idletasks()
|
||
sw = self.winfo_screenwidth()
|
||
sh = self.winfo_screenheight()
|
||
w = self.winfo_width()
|
||
h = self.winfo_height()
|
||
x = (sw - w) // 2
|
||
y = (sh - h) // 2
|
||
self.geometry(f"+{x}+{y}")
|
||
|
||
def _build_ui(self):
|
||
# ===== 顶部标题栏 =====
|
||
header = Frame(self, bg=COLORS["header_bg"], height=72)
|
||
header.pack(fill="x")
|
||
header.pack_propagate(False)
|
||
|
||
header_inner = Frame(header, bg=COLORS["header_bg"])
|
||
header_inner.pack(fill="both", expand=True, padx=28)
|
||
|
||
icon_label = Label(header_inner, text="📚", bg=COLORS["header_bg"],
|
||
fg="white", font=("微软雅黑", 24))
|
||
icon_label.pack(side="left", padx=(0, 14))
|
||
|
||
title_frame = Frame(header_inner, bg=COLORS["header_bg"])
|
||
title_frame.pack(side="left")
|
||
Label(title_frame, text="课堂点名抽背系统", bg=COLORS["header_bg"],
|
||
fg="white", font=("微软雅黑", 16, "bold")).pack(anchor="w")
|
||
Label(title_frame, text="首次使用配置向导", bg=COLORS["header_bg"],
|
||
fg="#bfdbfe", font=("微软雅黑", 10)).pack(anchor="w")
|
||
|
||
close_btn = RoundedButton(header_inner, text="✕", width=32, height=32, radius=16,
|
||
bg=COLORS["header_bg"], hover_bg="#1e40af",
|
||
pressed_bg="#1e3a8a", fg="white",
|
||
font=("微软雅黑", 12, "bold"), command=self._on_close)
|
||
close_btn.pack(side="right")
|
||
|
||
# ===== 步骤指示器 =====
|
||
step_container = Frame(self, bg=COLORS["card_bg"])
|
||
step_container.pack(fill="x", padx=20, pady=(16, 0))
|
||
self.step_indicator = StepIndicator(step_container, len(self.pages_content), 0)
|
||
self.step_indicator.pack()
|
||
|
||
Frame(self, bg=COLORS["border"], height=1).pack(fill="x", padx=40, pady=(8, 0))
|
||
|
||
# ===== 内容卡片 =====
|
||
content_wrapper = Frame(self, bg=COLORS["bg"])
|
||
content_wrapper.pack(fill="both", expand=True, padx=20, pady=16)
|
||
|
||
shadow = Frame(content_wrapper, bg="#cbd5e1")
|
||
shadow.pack(fill="both", expand=True, padx=4, pady=4)
|
||
|
||
self.card = Frame(shadow, bg=COLORS["card_bg"],
|
||
highlightbackground=COLORS["border"], highlightthickness=1)
|
||
self.card.pack(fill="both", expand=True, padx=(0, 2), pady=(0, 2))
|
||
|
||
self.card_inner = Frame(self.card, bg=COLORS["card_bg"])
|
||
self.card_inner.pack(fill="both", expand=True, padx=32, pady=24)
|
||
|
||
# 标题区
|
||
title_area = Frame(self.card_inner, bg=COLORS["card_bg"])
|
||
title_area.pack(fill="x")
|
||
|
||
self.page_icon = Label(title_area, text="", bg=COLORS["card_bg"],
|
||
font=("微软雅黑", 28))
|
||
self.page_icon.pack(side="left", padx=(0, 14))
|
||
|
||
title_text_frame = Frame(title_area, bg=COLORS["card_bg"])
|
||
title_text_frame.pack(side="left", fill="x", expand=True)
|
||
self.page_title = Label(title_text_frame, text="", bg=COLORS["card_bg"],
|
||
fg=COLORS["text_primary"], font=self.title_font, anchor="w")
|
||
self.page_title.pack(fill="x")
|
||
self.page_subtitle = Label(title_text_frame, text="", bg=COLORS["card_bg"],
|
||
fg=COLORS["text_secondary"], font=self.subtitle_font, anchor="w")
|
||
self.page_subtitle.pack(fill="x", pady=(2, 0))
|
||
|
||
Frame(self.card_inner, bg=COLORS["border"], height=1).pack(fill="x", pady=(16, 14))
|
||
|
||
# 正文内容区(可滚动)
|
||
self.content_scroll = Frame(self.card_inner, bg=COLORS["card_bg"])
|
||
self.content_scroll.pack(fill="both", expand=True)
|
||
|
||
self.content_canvas = tk.Canvas(self.content_scroll, bg=COLORS["card_bg"],
|
||
highlightthickness=0, bd=0)
|
||
self.content_scrollbar = tk.Scrollbar(self.content_scroll, orient="vertical",
|
||
command=self.content_canvas.yview)
|
||
self.content_canvas.configure(yscrollcommand=self.content_scrollbar.set)
|
||
self.content_canvas.pack(side="left", fill="both", expand=True)
|
||
self.content_scrollbar.pack(side="right", fill="y")
|
||
|
||
self.content_frame = Frame(self.content_canvas, bg=COLORS["card_bg"])
|
||
self.content_window = self.content_canvas.create_window((0, 0), window=self.content_frame,
|
||
anchor="nw")
|
||
self.content_frame.bind("<Configure>", self._on_content_configure)
|
||
self.content_canvas.bind("<Configure>", self._on_canvas_configure)
|
||
|
||
self.content_label = Label(self.content_frame, text="", bg=COLORS["card_bg"],
|
||
fg=COLORS["text_primary"], font=self.content_font,
|
||
justify="left", anchor="nw", wraplength=700)
|
||
self.content_label.pack(fill="both", expand=True, padx=4)
|
||
|
||
# 文件操作按钮区
|
||
self.file_btn_frame = Frame(self.card_inner, bg=COLORS["card_bg"])
|
||
self.btn_open_student = RoundedButton(
|
||
self.file_btn_frame, text="📄 打开 students.txt",
|
||
width=180, height=36, radius=8,
|
||
bg="#eff6ff", hover_bg="#dbeafe", pressed_bg="#bfdbfe",
|
||
fg="#1d4ed8", font=("微软雅黑", 10, "bold"),
|
||
command=lambda: open_file_with_notepad(STUDENT_FILE))
|
||
self.btn_open_text = RoundedButton(
|
||
self.file_btn_frame, text="📃 打开 texts.txt(课文)",
|
||
width=200, height=36, radius=8,
|
||
bg="#eff6ff", hover_bg="#dbeafe", pressed_bg="#bfdbfe",
|
||
fg="#1d4ed8", font=("微软雅黑", 10, "bold"),
|
||
command=lambda: open_file_with_notepad(TEXT_FILE))
|
||
self.btn_refresh_status = RoundedButton(
|
||
self.file_btn_frame, text="🔄 刷新检测",
|
||
width=100, height=36, radius=8,
|
||
bg=COLORS["btn_secondary"], hover_bg=COLORS["btn_secondary_hover"],
|
||
pressed_bg="#cbd5e1", fg=COLORS["btn_secondary_text"],
|
||
font=("微软雅黑", 10), command=self._refresh_file_status)
|
||
|
||
# 文件状态显示区
|
||
self.status_frame = Frame(self.card_inner, bg=COLORS["card_bg"])
|
||
self.student_status_label = Label(self.status_frame, text="", bg=COLORS["card_bg"],
|
||
font=("微软雅黑", 10))
|
||
self.text_status_label = Label(self.status_frame, text="", bg=COLORS["card_bg"],
|
||
font=("微软雅黑", 10))
|
||
|
||
self.hint_label = Label(
|
||
self.file_btn_frame,
|
||
text="💡 编辑完成后记得按 Ctrl+S 保存,再点击「刷新检测」确认",
|
||
bg=COLORS["card_bg"], fg=COLORS["warning"], font=self.hint_font, anchor="w")
|
||
|
||
# ===== 底部按钮栏 =====
|
||
bottom_bar = Frame(self, bg=COLORS["bg"], height=76)
|
||
bottom_bar.pack(fill="x", side="bottom")
|
||
bottom_bar.pack_propagate(False)
|
||
|
||
# 进度条
|
||
progress_bg = Frame(bottom_bar, bg=COLORS["border"], height=3)
|
||
progress_bg.pack(fill="x", side="top")
|
||
progress_bg.pack_propagate(False)
|
||
self.progress_fill = Frame(progress_bg, bg=COLORS["primary"], height=3, width=0)
|
||
self.progress_fill.pack(side="left")
|
||
|
||
bottom_inner = Frame(bottom_bar, bg=COLORS["bg"])
|
||
bottom_inner.pack(fill="both", expand=True, padx=28)
|
||
|
||
self.btn_skip = RoundedButton(
|
||
bottom_inner, text="跳过向导 →", width=110, height=40, radius=8,
|
||
bg=COLORS["bg"], hover_bg=COLORS["btn_secondary"],
|
||
pressed_bg=COLORS["btn_secondary_hover"], fg=COLORS["text_hint"],
|
||
font=("微软雅黑", 10), command=self._skip_wizard)
|
||
|
||
self.scroll_hint = Label(bottom_inner, text="⬇ 内容较多,鼠标滚轮下滑查看更多 ⬇",
|
||
bg=COLORS["bg"], fg="#2563eb", font=("微软雅黑", 10, "bold"))
|
||
|
||
self.btn_back = RoundedButton(
|
||
bottom_inner, text="← 上一步", width=110, height=40, radius=8,
|
||
bg=COLORS["btn_secondary"], hover_bg=COLORS["btn_secondary_hover"],
|
||
pressed_bg="#cbd5e1", fg=COLORS["btn_secondary_text"],
|
||
font=("微软雅黑", 11, "bold"), command=self.page_back)
|
||
self.btn_back.pack(side="left", pady=18)
|
||
|
||
self.btn_next = RoundedButton(
|
||
bottom_inner, text="下一步 →", width=140, height=40, radius=8,
|
||
bg=COLORS["primary"], hover_bg=COLORS["primary_hover"],
|
||
pressed_bg=COLORS["primary_pressed"], fg="white",
|
||
font=("微软雅黑", 11, "bold"), command=self.page_next)
|
||
|
||
self.btn_confirm = RoundedButton(
|
||
bottom_inner, text="✓ 我已编辑完成", width=170, height=40, radius=8,
|
||
bg=COLORS["success"], hover_bg=COLORS["success_hover"],
|
||
pressed_bg="#047857", fg="white",
|
||
font=("微软雅黑", 11, "bold"), command=self.on_confirm_done)
|
||
|
||
def _on_content_configure(self, event):
|
||
self.content_canvas.configure(scrollregion=self.content_canvas.bbox("all"))
|
||
|
||
def _on_canvas_configure(self, event):
|
||
self.content_canvas.itemconfig(self.content_window, width=event.width)
|
||
|
||
def _refresh_file_status(self):
|
||
stu_exists, stu_has_content, stu_lines = check_file_status(STUDENT_PATH)
|
||
txt_exists, txt_has_content, txt_lines = check_file_status(TEXT_PATH)
|
||
|
||
if stu_has_content:
|
||
self.student_status_label.config(
|
||
text=f" ✓ students.txt:已就绪({stu_lines} 行)",
|
||
fg=COLORS["success"])
|
||
else:
|
||
self.student_status_label.config(
|
||
text=" ✗ students.txt:为空或不存在",
|
||
fg=COLORS["danger"])
|
||
|
||
if txt_has_content:
|
||
self.text_status_label.config(
|
||
text=f" ✓ texts.txt:已就绪({txt_lines} 行)",
|
||
fg=COLORS["success"])
|
||
else:
|
||
self.text_status_label.config(
|
||
text=" ✗ texts.txt:为空或不存在",
|
||
fg=COLORS["danger"])
|
||
|
||
def _update_page(self):
|
||
page = self.pages_content[self.current_page]
|
||
self.page_icon.config(text=page["icon"])
|
||
self.page_title.config(text=page["title"])
|
||
self.page_subtitle.config(text=page["subtitle"])
|
||
self.content_label.config(text=page["text"])
|
||
self.confirm_edit_done = False
|
||
self.step_indicator.set_step(self.current_page)
|
||
|
||
# 更新进度条
|
||
progress = (self.current_page + 1) / len(self.pages_content) * 100
|
||
self.progress_fill.config(width=int(progress * 8.8))
|
||
|
||
# 重置文件按钮区
|
||
self.file_btn_frame.pack_forget()
|
||
self.btn_open_student.pack_forget()
|
||
self.btn_open_text.pack_forget()
|
||
self.btn_refresh_status.pack_forget()
|
||
self.status_frame.pack_forget()
|
||
self.student_status_label.pack_forget()
|
||
self.text_status_label.pack_forget()
|
||
self.hint_label.pack_forget()
|
||
self.btn_confirm.pack_forget()
|
||
self.btn_next.pack_forget()
|
||
self.btn_skip.pack_forget()
|
||
|
||
# 上一步按钮
|
||
if self.current_page == 0:
|
||
self.btn_back.set_disabled(True)
|
||
else:
|
||
self.btn_back.set_disabled(False)
|
||
|
||
# 跳过按钮(仅第一页显示)
|
||
if page.get("show_skip"):
|
||
self.btn_skip.pack(side="left", pady=18, padx=(16, 0))
|
||
|
||
# 下滑提示(中间位置)
|
||
self.scroll_hint.pack(side="left", expand=True, pady=18)
|
||
|
||
if page["need_confirm"]:
|
||
self.file_btn_frame.pack(fill="x", pady=(12, 0))
|
||
self.btn_open_student.pack(side="left", padx=(0, 10))
|
||
self.btn_open_text.pack(side="left", padx=(0, 10))
|
||
self.btn_refresh_status.pack(side="left")
|
||
self.hint_label.pack(fill="x", pady=(10, 0))
|
||
|
||
# 文件状态
|
||
self.status_frame.pack(fill="x", pady=(8, 0))
|
||
self.student_status_label.pack(anchor="w", pady=1)
|
||
self.text_status_label.pack(anchor="w", pady=1)
|
||
self._refresh_file_status()
|
||
|
||
self.btn_confirm.pack(side="right", pady=18)
|
||
else:
|
||
self.btn_next.pack(side="right", pady=18)
|
||
if self.current_page == len(self.pages_content) - 1:
|
||
self.btn_next.set_text("完成并启动 🚀")
|
||
else:
|
||
self.btn_next.set_text("下一步 →")
|
||
|
||
self.content_canvas.yview_moveto(0)
|
||
|
||
def _skip_wizard(self):
|
||
result = messagebox.askyesno("跳过向导", "确定要跳过向导直接进入软件吗?\n\n建议完成向导以确保数据源配置正确。")
|
||
if result:
|
||
set_wizard_finished()
|
||
self.destroy()
|
||
launch_main_app()
|
||
sys.exit(0)
|
||
|
||
def on_confirm_done(self):
|
||
stu_exists, stu_has_content, _ = check_file_status(STUDENT_PATH)
|
||
txt_exists, txt_has_content, _ = check_file_status(TEXT_PATH)
|
||
if not stu_has_content or not txt_has_content:
|
||
result = messagebox.askyesno("文件未就绪",
|
||
"检测到学生名单或课文库为空,确定要继续吗?\n\n软件启动后可能无法正常使用点名功能。")
|
||
if not result:
|
||
return
|
||
self.confirm_edit_done = True
|
||
self.page_next()
|
||
|
||
def page_next(self):
|
||
if self.current_page < len(self.pages_content) - 1:
|
||
self.current_page += 1
|
||
self._update_page()
|
||
else:
|
||
set_wizard_finished()
|
||
self.destroy()
|
||
launch_main_app()
|
||
sys.exit(0)
|
||
|
||
def page_back(self):
|
||
if self.current_page > 0:
|
||
self.current_page -= 1
|
||
self._update_page()
|
||
|
||
def _on_close(self):
|
||
result = messagebox.askyesno("确认退出", "确定要退出向导吗?\n\n退出后下次启动仍会重新打开向导。")
|
||
if result:
|
||
self.destroy()
|
||
sys.exit(0)
|
||
|
||
|
||
def kill_target_exe():
|
||
try:
|
||
subprocess.run(["taskkill", "/F", "/IM", APP_EXE], shell=True, capture_output=True)
|
||
except Exception:
|
||
pass
|
||
|
||
|
||
def launch_main_app():
|
||
exe_path = os.path.join(BASE_DIR, APP_EXE)
|
||
py_path = os.path.join(BASE_DIR, MAIN_PY)
|
||
|
||
if os.path.exists(exe_path):
|
||
subprocess.Popen([exe_path, "--show-tip"], cwd=BASE_DIR)
|
||
return True
|
||
|
||
if os.path.exists(py_path):
|
||
subprocess.Popen([sys.executable, py_path, "--show-tip"], cwd=BASE_DIR)
|
||
return True
|
||
|
||
messagebox.showerror(
|
||
"启动失败",
|
||
f"找不到主程序文件!\n\n请确保同目录下存在:\n• {APP_EXE}(打包版)\n• 或 {MAIN_PY}(源码版)"
|
||
)
|
||
return False
|
||
|
||
|
||
def is_wizard_finished():
|
||
cfg = configparser.ConfigParser()
|
||
if os.path.exists(INI_PATH):
|
||
try:
|
||
cfg.read(INI_PATH, encoding="utf-8")
|
||
return cfg.getboolean("status", "finished")
|
||
except Exception:
|
||
return False
|
||
return False
|
||
|
||
|
||
def set_wizard_finished():
|
||
cfg = configparser.ConfigParser()
|
||
cfg["status"] = {"finished": "True"}
|
||
try:
|
||
with open(INI_PATH, "w", encoding="utf-8") as f:
|
||
cfg.write(f)
|
||
except Exception as e:
|
||
print(f"写入ini失败: {e}")
|
||
|
||
|
||
def build_sample_files():
|
||
if not os.path.exists(STUDENT_PATH):
|
||
sample_students = """张三
|
||
李四
|
||
王五
|
||
赵六
|
||
小明
|
||
小红
|
||
小华
|
||
小刚
|
||
小美
|
||
小强
|
||
"""
|
||
with open(STUDENT_PATH, "w", encoding="utf-8") as f:
|
||
f.write(sample_students)
|
||
|
||
if not os.path.exists(TEXT_PATH):
|
||
sample_texts = """【岳阳楼记】
|
||
庆历四年春,滕子京谪守巴陵郡。越明年,政通人和,百废具兴。乃重修岳阳楼,增其旧制,刻唐贤今人诗赋于其上。属予作文以记之。
|
||
|
||
予观夫巴陵胜状,在洞庭一湖。衔远山,吞长江,浩浩汤汤,横无际涯;朝晖夕阴,气象万千。此则岳阳楼之大观也,前人之述备矣。然则北通巫峡,南极潇湘,迁客骚人,多会于此,览物之情,得无异乎?
|
||
|
||
若夫霪雨霏霏,连月不开,阴风怒号,浊浪排空;日星隐耀,山岳潜形;商旅不行,樯倾楫摧;薄暮冥冥,虎啸猿啼。登斯楼也,则有去国怀乡,忧谗畏讥,满目萧然,感极而悲者矣。
|
||
|
||
至若春和景明,波澜不惊,上下天光,一碧万顷;沙鸥翔集,锦鳞游泳;岸芷汀兰,郁郁青青。而或长烟一空,皓月千里,浮光跃金,静影沉璧,渔歌互答,此乐何极!登斯楼也,则有心旷神怡,宠辱偕忘,把酒临风,其喜洋洋者矣。
|
||
|
||
嗟夫!予尝求古仁人之心,或异二者之为,何哉?不以物喜,不以己悲;居庙堂之高则忧其民;处江湖之远则忧其君。是进亦忧,退亦忧。然则何时而乐耶?其必曰"先天下之忧而忧,后天下之乐而乐"乎。噫!微斯人,吾谁与归?
|
||
|
||
时六年九月十五日。
|
||
|
||
【醉翁亭记】
|
||
环滁皆山也。其西南诸峰,林壑尤美,望之蔚然而深秀者,琅琊也。山行六七里,渐闻水声潺潺而泻出于两峰之间者,酿泉也。峰回路转,有亭翼然临于泉上者,醉翁亭也。作亭者谁?山之僧智仙也。名之者谁?太守自谓也。太守与客来饮于此,饮少辄醉,而年又最高,故自号曰醉翁也。醉翁之意不在酒,在乎山水之间也。山水之乐,得之心而寓之酒也。
|
||
|
||
若夫日出而林霏开,云归而岩穴暝,晦明变化者,山间之朝暮也。野芳发而幽香,佳木秀而繁阴,风霜高洁,水落而石出者,山间之四时也。朝而往,暮而归,四时之景不同,而乐亦无穷也。
|
||
|
||
至于负者歌于途,行者休于树,前者呼,后者应,伛偻提携,往来而不绝者,滁人游也。临溪而渔,溪深而鱼肥。酿泉为酒,泉香而酒洌;山肴野蔌,杂然而前陈者,太守宴也。宴酣之乐,非丝非竹,射者中,弈者胜,觥筹交错,起坐而喧哗者,众宾欢也。苍颜白发,颓然乎其间者,太守醉也。
|
||
|
||
已而夕阳在山,人影散乱,太守归而宾客从也。树林阴翳,鸣声上下,游人去而禽鸟乐也。然而禽鸟知山林之乐,而不知人之乐;人知从太守游而乐,而不知太守之乐其乐也。醉能同其乐,醒能述以文者,太守也。太守谓谁?庐陵欧阳修也。
|
||
"""
|
||
with open(TEXT_PATH, "w", encoding="utf-8") as f:
|
||
f.write(sample_texts)
|
||
|
||
|
||
def open_file_with_notepad(filename):
|
||
fullpath = os.path.join(BASE_DIR, filename)
|
||
if os.path.exists(fullpath):
|
||
subprocess.Popen(["notepad.exe", fullpath])
|
||
else:
|
||
messagebox.showerror("错误", f"文件 {filename} 不存在!")
|
||
|
||
|
||
def main():
|
||
os.chdir(BASE_DIR)
|
||
kill_target_exe()
|
||
|
||
if is_wizard_finished():
|
||
if launch_main_app():
|
||
sys.exit(0)
|
||
|
||
build_sample_files()
|
||
app = ModernWizard()
|
||
app.mainloop()
|
||
|
||
|
||
if __name__ == "__main__":
|
||
try:
|
||
main()
|
||
except Exception as e:
|
||
root = Tk()
|
||
root.withdraw()
|
||
messagebox.showerror("程序异常", f"向导运行出错:{str(e)}")
|
||
root.destroy()
|
||
sys.exit(1)
|