''' Random_Picker 用Python实现的随机点名器 Copyright (C) 2022 海Cha This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . ''' import xlrd as excel from tkinter import * from tkinter.ttk import * import tkinter.messagebox as msgbox import random import sys import winsound import webbrowser import time import requests import os import base64 version = "v2.0.0.5" filename = "./data/names.xls" configname = "./data/config.data" selname = "./assets/sel.wav" procname = "./assets/proc.wav" iconname = "./assets/点名器.ico" gui_size = "550x140" appname = '点名器' author = "海Cha" copyright = appname + ' ' + version + ' Copyright (C) 2022 ' + author protection_override = False up_percent = 30 down_persent = 40 elim_rows = 1 percent_override = True elim_id = [] up_id = [] down_id = [] sound = True students = [] classes = [] subjects = [] filter = [] last_choice = "" gui = Tk() gui.withdraw() gui.title(appname) gui.geometry(gui_size) gui.resizable(0,0) gui.configure(bg = "white") topmost = False if_exit = False logging = True log_level = "INFO" log_file = "" class Student(object): name = "" subject = "" id = "" class_name = "" def __init__(self,id,class_name,subject,name): self.id = id self.class_name = class_name self.subject = subject self.name = name def Decode(str): decode = base64.b64decode(str).decode("utf-8") logs("DEBUG","获得base64解码:" + decode) return decode def newlog(type = "INFO"): global log_file if logging: if not os.path.exists(".\\log\\"): os.makedirs(".\\log\\") curtime = time.strftime('%Y-%m-%d [%H-%M-%S]', time.localtime()) log_file = ".\\log\\" + curtime + ".log" with open(log_file,mode = "w",encoding="utf-8") as log: log.write("点名器日志文件 " + log_file + "\n") log.write(copyright + '\n') log.write("若点名器软件出现预期外行为或崩溃,请将此文件发送给作者。\n") log.write("若需发布该日志文件到公开网络平台,请注意屏蔽日志文件中涉及的计算机敏感信息。\n") log.write("当前日志等级:" + type + "\n") log.write("若需改变日志记录等级,请在启动主程序时传入 --log-level-**INFO/WARN/ERROR/FATAL/SILENT**\n") log.write("若需关闭日志记录,请在启动主程序时传入 --disable-logging。\n") def logtype_to_int(type): if type == "DEBUG": return 0 elif type == "INFO": return 1 elif type == "WARN": return 2 elif type == "ERROR": return 3 elif type == "FATAL": return 4 elif type == "SILENT": return 5 elif type == "SYSTEM": return 6 def logs(type, message): if logging: curtime = time.strftime('%Y-%m-%d [%H-%M-%S]', time.localtime()) logline = "" if logtype_to_int(type) >= logtype_to_int(log_level): logline = "\n[" + type + "] " + curtime + " " + message with open(log_file,mode = 'a',encoding="utf-8") as log: log.write(logline) try: if len(sys.argv) > 1: for arg in sys.argv[1:]: if arg == "--licence": if msgbox.askokcancel('许可证信息', copyright + '\n本程序是开源程序,受开源协议 GNU General Public Licence v3.0 (GPL v3) 保护\n基于GPL v3协议,本程序没有任何质量保证。\n这是一个自由软件,欢迎再次分发。\n点击「确认」将会由系统默认浏览器打开LICENCE文件\n出现此对话框的原因是由于您在运行本软件时传入了 --licence 参数,欲启动主程序,请移除该参数。'): webbrowser.open_new_tab('https://www.gnu.org/licenses/gpl-3.0.html') if_exit = True elif arg == "--version": msgbox.showinfo('版本信息', copyright + '\n出现此对话框的原因是由于您在运行本软件时传入了 --version 参数,欲启动主程序,请移除该参数。') if_exit = True elif arg == "--help": msgbox.showinfo('帮助信息','----- 点名器 ' + version + ' -----\n--licence 显示许可证信息\n--version 显示版本信息\n--path 显示程序运行路径\n--help 显示此信息\n欲打开主程序GUI,则无需传参\n出现此对话框的原因是由于您在运行本软件时传入了 --help 参数,欲启动主程序,请移除该参数。') if_exit = True elif arg == "--path": msgbox.showinfo('路径信息',copyright + '\n当前程序运行在 ' + sys.argv[0] + ' 路径上\n出现此对话框的原因是由于您在运行本软件时传入了 --path 参数,欲启动主程序,请移除该参数。') if_exit = True elif arg == "--top-most": msgbox.showinfo('设置成功',copyright + '\n点名器将会置顶运行。\n出现此对话框的原因是由于您在运行本软件时传入了 --top-most 参数,欲关闭置顶运行,请移除该参数。') elif arg == "--disable-logging": msgbox.showinfo('设置成功',copyright + '\n调试日志已关闭。程序出现预期外动作或崩溃时,请移除此参数以获得日志。\n出现此对话框的原因是由于您在运行本软件时传入了 --disable-logging 参数,欲重启日志记录,请移除该参数。') logging = False elif arg == "--log-level-DEBUG": msgbox.showinfo('设置成功',copyright + '\n调试日志等级更改为 DEBUG\n该模式仅供程序运行出现问题排障使用,请勿在常规使用中调用此参数\n出现此对话框的原因是由于您在运行本软件时传入了 ' + arg + ' 参数。') log_level = "DEBUG" elif arg == "--log-level-INFO": # msgbox.showinfo('设置成功',copyright + '\n调试日志等级更改为 INFO\n出现此对话框的原因是由于您在运行本软件时传入了 ' + arg + ' 参数。') log_level = "INFO" elif arg == "--log-level-WARN": # msgbox.showinfo('设置成功',copyright + '\n调试日志等级更改为 WARN\n出现此对话框的原因是由于您在运行本软件时传入了 ' + arg + ' 参数。') log_level = "WARN" elif arg == "--log-level-ERROR": # msgbox.showinfo('设置成功',copyright + '\n调试日志等级更改为 ERROR\n出现此对话框的原因是由于您在运行本软件时传入了 ' + arg + ' 参数。') log_level = "ERROR" elif arg == "--log-level-FATAL": # msgbox.showinfo('设置成功',copyright + '\n调试日志等级更改为 FATAL\n出现此对话框的原因是由于您在运行本软件时传入了 ' + arg + ' 参数。') log_level = "FATAL" elif arg == "--log-level-SILENT": # msgbox.showinfo('设置成功',copyright + '\n调试日志等级更改为 SILENT\n出现此对话框的原因是由于您在运行本软件时传入了 ' + arg + ' 参数。') log_level = "SILENT" elif arg.find("--") != -1: msgbox.showerror('传参错误','无法识别参数 ' + arg + ' 请调整您的启动参数\n传入 --help以查看帮助信息\n出现此对话框的原因是由于您在运行本软件时传入了 ' + arg + ' 参数,欲启动主程序,请移除该参数。') if_exit = True except: if_exit = False finally: if if_exit: sys.exit(0) newlog(log_level) if logtype_to_int(log_level) != logtype_to_int("INFO"): logs("SYSTEM","日志等级调整为" + log_level) logs("SYSTEM","日志记录已激活") try: if logtype_to_int(log_level) <= logtype_to_int("DEBUG"): try: ip = requests.get('http://myip.ipip.net',timeout=5).text.replace('\n', '').replace('\r', '') except: ip = "当前IP:无公网IP" logs("DEBUG","-----系统基本信息-----") logs("DEBUG",ip) logs("DEBUG","运行平台:" + sys.platform) logs("DEBUG","当前运行目录:" + sys.argv[0] + '\n') try: logs("DEBUG","加载图片文件 " + iconname) file = open(iconname) file.close() gui.iconbitmap(iconname) except IOError: logs("ERROR","资源加载错误:在 ./assets目录下未能找到 点名器.ico 文件") if not msgbox.askokcancel('资源文件错误', '在 ./assets目录下未能找到 点名器.ico 文件,点击「确定」将加载默认图标,点击「取消」以关闭程序'): sys.exit() logs("WARN","图标加载错误,回滚到默认图标") try: logs("DEBUG","加载音频资源文件 " + selname) file = open(selname) file.close() file = open(procname) logs("DEBUG","加载音频资源文件 " + procname) file.close() except IOError: logs("ERROR","资源加载错误:在 ./assets目录下未能找到音频文件") if msgbox.askokcancel('资源文件错误', '在 ./assets目录下未能找到音频文件,点击「确定」将关闭声音,点击「取消」以关闭程序'): logs("WARN","声音已关闭") sound = False else: sys.exit() try: logs("DEBUG","加载数据库文件 " + filename) with excel.open_workbook(filename) as excel_data: table = excel_data.sheets()[0] # win32api.SetFileAttributes(filename,win32con.FILE_ATTRIBUTE_HIDDEN) except FileNotFoundError: logs("FATAL","数据库加载错误:在 ./data下未能找到 names.xls 文件") msgbox.showerror('找不到数据库文件', '在 ./data下未能找到 names.xls 文件,请确认您的文件存在') sys.exit() try: cols = table.ncols rows = table.nrows logs("DEBUG","读取数据库行数:" + str(cols) + " 列数:" + str(rows)) assert cols == 4 assert rows > 1 except AssertionError: logs("FATAL","数据库加载错误:数据库文件内行/列数不合法,当前行数:" + str(rows) + ' 当前列数:' + str(cols)) msgbox.showerror('数据库文件格式不正确', '数据库文件内行/列数不合法,请确认数据库文件拥有超过1的行数和等于4的列数,当前行数:' + str(rows) + ' 当前列数:' + str(cols)) sys.exit() try: logs("DEBUG","加载概率修正配置文件 " + configname) with open(configname,'r') as file: config = file.read().splitlines() for unit in config: logs("DEBUG","读取到概率修正密文:" + unit) unit = Decode(unit) temp = unit.split(",") assert temp[1] == '0' or temp[1] == '+' or temp[1] == '-' if temp[1] == '0': logs("DEBUG","应用概率修正:移除ID " + temp[0]) elim_id.append(int(temp[0])) elif temp[1] == '+': logs("DEBUG","应用概率修正:标记ID为提升 " + temp[0]) up_id.append(int(temp[0])) elif temp[1] == "-": logs("DEBUG","应用概率修正:标记ID为降低 " + temp[0]) down_id.append(int(temp[0])) else: pass except Exception as e: logs("DEBUG","加载概率修正出现错误:" + str(e)) percent_override = False up_percent = len(up_id) * 5 if len(up_id) <= 6 else 30 down_percent = len(down_id) * 8 if len(down_id) <= 5 else 40 logs("DEBUG","应用概率修正组概率:概率提升组:" + str(up_percent) + "% 概率降低组:" + str(down_percent) + "%") gui.deiconify() logs("DEBUG","数据库内容写入内存") for i in range(elim_rows,rows): tmp_list = [str(table.cell_value(i,j)) for j in range(0,cols)] if (int(float(tmp_list[0])) not in elim_id): student = Student(tmp_list[0],tmp_list[1],tmp_list[2],tmp_list[3]) logs("DEBUG","更新数据库内容到内存:" + str(student.id)) students.append(student) logs("DEBUG","创建数据库备份cur_stu") cur_stu = students[:] logs("DEBUG","创建班级列表") for student in students: if student.class_name not in classes: classes.append(student.class_name) if student.subject not in subjects: subjects.append(student.subject) cur_name = StringVar() def upd_name(name): logs("DEBUG","更新cur_name显示:" + name) cur_name.set(name) upd_name("") listbox = Listbox(gui, selectmode = MULTIPLE, height = 5) for tmp_class in classes: listbox.insert("end",tmp_class) def sel_class(flag = 1): global filter, listbox, classes, cur_stu, students, percent_override filter.clear() for selection in listbox.curselection(): # print(selection) logs("DEBUG","添加班级筛选:" + classes[selection]) filter.append(classes[selection]) logs("DEBUG","班级筛选名单:" + str(filter)) # print(filter) if filter: logs("INFO","已应用班级选择") if flag: msgbox.showinfo('班级选择已应用', '已应用当前班级选择', parent=gui) logs("DEBUG","概率修正已禁用") percent_override = False cur_stu.clear() for student in students: if student.class_name in filter: # print(student.name) cur_stu.append(student) else: logs("INFO","未选择任何班级,默认选择所有班级") if flag: msgbox.showwarning('班级选择已应用', '当前设置会选择所有班级的名单,这样对吗?', parent=gui) logs("DEBUG","概率修正已禁用") percent_override = False filter = classes[:] cur_stu = students[:] def choose(): global last_choice, cur_stu logs("INFO","开始新一轮随机挑选") logs("DEBUG","上一轮的选择:" + str(last_choice)) logs("DEBUG","重新应用班级筛选(静默)") sel_class(0) # 触发30%概率,从up中挑选 if (random.randint(1,100) < up_percent and percent_override): logs("DEBUG","从概率提升组中挑选") logs("DEBUG","防重选保护关闭") protection_override = True; temp = [] for student in cur_stu: if int(float(student.id)) in up_id: logs("DEBUG","学生id:" + str(student.id) + "学生姓名:" + str(student.name) + "加入至当前挑选名单") temp.append(student) logs("DEBUG","挑选名单重筛完毕") cur_stu = temp[:] elif (random.randint(1,100) < down_persent and percent_override): logs("DEBUG","概率降低组人员排除") logs("DEBUG","防重选保护关闭") protection_override = True; temp = []; for student in cur_stu: if int(float(student.id)) not in down_id: logs("DEBUG","学生id:" + str(student.id) + "学生姓名:" + str(student.name) + "加入至当前挑选名单") temp.append(student) logs("DEBUG","挑选名单重筛完毕") cur_stu = temp[:] else: protection_override = False if not cur_stu: protection_override = False sel_class(0) logs("DEBUG","开始随机挑选") choice = random.choice(cur_stu) logs("DEBUG","随机选中人员 id:" + choice.id + " 姓名:" + choice.name + " 学科:" + choice.subject) # print(choice.name) counter = 0 while (last_choice == choice.name and (not protection_override) and (len(cur_stu) > 1)) or choice.id in elim_id: logs("DEBUG","防重选保护激活:重选人员") choice = random.choice(cur_stu) logs("DEBUG","随机选中人员 id:" + choice.id + " 姓名:" + choice.name + " 学科:" + choice.subject) counter = counter + 1 if counter >= 100: logs("DEBUG","防重选保护关闭:重选次数过多") break logs("DEBUG","重选保护重筛完毕,应用选择人员") logs("SILENT","选中人员 id:" + choice.id + " 姓名:" + choice.name + " 学科:" + choice.subject) upd_name(choice.name) logs("DEBUG","更新班级Label显示:" + choice.class_name) class_label.config(text = choice.class_name) logs("DEBUG","更新学科Label显示:" + choice.subject) subject_label.config(text = choice.subject) logs("DEBUG","更新防重选保护人员记录:" + choice.name) last_choice = choice.name if sound: logs("DEBUG","播放 " + selname + " 选中音频文件") winsound.PlaySound(selname, winsound.SND_ASYNC | winsound.SND_FILENAME) logs("DEBUG","设置GUI控件样式") startramdonStyle=Style() startramdonStyle.configure("SR.TButton", font = ("黑体", 17, "bold"), foreground = "red", background = "white",width=29,relief="flat",highlightthickness = 0, bd = 0,pady=0, padx=0) setclassStyle=Style() setclassStyle.configure("SC.TButton", font = ("黑体", 15), background = "white",relief="flat",highlightthickness = 0, bd = 0,pady=0, padx=0) namelabelStyle=Style() namelabelStyle.configure("NL.TLabel", font = ("黑体", 30, "bold"), background = "white", foreground = "blue",width=15,anchor="center") classlabelStyle=Style() classlabelStyle.configure("CL.TLabel", font = ("黑体", 15), background = "white",width=20,anchor="center") subjectlabelStyle=Style() subjectlabelStyle.configure("SL.TLabel", font = ("黑体", 15), background = "white",width=20,anchor="center") logs("DEBUG","设置控件变量连接") startrandom = Button(gui, text = "立刻摇人!", command = choose, style="SR.TButton") setclass = Button(gui, text = "应用班级选用", command = sel_class, style="SC.TButton") scrool = Scrollbar(gui, command = listbox.yview) listbox.config(yscrollcommand=scrool.set) name_label = Label(gui, textvariable = cur_name, style="NL.TLabel") class_label = Label(gui, text = "", style="CL.TLabel") subject_label = Label(gui, text = "", style="SL.TLabel") # class_label.grid(row = 2, column = 1, sticky = N+S+E+W) # subject_label.grid(row = 2, column = 2, sticky = N+S+E+W) # name_label.grid(row = 1, column = 1, sticky = N+S) # listbox.grid(row = 1, column = 2, sticky = N+S) # startrandom.grid(row = 3, column = 1, sticky = N+S+E+W) # setclass.grid(row = 3, column = 2, sticky = N+S+E+W) # scrool.grid(row = 1, column = 3, sticky = N+S) logs("DEBUG","绘制GUI") class_label.place(relx=0.3,rely=0.5) subject_label.place(relx=0,rely=0.5) name_label.place(relx=0.05,rely=0.05) listbox.place(relx=0.7,rely=0,width=150,height=104) startrandom.place(relx=0,rely=0.7375,height=37) setclass.place(relx=0.7,rely=0.7375,height=37,width=164.5) scrool.place(relx=0.965,rely=0,height=104) if topmost: logs("INFO","应用在置顶模式下运行") gui.wm_attributes("-topmost", 1) gui.mainloop() except Exception as e: logs("FATAL","运行发生了错误:" + str(e))