はじめに
前回の記事でスクレイピングでレースのデータを取得しました。
そのデータを使ってGUIのアプリケーションを作ります。
オブジェクト指向を使ったtkinterの具体的な実装法のメモになればいいかなと。
様々なサイトを参考にさせていただきました。
手順
- スクレイピングでレースデータを取得
- データを使ってGUIアプリケーションを作る(検索&TODOリスト登録)【今回やる】
使用開発環境
- Python 3.8.3
- pandas 1.0.5
- tkinter 8.6.10
- エディタ:VScode
要件
適当に要件を決めます。
- レースを名前で検索できる
- 検索したレースを選択してTODOリストに追加できる
- TODOリストのチェックボックスをチェックするとそのレースを消すことができる
- レースを期間で昇順ソートできる
- レースのデータ(期間、場所など)も表示させる
ソースコード
順番に実装していきます。
インポート、定数の宣言
この辺りの定数は他の実装をしつつ定義したものです。
ほとんどがtkinterのGUI設計用の定数です。
import pandas as pd
from tkinter import *
PATH='race.csv'
SEP=20
CHK_X=10
CHK_Y=10
LIST_X=10
LIST_Y=80
SER_ENT_WIDTH=20
SER_ENT_X=10
SER_ENT_Y=10
MAIN_TITLE='ウマ娘 レース計画ソフト'
MAIN_WINDOW_SIZE='600x500'
SER_BTN_TEXT='検索'
SER_BTN_X=10
SER_BTN_Y=50
UPD_BTN_TEXT = '更新'
UPD_BTN_X=80
UPD_BTN_Y=50
ADD_BTN_TEXT='レースを追加'
ALL_BTN_TEXT='全選択'
ALL_BTN_X=130
DONE_RACE = 0
MONTH_ZEN = 0.1
MONTH_KOU = 0.2
Raceクラスの定義
レースに必要なプロパティと、レースデータを表示させるメソッドを定義する。
要件の、「レースのデータ(期間、場所など)も表示させる」はこれで実装完了です。
class Race:
def __init__(self, year, month, name, race_class, place, length, direction, tk):
self.year = year
self.month = month
self.name = name
self.race_class = race_class
self.place = place
self.length = length
self.direction = direction
self.bln = BooleanVar()
self.bln.set(False)
self.chk = Checkbutton(tk, variable=self.bln, text=self.generate_view_race())
def generate_view_race(self):
txt = f'{self.year} {self.month} {self.race_class} {self.name} ' + \
f'{self.length} {self.place} {self.direction}'
return txt
アプリのクラスを定義
tkinterのウェジットをインスタンスのプロパティとして実装します。
class Race_plan_app():
def __init__(self):
# メイン画面を定義
self.tk = Tk()
self.tk.title(MAIN_TITLE)
self.tk.geometry(MAIN_WINDOW_SIZE)
# 検索窓を定義
self.search_entry = Entry(width=SER_ENT_WIDTH)
self.search_entry.place(x=SER_ENT_X, y=SER_ENT_Y)
# 検索ボタンを定義
self.search_btn = Button(self.tk, text=SER_BTN_TEXT, command=self.search_race)
self.search_btn.place(x=SER_BTN_X, y=SER_BTN_Y)
# 更新ボタンを定義
self.update_btn = Button(self.tk, text=UPD_BTN_TEXT, command=self.update)
self.update_btn.place(x=UPD_BTN_X, y=UPD_BTN_Y)
# レースのリストを入れるプロパティを定義
self.race_list = []
self.read_race_data()
# レースのデータを読み込む
def read_race_data(self):
self.df_race = pd.read_csv(PATH)
def run(self):
self.tk.mainloop()
基本的なウェジットは実装完了です。
あとは、runメソッドを呼び出すことでmainloopさせるように実装しました(やり方違ってたらすみません!)。
race_app = Race_plan_app()
race_app.run()
ただ、ボタンで起動するメソッドがまだ定義されてないのでこれだけ実行してもエラーを吐かれます。
## 検索機能の実装
class Race_plan_app():
# ~~ 省略 ~~
# レース検索機能
def search_race(self):
# 入力されたレース名をもとにデータフレームから検索する
self.ent_txt = self.search_entry.get()
self.search_result = self.df_race[self.df_race['レース名'].str.contains(self.ent_txt)]
self.search_result = list(self.search_result.itertuples())
# 新しいウィンドウを表示
new_window = Toplevel()
new_window.title('レース選択')
new_window.geometry('500x1000')
# 検索したレースデータをチェックボックス付きで表示(インスタンスも作成)
self.search_races = []
for i in range(len(self.search_result)):
self.search_races.append(Race(self.search_result[i][2], self.search_result[i][3], self.search_result[i][4], self.search_result[i][5],
self.search_result[i][6], self.search_result[i][7], self.search_result[i][8], new_window))
self.search_races[-1].chk.place(x=CHK_X, y=CHK_Y + SEP*i)
# TODOリストへのレース追加を行う
add_btn = Button(new_window, text=ADD_BTN_TEXT, command=self.add_race)
add_btn.place(x=CHK_X, y=CHK_Y + SEP*len(self.search_result))
# 検索結果を全部選択する
all_btn = Button(new_window, text=ALL_BTN_TEXT, command=self.chose_all)
all_btn.place(x=ALL_BTN_X, y=CHK_Y + SEP*len(self.search_result))
new_window.mainloop()
これで「レースを名前で検索できる」の実装完了です。
キモとしては、新しいウィンドウを表示させるにはToplevelメソッドを使うところです。
そのウィンドウの変数を使って新しいウィンドウに表示させるウィジットを定義していきます。
最後にmainloopさせているので、ウィンドウが閉じられたら新しいウィンドウの処理も停止します。
検索だけでなく、レースの結果をTODOリストに追加する機能も実装する必要があります。
各ボタンに設定しているメソッドを実装していきます。
## TODOリストへの追加
class Race_plan_app():
# ~~ 省略 ~~
# TODOリストに追加する
def add_race(self):
for i in range(len(self.search_races)):
if self.search_races[i].bln.get():
race = self.search_races[i]
self.race_list.append(Race(race.year, race.month, race.name, race.race_class,
race.place, race.length, race.direction, self.tk))
self.update()
# レースの検索結果を全部選択する
def chose_all(self):
for i in self.search_races:
i.bln.set(True)
これで「検索したレースを選択してTODOリストに追加できる」機能の実装完了です。
これらは新しいウィンドウのボタンウィジットに登録しています。
ボタンに登録するメソッドもインスタンスないのメソッドを使用できるのでわかりやすいですね。
検索結果を全選択する機能も便利かなと思ってついでにつけました。
更新機能と、ソート機能
ちょっと長くなりますが、以下のように実装。
更新機能は、ボタン以外にもレースを追加する際に更新を行う。
更新を行うと、レースのソート、完了済みのレースを削除という処理が行われる。
ソートは…汚いです。動けば正義という気持ちで実装しました…
class Race_plan_app():
# ~~ 省略 ~~
# 更新する
def update(self):
# TODOリストのレースをチェックボックス付きで表示
s = 0
for i in range(len(self.race_list)):
if self.race_list[i].bln.get():
self.race_list[i].chk.destroy()
self.race_list[i] = DONE_RACE
else:
self.race_list[i].chk.place(x=LIST_X, y=LIST_Y + SEP*s)
s+=1
# 完了済みのレースは削除
while DONE_RACE in self.race_list:
self.race_list.remove(DONE_RACE)
# レースをソートする
self.sort_race()
# レースを昇順にする
def sort_race(self):
# 年、月毎にデータを分ける、月も前後半で昇順になるようにする
race_yms = []
for race in self.race_list:
race_yms.append([int(race.year[0]),
int(race.month[:-3])])
if race.month[-2:] == '前半':
race_yms[-1][1] += MONTH_ZEN
elif race.month[-2:] == '後半':
race_yms[-1][1] += MONTH_KOU
years = [i[0] for i in race_yms]
# 年をソートする
for i in range(len(self.race_list)):
for j in range(len(self.race_list)):
if race_yms[i][0] < race_yms[j][0]:
tmp = race_yms[i]
race_yms[i] = race_yms[j]
race_yms[j] = tmp
tmp = self.race_list[i]
self.race_list[i] = self.race_list[j]
self.race_list[j] = tmp
# 1年目を月毎にソート
if 1 in years:
ri = self.count_rindex([i[0] for i in race_yms], 1) # 1年目の最後尾のインデックス番号取得
for i in range(len(self.race_list[:ri+1])):
for j in range(len(self.race_list[:ri+1])):
if race_yms[i][1] < race_yms[j][1]:
tmp = race_yms[i]
race_yms[i] = race_yms[j]
race_yms[j] = tmp
tmp = self.race_list[i]
self.race_list[i] = self.race_list[j]
self.race_list[j] = tmp
# 2年目を月毎にソート
if 2 in years:
ind = [i[0] for i in race_yms].index(2)
ri = self.count_rindex([i[0] for i in race_yms], 2)
for i in range(ind, len(self.race_list[:ri+1])):
for j in range(ind, len(self.race_list[:ri+1])):
if race_yms[i][1] < race_yms[j][1]:
tmp = race_yms[i]
race_yms[i] = race_yms[j]
race_yms[j] = tmp
tmp = self.race_list[i]
self.race_list[i] = self.race_list[j]
self.race_list[j] = tmp
# 3年目を月毎にソート
if 3 in years:
ind = [i[0] for i in race_yms].index(3)
for i in range(ind, len(self.race_list)):
for j in range(ind, len(self.race_list)):
if race_yms[i][1] < race_yms[j][1]:
tmp = race_yms[i]
race_yms[i] = race_yms[j]
race_yms[j] = tmp
tmp = self.race_list[i]
self.race_list[i] = self.race_list[j]
self.race_list[j] = tmp
# 年ごとの最終要素のインデックス番号を取得するメソッド
def count_rindex(self, lis, n):
return len(lis) - (lis[::-1].index(n)+1)
これで、「TODOリストのチェックボックスをチェックするとそのレースを消すことができる」、「レースを期間で昇順ソートできる」という機能の実装完了です。
使用例
有馬記念で検索してみるとこんな感じ。2年目と3年目の違いで、2年目を追加してみます。
色々追加して、2年目のレースが終わったと仮定すると、
このようになります。
今後の展望
追加するなら、セーブ機能ですね。
実装予定ではあったんですが、自分用だけなら別にいらんなと思って実装してません。
まとめ
簡潔にはなりますが、ウマ娘のレース管理アプリ作成でした。
自分が色々とPythonで書いていく中で考えた書き方なので癖があるかもしれませんが、参考になれば幸いです。
参考文献
なんとか記憶を掘り出して参考サイトとか思い出しました。漏れがあれば申し訳ないです。
- Pythonによるプログラミング
- 【Python】GUI 画面(ウィンドウ)を作る(tkinter)
- 【Python】ボタンのクリックイベントを取得する
- Entryの値を取得してみる · tkinter - Nな人(N na hito)
- 【Python】チェックボックス(チェックボタン)を作成する(Checkbutton)
- Tkinter のボタンをクリックして新しいウィンドウを作成する方法
- Python Tkinter 入門メモ