PYTHON-开发运动会计分管理软件
项目概述
1. 软件功能
我们的运动会计分管理软件具有以下功能:
添加运动员:提供接口让用户输入运动员的基本信息,包括分组、姓名、名次。管理分组:支持创建、修改和删除运动员分组,方便组织和查看。得分计算:根据运动员名次自动计算得分,并在用户界面中更新。查看总分:统计所有运动员的总分、平均分、最高分和最低分,并支持分组汇总。数据导出:允许用户将所有运动员信息导出为 CSV 文件,便于存档和后续使用。切换破纪录状态:通过右键菜单允许用户方便地切换运动员的破纪录状态,影响得分计算。2. 技术栈
本项目所需的技术栈包括:
Python:作为编程语言实现逻辑和数据处理。tkinter:构建图形用户界面,快速开发桌面应用。CSV模块:用于处理数据导出与导入的标准库。3. 软件开发环境
开发此软件时,需确保您的计算机上安装了 Python(建议使用 Python 3.7 及以上版本)。同时,我们使用的 tkinter
库是 Python 的标准库,不需要单独安装。CSV模块也是Python自带的模块,您可以直接使用。
安装与基础设置
安装 Python:
前往 Python 官网 下载并安装适合您操作系统的版本。请确保勾选“Add Python to PATH”的选项。创建项目文件夹:
在您的计算机上创建一个新的文件夹,命名为ScoreManagementApp
,在其中创建一个文件 score_app.py
,并在里面编写代码。 环境配置:
在终端中使用pip
安装所需的第三方库(如有需要)。本项目不依赖于外部库,直接使用tkinter
和csv
标准库。 开发步骤详解
1. 数据结构设计
通过使用数据类 (dataclass
) 来定义运动员的信息结构,使得数据的管理和访问更加方便。
from dataclasses import dataclass@dataclassclass Athlete: group: str name: str rank: int score: int is_record: bool = False
上述代码简洁地定义了运动员的各种属性,包括其所属分组、姓名、名次、得分和破纪录状态。使用数据类的好处是可以更直观地管理运动员信息,并提升代码的可读性。
2. 创建主应用程序类 ScoreApp
以下是实现 ScoreApp
类的基本框架和关键功能。该类负责所有界面组件的管理以及实现各项业务逻辑。
import tkinter as tkfrom tkinter import ttk, messagebox, filedialog, Menu, OptionMenu, StringVarimport csvclass ScoreApp: def __init__(self, root): self.root = root self.root.title("运动会计分管理软件") # 初始化名次和分数对应关系 self.rank_to_score = {1: 9, 2: 7, 3: 6, 4: 5, 5: 4, 6: 3, 7: 2, 8: 1} # 可用分组 self.available_groups = {"组A"} # 表格呈现运动员信息 self.tree = ttk.Treeview(root, columns=("分组", "姓名", "名次", "分数", "破纪录"), show='headings') self.setup_treeview() # 控件布局和功能定义 self.setup_controls() # 初始化运动员列表 self.scores = [] self.group_filter_var = StringVar(root) self.group_filter_var.set("所有分组") self.setup_group_selection() def setup_treeview(self): """ 设置Treeview组件 """ self.tree.heading("分组", text="分组") self.tree.heading("姓名", text="姓名") self.tree.heading("名次", text="名次") self.tree.heading("分数", text="分数") self.tree.heading("破纪录", text="破纪录") for col in ("分组", "姓名", "名次", "分数", "破纪录"): self.tree.column(col, width=100) self.tree.grid(row=0, column=0, columnspan=5, padx=10, pady=10) def setup_controls(self): """ 创建按钮和控制框 """ tk.Button(self.root, text="添加运动员", command=self.open_add_athlete_dialog).grid(row=1, column=0, padx=5, pady=5) tk.Button(self.root, text="管理分组", command=self.open_group_management).grid(row=1, column=1, padx=5, pady=5) tk.Button(self.root, text="查看总分", command=self.view_total_scores).grid(row=1, column=2, padx=5, pady=5) tk.Button(self.root, text="导出数据", command=self.export_to_csv).grid(row=1, column=3, padx=5, pady=5) tk.Button(self.root, text="删除选中行", command=self.delete_selected).grid(row=1, column=4, padx=5, pady=5) tk.Button(self.root, text="设置分数", command=self.open_settings).grid(row=1, column=5, padx=5, pady=5) def setup_group_selection(self): """ 设置分组选择下拉框 """ # 分组选择实现略
上述代码展示了如何初始化应用程序的窗口并构建基本元素,比如设置表格(Treeview
)和按钮。设计时需要确保用户界面友好,并能方便地访问不同功能。
3. 添加运动员信息
添加运动员的对话框包括必要的输入字段,并在用户确认后将数据录入到列表中。
def open_add_athlete_dialog(self): """ 打开添加运动员的对话框 """ add_window = tk.Toplevel(self.root) add_window.title("添加运动员") tk.Label(add_window, text="分组:").grid(row=0, column=0, padx=10, pady=5) group_entry = StringVar(add_window) group_entry.set("选择分组或自定义") group_menu = OptionMenu(add_window, group_entry, *self.available_groups) group_menu.grid(row=0, column=1, padx=10, pady=5) tk.Label(add_window, text="姓名:").grid(row=1, column=0, padx=10, pady=5) name_entry = tk.Entry(add_window) name_entry.grid(row=1, column=1, padx=10, pady=5) tk.Label(add_window, text="名次:").grid(row=2, column=0, padx=10, pady=5) rank_entry = tk.Entry(add_window) rank_entry.grid(row=2, column=1, padx=10, pady=5) tk.Button(add_window, text="确认", command=lambda: self.add_score(group_entry.get(), name_entry.get(), rank_entry.get(), add_window)).grid(row=3, columnspan=3, pady=10)def add_score(self, group, name, rank_str, add_window): """ 添加运动员信息 """ if group and name and rank_str.isdigit() and int(rank_str) in self.rank_to_score: rank = int(rank_str) score = self.rank_to_score[rank] athlete = Athlete(group, name, rank, score) self.scores.append(athlete) self.tree.insert("", "end", values=(athlete.group, athlete.name, athlete.rank, athlete.score, "✔" if athlete.is_record else "✖")) add_window.destroy() else: messagebox.showwarning("输入错误", "请确保所有输入有效!")
open_add_athlete_dialog
方法负责展示添加运动员信息的输入窗口,而 add_score
方法则实现将输入的运动员信息添加到数据结构和界面表格中。输入验证确保数据的有效性,避免无效的输入占用系统资源。
4. 查看总分和数据导出
审核和导出运动员总分也是软件必不可少的功能。我们将通过一个弹出的新窗口展示统计数据,并支持将数据导出为 CSV 格式以备存档。
def view_total_scores(self): total_window = tk.Toplevel(self.root) total_window.title("总分") total_label = tk.Label(total_window, text="所有运动员总分:") total_label.pack(padx=10, pady=5) total_score = sum(athlete.score * (2 if athlete.is_record else 1) for athlete in self.scores) all_scores_label = tk.Label(total_window, text=f"总分: {total_score}") all_scores_label.pack(padx=10, pady=5) # 计算和展示平均分、最高分、最低分 average_score = total_score / len(self.scores) if self.scores else 0 highest_score = max(athlete.score for athlete in self.scores) if self.scores else 0 lowest_score = min(athlete.score for athlete in self.scores) if self.scores else 0 stats_label = tk.Label(total_window, text=f"平均分: {average_score:.2f}, 最高分: {highest_score}, 最低分: {lowest_score}") stats_label.pack(padx=10, pady=5)def export_to_csv(self): filename = filedialog.asksaveasfilename(defaultextension=".csv", filetypes=[("CSV files", "*.csv")]) if filename: with open(filename, mode='w', newline='', encoding='utf-8') as file: writer = csv.writer(file) writer.writerow(["分组", "姓名", "名次", "分数", "破纪录"]) for athlete in self.scores: writer.writerow([athlete.group, athlete.name, athlete.rank, athlete.score, athlete.is_record]) messagebox.showinfo("成功", f"数据已导出到 {filename}")
view_total_scores
方法计算所有运动员的分数,并将其展示在新窗口中,而 export_to_csv
则构建 CSV 文件并保存用户指定的位置。此功能对于后续的数据分析和管理至关重要,帮助用户轻松整理和传递信息。
5. 管理分组和删除运动员
用户需要能够动态地管理运动员组和删除不再需要的运动员记录。以下是相关的实现:
def open_group_management(self): group_window = tk.Toplevel(self.root) group_window.title("管理分组") tk.Label(group_window, text="自定义分组:").grid(row=0, column=0, padx=10, pady=5) custom_group_entry = tk.Entry(group_window) custom_group_entry.grid(row=0, column=1, padx=10, pady=5) tk.Button(group_window, text="添加分组", command=lambda: self.add_custom_group(custom_group_entry.get())).grid(row=0, column=2, padx=10, pady=5) tk.Label(group_window, text="当前分组:").grid(row=1, column=0, padx=10, pady=5) current_groups_var = StringVar(value=", ".join(sorted(self.available_groups))) tk.Entry(group_window, textvariable=current_groups_var, state='readonly').grid(row=1, column=1, padx=10, pady=5) tk.Label(group_window, text="删除分组:").grid(row=2, column=0, padx=10, pady=5) delete_group_entry = tk.Entry(group_window) delete_group_entry.grid(row=2, column=1, padx=10, pady=5) tk.Button(group_window, text="删除分组", command=lambda: self.delete_custom_group(delete_group_entry.get())).grid(row=2, column=2, padx=10, pady=5)def add_custom_group(self, group_name): if group_name and group_name not in self.available_groups: self.available_groups.add(group_name) self.setup_group_selection() messagebox.showinfo("成功", f"已添加自定义分组: {group_name}")def delete_selected(self): selected_item = self.tree.selection() if selected_item: for item in selected_item: index = self.tree.index(item) self.tree.delete(item) del self.scores[index] self.update_displayed_athletes() else: messagebox.showwarning("选择错误", "请先选择一行!")
open_group_management
方法启用分组的添加和删除,增强了对运动员的组织能力,而 delete_selected
则允许用户删除选中的运动员记录,使得整体管理变得灵活。
6. 用户交互的多样性
在软件中,右键菜单为用户提供了灵活的操作手段,比如切换运动员的破纪录状态。调用右键菜单的相关实现代码如下:
def show_popup(self, event): try: selected = self.tree.identify_row(event.y) if selected: self.tree.selection_set(selected) self.popup_menu.post(event.x_root, event.y_root) except Exception as e: print(f"Error showing popup menu: {e}")self.popup_menu = Menu(root, tearoff=0)self.popup_menu.add_command(label="切换破纪录", command=self.toggle_record)
通过实现相应的事件绑定,我们使得软件的交互性更强,更加贴合用户的操作习惯。
7. 代码结构与模块化
在整个应用中,保持良好的代码结构和模块化设计是至关重要的。这不仅能提升代码的可读性,还能帮助团队中的其他开发者快速理解和修改代码。
可以将不同的功能区域分开为不同的模块,例如将与数据存储、数据处理、用户界面等相关的功能分别放入不同的文件中。使用 Python 的模块导入功能来连接这些组件,可以有效管理大型项目,提升维护性。
总结
本篇博客详细介绍了如何使用 Python 的 tkinter
库创建一个运动会计分管理软件。
作为示例为研究GUI和数据结构打下基础
完整源码:
import tkinter as tkfrom tkinter import ttk, simpledialog, messagebox, Menu, OptionMenu, StringVar, filedialogfrom dataclasses import dataclassimport csv# 数据类: 运动员信息@dataclassclass Athlete: group: str name: str rank: int score: int is_record: bool = Falseclass ScoreApp: def __init__(self, root): self.root = root self.root.title("运动会计分软件") # 默认名次与分数对应关系 self.rank_to_score = { 1: 9, 2: 7, 3: 6, 4: 5, 5: 4, 6: 3, 7: 2, 8: 1 } # 可用分组 self.available_groups = {"组A"} # 默认分组 # 表格呈现 self.tree = ttk.Treeview(root, columns=("分组", "姓名", "名次", "分数", "破纪录"), show='headings') self.tree.heading("分组", text="分组") self.tree.heading("姓名", text="姓名") self.tree.heading("名次", text="名次") self.tree.heading("分数", text="分数") self.tree.heading("破纪录", text="破纪录") self.tree.column("分组", width=100) self.tree.column("姓名", width=200) self.tree.column("名次", width=100) self.tree.column("分数", width=100) self.tree.column("破纪录", width=100) self.tree.grid(row=0, column=0, columnspan=5, padx=10, pady=10) # 添加按钮 self.add_button = tk.Button(root, text="添加运动员", command=self.open_add_athlete_dialog) self.add_button.grid(row=1, column=0, padx=5, pady=5) # 管理分组按钮 self.group_management_button = tk.Button(root, text="管理分组", command=self.open_group_management) self.group_management_button.grid(row=1, column=1, padx=5, pady=5) # 查看总分按钮 self.total_button = tk.Button(root, text="查看总分", command=self.view_total_scores) self.total_button.grid(row=1, column=2, padx=5, pady=5) # 导出按钮 self.export_button = tk.Button(root, text="导出数据", command=self.export_to_csv) self.export_button.grid(row=1, column=3, padx=5, pady=5) # 删除按钮 self.delete_button = tk.Button(root, text="删除选中行", command=self.delete_selected) self.delete_button.grid(row=1, column=4, padx=5, pady=5) # 设置按钮 self.settings_button = tk.Button(root, text="设置分数", command=self.open_settings) self.settings_button.grid(row=1, column=5, padx=5, pady=5) # 右键菜单 self.popup_menu = Menu(root, tearoff=0) self.popup_menu.add_command(label="切换破纪录", command=self.toggle_record) self.popup_menu.add_separator() # 这里调用 open_group_management self.popup_menu.add_command(label="设置分组", command=self.open_group_management) # 绑定右键点击 self.tree.bind("<Button-3>", self.show_popup) # 运动员信息列表 self.scores = [] # 分组过滤变量 self.group_filter_var = StringVar(root) self.group_filter_var.set("所有分组") self.setup_group_selection() def setup_group_selection(self): """ 设置和显示分组选择下拉框 """ groups = sorted(self.available_groups) + ["所有分组"] group_menu = OptionMenu(self.root, self.group_filter_var, *groups, command=self.update_displayed_athletes) group_menu.grid(row=1, column=6, padx=5, pady=5) def update_displayed_athletes(self, *args): """ 根据选择的分组更新显示的运动员 """ self.tree.delete(*self.tree.get_children()) filtered_scores = [athlete for athlete in self.scores if self.group_filter_var.get() in ("所有分组", athlete.group)] for athlete in filtered_scores: self.tree.insert("", "end", values=(athlete.group, athlete.name, athlete.rank, athlete.score, "✔" if athlete.is_record else "✖")) def open_add_athlete_dialog(self): """ 打开添加运动员的对话框 """ add_window = tk.Toplevel(self.root) add_window.title("添加运动员") # 输入字段 tk.Label(add_window, text="分组:").grid(row=0, column=0, padx=10, pady=5) group_entry = StringVar(add_window) group_entry.set("选择分组或自定义") group_menu = OptionMenu(add_window, group_entry, *self.available_groups) group_menu.grid(row=0, column=1, padx=10, pady=5) custom_group_entry = tk.Entry(add_window) # 自定义分组输入 custom_group_entry.grid(row=0, column=2, padx=10, pady=5) tk.Label(add_window, text="姓名:").grid(row=1, column=0, padx=10, pady=5) name_entry = tk.Entry(add_window) name_entry.grid(row=1, column=1, padx=10, pady=5) tk.Label(add_window, text="名次:").grid(row=2, column=0, padx=10, pady=5) rank_entry = tk.Entry(add_window) rank_entry.grid(row=2, column=1, padx=10, pady=5) # 确认按钮 tk.Button(add_window, text="确认", command=lambda: self.add_score(custom_group_entry.get(), group_entry.get(), name_entry.get(), rank_entry.get(), add_window)).grid(row=3, columnspan=3, pady=10) def add_score(self, custom_group, group, name, rank_str, add_window): """ 添加运动员信息 """ group_to_use = custom_group if custom_group else group if group_to_use and name and rank_str.isdigit() and int(rank_str) in self.rank_to_score: rank = int(rank_str) score = self.rank_to_score[rank] athlete = Athlete(group_to_use, name, rank, score) # 使用数据类 self.scores.append(athlete) # 添加运动员 self.tree.insert("", "end", values=(athlete.group, athlete.name, athlete.rank, athlete.score, "✔" if athlete.is_record else "✖")) add_window.destroy() # 关闭添加窗口 self.sort_treeview() # 添加后自动排序 self.available_groups.add(group_to_use) # 自定义分组添加到集合 self.setup_group_selection() # 更新分组选择 else: messagebox.showwarning("输入错误", "请确保所有输入有效!") def sort_treeview(self): """ 按照分数从高到低排序 """ sorted_scores = sorted(self.scores, key=lambda x: x.score, reverse=True) self.tree.delete(*self.tree.get_children()) # 清空当前显示的所有行 # 重新显示排序后的运动员信息 for athlete in sorted_scores: self.tree.insert("", "end", values=(athlete.group, athlete.name, athlete.rank, athlete.score, "✔" if athlete.is_record else "✖")) def view_total_scores(self): """ 查看总分窗口 """ total_window = tk.Toplevel(self.root) total_window.title("总分") total_label = tk.Label(total_window, text="所有运动员总分:") total_label.pack(padx=10, pady=5) total_score = sum(athlete.score * (2 if athlete.is_record else 1) for athlete in self.scores) all_scores_label = tk.Label(total_window, text=f"总分: {total_score}") all_scores_label.pack(padx=10, pady=5) # 计算平均分、最高分、最低分 if self.scores: average_score = total_score / len(self.scores) highest_score = max(athlete.score for athlete in self.scores) lowest_score = min(athlete.score for athlete in self.scores) else: average_score = highest_score = lowest_score = 0 stats_label = tk.Label(total_window, text=f"平均分: {average_score:.2f}, 最高分: {highest_score}, 最低分: {lowest_score}") stats_label.pack(padx=10, pady=5) # 各分组的总分 group_scores = {} for athlete in self.scores: group = athlete.group group_score = athlete.score * (2 if athlete.is_record else 1) group_scores[group] = group_scores.get(group, 0) + group_score # 显示各组的总分 group_total_label = tk.Label(total_window, text="各分组总分:") group_total_label.pack(padx=10, pady=5) for group, group_total in group_scores.items(): group_score_label = tk.Label(total_window, text=f"{group}: {group_total}") group_score_label.pack(padx=10, pady=5) def export_to_csv(self): """ 弹出文件对话框选择保存路径并导出 CSV """ filename = filedialog.asksaveasfilename(defaultextension=".csv", filetypes=[("CSV files", "*.csv")]) if filename: with open(filename, mode='w', newline='', encoding='utf-8') as file: writer = csv.writer(file) writer.writerow(["分组", "姓名", "名次", "分数", "破纪录"]) for athlete in self.scores: writer.writerow([athlete.group, athlete.name, athlete.rank, athlete.score, athlete.is_record]) messagebox.showinfo("成功", f"数据已导出到 {filename}") def delete_selected(self): selected_item = self.tree.selection() if selected_item: for item in selected_item: index = self.tree.index(item) self.tree.delete(item) # 从列表中删除相应的运动员信息 del self.scores[index] self.update_displayed_athletes() # 更新显示 else: messagebox.showwarning("选择错误", "请先选择一行!") def open_settings(self): """ 打开设置分数的窗口 """ settings_window = tk.Toplevel(self.root) settings_window.title("设置分数") self.rank_income_vars = {} for rank in range(1, 9): tk.Label(settings_window, text=f"第{rank}名分数:").grid(row=rank-1, column=0, padx=5, pady=5) var = tk.StringVar(value=self.rank_to_score.get(rank, 0)) self.rank_income_vars[rank] = var tk.Entry(settings_window, textvariable=var).grid(row=rank-1, column=1, padx=5, pady=5) tk.Button(settings_window, text="确认", command=self.save_settings).grid(row=8, columnspan=2, pady=10) def save_settings(self): for rank, var in self.rank_income_vars.items(): try: score = int(var.get()) self.rank_to_score[rank] = score except ValueError: messagebox.showwarning("输入错误", f"名次 {rank} 的分数无效,请输入数字!") return for index, athlete in enumerate(self.scores): # 更新分数 if athlete.rank in self.rank_to_score: score = self.rank_to_score[athlete.rank] athlete.score = score # 更新分数 if index < len(self.tree.get_children()): # 确保索引有效 self.tree.item(self.tree.get_children()[index], values=(athlete.group, athlete.name, athlete.rank, score, athlete.is_record)) messagebox.showinfo("成功", "分数设置已更新!") def toggle_record(self): selected_item = self.tree.selection() if selected_item: item = selected_item[0] index = self.tree.index(item) athlete = self.scores[index] # 获取运动员信息 athlete.is_record = not athlete.is_record # 切换破纪录状态 # 更新分数 if athlete.is_record: athlete.score *= 2 else: athlete.score //= 2 # 更新树形控件 self.tree.item(item, values=(athlete.group, athlete.name, athlete.rank, athlete.score, "✔" if athlete.is_record else "✖")) def show_popup(self, event): try: selected = self.tree.identify_row(event.y) if selected: self.tree.selection_set(selected) self.popup_menu.post(event.x_root, event.y_root) except Exception as e: print(f"Error showing popup menu: {e}") def open_group_management(self): """ 打开分组管理窗口 """ group_window = tk.Toplevel(self.root) group_window.title("管理分组") tk.Label(group_window, text="自定义分组:").grid(row=0, column=0, padx=10, pady=5) custom_group_entry = tk.Entry(group_window) custom_group_entry.grid(row=0, column=1, padx=10, pady=5) tk.Button(group_window, text="添加分组", command=lambda: self.add_custom_group(custom_group_entry.get())).grid(row=0, column=2, padx=10, pady=5) tk.Label(group_window, text="当前分组:").grid(row=1, column=0, padx=10, pady=5) current_groups_var = StringVar(value=", ".join(sorted(self.available_groups))) tk.Entry(group_window, textvariable=current_groups_var, state='readonly').grid(row=1, column=1, padx=10, pady=5) tk.Label(group_window, text="删除分组:").grid(row=2, column=0, padx=10, pady=5) delete_group_entry = tk.Entry(group_window) delete_group_entry.grid(row=2, column=1, padx=10, pady=5) tk.Button(group_window, text="删除分组", command=lambda: self.delete_custom_group(delete_group_entry.get())).grid(row=2, column=2, padx=10, pady=5) def add_custom_group(self, group_name): """ 添加自定义分组 """ if group_name and group_name not in self.available_groups: self.available_groups.add(group_name) self.setup_group_selection() # 更新分组选择菜单 messagebox.showinfo("成功", f"已添加自定义分组: {group_name}") else: messagebox.showwarning("输入错误", "分组名称不能为空或已存在!") def delete_custom_group(self, group_name): """ 删除自定义分组 """ if group_name in self.available_groups: self.available_groups.remove(group_name) self.setup_group_selection() # 更新分组选择菜单 messagebox.showinfo("成功", f"已删除分组: {group_name}") else: messagebox.showwarning("输入错误", "该分组不存在!") def show_help(self): help_message = ( "本软件用于运动会计分管理...\n" "使用说明:\n" "1. 请依次输入运动员信息,选择分组或自定义分组。\n" "2. 点击“添加运动员”来记录。\n" "3. 点击“查看总分”可以查看所有运动员及各分组的总分。\n" "4. 右键点击运动员可以切换破纪录状态。\n" "5. 点击“导出数据”将运动员信息导出为CSV文件。" ) messagebox.showinfo("帮助", help_message)if __name__ == "__main__": root = tk.Tk() app = ScoreApp(root) root.mainloop()#这个示例代码用了tkinter库来做GUI,其中我采用的的是Tree组件使界面有一个类似于电子表格的管理方式#scv库的作用是可以让统计的数据导出#其中的tkinter库和scv库都是自带的#现在看一下运行后的成果#这里选用如果破纪录是翻倍分数
源码地址:https://download.csdn.net/download/HYP_Coder/90001597?spm=1001.2014.3001.5503
B站视频:嗯?别进来!_哔哩哔哩_bilibili