用python给朋友写一个培训用的计分器软件
背景:朋友经常培训,涉及到分组得分。一般都是线下登记小组得分,但是这样每次会出错或者结束后还要手动统计。于是帮其做了一个这个软件。主要功能:默认三个小组,也可以自己在进行添加。数据会实时保存到桌面的excle文件中,防止意外造成数据丢失。有一个悬浮球的功能,可以一直显示在ppt上面,实现快速打开程序界面加分。
其他说明:初学,不太懂,很多代码都是百度来的。还有一些细节处理不是很好,但是能力有限,不会处理,已经放弃了。
已知问题:1、打包的exe文件12MB比较大。2、不知道是不是我电脑问题还是程序问题,每次打开软件需要5秒左右,感觉比较久。3、悬浮球的问题,设计思路是一个png透明的球形logo,但是最终的效果是在深色背景下出现白色的毛边导致图像不是很清晰(原图png是没问题的)
使用方法:打开程序,然后会自动在桌面创建一个excle文件用于保存数据,每次点击程序的确定按钮都会对数据进行保存,分别记录总分和每次添加的分数。退出程序只能在系统托盘处的图标点击退出。
已经打包成成品,可以正常使用。
下面是源码:
主程序代码和悬浮球代码
import tkinter as tk# 导入 tkinter 库,用于创建 GUI 界面
from tkinter import ttk, messagebox# 导入 ttk 和 messagebox 模块,用于创建更丰富的 GUI 元素和消息框
import pystray# 导入 pystray 库,用于创建系统托盘图标
from pystray import MenuItem as item# 导入 MenuItem 类,用于创建系统托盘菜单项
from PIL import Image, ImageDraw# 导入 PIL 库,用于图像处理
import threading# 导入 threading 模块,用于多线程操作
import os# 导入 os 模块,用于与操作系统进行交互
from custom_dialog import CustomDialog# 导入自定义对话框模块
import openpyxl# 导入 openpyxl 库,用于处理 Excel 文件
from datetime import datetime# 导入 datetime 模块,用于处理日期和时间
from floatingball import FloatingBall
from resource_p import Resource_path
class ScoreCalculator(tk.Tk):# 创建一个名为 ScoreCalculator 的类,继承自 tk.Tk
def __init__(self):# 初始化方法
super().__init__()# 调用父类的初始化方法
self.title("SW计分器")# 设置窗口标题
self.geometry("550x350")# 设置窗口大小
self.configure(bg="#f5f5f5")# 设置窗口背景颜色
self.resource_path = Resource_path()# 实例化 Resource_path 对象
self.icon_path = self.resource_path.resource_path("img_s.ico")# 设置图标路径
self.wm_iconbitmap(self.icon_path)# 设置窗口图标
self.create_tray_icon()# 创建系统托盘图标
self.groups = {"组别1": 0, "组别2": 0, "组别3": 0}# 初始化分组字典
self.group_vars = {}# 存储分组变量的字典
self.group_frames = {}# 存储分组框架的字典
self.create_widgets()# 调用创建组件的方法
self.protocol("WM_DELETE_WINDOW", self.hide_window)# 设置窗口关闭时的处理方法
self.tray_icon = None# 初始化系统托盘图标变量
self.tray_thread = None# 初始化系统托盘线程变量
# 创建悬浮球
self.floating_ball_root = tk.Toplevel(self)
screen_width = self.winfo_screenwidth()# 获取屏幕宽度和高度
screen_height = self.winfo_screenheight()
# 设置悬浮球的默认位置为屏幕右下角,假设悬浮球大小为50x50
self.floating_ball_root.geometry(f"+{screen_width - 200}+{screen_height - 800}")
# self.floating_ball_root.geometry("+100+100")# 设置悬浮球的初始位置
self.floating_ball = FloatingBall(self.floating_ball_root, self.show_window)
self.excel_file = self.create_excel_file()# 创建 Excel 文件excel_file = file_path
def create_excel_file(self):# 创建 Excel 文件的方法
try:# 尝试创建 Excel 文件
desktop_dir = os.path.join(os.path.expanduser("~"), 'Desktop')# 获取桌面路径
now = datetime.now().strftime("%m%d_%H%M%S")# 获取当前时间
file_name = f"SW计分器_{now}.xlsx"# 构建文件名
file_path = os.path.join(desktop_dir, file_name)# 构建文件路径
workbook = openpyxl.Workbook()# 创建 Excel 工作簿对象
sheet = workbook.active# 获取活动工作表
sheet.title = "Scores"# 设置工作表标题
sheet.append(["分组", "总分"])# 添加标题行
for group_name in self.groups.keys():# 遍历分组字典
sheet.append()# 添加分组信息到表格,key=group_name value=0
workbook.save(file_path)# 保存 Excel 文件
return file_path# 返回文件路径
except Exception as e:# 捕获异常
messagebox.showerror("文件错误", f"无法创建 Excel 文件: {e}")# 显示错误消息框
print(f"错误: {e}")# 打印错误信息
return None# 返回 None
def append_to_excel(self):# 向 Excel 文件追加数据的方法
workbook = openpyxl.load_workbook(self.excel_file)# 加载 Excel 文件
sheet = workbook.active# 获取活动工作表
last_column = sheet.max_column + 1# 获取当前工作表中已使用的最大列数,然后加1得到新的数据要写入的列索引。
for i, (group_name, total) in enumerate(self.groups.items(), start=2):# 遍历分组字典
score = int(self.group_vars.get())# 获取分数值
sheet.cell(row=i, column=last_column, value=score)# 将分数值写入单元格
# sheet.cell(row=i, column=1, value=group_name)# 更新组名
sheet.cell(row=i, column=2, value=total)# 更新总分列
now = datetime.now().strftime("%H:%M:%S")# 获取当前时间
sheet.cell(row=1, column=last_column, value=now)# 在最后一列的顶部写入时间
workbook.save(self.excel_file)# 保存 Excel 文件
def create_widgets(self):# 创建窗口组件的方法
title_frame = tk.Frame(self, bg="#f5f5f5")# 创建标题框架
title_frame.grid(row=0, column=0, columnspan=3, sticky="ew")# 设置标题框架的位置和大小
title_frame.grid_columnconfigure(0, weight=1)# 配置标题框架的列
title_frame.grid_columnconfigure(1, weight=1)
title_frame.grid_columnconfigure(2, weight=1)
tk.Button(title_frame, text="组别", bg="#4CAF50", fg="white", font=("Arial", 14), width=5, relief=tk.FLAT).grid(
row=0, column=0, padx=60, pady=10)# 创建按钮,用于显示组别信息
tk.Button(title_frame, text="总分", bg="#4CAF50", fg="white", font=("Arial", 14), width=5, relief=tk.FLAT).grid(
row=0, column=1, padx=20, pady=10)# 创建按钮,用于显示总分信息
tk.Button(title_frame, text="加分", bg="#4CAF50", fg="white", font=("Arial", 14), width=5, relief=tk.FLAT).grid(
row=0, column=2, padx=20, pady=10)# 创建按钮,用于加分操作
self.group_frame = tk.Frame(self, bg="#f5f5f5")# 创建分组框架
self.group_frame.grid(row=1, column=0, columnspan=3, pady=10)# 设置分组框架的位置和大小
for group in self.groups:# 遍历分组字典
self.add_group_frame(group)# 调用添加分组框架的方法
button_frame = tk.Frame(self, bg="#f5f5f5")# 创建按钮框架
button_frame.grid(row=2, column=0, columnspan=3, pady=10)# 设置按钮框架的位置和大小
self.add_group_button = tk.Button(button_frame, text="添加组别", command=self.add_group, bg="#2196F3", fg="white",
font=("Arial", 12), width=15)# 创建按钮,用于添加分组
self.add_group_button.pack(side=tk.LEFT, padx=50, pady=20)# 放置按钮的位置和外边距
self.confirm_button = tk.Button(button_frame, text="确定", command=self.update_scores, bg="#2196F3", fg="white",
font=("Arial", 12), width=15)# 创建按钮,用于确认操作
self.confirm_button.pack(side=tk.RIGHT, padx=60, pady=10)# 放置按钮的位置和外边距
def add_group_frame(self, group_name):# 添加分组框架的方法
frame = tk.Frame(self.group_frame, bg="#f5f5f5")# 创建分组框架
frame.pack(fill=tk.X, pady=5)# 设置分组框架的填充和外边距
group_var = tk.StringVar(value=group_name)# 创建分组变量
self.group_vars = group_var# 将分组变量存储到字典中
group_name_entry = tk.Entry(frame, textvariable=group_var, font=("Arial", 12), width=15,
justify="center")# 创建分组名称输入框
group_name_entry.pack(side=tk.LEFT, padx=20)# 放置输入框的位置和外边距
total_label = tk.Label(frame, text=str(self.groups), font=("Arial", 12), bg="#f5f5f5", width=15,
justify="center")# 创建总分标签
total_label.pack(side=tk.LEFT, padx=20)# 放置标签的位置和外边距
score_var = tk.StringVar(value="0")# 创建分数变量
self.group_vars = score_var# 将分数变量存储到字典中
score_combo = ttk.Combobox(frame, textvariable=score_var, font=("Arial", 12), width=3)# 创建分数下拉框
score_combo["values"] = ("0", "1", "2", "3", "4", "5", "10", "15", "20")# 设置下拉框的值
score_combo.pack(side=tk.LEFT, padx=20)# 放置下拉框的位置和外边距
score_combo.current(0)# 设置默认选中项为第一项
self.group_frames = {"frame": frame, "total_label": total_label}# 存储分组框架和总分标签的信息
def update_scores(self):# 更新分数的方法
for group_name, data in self.group_frames.items():# 遍历分组字典
try:
score = int(self.group_vars.get())# 获取分数值
self.groups += score# 更新分组总分
data["total_label"].config(text=str(self.groups))# 更新总分标签的文本
except ValueError:# 如果值错误
messagebox.showerror("输入错误", f"请为 {group_name} 组输入有效的数字")# 显示错误消息框
self.append_to_excel()# 向 Excel 文件追加数据
for group_name in self.groups.keys():# 遍历分组字典的键
self.group_vars.set("0")# 将分数变量重置为0
def add_group(self):# 添加分组的方法
dialog = CustomDialog(self, title="SW计分器")# 创建自定义对话框
new_group_name = dialog.result# 获取对话框结果
if new_group_name:# 如果有新的分组名称
self.groups = 0# 添加新的分组
self.add_group_frame(new_group_name)# 添加新的分组框架
workbook = openpyxl.load_workbook(self.excel_file)# 加载 Excel 文件
sheet = workbook.active# 获取活动工作表
sheet.append()# 在工作表末尾添加新分组的数据
workbook.save(self.excel_file)# 保存 Excel 文件
def hide_window(self):# 隐藏窗口的方法
self.withdraw()# 隐藏窗口
def show_window(self, icon=None, item=None):# 显示窗口的方法
self.deiconify()# 显示窗口
def hide_floating_ball(self):
self.floating_ball.hide()
def show_floating_ball(self):
self.floating_ball.show()
def create_tray_icon(self, ):# 创建系统托盘图标的方法
try:# 尝试加载图标文件
image_path = self.resource_path.resource_path("img_s.png")# 获取图标路径
image = Image.open(image_path)# 打开图标文件
except Exception as e:#
image = Image.new("RGB", (64, 64), (255, 255, 255))# 创建一个空白图像
draw = ImageDraw.Draw(image)# 创建绘图对象
draw.rectangle((0, 0, 64, 64), fill="blue")# 绘制蓝色矩形
menu = pystray.Menu(
item('显示悬浮球', lambda icon, item: self.show_floating_ball()),
item('隐藏悬浮球', lambda icon, item: self.hide_floating_ball()),
item('显示', lambda icon, item: self.show_window()),import tkinter as tk# 导入 tkinter 库,用于创建 GUI 界面
from PIL import Image, ImageDraw, ImageTk# 导入 PIL 库,用于图像处理
import os# 导入 os 模块,用于与操作系统进行交互
import sys# 导入 sys 模块,用于访问与 Python 解释器相关的变量和函数
from resource_p import Resource_path
#悬浮球代码
class FloatingBall:
def __init__(self, root, show_main_window_callback):
self.root = root
self.show_main_window_callback = show_main_window_callback
self.root.overrideredirect(True)# 去除窗口边框
self.root.attributes("-topmost", True)# 保持窗口在顶层
self.root.attributes("-alpha", 1)# 设置窗口透明度
self.root.wm_attributes("-transparentcolor", "white")# 增加
# 创建 Canvas 用于绘制图标
self.canvas = tk.Canvas(root, width=50, height=50, highlightthickness=0)
self.canvas.pack()
# 加载自定义图标
self.resource_path = Resource_path()# 实例化 Resource_path 对象
self.icon_path = self.resource_path.resource_path("img_9.ico")# 你的自定义图标路径
icon_image = Image.open(self.icon_path)
background = Image.new("RGBA", icon_image.size, (255, 255, 255, 255))# 增加
icon_image = Image.alpha_composite(background, icon_image)# 增加
icon_image = icon_image.resize((50, 50), Image.ANTIALIAS)# 调整图标大小
# 将图像转换为 PhotoImage 对象
self.icon_photo = ImageTk.PhotoImage(icon_image)
# 在 Canvas 上绘制图标
self.canvas.create_image(25, 25, image=self.icon_photo)
# 绑定事件
self.canvas.bind("<ButtonPress-1>", self.start_move)
self.canvas.bind("<ButtonRelease-1>", self.stop_move)
self.canvas.bind("<B1-Motion>", self.do_move)
self.canvas.bind("<Button-3>", self.show_menu)# 右击事件
self.moving = False# 用于判断是否正在移动
def start_move(self, event):
self.x = event.x
self.y = event.y
self.moving = False
def stop_move(self, event):
if not self.moving:
self.show_main_window_callback()
self.x = self.y = 0
self.moving = False
def do_move(self, event):
self.moving = True
deltax = event.x - self.x
deltay = event.y - self.y
x = self.root.winfo_x() + deltax
y = self.root.winfo_y() + deltay
self.root.geometry(f"+{x}+{y}")
def hide(self):
self.root.withdraw()# 隐藏悬浮球
def show(self):
self.root.deiconify()# 显示悬浮球
def show_menu(self, event):
menu = tk.Menu(self.root, tearoff=False)
menu.add_command(label="隐藏悬浮球", command=self.hide)
menu.post(event.x_root, event.y_root)
def resource_path(self, relative_path):# 定义资源路径方法,用于获取相对路径的绝对路径
try:# 尝试从 PyInstaller 生成的临时目录获取路径
base_path = sys._MEIPASS# 获取临时目录路径
except Exception:# 如果出现异常,则使用当前工作目录
base_path = os.path.abspath(".")# 获取当前工作目录路径
return os.path.join(base_path, relative_path)# 返回资源文件的绝对路径
item('退出', self.quit_program))# 创建系统托盘菜单
self.tray_icon = pystray.Icon("name", image, "SW计分器", menu)# 创建系统托盘图标
self.tray_thread = threading.Thread(target=self.tray_icon.run)# 创建系统托盘图标运行的线程
self.tray_thread.start()# 启动线程
def quit_program(self, icon, item):# 退出程序的方法
if messagebox.askokcancel("退出", "确定退出程序?"):
icon.stop()# 停止系统托盘图标
self.quit()# 退出程序
最后是打包成品软件
已经打包成成品,可以正常使用。
https://wwp.lanzoue.com/ilQ3o216dqdc 密码:52pj 本帖最后由 lastmu 于 2024-6-8 08:58 编辑
挺好用,可适用于各种竞赛项目的计分,原来我是用表格做的。
我提个建议:
1、只有加分,还需要有减分。
2、加减分后没有日志,如果有日志就更好了。
使用中发现,如果关掉窗口,内容没有保存,如果把积分丢失的话,这问题就大了。 我再把效果图给大家发一下:
大神无处不在。 这个不错,有点实用价值 menu = pystray.Menu(
item('显示悬浮球', lambda icon, item: self.show_floating_ball()),
item('隐藏悬浮球', lambda icon, item: self.hide_floating_ball()),
item('显示', lambda icon, item: self.show_window()),)
这部分报错了 在本地环境下跑了一下,需要降级Pillow的版本,比如使用9.5.0版本
先卸载
pip uninstall -y Pillow
再重新安装
pip install Pillow==9.5.0
望对大家有帮助! 昨天正好在网上看到一个UI可以再美化一下https://img.picgo.net/2024/06/08/11ace32845e8be687.png 很实用,谢谢分享{:1_921:}