羽毛球比赛记分软件
功能:支持单打/双打比赛,自动计分,局数管理,换边,暂停等
软件采用Python Tkinter开发,界面包含比分显示、选手名称设置、发球权标识等模块。
特色功能包括:
遵循羽毛球21分制规则,支持20平后净胜2分获胜;提供3局2胜制比赛管理;支持全屏显示比赛结果;可导出比赛数据为CSV文件;提供快捷键操作(空格/回车键计分)
软件适用于正式比赛和日常训练,能有效提升记分效率和观赛体验。
# -*- coding: utf-8 -*-
"""
羽毛球比赛记分软件
功能:支持单打/双打比赛,自动计分,局数管理,换边,暂停等
"""
import tkinter as tk
from tkinter import ttk, messagebox, filedialog
import json
import os
import csv
from datetime import datetime
class BadmintonScoreApp:
def __init__(self, root):
self.root = root
self.root.title("🏸 羽毛球比赛记分器")
self.root.geometry("900x700")
self.root.minsize(800, 600)
# 比赛状态
self.match_type = tk.StringVar(value="singles") # singles/doubles
self.current_server = tk.StringVar(value="left") # left/right
self.left_score = tk.IntVar(value=0)
self.right_score = tk.IntVar(value=0)
self.left_games = tk.IntVar(value=0)
self.right_games = tk.IntVar(value=0)
self.is_paused = tk.BooleanVar(value=False)
self.is_match_over = tk.BooleanVar(value=False)
# 选手名称
self.left_player_name = tk.StringVar(value="左侧选手")
self.right_player_name = tk.StringVar(value="右侧选手")
# 自定义全屏标题
self.custom_title = tk.StringVar(value="🏸 羽毛球联谊赛 🏸")
self.target_games = tk.IntVar(value=2) # 3局2胜
self.pause_reason = tk.StringVar(value="")
# 比赛历史
self.match_history = []
self.setup_ui()
self.setup_styles()
# 绑定快捷键
self.root.bind('
self.root.bind('
self.root.bind('
', lambda e: self.toggle_pause())
self.root.bind('
', lambda e: self.toggle_pause())
self.root.bind('
self.root.bind('
# 确保根窗口获取焦点
self.root.bind('
def focus_root(self, event=None):
"""确保根窗口获取焦点"""
# 检查焦点是否在输入框或文本框等可编辑控件上,如果是则不阻止
focus_widget = self.root.focus_get()
if focus_widget is not None:
# 允许 Entry、Text、Combobox 等可编辑控件正常接收键盘事件
widget_class = focus_widget.winfo_class()
if widget_class in ['Entry', 'Text', 'Combobox']:
return None # 允许事件继续传播
self.root.focus_set()
return 'break' # 阻止事件继续传播
def handle_space_key(self, event=None):
"""处理空格键 - 左侧得分"""
if not self.is_paused.get() and not self.is_match_over.get():
self.add_point("left")
return 'break' # 阻止事件传播,防止按钮捕获
def handle_enter_key(self, event=None):
"""处理回车键 - 右侧得分"""
if not self.is_paused.get() and not self.is_match_over.get():
self.add_point("right")
return 'break' # 阻止事件传播,防止按钮捕获
def setup_styles(self):
"""设置界面样式"""
style = ttk.Style()
style.theme_use('clam')
# 按钮样式
style.configure('TButton', font=('Microsoft YaHei', 10))
style.configure('Large.TButton', font=('Microsoft YaHei', 14, 'bold'), padding=10)
style.configure('Danger.TButton', font=('Microsoft YaHei', 10), foreground='red')
# 标签样式
style.configure('Title.TLabel', font=('Microsoft YaHei', 24, 'bold'))
style.configure('Score.TLabel', font=('Microsoft YaHei', 72, 'bold'))
style.configure('Name.TLabel', font=('Microsoft YaHei', 14))
# 全屏窗口样式
style.configure('Dark.TFrame', background='#2C3E50')
def setup_ui(self):
"""设置用户界面"""
# 主框架
main_frame = ttk.Frame(self.root, padding="20")
main_frame.pack(fill=tk.BOTH, expand=True)
# 顶部标题和设置区域
top_frame = ttk.Frame(main_frame)
top_frame.pack(fill=tk.X, pady=(0, 20))
# 标题
title_label = ttk.Label(top_frame, text="🏸 羽毛球师生联谊赛@zhoujunyacs 🏸", style='Title.TLabel')
title_label.pack(side=tk.LEFT)
# 设置区域
settings_frame = ttk.Frame(top_frame)
settings_frame.pack(side=tk.RIGHT)
# 比赛类型选择
ttk.Label(settings_frame, text="比赛类型:").grid(row=0, column=0, padx=5)
ttk.Radiobutton(settings_frame, text="单打", variable=self.match_type,
value="singles", command=self.update_player_labels).grid(row=0, column=1, padx=5)
ttk.Radiobutton(settings_frame, text="双打", variable=self.match_type,
value="doubles", command=self.update_player_labels).grid(row=0, column=2, padx=5)
# 比赛项目选择
ttk.Label(settings_frame, text="比赛项目:").grid(row=0, column=3, padx=5)
self.match_project = tk.StringVar(value="第一男双")
project_combo = ttk.Combobox(settings_frame, textvariable=self.match_project,
values=["第一男双", "第二男双", "第三男双", "第一混双", "第二混双", "第三混双",
"女双", "女单", "男单"], width=10, state="readonly")
project_combo.grid(row=0, column=4, padx=5)
# 绑定比赛项目变更事件,实时更新全屏显示
project_combo.bind("<
# 局数设置
ttk.Label(settings_frame, text="目标局数:").grid(row=0, column=5, padx=5)
self.target_games = tk.IntVar(value=3) # 默认3局2胜
games_entry = ttk.Entry(settings_frame, textvariable=self.target_games, width=5)
games_entry.grid(row=0, column=6, padx=5)
ttk.Label(settings_frame, text="局").grid(row=0, column=7, padx=2)
# 自定义全屏标题(第二行)
title_frame = ttk.Frame(settings_frame)
title_frame.grid(row=1, column=0, columnspan=8, pady=(10, 0))
ttk.Label(title_frame, text="全屏标题:").pack(side=tk.LEFT)
tk.Entry(title_frame, textvariable=self.custom_title, width=30, font=('Microsoft YaHei', 10)).pack(side=tk.LEFT, padx=5)
ttk.Label(title_frame, text="(全屏时显示的自定义标题)", font=('Microsoft YaHei', 9),
foreground='gray').pack(side=tk.LEFT)
# 中间比赛区域
match_frame = ttk.Frame(main_frame)
match_frame.pack(fill=tk.BOTH, expand=True, pady=20)
# 左侧选手区域
left_frame = ttk.LabelFrame(match_frame, text="左侧", padding="10")
left_frame.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=(0, 10))
# 左侧选手名称输入
left_name_frame = ttk.Frame(left_frame)
left_name_frame.pack(fill=tk.X, pady=(0, 10))
ttk.Label(left_name_frame, text="选手名称:").pack(side=tk.LEFT)
tk.Entry(left_name_frame, textvariable=self.left_player_name, width=15, font=('Microsoft YaHei', 10)).pack(side=tk.LEFT, padx=5)
# 左侧局数
games_frame = ttk.Frame(left_frame)
games_frame.pack(fill=tk.X, pady=5)
ttk.Label(games_frame, text="已胜局数:", font=('Microsoft YaHei', 12)).pack(side=tk.LEFT)
# 左侧局数减按钮
tk.Button(games_frame, text="-", font=('Microsoft YaHei', 12, 'bold'),
width=2, command=lambda: self.modify_games("left", -1)).pack(side=tk.LEFT, padx=2)
left_games_label = ttk.Label(games_frame, textvariable=self.left_games, font=('Microsoft YaHei', 24, 'bold'),
foreground='green')
left_games_label.pack(side=tk.LEFT, padx=5)
# 左侧局数加按钮
tk.Button(games_frame, text="+", font=('Microsoft YaHei', 12, 'bold'),
width=2, command=lambda: self.modify_games("left", 1)).pack(side=tk.LEFT, padx=2)
# 左侧比分显示
self.left_score_label = ttk.Label(left_frame, textvariable=self.left_score,
style='Score.TLabel', foreground='blue')
self.left_score_label.pack(pady=10)
# 左侧发球标记
self.left_server_label = ttk.Label(left_frame, text="🏸 发球权", font=('Microsoft YaHei', 14),
foreground='orange' if self.current_server.get() == "left" else 'gray')
self.left_server_label.pack(pady=5)
# 左侧加分按钮
btn_frame = ttk.Frame(left_frame)
btn_frame.pack(fill=tk.X, pady=10)
add_btn = ttk.Button(btn_frame, text="+1 分 (空格)", command=lambda: self.add_point("left"),
style='Large.TButton')
add_btn.pack(side=tk.LEFT, expand=True, fill=tk.X, padx=2)
# 右侧选手区域
right_frame = ttk.LabelFrame(match_frame, text="右侧", padding="10")
right_frame.pack(side=tk.RIGHT, fill=tk.BOTH, expand=True, padx=(10, 0))
# 右侧选手名称输入
right_name_frame = ttk.Frame(right_frame)
right_name_frame.pack(fill=tk.X, pady=(0, 10))
ttk.Label(right_name_frame, text="选手名称:").pack(side=tk.LEFT)
tk.Entry(right_name_frame, textvariable=self.right_player_name, width=15, font=('Microsoft YaHei', 10)).pack(side=tk.LEFT, padx=5)
# 右侧局数
games_frame = ttk.Frame(right_frame)
games_frame.pack(fill=tk.X, pady=5)
ttk.Label(games_frame, text="已胜局数:", font=('Microsoft YaHei', 12)).pack(side=tk.LEFT)
# 右侧局数减按钮
tk.Button(games_frame, text="-", font=('Microsoft YaHei', 12, 'bold'),
width=2, command=lambda: self.modify_games("right", -1)).pack(side=tk.LEFT, padx=2)
right_games_label = ttk.Label(games_frame, textvariable=self.right_games, font=('Microsoft YaHei', 24, 'bold'),
foreground='green')
right_games_label.pack(side=tk.LEFT, padx=5)
# 右侧局数加按钮
tk.Button(games_frame, text="+", font=('Microsoft YaHei', 12, 'bold'),
width=2, command=lambda: self.modify_games("right", 1)).pack(side=tk.LEFT, padx=2)
# 右侧比分显示
self.right_score_label = ttk.Label(right_frame, textvariable=self.right_score,
style='Score.TLabel', foreground='red')
self.right_score_label.pack(pady=10)
# 右侧发球标记
self.right_server_label = ttk.Label(right_frame, text="🏸 发球权", font=('Microsoft YaHei', 14),
foreground='orange' if self.current_server.get() == "right" else 'gray')
self.right_server_label.pack(pady=5)
# 右侧加分按钮
btn_frame = ttk.Frame(right_frame)
btn_frame.pack(fill=tk.X, pady=10)
add_btn = ttk.Button(btn_frame, text="+1 分 (回车)", command=lambda: self.add_point("right"),
style='Large.TButton')
add_btn.pack(side=tk.LEFT, expand=True, fill=tk.X, padx=2)
# 底部控制区域
control_frame = ttk.Frame(main_frame)
control_frame.pack(fill=tk.X, pady=15)
# 左边按钮组
left_buttons = ttk.Frame(control_frame)
left_buttons.pack(side=tk.LEFT, padx=5)
# 创建按钮时设置 takefocus=0 防止捕获键盘焦点
change_sides_btn = ttk.Button(left_buttons, text="🔄 换边", command=self.change_sides,
width=10, takefocus=0)
change_sides_btn.pack(side=tk.LEFT, padx=2)
ttk.Button(left_buttons, text="⏸ 暂停 (P)", command=self.toggle_pause,
width=12, takefocus=0).pack(side=tk.LEFT, padx=2)
# 暂停显示
self.pause_label = ttk.Label(control_frame, text="", font=('Microsoft YaHei', 12, 'bold'),
foreground='red')
self.pause_label.pack(side=tk.LEFT, padx=10)
# 中间按钮组 - 使用pack的expand和fill使按钮随窗口大小调整
center_buttons = ttk.Frame(control_frame)
center_buttons.pack(side=tk.LEFT, expand=True, fill=tk.X, padx=10)
# 全屏结果显示按钮
self.fullscreen_btn = ttk.Button(center_buttons, text="📺 全屏显示结果",
command=self.show_fullscreen_result,
width=15, takefocus=0)
self.fullscreen_btn.pack(side=tk.LEFT, expand=True, fill=tk.X, padx=2)
# 导出按钮
export_btn = ttk.Button(center_buttons, text="📊 导出结果",
command=self.export_match_data,
width=12, takefocus=0)
export_btn.pack(side=tk.LEFT, expand=True, fill=tk.X, padx=2)
# 右边按钮组
right_buttons = ttk.Frame(control_frame)
right_buttons.pack(side=tk.RIGHT, padx=5)
ttk.Button(right_buttons, text="↩ 撤销", command=self.undo_point,
width=8, takefocus=0).pack(side=tk.RIGHT, padx=2)
ttk.Button(right_buttons, text="🔁 重置 (R)", command=self.reset_match,
width=10, takefocus=0).pack(side=tk.RIGHT, padx=2)
# 状态栏
status_frame = ttk.Frame(main_frame)
status_frame.pack(fill=tk.X, pady=(10, 0))
self.status_label = ttk.Label(status_frame, text="准备开始比赛", font=('Microsoft YaHei', 10))
self.status_label.pack(side=tk.LEFT)
# 快捷键提示
hint_label = ttk.Label(status_frame, text="快捷键: 空格=左侧得分 | 回车=右侧得分 | P=暂停 | R=重置",
font=('Microsoft YaHei', 9), foreground='gray')
hint_label.pack(side=tk.RIGHT)
# 绑定窗口关闭事件
self.root.protocol("WM_DELETE_WINDOW", self.on_close)
def update_player_labels(self):
"""更新选手标签"""
if self.match_type.get() == "singles":
self.left_player_name.set("单打选手A")
self.right_player_name.set("单打选手B")
else:
self.left_player_name.set("双打组合A")
self.right_player_name.set("双打组合B")
def modify_games(self, side, delta):
"""手动修改局数"""
if side == "left":
new_value = max(0, self.left_games.get() + delta)
self.left_games.set(new_value)
else:
new_value = max(0, self.right_games.get() + delta)
self.right_games.set(new_value)
self.status_label.config(text=f"已调整局数: 左侧 {self.left_games.get()} - 右侧 {self.right_games.get()}")
def on_project_changed(self, event=None):
"""比赛项目变更时重置比分和局数"""
# 重置当前局比分
self.left_score.set(0)
self.right_score.set(0)
self.current_server.set("left")
self.update_server_display()
# 重置局数计数
self.left_games.set(0)
self.right_games.set(0)
# 重置暂停和比赛结束状态
self.is_paused.set(False)
self.is_match_over.set(False)
self.pause_label.config(text="")
# 提示用户
self.status_label.config(text=f"比赛项目已变更为: {self.match_project.get()},已重置比分和局数")
def add_point(self, side):
"""加分"""
if self.is_paused.get() or self.is_match_over.get():
return
# 记录用于撤销
self.last_action = {
'side': side,
'left_score': self.left_score.get(),
'right_score': self.right_score.get(),
'server': self.current_server.get()
}
if side == "left":
new_score = self.left_score.get() + 1
self.left_score.set(new_score)
# 羽毛球规则:得分方获得发球权
self.current_server.set("left")
self.status_label.config(text=f"左侧选手得分!当前比分: {self.left_score.get()}-{self.right_score.get()}")
else:
new_score = self.right_score.get() + 1
self.right_score.set(new_score)
# 羽毛球规则:得分方获得发球权
self.current_server.set("right")
self.status_label.config(text=f"右侧选手得分!当前比分: {self.left_score.get()}-{self.right_score.get()}")
self.update_server_display()
self.check_game_winner()
self.check_match_winner()
def update_server_display(self):
"""更新发球权显示"""
if self.current_server.get() == "left":
self.left_server_label.config(foreground='orange')
self.right_server_label.config(foreground='gray')
else:
self.left_server_label.config(foreground='gray')
self.right_server_label.config(foreground='orange')
def check_game_winner(self):
"""检查单局获胜者"""
left = self.left_score.get()
right = self.right_score.get()
winner = None
# 羽毛球规则:先得21分者获胜,但如果比分到20平,则需要净胜2分才获胜
if left >= 21 or right >= 21:
if left >= 20 and right >= 20:
# 20平后,任何一方净胜2分即获胜(无上限)
if left - right >= 2:
winner = "left"
elif right - left >= 2:
winner = "right"
else:
# 未到20平,正常21分获胜
if left >= 21 and left - right >= 1:
winner = "left"
elif right >= 21 and right - left >= 1:
winner = "right"
if winner:
self.root.bell()
if winner == "left":
self.left_games.set(self.left_games.get() + 1)
winner_name = self.left_player_name.get()
else:
self.right_games.set(self.right_games.get() + 1)
winner_name = self.right_player_name.get()
messagebox.showinfo("🎉 本局结束", f"{winner_name} 获胜!\n比分: {left}-{right}")
# 记录到历史
self.match_history.append({
'time': datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
'left': self.left_player_name.get(),
'right': self.right_player_name.get(),
'score': f"{left}-{right}",
'winner': winner_name
})
# 重置比分
self.left_score.set(0)
self.right_score.set(0)
self.current_server.set("left")
self.update_server_display()
self.status_label.config(text=f"局数: {self.left_games.get()}-{self.right_games.get()}")
def check_match_winner(self):
"""检查整场比赛获胜者"""
target = self.target_games.get()
if self.left_games.get() >= target:
self.is_match_over.set(True)
self.root.bell()
winner = self.left_player_name.get()
messagebox.showinfo("🏆 比赛结束", f"恭喜 {winner} 赢得比赛!")
self.status_label.config(text=f"比赛结束!{winner} 获胜")
# 自动保存比赛结果到txt文件
self.save_match_result(winner)
elif self.right_games.get() >= target:
self.is_match_over.set(True)
self.root.bell()
winner = self.right_player_name.get()
messagebox.showinfo("🏆 比赛结束", f"恭喜 {winner} 赢得比赛!")
self.status_label.config(text=f"比赛结束!{winner} 获胜")
# 自动保存比赛结果到txt文件
self.save_match_result(winner)
def save_match_result(self, winner):
"""保存比赛结果到txt文件"""
try:
# 获取当前时间戳作为文件名
timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
file_name = f"比赛结果_{self.match_project.get()}_{timestamp}.txt"
file_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), file_name)
with open(file_path, 'w', encoding='utf-8') as f:
f.write("=" * 50 + "\n")
f.write("🏸 羽毛球比赛结果\n")
f.write("=" * 50 + "\n\n")
f.write(f"比赛项目: {self.match_project.get()}\n")
f.write(f"比赛时间: {datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n\n")
f.write("-" * 50 + "\n")
f.write("对阵信息\n")
f.write("-" * 50 + "\n")
f.write(f"左侧选手: {self.left_player_name.get()}\n")
f.write(f"右侧选手: {self.right_player_name.get()}\n\n")
f.write("-" * 50 + "\n")
f.write("比赛结果\n")
f.write("-" * 50 + "\n")
f.write(f"左侧最终比分: {self.left_score.get()}\n")
f.write(f"右侧最终比分: {self.right_score.get()}\n")
f.write(f"左侧获胜局数: {self.left_games.get()}\n")
f.write(f"右侧获胜局数: {self.right_games.get()}\n")
f.write(f"总局数: {self.target_games.get()}\n\n")
f.write("=" * 50 + "\n")
f.write(f"🏆 获胜者: {winner}\n")
f.write("=" * 50 + "\n\n")
f.write("自动生成 by 羽毛球记分器\n")
self.status_label.config(text=f"比赛结果已保存到: {file_name}")
except Exception as e:
self.status_label.config(text=f"保存结果失败: {str(e)}")
def undo_point(self):
"""撤销上一步操作"""
if hasattr(self, 'last_action') and self.last_action:
action = self.last_action
# 恢复分数
self.left_score.set(action['left_score'])
self.right_score.set(action['right_score'])
# 恢复发球权
if action['side'] == "left":
self.current_server.set("right" if self.current_server.get() == "left" else "left")
else:
self.current_server.set("left" if self.current_server.get() == "right" else "right")
self.update_server_display()
self.status_label.config(text="已撤销上一步操作")
self.last_action = None
def toggle_pause(self):
"""暂停/继续"""
self.is_paused.set(not self.is_paused.get())
if self.is_paused.get():
self.pause_label.config(text="⏸ 已暂停")
self.status_label.config(text="比赛暂停中...")
else:
self.pause_label.config(text="")
self.status_label.config(text="比赛继续")
def change_sides(self):
"""换边"""
# 交换选手名称
left_name = self.left_player_name.get()
right_name = self.right_player_name.get()
self.left_player_name.set(right_name)
self.right_player_name.set(left_name)
# 交换比分
left_score = self.left_score.get()
right_score = self.right_score.get()
self.left_score.set(right_score)
self.right_score.set(left_score)
# 交换局数
left_games = self.left_games.get()
right_games = self.right_games.get()
self.left_games.set(right_games)
self.right_games.set(left_games)
# 切换发球权
self.current_server.set("right" if self.current_server.get() == "left" else "left")
self.update_server_display()
self.status_label.config(text="已换边")
def reset_match(self):
"""重置比赛"""
if messagebox.askyesno("确认", "确定要重置比赛吗?所有比分和局数将被清除!"):
self.left_score.set(0)
self.right_score.set(0)
self.left_games.set(0)
self.right_games.set(0)
self.current_server.set("left")
self.is_paused.set(False)
self.is_match_over.set(False)
self.pause_label.config(text="")
self.last_action = None
self.update_server_display()
self.status_label.config(text="比赛已重置,准备开始新比赛")
def on_close(self):
"""窗口关闭事件"""
if messagebox.askyesno("退出", "确定要退出记分器吗?"):
# 保存比赛历史
if self.match_history:
try:
history_file = os.path.join(os.path.dirname(os.path.abspath(__file__)),
"match_history.json")
with open(history_file, 'w', encoding='utf-8') as f:
json.dump(self.match_history, f, ensure_ascii=False, indent=2)
except:
pass
self.root.destroy()
def export_match_data(self):
"""导出比赛数据"""
try:
# 判断当前比赛的获胜者
winner = None
if self.left_games.get() > self.right_games.get():
winner = self.left_player_name.get()
elif self.right_games.get() > self.left_games.get():
winner = self.right_player_name.get()
else:
winner = "平局"
# 弹出保存对话框
file_path = filedialog.asksaveasfilename(
defaultextension=".csv",
filetypes=[("CSV文件", "*.csv"), ("所有文件", "*.*")],
initialfile=f"比赛结果_{self.match_project.get()}_{datetime.now().strftime('%Y%m%d_%H%M%S')}.csv",
title="保存比赛结果"
)
if file_path:
with open(file_path, 'w', encoding='utf-8-sig') as f:
# 写入CSV头部
f.write("比赛项目,比赛时间,左侧选手,右侧选手,左侧得分,右侧得分,左侧局数,右侧局数,总局数,获胜者\n")
# 写入比赛数据
f.write(f"{self.match_project.get()},{datetime.now().strftime('%Y-%m-%d %H:%M:%S')},")
f.write(f"{self.left_player_name.get()},{self.right_player_name.get()},")
f.write(f"{self.left_score.get()},{self.right_score.get()},")
f.write(f"{self.left_games.get()},{self.right_games.get()},")
f.write(f"{self.target_games.get()},{winner}\n")
# 写入详细记录
f.write("\n\n比赛记录\n")
f.write("时间,左侧选手,右侧选手,比分,获胜者\n")
for record in self.match_history:
f.write(f"{record['time']},{record['left']},{record['right']},{record['score']},{record['winner']}\n")
messagebox.showinfo("导出成功", f"比赛结果已成功导出到:\n{file_path}")
self.status_label.config(text=f"已导出比赛结果到: {os.path.basename(file_path)}")
except Exception as e:
messagebox.showerror("导出失败", f"导出比赛结果时出错:\n{str(e)}")
self.status_label.config(text=f"导出失败: {str(e)}")
def show_fullscreen_result(self):
"""全屏显示比赛结果(自适应所有屏幕尺寸)"""
# 如果已经存在全屏窗口,先关闭
if hasattr(self, 'fullscreen_window') and self.fullscreen_window and self.fullscreen_window.winfo_exists():
self.fullscreen_window.destroy()
# 创建全屏窗口(作为独立窗口,不阻塞主窗口)
self.fullscreen_window = tk.Toplevel(self.root)
self.fullscreen_window.title("🏆 比赛结果")
self.fullscreen_window.attributes('-fullscreen', True)
# 获取屏幕尺寸
screen_width = self.root.winfo_screenwidth()
screen_height = self.root.winfo_screenheight()
# 根据屏幕尺寸动态计算字体大小和间距
# 基础缩放比例(以1920x1080为基准)
scale_x = screen_width / 1920
scale_y = screen_height / 1080
scale = min(scale_x, scale_y) # 保持宽高比
# 动态计算字体大小(确保在不同分辨率下都清晰可见)
title_font_size = max(int(48 * scale), 24) # 标题字体
project_font_size = max(int(24 * scale), 14) # 项目名称字体
rules_font_size = max(int(14 * scale), 10) # 规则提示字体
player_font_size = max(int(36 * scale), 20) # 选手名字字体
vs_font_size = max(int(32 * scale), 18) # VS字体
score_font_size = max(int(120 * scale), 60) # 比分字体
games_font_size = max(int(24 * scale), 14) # 局数字体
server_font_size = max(int(20 * scale), 12) # 发球权字体
hint_font_size = max(int(14 * scale), 10) # 快捷键提示字体
# 动态计算间距
padding_x = max(int(50 * scale), 20)
padding_y = max(int(30 * scale), 15)
title_padding_y = max(int(20 * scale), 10)
section_spacing = max(int(30 * scale), 15)
# 背景颜色
self.fullscreen_window.configure(background='#2C3E50')
# 主框架
main_frame = ttk.Frame(self.fullscreen_window, style='Dark.TFrame')
main_frame.pack(fill=tk.BOTH, expand=True, padx=padding_x, pady=padding_y)
# 最上面添加:自定义全屏标题
event_title = tk.Label(main_frame, text=self.custom_title.get(),
font=('Microsoft YaHei', title_font_size, 'bold'),
background='#2C3E50', foreground='#F1C40F')
event_title.pack(pady=(title_padding_y, section_spacing))
# 保存引用以便实时更新
self._event_title_label = event_title
# 绑定标题变更事件
self.custom_title.trace_add('write', lambda *args: event_title.config(text=self.custom_title.get()))
# 比赛项目名称(保存引用以便实时更新)
self._project_label = tk.Label(main_frame, text=f"📋 {self.match_project.get()}",
font=('Microsoft YaHei', project_font_size),
background='#2C3E50', foreground='#BDC3C7')
self._project_label.pack(pady=(0, section_spacing))
# 比赛规则提示
rules_label = tk.Label(main_frame,
text="📖 比赛规则: 3局2胜制 | 每局21分 | 20平后净胜2分获胜",
font=('Microsoft YaHei', rules_font_size),
background='#2C3E50', foreground='#95A5A6')
rules_label.pack(pady=(0, section_spacing))
# 选手信息框架
players_frame = ttk.Frame(main_frame, style='Dark.TFrame')
players_frame.pack(fill=tk.X, pady=section_spacing)
# 左侧选手(保存引用)
self._left_player_label = tk.Label(players_frame, text=self.left_player_name.get(),
font=('Microsoft YaHei', player_font_size, 'bold'),
background='#2C3E50', foreground='#3498DB')
self._left_player_label.pack(side=tk.LEFT, expand=True)
# VS
vs_label = tk.Label(players_frame, text="VS",
font=('Microsoft YaHei', vs_font_size, 'bold'),
background='#2C3E50', foreground='#E74C3C')
vs_label.pack(side=tk.LEFT, padx=max(int(40 * scale), 15))
# 右侧选手(保存引用)
self._right_player_label = tk.Label(players_frame, text=self.right_player_name.get(),
font=('Microsoft YaHei', player_font_size, 'bold'),
background='#2C3E50', foreground='#E74C3C')
self._right_player_label.pack(side=tk.LEFT, expand=True)
# 比分框架
score_frame = ttk.Frame(main_frame, style='Dark.TFrame')
score_frame.pack(fill=tk.X, pady=section_spacing)
# 左侧比分(保存引用)
self._left_score_label = tk.Label(score_frame, text=str(self.left_score.get()),
font=('Microsoft YaHei', score_font_size, 'bold'),
background='#2C3E50', foreground='#3498DB')
self._left_score_label.pack(side=tk.LEFT, expand=True)
# 比分分隔符
score_sep = tk.Label(score_frame, text="-",
font=('Microsoft YaHei', int(score_font_size * 0.8), 'bold'),
background='#2C3E50', foreground='#FFFFFF')
score_sep.pack(side=tk.LEFT, padx=max(int(30 * scale), 10))
# 右侧比分(保存引用)
self._right_score_label = tk.Label(score_frame, text=str(self.right_score.get()),
font=('Microsoft YaHei', score_font_size, 'bold'),
background='#2C3E50', foreground='#E74C3C')
self._right_score_label.pack(side=tk.LEFT, expand=True)
# 局数信息(保存引用)
self._games_label = tk.Label(main_frame,
text=f"局数: {self.left_games.get()} - {self.right_games.get()} (共{self.target_games.get()}局)",
font=('Microsoft YaHei', games_font_size),
background='#2C3E50', foreground='#BDC3C7')
self._games_label.pack(pady=section_spacing)
# 发球权提示(保存引用)
if self.current_server.get() == "left":
self._server_label = tk.Label(main_frame,
text=f"🔵 {self.left_player_name.get()} 发球",
font=('Microsoft YaHei', server_font_size),
background='#2C3E50', foreground='#3498DB')
else:
self._server_label = tk.Label(main_frame,
text=f"🔴 {self.right_player_name.get()} 发球",
font=('Microsoft YaHei', server_font_size),
background='#2C3E50', foreground='#E74C3C')
self._server_label.pack(pady=section_spacing)
# 快捷键提示(底部显示)
hint_frame = ttk.Frame(main_frame, style='Dark.TFrame')
hint_frame.pack(side=tk.BOTTOM, pady=(max(int(30 * scale), 15), max(int(20 * scale), 10)))
tk.Label(hint_frame, text="空格=左侧得分 | 回车=右侧得分 | R=重置 | ESC=退出全屏",
font=('Microsoft YaHei', hint_font_size),
background='#2C3E50', foreground='#7F8C8D').pack()
# 退出全屏函数
def exit_fullscreen(event=None):
self.fullscreen_window.destroy()
self.fullscreen_window = None
# 绑定快捷键
self.fullscreen_window.bind('
self.fullscreen_window.bind('
self.fullscreen_window.bind('
self.fullscreen_window.bind('r', lambda e: self.reset_match())
self.fullscreen_window.bind('R', lambda e: self.reset_match())
# 启动实时更新(200毫秒间隔)
self.update_fullscreen()
# 确保全屏窗口获取焦点
self.fullscreen_window.focus_set()
def update_fullscreen(self):
"""实时更新全屏显示"""
# 检查全屏窗口是否还存在
if not hasattr(self, 'fullscreen_window') or not self.fullscreen_window or not self.fullscreen_window.winfo_exists():
return
try:
# 更新比赛项目
if hasattr(self, '_project_label') and self._project_label.winfo_exists():
self._project_label.config(text=f"📋 {self.match_project.get()}")
# 更新左侧选手名称
if hasattr(self, '_left_player_label') and self._left_player_label.winfo_exists():
self._left_player_label.config(text=self.left_player_name.get())
# 更新右侧选手名称
if hasattr(self, '_right_player_label') and self._right_player_label.winfo_exists():
self._right_player_label.config(text=self.right_player_name.get())
# 更新比分
if hasattr(self, '_left_score_label') and self._left_score_label.winfo_exists():
self._left_score_label.config(text=str(self.left_score.get()))
if hasattr(self, '_right_score_label') and self._right_score_label.winfo_exists():
self._right_score_label.config(text=str(self.right_score.get()))
# 更新局数
if hasattr(self, '_games_label') and self._games_label.winfo_exists():
self._games_label.config(text=f"局数: {self.left_games.get()} - {self.right_games.get()} (共{self.target_games.get()}局)")
# 更新发球权
if hasattr(self, '_server_label') and self._server_label.winfo_exists():
if self.current_server.get() == "left":
self._server_label.config(text=f"� {self.left_player_name.get()} 发球")
self._server_label.config(foreground='#3498DB')
else:
self._server_label.config(text=f"🔴 {self.right_player_name.get()} 发球")
self._server_label.config(foreground='#E74C3C')
# 继续定时更新(200毫秒)
self.fullscreen_window.after(200, self.update_fullscreen)
except Exception as e:
# 如果出错,停止更新
print(f"更新全屏显示时出错: {e}")
def main():
root = tk.Tk()
app = BadmintonScoreApp(root)
root.mainloop()
if __name__ == "__main__":
main()