はじめに
競馬のデータ分析をテーマに、オッズやレース結果、騎手の成績などの情報を効率的に取得・活用することを目的としCustomTkinterを活用したGUIアプリを作成しました。将来的に機械学習の実装をしたいと考えておりますが、まだその実装はできておりません。ここではGUIに焦点を絞りソースコードを紹介し、簡易な解説を行います。

目次
GUI_main.py
このスクリプトは、Pythonのモジュールを利用してGUIアプリケーションを起動するエントリーポイントとなっています。
ソースコード
from Libs.GUI import GUI_main
if __name__ == "__main__":
GUI_main.run_GUI()
header.py
ソースコード
import customtkinter as ctk
class Header(ctk.CTkFrame):
def __init__(self, App):
super().__init__(App)
self.menu = ctk.CTkButton(
self,
font=App.font_set,
text="≡",
width=25,
height=25,
command=self.call_back,
)
self.menu.grid(row=0, column=0, padx=5, pady=5)
self.title = ctk.CTkLabel(self, font=App.font_set, text="")
self.title.grid(row=0, column=1, padx=5, pady=5)
self.grid_rowconfigure(0, weight=1)
self.grid_columnconfigure(1, weight=1)
# menuフレームの表示、非表示を制御するメソッド
def call_back(self):
self.master.menu_bool = not self.master.menu_bool
if self.master.menu_bool:
self.master.menu.grid(row=1, column=0, padx=5, pady=5, sticky="nsew")
self.master.active_frame.grid(
row=1, column=1, padx=5, pady=5, sticky="nsew"
)
self.master.grid_columnconfigure(0, weight=0)
self.master.grid_columnconfigure(1, weight=1)
else:
self.master.menu.grid_forget()
self.master.active_frame.grid(
row=1, column=0, padx=5, pady=5, sticky="nsew", columnspan=2
)
self.master.grid_columnconfigure(0, weight=1
Header
クラスの概要
Header
クラスはcustomtkinter.CTkFrame
を継承しており、アプリのヘッダー部分を管理します。menu
ボタンをクリックするとcall_back()
メソッドが実行され、メニューの表示・非表示を切り替えます。title
ラベルはアプリのタイトルを表示するための要素です。
call_back()
メソッドの動作
self.master.menu_bool
の状態を切り替えることで、メニューの表示・非表示を管理します。menu.grid()
とmenu.grid_forget()
を適切に使用して、メニューのレイアウトを変更します。grid_columnconfigure()
を活用し、レイアウトの動的な調整を行います。
menu.py
ソースコード
from functools import partial
import customtkinter as ctk
class Menu(ctk.CTkFrame):
def __init__(self, App):
super().__init__(App)
self.func_buttons = []
self.create_buttons(App)
def create_buttons(self, App):
# ボタンを生成し、グリッドに配置
for i, name in enumerate(App.menu_buttons.keys()):
button = ctk.CTkButton(
self,
corner_radius=0,
height=38,
border_spacing=6,
text=name,
fg_color="transparent",
text_color="gray90",
hover_color="gray30",
anchor="w",
font=App.font_set,
command=partial(self.call_back, name),
)
button.grid(row=i + 1, column=0, sticky="ew")
self.func_buttons.append(button)
def call_back(self, name):
# ボタンの色とアクティブフレームの切り替え
for button in self.func_buttons:
button.configure(
fg_color="gray25" if button._text == name else "transparent"
)
# フレームとヘッダーの更新
self.master.header.title.configure(text=name)
self.master.active_frame.grid_forget()
self.master.active_frame = getattr(
self.master, self.master.menu_buttons[name][0]
)
self.master.active_frame.grid(row=1, column=1, padx=5, pady=5, sticky="nsew")
Menu
クラスの概要
Menu
クラスはcustomtkinter.CTkFrame
を継承しており、アプリのメニュー部分を管理します。create_buttons()
メソッドで、アプリに必要なメニューボタンを動的に生成します。call_back()
メソッドで、ボタンの状態やフレームの切り替えを行います。
create_buttons()
メソッドの動作
App.menu_buttons
のキーを取得し、それぞれに対してCTkButton
を作成。command
引数にpartial(self.call_back, name)
を渡し、ボタンが押されたときにcall_back()
メソッドが実行されるように設定。- すべてのボタンを
self.func_buttons
に保存し、後でアクセスできるようにします。
call_back()
メソッドの動作
- クリックされたボタンの色を変更し、現在アクティブなフレームを
self.master.active_frame
に切り替え。 self.master.header.title
を更新して、選択されたメニュー名をヘッダーに表示。grid_forget()
を使い、前のアクティブフレームを非表示にして新しいフレームを表示。
msgbox.py
ソースコード
import tkinter as tk
import customtkinter as ctk
class MsgBox(ctk.CTkFrame):
def __init__(self, parent):
super().__init__(parent)
self.font_set = parent.font_set
self.create_widgets()
def create_widgets(self):
# タイトルラベルの設定
self.title = ctk.CTkLabel(self, font=self.font_set, text="出力")
self.title.grid(row=0, column=0, padx=5, pady=5, sticky="w")
# テキストボックスの設定
self.msgbox = ctk.CTkTextbox(self, font=self.font_set)
self.msgbox.grid(row=1, column=0, padx=5, pady=5, sticky="nsew")
# グリッドの行・列の設定
self.grid_rowconfigure(1, weight=1)
self.grid_columnconfigure(0, weight=1)
def print(self, text, line_brake=True):
"""テキストボックスにメッセージを出力します。"""
if line_brake:
self.msgbox.insert(tk.END, f"{text}\n")
else:
self.msgbox.insert(tk.END, f"{text}")
self.msgbox.update()
MsgBox
クラスの概要
MsgBox
クラスはcustomtkinter.CTkFrame
を継承しており、メッセージの出力領域を管理します。create_widgets()
メソッドで、ラベルとテキストボックスを作成し、レイアウトを設定します。print()
メソッドを使用して、外部からメッセージをテキストボックスに出力できます。
create_widgets()
メソッドの動作
CTkLabel
を用いてタイトルを表示。CTkTextbox
を作成し、メッセージを表示するエリアを確保。grid_rowconfigure()
とgrid_columnconfigure()
でレイアウトの柔軟性を確保。
print()
メソッドの動作
text
をテキストボックスに挿入。line_brake=True
の場合は、改行を追加。msgbox.update()
で即座に変更を反映。
preparing.py
ソースコード
import customtkinter as ctk
from ...Preparing.date_scraper import date_scraper
from ...Preparing.horse_html_fetcher import horse_html_fetcher
from ...Preparing.horse_parser import horse_parser
from ...Preparing.race_html_fetcher import race_html_fetcher
from ...Preparing.race_html_parser import race_html_parser
from ...Preparing.race_id_loader import load_race_ids
from ...Preparing.race_id_scraper import RaceIdScraper
from ...Preparing.range_loader import range_loader
from .msgbox import MsgBox
from .set_range import SetRange
class Preparing(ctk.CTkFrame):
def __init__(self, App):
super().__init__(App)
self.font_set = App.font_set
self.initialize_components()
def initialize_components(self):
"""GUIコンポーネントの初期化"""
# メッセージボックスとプログレスバー
self.msgbox = ProgressbarMsgbox(self)
self.progressbar = self.msgbox.progressbar
self.msgbox.grid(row=3, column=0, padx=5, pady=5, sticky="nsew")
# レース結果取得用フレーム
self.race_result = RaceResultScraper(self)
self.race_result.grid(row=0, column=0, padx=5, pady=5, sticky="ew")
# 馬情報取得用フレーム
self.horse = HorseScraper(self)
self.horse.grid(row=1, column=0, padx=5, pady=5, sticky="ew")
# レイアウトの設定
self.grid_rowconfigure(3, weight=1)
self.grid_columnconfigure(0, weight=1)
class BaseScraper(ctk.CTkFrame):
def __init__(self, parent, title_text, run_callback):
super().__init__(parent)
self.font_set = parent.font_set
self.create_widgets(title_text, run_callback)
def create_widgets(self, title_text, run_callback):
"""ウィジェットの作成"""
# タイトル
self.title = ctk.CTkLabel(self, font=self.font_set, text=title_text)
self.title.grid(row=0, column=0, padx=5, pady=5, sticky="w")
# 上書きチェックボックス
self.overwrite = ctk.CTkCheckBox(self, text="Overwrite", font=self.font_set)
self.overwrite.deselect()
self.overwrite.grid(row=0, column=1, padx=5, pady=5, sticky="ew")
# 実行ボタン
self.run = ctk.CTkButton(
self,
text="実行",
font=self.font_set,
command=lambda: self.pre_run(run_callback),
)
self.run.grid(row=1, column=1, padx=5, pady=5, sticky="ew")
# 入力フレーム
self.set_range = SetRange(self)
self.set_range.configure(fg_color="transparent")
self.set_range.grid(row=1, column=0, padx=5, pady=5, sticky="ew")
# レイアウト設定
self.grid_columnconfigure(0, weight=1)
def pre_run(self, run_callback):
"""共通の前処理(overwrite と ym_list の取得)を行った後に実行"""
overwrite = self.overwrite.get()
y1, m1 = self.set_range.y1.get(), self.set_range.m1.get()
y2, m2 = self.set_range.y2.get(), self.set_range.m2.get()
# データ取得
ym_list = range_loader(y1, m1, y2, m2, self.master)
# `ym_list` が `None` の場合、処理を中断
if ym_list is None:
return
# サブクラスの処理を呼び出す
run_callback(overwrite, ym_list)
class RaceResultScraper(BaseScraper):
def __init__(self, parent):
super().__init__(parent, "レース結果の取得・更新", self.run_callback)
def run_callback(self, overwrite, ym_list):
"""レース結果の取得・更新を行う"""
# レース開催日取得
date_list = date_scraper(ym_list, self.master)
# レースID取得
race_id_scraper = RaceIdScraper(self.master, overwrite)
race_id_scraper.scrape_race_id(date_list)
# GUIの更新を待つ(フリーズ防止)
self.master.update_idletasks()
# レース結果HTMLの取得
race_html_fetcher(ym_list, self.master, overwrite)
race_html_parser(ym_list, self.master, overwrite)
class HorseScraper(BaseScraper):
def __init__(self, parent):
super().__init__(parent, "馬情報の取得・更新", self.run_callback)
def run_callback(self, overwrite, ym_list):
race_id_list = load_race_ids(ym_list, self.master)
if not race_id_list:
return
horse_id_list = horse_html_fetcher(race_id_list, self.master, overwrite)
horse_parser(horse_id_list, self.master, overwrite)
class ProgressbarMsgbox(MsgBox):
def __init__(self, parent):
super().__init__(parent)
self.initialize_widgets()
def initialize_widgets(self):
"""プログレスバーとメッセージボックスの初期化"""
self.progressbar = ctk.CTkProgressBar(self, orientation="horizontal")
self.progressbar.set(0)
self.progressbar.grid(row=0, column=1, padx=5, sticky="ew")
self.title.grid(row=0, column=0, padx=5, pady=5, sticky="w")
self.msgbox.grid(row=1, column=0, padx=5, pady=5, sticky="nsew", columnspan=2)
# レイアウト設定
self.grid_rowconfigure(1, weight=1)
self.grid_columnconfigure(1, weight=1)
Preparing
クラスの概要
Preparing
クラスはcustomtkinter.CTkFrame
を継承しており、競馬データ取得の主要な処理を統括します。initialize_components()
メソッドで、各種データ取得フレームやメッセージボックスを設定します。
BaseScraper
クラスの概要
BaseScraper
クラスは、共通のデータ取得ロジックを提供するベースクラスです。create_widgets()
メソッドを用いて、タイトル、チェックボックス、実行ボタンなどのGUIコンポーネントを作成。pre_run()
メソッドで、処理の前処理として日付範囲の取得を行い、サブクラスで定義されたrun_callback()
を呼び出します。
RaceResultScraper
クラスの概要
RaceResultScraper
はBaseScraper
を継承し、レース結果の取得と更新を管理します。run_callback()
メソッドで、レース開催日やレースIDを取得し、結果の解析を行います。
HorseScraper
クラスの概要
HorseScraper
はBaseScraper
を継承し、馬の情報取得と解析を行います。run_callback()
メソッドで、レースIDの読み込み、馬情報の取得、データ解析を実行します。
ProgressbarMsgbox
クラスの概要
ProgressbarMsgbox
はMsgBox
クラスを継承し、進捗状況を表示するプログレスバーを追加したメッセージボックスを提供します。initialize_widgets()
メソッドで、プログレスバーとメッセージボックスをレイアウトし、ユーザーに処理の進行状況を可視化します。
set_range.py
ソースコード
import datetime
import customtkinter as ctk
class SetRange(ctk.CTkFrame):
def __init__(self, parent):
super().__init__(parent)
self.font_set = ("meiryo", 14)
self.dt_now = datetime.datetime.now()
self.year_list = [str(i) for i in range(2008, self.dt_now.year + 1)]
self.month_list = [str(i) for i in range(1, 13)]
# インスタンス変数としてコンポーネントを定義
self.from_label = ctk.CTkLabel(self, text="From", font=self.font_set)
self.y1 = ctk.CTkComboBox(self, values=self.year_list, font=self.font_set)
self.y1_label = ctk.CTkLabel(self, text="年", font=self.font_set)
self.m1 = ctk.CTkComboBox(self, values=self.month_list, font=self.font_set)
self.m1_label = ctk.CTkLabel(self, text="月", font=self.font_set)
self.spacer = ctk.CTkLabel(self, text=" ", font=self.font_set)
self.to_label = ctk.CTkLabel(self, text="To", font=self.font_set)
self.y2 = ctk.CTkComboBox(self, values=self.year_list, font=self.font_set)
self.y2_label = ctk.CTkLabel(self, text="年", font=self.font_set)
self.m2 = ctk.CTkComboBox(self, values=self.month_list, font=self.font_set)
self.m2_label = ctk.CTkLabel(self, text="月", font=self.font_set)
# コンポーネントの配置
self.from_label.grid(row=0, column=0, padx=5, pady=5, sticky="ew")
self.y1.grid(row=0, column=1, padx=5, pady=5, sticky="ew")
self.y1_label.grid(row=0, column=2, padx=5, pady=5, sticky="ew")
self.m1.grid(row=0, column=3, padx=5, pady=5, sticky="ew")
self.m1_label.grid(row=0, column=4, padx=5, pady=5, sticky="ew")
self.spacer.grid(row=0, column=5, padx=5, pady=5, sticky="ew")
self.to_label.grid(row=0, column=6, padx=5, pady=5, sticky="ew")
self.y2.grid(row=0, column=7, padx=5, pady=5, sticky="ew")
self.y2_label.grid(row=0, column=8, padx=5, pady=5, sticky="ew")
self.m2.grid(row=0, column=9, padx=5, pady=5, sticky="ew")
self.m2_label.grid(row=0, column=10, padx=5, pady=5, sticky="ew")
# 列幅の設定
for col in [1, 3, 7, 9]:
self.grid_columnconfigure(col, weight=1)
SetRange
クラスの概要
SetRange
クラスはcustomtkinter.CTkFrame
を継承しており、日付範囲の選択を可能にするウィジェットを提供します。year_list
とmonth_list
を生成し、過去2008年から現在までの年、および1~12月の月を選択できるようにします。CTkComboBox
を使用して年と月の選択を実装します。
create_widgets()
メソッドの動作
CTkLabel
を用いて「From」と「To」のラベルを配置。CTkComboBox
を用いて、開始年・月 (y1
,m1
) と終了年・月 (y2
,m2
) を選択可能に。grid()
を用いて、ウィジェットを適切にレイアウト。grid_columnconfigure()
を使用し、年と月の選択部分に対してレスポンシブなデザインを適用。
コメント