Files
Random-Picker/点名器.pyw
2023-02-16 00:22:52 +08:00

444 lines
20 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'''
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 <https://www.gnu.org/licenses/>.
'''
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))