Files
ClassAux/main.py
2025-09-23 00:17:15 +08:00

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()