Python x Kivy でバスケットボールのスコア記録アプリを作る

PythonとKivyを使ったアプリ開発のテストとしてバスケットボールのスコア記録アプリを作成してみました。(が、試しに作成しただけのものですので、実際には使えませんので、今後、ちゃんと使えるものを作ります。

アプリの概要

このアプリは以下の機能を提供します:

  • クォーターの選択と追加
  • チームおよび選手の選択
  • 得点の追加
  • スコアのリアルタイム表示
  • 直近のスコアの表示
  • スコアのリセット

画面イメージ

使用技術

  • プログラミング言語: Python
  • フレームワーク: Kivy

コード

from kivy.app import App
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.label import Label
from kivy.uix.button import Button
from kivy.uix.togglebutton import ToggleButton
from kivy.uix.spinner import Spinner
from kivy.uix.scrollview import ScrollView
from kivy.uix.gridlayout import GridLayout
from kivy.uix.popup import Popup
from kivy.uix.screenmanager import ScreenManager, Screen
from kivy.uix.actionbar import ActionBar, ActionView, ActionPrevious, ActionButton, ActionGroup
from datetime import datetime

class HighlightableButton(Button):
    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        self.selected = False
        self.height = 50

    def on_touch_down(self, touch):
        if super().on_touch_down(touch):
            self.selected = not self.selected
            if self.selected:
                self.background_color = (1, 0, 0, 1)  # 赤色に変更
            else:
                self.background_color = (1, 1, 1, 1)  # 元の色に戻す
            return True
        return False

class MainScreen(Screen):
    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        self.scores = []  # 各得点を記録するリスト
        self.current_period = '1Q'
        self.current_id = 1
        self.selected_player = None
        self.selected_score_id = None  # 修正対象のスコアID
        self.ot_count = 0  # 延長戦の数を初期化

        self.layout = BoxLayout(orientation='vertical')

        # アクションバーを追加
        action_bar = ActionBar(pos_hint={'top': 1})
        action_view = ActionView()
        action_previous = ActionPrevious(title="Basketball Score App", with_previous=False)
        action_view.add_widget(action_previous)

        action_group = ActionGroup(text='Menu')
        reset_button = ActionButton(text='Reset')
        reset_button.bind(on_release=self.confirm_reset)
        view_scores_button = ActionButton(text='View Scores')
        view_scores_button.bind(on_release=self.switch_to_score_screen)
        action_group.add_widget(view_scores_button)
        action_group.add_widget(reset_button)

        action_view.add_widget(action_group)
        action_bar.add_widget(action_view)
        self.layout.add_widget(action_bar)

        # クォーター選択とAdd OTボタンを含むレイアウト
        self.period_layout = BoxLayout(size_hint_y=None, height=50)
        self.period_selector = Spinner(
            text='1Q',
            values=['1Q', '2Q', '3Q', '4Q'],
            size_hint=(None, None),
            size=(100, 44),
        )
        self.period_selector.bind(text=self.set_period)
        self.period_layout.add_widget(self.period_selector)

        self.add_ot_button = Button(text='Add OT', on_press=self.add_ot_period, size_hint=(None, None), size=(100, 44), height=50)
        self.period_layout.add_widget(self.add_ot_button)

        self.layout.add_widget(self.period_layout)

        self.current_period_label = Label(text=f'Current Period: {self.current_period}', font_size='20sp')
        self.layout.add_widget(self.current_period_label)

        self.team_selector = BoxLayout(size_hint_y=None, height=50)
        self.team_a_button = ToggleButton(text='Team A', group='team', state='down', size_hint_y=None, height=50)
        self.team_b_button = ToggleButton(text='Team B', group='team', size_hint_y=None, height=50)
        self.team_selector.add_widget(self.team_a_button)
        self.team_selector.add_widget(self.team_b_button)
        self.layout.add_widget(self.team_selector)

        self.player_selector = BoxLayout(size_hint_y=0.2)
        for i in range(4, 16):
            btn = ToggleButton(text=str(i), group='player', on_press=self.select_player, size_hint_y=None, height=50)
            self.player_selector.add_widget(btn)
        self.layout.add_widget(self.player_selector)

        self.point_buttons = BoxLayout(size_hint_y=0.2)
        self.point_buttons.add_widget(Button(text='1 Point', on_press=lambda instance: self.add_points(1), size_hint_y=None, height=50))
        self.point_buttons.add_widget(Button(text='2 Points', on_press=lambda instance: self.add_points(2), size_hint_y=None, height=50))
        self.point_buttons.add_widget(Button(text='3 Points', on_press=lambda instance: self.add_points(3), size_hint_y=None, height=50))
        self.layout.add_widget(self.point_buttons)

        self.score_label = Label(text=self.get_score_text(), font_size='20sp')
        self.layout.add_widget(self.score_label)

        # 直近のスコア表示エリア
        self.recent_scores_label = Label(text='Recent Scores:', font_size='20sp')
        self.layout.add_widget(self.recent_scores_label)

        self.recent_scroll_view = ScrollView(size_hint=(1, 0.4))
        self.recent_score_list = GridLayout(cols=1, size_hint_y=None)
        self.recent_score_list.bind(minimum_height=self.recent_score_list.setter('height'))
        self.recent_scroll_view.add_widget(self.recent_score_list)
        self.layout.add_widget(self.recent_scroll_view)

        self.add_widget(self.layout)

    def get_score_text(self):
        summary = {'1Q': {'A': 0, 'B': 0}, '2Q': {'A': 0, 'B': 0}, '3Q': {'A': 0, 'B': 0}, '4Q': {'A': 0, 'B': 0}}
        for score in self.scores:
            if score['period'] not in summary:
                summary[score['period']] = {'A': 0, 'B': 0}
            summary[score['period']][score['team']] += score['points']

        score_text = ''
        for period, scores in summary.items():
            score_text += f'{period}: Team A: {scores["A"]} - Team B: {scores["B"]}\n'
        return score_text.strip()

    def set_period(self, spinner, text):
        self.current_period = text
        self.current_period_label.text = f'Current Period: {self.current_period}'

    def select_player(self, instance):
        self.selected_player = instance.text

    def add_points(self, points):
        if not self.selected_player:
            return  # 選手が選択されていない場合は何もしない

        team = 'A' if self.team_a_button.state == 'down' else 'B'
        score_entry = {
            'id': self.current_id,
            'team': team,
            'period': self.current_period,
            'points': points,
            'time': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
            'player': self.selected_player
        }
        self.scores.append(score_entry)
        self.current_id += 1
        self.update_score()
        self.update_recent_scores()

    def update_score(self):
        self.score_label.text = self.get_score_text()

    def update_recent_scores(self):
        self.recent_score_list.clear_widgets()
        recent_scores = self.scores[-5:]  # 直近5件のスコアを取得
        for score in recent_scores:
            score_text = f'ID: {score["id"]}, Team: {score["team"]}, Period: {score["period"]}, Points: {score["points"]}, Player: {score["player"]}, Time: {score["time"]}'
            label = Label(text=score_text, size_hint_y=None, height=40)
            self.recent_score_list.add_widget(label)

    def confirm_reset(self, instance):
        content = BoxLayout(orientation='vertical')
        message = Label(text='Are you sure you want to reset all scores?', size_hint_y=0.8)
        button_layout = BoxLayout(size_hint_y=0.2)
        yes_button = Button(text

='Yes', on_press=self.reset_scores, size_hint_y=None, height=50)
        no_button = Button(text='No', on_press=lambda x: self.reset_popup.dismiss(), size_hint_y=None, height=50)
        button_layout.add_widget(yes_button)
        button_layout.add_widget(no_button)
        content.add_widget(message)
        content.add_widget(button_layout)
        self.reset_popup = Popup(title='Confirm Reset', content=content, size_hint=(0.6, 0.4))
        self.reset_popup.open()

    def reset_scores(self, instance):
        self.reset_popup.dismiss()
        self.scores = []
        self.current_id = 1
        self.selected_player = None
        self.selected_score_id = None
        self.ot_count = 0  # 延長戦の数をリセット
        self.period_selector.values = ['1Q', '2Q', '3Q', '4Q']
        self.period_selector.text = '1Q'
        self.set_period(self.period_selector, '1Q')
        self.update_score()
        self.update_recent_scores()

    def add_ot_period(self, instance):
        self.ot_count += 1
        ot_period = f'OT{self.ot_count}'
        self.period_selector.values = list(self.period_selector.values) + [ot_period]
        self.period_selector.text = ot_period
        self.set_period(self.period_selector, ot_period)

    def switch_to_score_screen(self, instance):
        self.manager.current = 'score_screen'
        self.manager.get_screen('score_screen').update_score_list(self.scores, self.ot_count)

class ScoreScreen(Screen):
    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        self.layout = BoxLayout(orientation='vertical')

        # クォーター選択用のスピナーを追加
        self.period_selector = Spinner(
            text='All',
            values=['All', '1Q', '2Q', '3Q', '4Q'],
            size_hint=(None, None),
            size=(100, 44)
        )
        self.period_selector.bind(text=self.filter_scores)
        self.layout.add_widget(self.period_selector)

        self.scroll_view = ScrollView(size_hint=(1, 0.8))
        self.score_list = GridLayout(cols=1, size_hint_y=None)
        self.score_list.bind(minimum_height=self.score_list.setter('height'))
        self.scroll_view.add_widget(self.score_list)
        self.layout.add_widget(self.scroll_view)

        self.edit_button = Button(text='Edit Selected', on_press=self.edit_selected_score, size_hint_y=None, height=50)
        self.layout.add_widget(self.edit_button)

        self.back_button = Button(text='Back', on_press=self.switch_to_main_screen, size_hint_y=None, height=50)
        self.layout.add_widget(self.back_button)

        self.selected_score_id = None

        self.add_widget(self.layout)

    def update_score_list(self, scores, ot_count):
        self.scores = scores
        self.ot_count = ot_count
        self.update_period_selector()
        self.filter_scores(self.period_selector, self.period_selector.text)

    def update_period_selector(self):
        self.period_selector.values = ['All', '1Q', '2Q', '3Q', '4Q'] + [f'OT{i}' for i in range(1, self.ot_count + 1)]

    def filter_scores(self, spinner, text):
        self.score_list.clear_widgets()
        filtered_scores = [score for score in self.scores if text == 'All' or score['period'] == text]
        for score in filtered_scores:
            score_text = f'ID: {score["id"]}, Team: {score["team"]}, Period: {score["period"]}, Points: {score["points"]}, Player: {score["player"]}, Time: {score["time"]}'
            btn = HighlightableButton(text=score_text, size_hint_y=None, height=50)
            btn.bind(on_press=lambda instance, score_id=score['id']: self.select_score(score_id, instance))
            self.score_list.add_widget(btn)

    def select_score(self, score_id, instance):
        self.selected_score_id = score_id
        for btn in self.score_list.children:
            if btn is instance:
                btn.background_color = (1, 0, 0, 1)  # 赤色に変更
            else:
                btn.background_color = (1, 1, 1, 1)  # 元の色に戻す

    def edit_selected_score(self, instance):
        if self.selected_score_id is None:
            return  # 修正対象のスコアが選択されていない場合は何もしない

        score_to_edit = next((score for score in self.scores if score['id'] == self.selected_score_id), None)
        if score_to_edit:
            content = BoxLayout(orientation='vertical')
            self.edit_period = Spinner(text=score_to_edit['period'], values=['1Q', '2Q', '3Q', '4Q'] + [f'OT{i}' for i in range(1, self.ot_count + 1)], size_hint=(None, None), size=(100, 44))
            self.edit_team_a_button = ToggleButton(text='Team A', group='edit_team', state='down' if score_to_edit['team'] == 'A' else 'normal', size_hint_y=None, height=50)
            self.edit_team_b_button = ToggleButton(text='Team B', group='edit_team', state='down' if score_to_edit['team'] == 'B' else 'normal', size_hint_y=None, height=50)
            self.edit_points = Spinner(text=str(score_to_edit['points']), values=['1', '2', '3'], size_hint=(None, None), size=(100, 44))
            self.edit_player_selector = BoxLayout(size_hint_y=0.2)
            self.edit_player_buttons = []
            for i in range(4, 16):
                btn = ToggleButton(text=str(i), group='edit_player', state='down' if score_to_edit['player'] == str(i) else 'normal', size_hint_y=None, height=50)
                self.edit_player_buttons.append(btn)
                self.edit_player_selector.add_widget(btn)
            save_button = Button(text='Save', on_press=self.confirm_save, size_hint_y=None, height=50)
            content.add_widget(self.edit_period)
            content.add_widget(self.edit_team_a_button)
            content.add_widget(self.edit_team_b_button)
            content.add_widget(self.edit_points)
            content.add_widget(self.edit_player_selector)
            content.add_widget(save_button)
            self.edit_popup = Popup(title='Edit Score', content=content, size_hint=(0.8, 0.8))
            self.edit_popup.open()

    def confirm_save(self, instance):
        content = BoxLayout(orientation='vertical')
        message = Label(text='Are you sure you want to save the changes?', size_hint_y=0.8)
        button_layout = BoxLayout(size_hint_y=0.2)
        yes_button = Button(text='Yes', on_press=self.save_edited_score, size_hint_y=None, height=50)
        no_button = Button(text='No', on_press=lambda x: self.save_popup.dismiss(), size_hint_y=None, height=50)
        button_layout.add_widget(yes_button)
        button_layout.add_widget(no_button)
        content.add_widget(message)
        content.add_widget(button_layout)
        self.save_popup = Popup(title='Confirm Save', content=content, size_hint=(0.6, 0.4))
        self.save_popup.open()

    def save_edited_score(self, instance):
        if self.selected_score_id is None:
            return

        score_to_edit = next((score for score in self.scores if score['id'] == self.selected_score_id), None)
        if score_to_edit:
            score_to_edit['period'] = self.edit_period.text
            score_to_edit['team'] = 'A' if self.edit_team_a_button.state == 'down' else 'B'
            score_to_edit['points'] = int(self.edit_points.text)
            score_to_edit['player'] = next(btn.text for btn in self.edit_player_buttons if btn.state == 'down')
            self.edit_popup.dismiss()
            self.save_popup.dismiss()
            self.update_score_list(self.scores, self.ot_count)

    def switch_to_main_screen(self, instance):
        self.manager.current = 'main_screen'

class BasketballScoreApp(App):
    def build(self):
        sm = ScreenManager()
        self.main_screen = MainScreen(name='main_screen')
        self.score_screen = ScoreScreen(name='score_screen')
        sm.add_widget(self.main_screen)
        sm.add_widget(self.score_screen)
        return sm

if __name__ == '__main__':
    BasketballScoreApp().run()

仕様書

概要

Basketball Score Appは、バスケットボールの試合中に得点を記録し、リアルタイムでスコアを表示するためのアプリケーションです。このアプリケーションは、延長戦の追加、特定のクォーターの選択、チームおよび選手の選択、得点の追加、スコアの表示およびリセット機能を提供します。

使用技術

  • プログラミング言語: Python
  • フレームワーク: Kivy

クラスと機能

HighlightableButton クラス

  • __init__(self, **kwargs): ボタンの初期化。
  • on_touch_down(self, touch): ボタンがタッチされたときに色

を変更。

MainScreen クラス

  • __init__(self, **kwargs): メイン画面の初期化。得点記録用のリスト、現在のクォーター、選択された選手、延長戦の数などを初期化。
  • get_score_text(self): 現在のスコアを取得し、テキストとして返す。
  • set_period(self, spinner, text): 現在のクォーターを設定。
  • select_player(self, instance): 選択された選手を設定。
  • add_points(self, points): 選択された選手に得点を追加。
  • update_score(self): スコアラベルを更新。
  • update_recent_scores(self): 直近の得点を更新。
  • confirm_reset(self, instance): スコアのリセットを確認するポップアップを表示。
  • reset_scores(self, instance): スコアをリセット。
  • add_ot_period(self, instance): 延長戦を追加。
  • switch_to_score_screen(self, instance): スコア画面に切り替え。

ScoreScreen クラス

  • __init__(self, **kwargs): スコア画面の初期化。
  • update_score_list(self, scores, ot_count): スコアリストを更新。
  • update_period_selector(self): クォーター選択を更新。
  • filter_scores(self, spinner, text): 選択されたクォーターのスコアをフィルタリング。
  • select_score(self, score_id, instance): 選択されたスコアを設定。
  • edit_selected_score(self, instance): 選択されたスコアを編集。
  • confirm_save(self, instance): スコアの保存を確認するポップアップを表示。
  • save_edited_score(self, instance): 編集されたスコアを保存。
  • switch_to_main_screen(self, instance): メイン画面に切り替え。

以上が、PythonとKivyを使って作成したバスケットボールのスコア記録アプリのコードと仕様書です。冒頭でも書きましたが、試しに作っただけのもので実際には使えませんので、今後改めて実際に利用できるアプリを開発したいと思います。

関連記事

コメント

タイトルとURLをコピーしました