517 lines
21 KiB
Python
517 lines
21 KiB
Python
import tkinter as tk
|
|
from tkinter import ttk, messagebox, filedialog
|
|
import yaml
|
|
import colorama
|
|
import threading
|
|
import random
|
|
import time
|
|
import sys
|
|
import csv
|
|
import json
|
|
import functools
|
|
from PIL import Image
|
|
Image.CUBIC = Image.BICUBIC
|
|
import ttkbootstrap as tb
|
|
from datetime import datetime
|
|
import os
|
|
|
|
class ClassAuxApp:
|
|
def __init__(self):
|
|
self.version = 'v2.7.0 Dev'
|
|
self.config = None
|
|
self.current_member = {"pause": None, "show": "", "genuine": ""}
|
|
self.member_sum = 0
|
|
self.stdtime = 0.1
|
|
self.random_stop = 0
|
|
self.LICENSE = None
|
|
self.member = None
|
|
self.is_paused = 1
|
|
self.is_tab_shown = 1
|
|
self.default_tab_layout = None
|
|
self.is_first = 1
|
|
self.db = None
|
|
self.marking_colormark = dict()
|
|
self.is_dark = None
|
|
self.mk_fg = []
|
|
self.mk_bg = []
|
|
self.btnbg = None
|
|
self.style = None
|
|
self.btnfg = None
|
|
self.ttkthemename = None
|
|
self.marking_buttons = []
|
|
self.omember = None
|
|
|
|
self.about_text = f"""CLASSAUX - 抽取软件 改进型
|
|
带轻量级数据库和随机功能的教学管理辅助程序 版本 {self.version}
|
|
版权所有 (C) 2025 Wang Zhiyu
|
|
本程序为共产版权的自由软件, 在自由软件联盟发布的GNU通用公共许可协议的约束下,
|
|
你可以对其进行再发布及修改。协议版本为第三版。
|
|
我们希望发布的这款程序有用, 但不保证, 甚至不保证它有经济价值和适合特定用途。
|
|
详情参见 GNU 通用公共许可协议
|
|
你理当已收到一份GNU通用公共许可协议的副本,
|
|
如果没有, 请查阅 <http://www.gnu.org/licenses/>
|
|
--------------------------------------------------------------------------------
|
|
使用的第三方资源与开放源代码库:
|
|
Colorama - Cross-platform colored terminal text
|
|
Tkinter - Python interface to Tcl/Tk
|
|
Pillow - Python Imaging Library
|
|
PyYaml - YAML parser and emitter for Python
|
|
TTkBootstrap - Bootstrap-themed widgets for Tkinter
|
|
Sarasa Gothic (更纱黑体)
|
|
--------------------------------------------------------------------------------
|
|
开源软件是一项立足于协作精神的事业,
|
|
它的运作和产出不受任何单一个人或者机构的控制。
|
|
不管您来自何方,我们都欢迎您加入并做出贡献。
|
|
开放源代码/报告程序缺陷: https://github.com/david-ajax/classaux
|
|
--------------------------------------------------------------------------------
|
|
"""
|
|
|
|
self.init_console()
|
|
self.load_config()
|
|
self.init_theme()
|
|
|
|
def init_console(self):
|
|
"""初始化控制台输出"""
|
|
print("确保此终端支持 UTF-8 字符输出!")
|
|
print(f"抽取程序 {self.version}")
|
|
print("开放源代码: https://github.com/david-ajax/classaux")
|
|
|
|
def load_config(self):
|
|
"""加载配置文件"""
|
|
try:
|
|
print("读取 YAML 配置文件与 GPL 协议 ", end="")
|
|
with open("COPYING", encoding='utf-8') as license_file:
|
|
self.LICENSE = license_file.read()
|
|
|
|
with open("config.yaml", encoding='utf-8') as config_file:
|
|
self.config = yaml.safe_load(config_file)
|
|
self.omember = list(self.config['member'])
|
|
|
|
print(colorama.Fore.GREEN + "成功" + colorama.Style.RESET_ALL)
|
|
|
|
# 加载数据库
|
|
db_file = self.config['info'].get('db', 'db.csv')
|
|
print(f"读取数据库 {db_file} ", end="")
|
|
self.load_database(db_file)
|
|
print(colorama.Fore.GREEN + "成功" + colorama.Style.RESET_ALL)
|
|
|
|
print(f"配置文件: {self.config['info']['name']}")
|
|
self.member_sum = len(self.config['member'])
|
|
print(f"成员人数: {self.member_sum}")
|
|
print(f"随机刻间隔时间: {self.stdtime}s")
|
|
|
|
except Exception as e:
|
|
print(colorama.Fore.RED + f"失败: {e}" + colorama.Style.RESET_ALL)
|
|
sys.exit(1)
|
|
|
|
def load_database(self, db_file):
|
|
"""加载数据库文件"""
|
|
try:
|
|
with open(db_file, encoding='utf-8-sig') as db_file:
|
|
self.db = list(csv.reader(db_file, delimiter=',', quotechar='"'))
|
|
# 过滤注释行
|
|
self.db = [row for row in self.db if row[0] != 'NOTE']
|
|
except FileNotFoundError:
|
|
print(colorama.Fore.YELLOW + "数据库文件不存在,创建空数据库" + colorama.Style.RESET_ALL)
|
|
self.db = []
|
|
|
|
def init_theme(self):
|
|
"""初始化主题设置"""
|
|
self.is_dark = self.config.get("darkmode", False)
|
|
|
|
if not self.is_dark:
|
|
self.mk_bg = ["#FFFFFF", "#009900", "#ff6666", "#0088ff", "#FFA500"]
|
|
self.mk_fg = ["#000000", "#FFFFFF", "#FFFFFF", "#FFFFFF", "#000000"]
|
|
self.ttkthemename = "cosmo"
|
|
self.btnbg = [('pressed', '#2097ff'), ('active', '#9bcbff')]
|
|
self.btnfg = [('pressed', 'white'), ('active', '#36393b')]
|
|
self.sbtnbg = 'white'
|
|
self.sbtnfg = '#36393b'
|
|
else:
|
|
self.mk_fg = ["#FFFFFF", "#00ff00", "#ff6666", "#9999ff", "#FFA500"]
|
|
self.mk_bg = ["#202020", "#005500", "#550000", "#000055", "#333300"]
|
|
self.ttkthemename = "darkly"
|
|
self.btnbg = [('pressed', '#242424'), ('active', '#242424')]
|
|
self.btnfg = [('pressed', '#FFFFFF'), ('active', '#dddddd')]
|
|
self.sbtnbg = "#202020"
|
|
self.sbtnfg = 'white'
|
|
|
|
def random_gen(self):
|
|
"""随机生成器线程函数"""
|
|
self.member = self.config['member']
|
|
while not self.random_stop:
|
|
random.shuffle(self.member)
|
|
for i in self.member:
|
|
if self.random_stop:
|
|
break
|
|
if i['weight'] > 0:
|
|
for k in range(i['weight']):
|
|
self.current_member["genuine"] = i['name']
|
|
time.sleep(self.stdtime)
|
|
|
|
def matrix_gen(self, format_type="linear"):
|
|
"""生成矩阵/列表视图"""
|
|
members = list(self.config["member"])
|
|
random.shuffle(members)
|
|
|
|
if format_type == "linear":
|
|
return "\n".join([f"{i+1}. {member['name']}" for i, member in enumerate(members)])
|
|
elif format_type == "ascii_table":
|
|
# 简单的ASCII表格
|
|
table = "序号 | 姓名\n" + "-" * 20 + "\n"
|
|
for i, member in enumerate(members):
|
|
table += f"{i+1:3} | {member['name']}\n"
|
|
return table
|
|
elif format_type == "csv":
|
|
return "序号,姓名\n" + "\n".join([f"{i+1},{member['name']}" for i, member in enumerate(members)])
|
|
elif format_type == "markdown":
|
|
return "| 序号 | 姓名 |\n|-----|------|\n" + "\n".join([f"| {i+1} | {member['name']} |" for i, member in enumerate(members)])
|
|
else:
|
|
return "\n".join([member['name'] for member in members])
|
|
|
|
def export_to_file(self, content, file_type="txt"):
|
|
"""导出内容到文件"""
|
|
try:
|
|
filename = filedialog.asksaveasfilename(
|
|
defaultextension=f".{file_type}",
|
|
filetypes=[(f"{file_type.upper()} files", f"*.{file_type}")]
|
|
)
|
|
if filename:
|
|
with open(filename, 'w', encoding='utf-8') as f:
|
|
f.write(content)
|
|
messagebox.showinfo("成功", f"文件已导出到: {filename}")
|
|
except Exception as e:
|
|
messagebox.showerror("错误", f"导出失败: {e}")
|
|
|
|
def copy_to_clipboard(self, content):
|
|
"""复制内容到剪贴板"""
|
|
try:
|
|
self.root.clipboard_clear()
|
|
self.root.clipboard_append(content)
|
|
messagebox.showinfo("成功", "内容已复制到剪贴板")
|
|
except Exception as e:
|
|
messagebox.showerror("错误", f"复制失败: {e}")
|
|
|
|
def marking_change_color(self, button):
|
|
"""标记视图中的颜色切换"""
|
|
if id(button) not in self.marking_colormark:
|
|
self.marking_colormark[id(button)] = 0
|
|
|
|
current_color = self.marking_colormark[id(button)]
|
|
next_color = (current_color + 1) % len(self.mk_bg)
|
|
self.marking_colormark[id(button)] = next_color
|
|
|
|
# 创建新的样式
|
|
style_name = f"MarkingX{id(button)}.TButton"
|
|
self.style.configure(style_name,
|
|
background=self.mk_bg[next_color],
|
|
foreground=self.mk_fg[next_color])
|
|
self.style.map(style_name,
|
|
background=[('pressed', self.mk_bg[next_color]),
|
|
('active', self.mk_bg[next_color])],
|
|
foreground=[('pressed', self.mk_fg[next_color]),
|
|
('active', self.mk_fg[next_color])])
|
|
button.config(style=style_name)
|
|
|
|
def marking_clear(self):
|
|
"""清空所有标记"""
|
|
self.marking_colormark.clear()
|
|
for button in self.marking_buttons:
|
|
self.marking_change_color(button)
|
|
|
|
def toggle(self):
|
|
"""切换抽取状态"""
|
|
if not self.is_paused:
|
|
self.current_member["show"] = self.current_member["genuine"]
|
|
self.is_paused = 1
|
|
self.electing_button.config(text="开始")
|
|
else:
|
|
self.electing_button.config(text="暂停")
|
|
self.is_paused = 0
|
|
|
|
def toggle_menu(self):
|
|
"""切换选项卡显示"""
|
|
if not self.is_tab_shown:
|
|
self.style.layout('TNotebook.Tab', self.default_tab_layout)
|
|
self.is_tab_shown = 1
|
|
else:
|
|
self.style.layout('TNotebook.Tab', [])
|
|
self.is_tab_shown = 0
|
|
|
|
def toggle_dark(self):
|
|
"""切换暗色模式"""
|
|
self.is_dark = not self.is_dark
|
|
self.config["darkmode"] = self.is_dark
|
|
self.init_theme()
|
|
self.style.theme_use(self.ttkthemename)
|
|
self.marking_clear()
|
|
self.is_tab_shown = not self.is_tab_shown
|
|
self.toggle_menu()
|
|
|
|
# 保存主题设置
|
|
self.save_config()
|
|
|
|
def save_config(self):
|
|
"""保存配置到文件"""
|
|
try:
|
|
with open("config.yaml", 'w', encoding='utf-8') as f:
|
|
yaml.dump(self.config, f, allow_unicode=True, default_flow_style=False)
|
|
except Exception as e:
|
|
print(f"保存配置失败: {e}")
|
|
|
|
def update_data(self):
|
|
"""更新显示数据"""
|
|
if not self.is_paused:
|
|
self.electing_label.config(text=random.choice(self.config['member'])["name"])
|
|
else:
|
|
self.electing_label.config(text=self.current_member["show"])
|
|
self.electing_label.after(int(1000 * self.stdtime * 0.75), self.update_data)
|
|
|
|
def create_interface(self):
|
|
"""创建主界面"""
|
|
self.root = tb.Window(themename=self.ttkthemename)
|
|
self.root.title(f"抽取程序 {self.version}")
|
|
self.root.geometry("900x700")
|
|
|
|
# 设置图标
|
|
try:
|
|
self.root.iconphoto(False, tk.PhotoImage(file='logo.png'))
|
|
except:
|
|
pass
|
|
|
|
# 创建样式
|
|
self.style = tb.Style()
|
|
self.setup_styles()
|
|
|
|
# 创建笔记本(选项卡)
|
|
self.create_notebook()
|
|
|
|
if self.default_tab_layout == None:
|
|
self.default_tab_layout = self.style.layout('TNotebook.Tab')
|
|
|
|
self.toggle_menu()
|
|
|
|
# 初始化各个选项卡
|
|
self.create_electing_tab()
|
|
self.create_marking_tab()
|
|
self.create_batch_tab()
|
|
self.create_about_tab()
|
|
self.create_edit_tab()
|
|
self.create_config_tab()
|
|
|
|
# 设置关闭事件
|
|
self.root.protocol("WM_DELETE_WINDOW", self.on_closing)
|
|
|
|
# 启动数据更新
|
|
self.update_data()
|
|
self.root.mainloop()
|
|
|
|
def setup_styles(self):
|
|
"""设置样式"""
|
|
self.style.configure("Marking.TButton", background=self.sbtnbg, foreground=self.sbtnfg)
|
|
self.style.map("Marking.TButton", background=self.btnbg, foreground=self.btnfg)
|
|
self.style.configure("TabToggle.TButton", background=self.sbtnbg, foreground=self.sbtnfg)
|
|
self.style.map("TabToggle.TButton", background=self.btnbg, foreground=self.btnfg)
|
|
|
|
def create_notebook(self):
|
|
"""创建选项卡容器"""
|
|
self.notebook = ttk.Notebook(self.root)
|
|
self.notebook.pack(fill='both', expand=True, padx=10, pady=10)
|
|
|
|
# 创建各个选项卡框架
|
|
self.tab_electing = tk.Frame(self.notebook)
|
|
self.tab_marking = tk.Frame(self.notebook)
|
|
self.tab_batch = tk.Frame(self.notebook)
|
|
self.tab_edit = tk.Frame(self.notebook)
|
|
self.tab_about = tk.Frame(self.notebook)
|
|
self.tab_config = tk.Frame(self.notebook)
|
|
|
|
# 添加选项卡
|
|
self.notebook.add(self.tab_electing, text='单次抽签')
|
|
self.notebook.add(self.tab_marking, text='标记视图')
|
|
self.notebook.add(self.tab_batch, text='批量视图')
|
|
self.notebook.add(self.tab_edit, text='编辑数据库')
|
|
self.notebook.add(self.tab_config, text='设置')
|
|
self.notebook.add(self.tab_about, text='关于')
|
|
|
|
def create_electing_tab(self):
|
|
"""创建单次抽签选项卡"""
|
|
# 主显示标签
|
|
self.electing_label = ttk.Label(self.tab_electing, text="待抽取",
|
|
font=("Sarasa UI SC", 70, 'bold'))
|
|
self.electing_label.place(relx=0.5, rely=0.35, anchor='center')
|
|
|
|
# 控制按钮
|
|
self.electing_button = ttk.Button(self.tab_electing, text="开始",
|
|
command=self.toggle)
|
|
self.electing_button.place(relx=0.5, rely=0.8, relwidth=0.25,
|
|
relheight=0.15, anchor='center')
|
|
|
|
# 辅助按钮
|
|
ttk.Button(self.tab_electing, text="选项卡", command=self.toggle_menu,
|
|
style="TabToggle.TButton").place(relx=0.01, rely=0.99,
|
|
relwidth=0.12, relheight=0.08, anchor='sw')
|
|
|
|
ttk.Button(self.tab_electing, text="暗色调", command=self.toggle_dark,
|
|
style="TabToggle.TButton").place(relx=0.99, rely=0.99,
|
|
relwidth=0.12, relheight=0.08, anchor='se')
|
|
|
|
def create_marking_tab(self):
|
|
"""创建标记视图选项卡"""
|
|
# 控制按钮
|
|
ttk.Button(self.tab_marking, text="清空", command=self.marking_clear
|
|
).place(relx=0.5, rely=0.99, relwidth=0.13,
|
|
relheight=0.08, anchor='s')
|
|
|
|
# 标记按钮网格
|
|
self.create_marking_grid()
|
|
|
|
def create_marking_grid(self):
|
|
"""创建标记网格"""
|
|
members = list(self.omember)
|
|
# 补全到8的倍数
|
|
for i in range((8 - len(members) % 8) % 8):
|
|
members.append({"name": "占位符"})
|
|
|
|
for i, member in enumerate(members):
|
|
# 创建标记按钮
|
|
btn_text = f" {member['name']} " if len(member["name"]) == 2 else member["name"]
|
|
button = ttk.Button(self.tab_marking, text=btn_text,
|
|
style="Marking.TButton")
|
|
|
|
# 绑定点击事件
|
|
button.config(command=functools.partial(self.marking_change_color, button))
|
|
button.grid(column=i % 8, row=i // 8, padx=5, pady=5, sticky='nsew')
|
|
|
|
# 配置网格权重
|
|
self.tab_marking.grid_columnconfigure(i % 8, weight=1)
|
|
self.tab_marking.grid_rowconfigure(i // 8, weight=1)
|
|
|
|
self.marking_buttons.append(button)
|
|
self.marking_change_color(button) # 初始化颜色
|
|
|
|
def create_batch_tab(self):
|
|
"""创建批量视图选项卡"""
|
|
# 结果显示文本框
|
|
self.batch_text = tk.Text(self.tab_batch, wrap=tk.WORD, font=("Sarasa UI SC", 10))
|
|
self.batch_text.place(relx=0.03, rely=0.03, relwidth=0.94, relheight=0.79)
|
|
|
|
# 格式选择
|
|
self.format_var = tk.StringVar(value="linear")
|
|
format_combo = ttk.Combobox(self.tab_batch, textvariable=self.format_var,
|
|
values=("linear", "ascii_table", "csv", "markdown"))
|
|
format_combo.place(relx=0.01, rely=0.91, relwidth=0.3, relheight=0.08, anchor='sw')
|
|
format_combo.set("linear")
|
|
|
|
# 生成按钮
|
|
ttk.Button(self.tab_batch, text="生成新随机",
|
|
command=self.generate_batch).place(relx=0.5, rely=0.845,
|
|
relwidth=0.25, relheight=0.13, anchor='n')
|
|
|
|
# 操作按钮
|
|
ttk.Button(self.tab_batch, text="复制到剪贴板",
|
|
command=lambda: self.copy_to_clipboard(self.batch_text.get(1.0, tk.END)),
|
|
style="TabToggle.TButton").place(relx=0.99, rely=0.99,
|
|
relwidth=0.3, relheight=0.08, anchor='se')
|
|
|
|
ttk.Button(self.tab_batch, text="导出至文件",
|
|
command=lambda: self.export_to_file(self.batch_text.get(1.0, tk.END), "txt"),
|
|
style="TabToggle.TButton").place(relx=0.99, rely=0.91,
|
|
relwidth=0.3, relheight=0.08, anchor='se')
|
|
|
|
# 初始生成内容
|
|
self.generate_batch()
|
|
|
|
def generate_batch(self):
|
|
"""生成批量视图内容"""
|
|
content = self.matrix_gen(self.format_var.get())
|
|
self.batch_text.delete(1.0, tk.END)
|
|
self.batch_text.insert(1.0, content)
|
|
|
|
def create_edit_tab(self):
|
|
"""创建编辑数据库选项卡"""
|
|
# 简单的编辑界面
|
|
label = ttk.Label(self.tab_edit, text="数据库编辑功能开发中...",
|
|
font=("Sarasa UI SC", 16))
|
|
label.pack(expand=True)
|
|
|
|
# 显示当前数据库信息
|
|
info_text = f"当前数据库记录数: {len(self.db)}"
|
|
ttk.Label(self.tab_edit, text=info_text).pack(pady=10)
|
|
|
|
def create_config_tab(self):
|
|
"""创建设置选项卡"""
|
|
# 主题设置
|
|
ttk.Label(self.tab_config, text="外观设置",
|
|
font=("Sarasa UI SC", 14, 'bold')).grid(row=0, column=0,
|
|
sticky='w', padx=5, pady=5)
|
|
|
|
self.theme_var = tk.StringVar(value=self.ttkthemename)
|
|
theme_combo = ttk.Combobox(self.tab_config, textvariable=self.theme_var,
|
|
values=["cosmo", "flatly", "darkly", "litera", "minty"])
|
|
theme_combo.grid(row=1, column=0, sticky='w', padx=5, pady=5)
|
|
theme_combo.bind('<<ComboboxSelected>>', self.change_theme)
|
|
|
|
# 其他设置
|
|
ttk.Label(self.tab_config, text="其他设置",
|
|
font=("Sarasa UI SC", 14, 'bold')).grid(row=2, column=0,
|
|
sticky='w', padx=5, pady=5)
|
|
|
|
self.speed_var = tk.DoubleVar(value=self.stdtime)
|
|
speed_scale = ttk.Scale(self.tab_config, from_=0.05, to=0.5,
|
|
variable=self.speed_var, orient='horizontal')
|
|
speed_scale.grid(row=3, column=0, sticky='we', padx=5, pady=5)
|
|
speed_scale.bind('<ButtonRelease-1>', self.change_speed)
|
|
|
|
ttk.Label(self.tab_config, text=f"速度: {self.stdtime}s").grid(row=4, column=0, sticky='w', padx=5)
|
|
|
|
def change_theme(self, event):
|
|
"""更改主题"""
|
|
self.ttkthemename = self.theme_var.get()
|
|
self.style.theme_use(self.ttkthemename)
|
|
|
|
def change_speed(self, event):
|
|
"""更改速度"""
|
|
self.stdtime = self.speed_var.get()
|
|
# 更新显示
|
|
for widget in self.tab_config.grid_slaves():
|
|
if isinstance(widget, ttk.Label) and widget.cget('text').startswith("速度:"):
|
|
widget.config(text=f"速度: {self.stdtime}s")
|
|
|
|
def create_about_tab(self):
|
|
"""创建关于选项卡"""
|
|
about_text = tk.Text(self.tab_about, wrap=tk.WORD,
|
|
font=("Sarasa UI SC", 10))
|
|
about_text.pack(fill='both', expand=True, padx=10, pady=10)
|
|
|
|
about_text.insert(tk.END, self.about_text)
|
|
about_text.insert(tk.END, "\n\n" + "="*50 + "\n")
|
|
about_text.insert(tk.END, "GNU GENERAL PUBLIC LICENSE\n")
|
|
about_text.insert(tk.END, "="*50 + "\n")
|
|
about_text.insert(tk.END, self.LICENSE[:2000] + "...") # 只显示部分许可证内容
|
|
|
|
about_text.configure(state="disabled")
|
|
|
|
def on_closing(self):
|
|
"""程序关闭处理"""
|
|
self.random_stop = 1
|
|
self.root.destroy()
|
|
|
|
def run(self):
|
|
"""运行程序"""
|
|
print("启动随机生成器守护线程 ", end="")
|
|
random_thr = threading.Thread(target=self.random_gen)
|
|
random_thr.daemon = True
|
|
random_thr.start()
|
|
print(colorama.Fore.GREEN + "成功" + colorama.Style.RESET_ALL)
|
|
|
|
print("初始化 GUI ", end="")
|
|
print(colorama.Fore.YELLOW + "正运行" + colorama.Style.RESET_ALL)
|
|
|
|
self.create_interface()
|
|
print(colorama.Fore.GREEN + "结束" + colorama.Style.RESET_ALL)
|
|
|
|
if __name__ == "__main__":
|
|
app = ClassAuxApp()
|
|
app.run()
|