はじめに
本記事では、以下の記事を参考にさせていただき作成したものです。
こちらの内容について以下の要素を追加しました:
- 現在のFletのバージョン(0.25.2)での記法に
- 祝日や休日については色を付ける
- 入力した月のカレンダーを表示
↓ 今回作成したアプリ ↓
※このカレンダーによると今年は5/7,5/8,5/9を休みにすれば、9連休にすることが可能ですね。
1. 準備
日本の祝日についてはAPIなど利用できるものはいくつかありますが、今回は以下のCSVをcalendarApp.py
ファイルと同階層に配置し、こちらのsyukujitsu.csv
を読み取ります。
2. コードの簡単な説明
基本的なFletアプリの呼び出し方法に基づき、main
関数を定義:
main関数
def main(page: Page):
page.title = 'Calendar View'
page.window.min_width = 460
page.window.width = 460
page.add(MyCalendar())
if __name__ == '__main__':
ft.app(main)
MyCalendar
クラスに必要なコンポーネントを詰めて定義:
MyCalendar
class MyCalendar(Column):
def __init__(self):
super().__init__()
....中略....
def btn_clicked(self, e: ControlEvent):
....中略....
def last_month_clicked(self, e: ControlEvent):
....中略....
def next_month_clicked(self, e: ControlEvent):
....中略....
def calendar_update(self, year: int, month: int):
....中略....
def before_update(self):
....中略....
- btn_clicked : 「更新」ボタンを押下したらコンポーネントの更新を行う
- last_month_clicked : 先月のカレンダーを表示させる
- next_month_clicked : 翌月のカレンダーを表示させる
- calendar_update : ドロップダウンリスト(年と月)の値を取得し、対応する月のカレンダーを表示させる
- before_update : コンポーネントの更新が行われる前に呼び出される関数。ドロップダウンリスト(年と月)の値を取得し、
calendar_update
の引数に取得した年と月を渡して呼び出す
他、GridView
初期化用のクラスとGridView
のItem用のクラスを定義しています。
3. 最終的なコード
calendarApp.py
import os
import csv
import flet as ft
from datetime import datetime, timezone, timedelta
from calendar import Calendar
from flet import (
alignment,
border,
border_radius,
Container,
ControlEvent,
Colors,
Column,
Dropdown,
dropdown,
FontWeight,
GridView,
Icon,
Icons,
MainAxisAlignment,
Row,
Text,
TextAlign,
TextButton,
padding,
Page,
)
weeks = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']
datetime_now = datetime.now(timezone(timedelta(hours=9), 'JST')).strftime('%Y-%m-%d %H:%M:%S')
dir_path = os.path.dirname(__file__)
with open(os.path.join(dir_path, 'syukujitsu.csv'), 'r', encoding='shift-jis') as r:
read_holidays = [row for row in csv.reader(r)]
class MyCalendar(Column):
def __init__(self):
super().__init__()
self._today = datetime.now(timezone(timedelta(hours=9), 'JST'))
self._years = [i for i in range(self._today.year-20, self._today.year + 20 + 1)] # 現在より20年前から20年後までを表示
self._months = [i for i in range(1, 12 + 1)] # 1月から12月まで
self._year_dropdown = Dropdown(
value=self._today.year,
label='Year',
options=[dropdown.Option(str(i)) for i in self._years],
border=None,
width=160,
text_size=16,
)
self._month_dropdown = Dropdown(
value=self._today.month,
label='Month',
options=[dropdown.Option(str(i)) for i in self._months],
border=None,
width=160,
text_size=16,
)
self._this_month = Text(
width=240, text_align=TextAlign.CENTER, size=18, weight=FontWeight.W_700, color=Colors.GREY_700
)
self._input_fields = Row(
[
self._year_dropdown, self._month_dropdown,
TextButton(text='更新', width=60, on_click=self.btn_clicked,),
],
alignment=MainAxisAlignment.END,
width=420,
)
self._titles = Row(
[
TextButton(
content=Row([Icon(Icons.ARROW_LEFT), Text('先月')], spacing=1), # Icon -> Text の順で表示
width=80,
on_click=self.last_month_clicked,
),
self._this_month,
TextButton(
content=Row([Text('翌月'), Icon(Icons.ARROW_RIGHT)], spacing=1), # Text -> Icon の順で表示
width=80,
on_click=self.next_month_clicked,
),
],
alignment=MainAxisAlignment.SPACE_BETWEEN,
width=420,
)
self._calendar_grid = CalendarGridView()
self._calendar_obj = Calendar(firstweekday=6)
self.controls = [
self._input_fields,
self._titles,
self._calendar_grid,
]
def btn_clicked(self, e: ControlEvent):
self.update()
def last_month_clicked(self, e: ControlEvent):
input_year = self._year_dropdown.value
input_month = self._month_dropdown.value
if (input_month - 1 <= 0): # 1月の場合、昨年の12月を返す
input_year -= 1
input_month = 12
else:
input_month -= 1
self._year_dropdown.value = input_year
self._month_dropdown.value = input_month
self.update()
def next_month_clicked(self, e: ControlEvent):
input_year = int(self._year_dropdown.value)
input_month = int(self._month_dropdown.value)
if (input_month + 1 > 12): # 12月の場合、翌年の1月を返す
input_year += 1
input_month = 1
else:
input_month += 1
self._year_dropdown.value = input_year
self._month_dropdown.value = input_month
self.update()
def calendar_update(self, year: int, month: int):
self._calendar_grid.controls.clear() # GridView.Controlsの初期化
holidays_filter = {
x[0].split('/')[-1]: x[1]
for x in read_holidays if f'{year}/{month}/' in x[0]
} # YYYY年MM月の休日を{ 'day' : 'Holiday Name' ,...}の形式で抽出
for w in weeks:
self._calendar_grid.controls.append(CalendarItem(w))
for week in self._calendar_obj.monthdayscalendar(int(year), int(month)):
for idx, day in enumerate(week):
holiday = holidays_filter.get(str(day), '')
match idx:
case 0: # Sunは赤色
text_color = Colors.RED_ACCENT
case 6: # Satは青色
text_color = Colors.BLUE_ACCENT
case _: # 祝日の場合は赤色、それ以外はグレー
text_color = Colors.RED_ACCENT if holiday else Colors.GREY_500
self._calendar_grid.controls.append(CalendarItem(day, holiday, text_color))
def before_update(self):
input_year = self._year_dropdown.value
input_month = self._month_dropdown.value
if input_year and input_month:
self._this_month.value = f'{input_year}年{input_month}月'
self.calendar_update(input_year, input_month)
class CalendarItem(Container):
def __init__(self, day: int | str, holiday: str = '', text_color: str = Colors.GREY_500):
super().__init__()
self.day = str(day) if day != 0 else ''
self.holiday = holiday if holiday != '休日' else '振替休日'
self.text_color = text_color
self.padding = padding.all(5)
self.border = border.all(0.1)
self.border_radius = border_radius.all(10)
self.alignment = alignment.center
self._day = Text(
self.day, color=self.text_color, weight=FontWeight.W_700, width=50, text_align=TextAlign.CENTER
)
self._holiday = Text(self.holiday, width=50, text_align=TextAlign.CENTER, size=9, color=Colors.GREY)
self.content = Column(
controls=[self._day, self._holiday],
spacing=1,
)
class CalendarGridView(GridView):
def __init__(self, item_width: int = 60):
super().__init__()
self.item_width = item_width
self.width = self.item_width * 7
self.max_extent = self.item_width
self.child_aspect_ratio = 1.0
self.spacing = 1
self.run_spacing = 1
def main(page: Page):
page.title = 'Calendar View'
page.window.min_width = 460
page.window.width = 460
page.add(MyCalendar())
if __name__ == '__main__':
ft.app(main)
4. 最後に
@kiiiiwy さんの記事から着想を得て今回のカレンダーアプリを作成いたしました。
カレンダーの週表示は鉛直方向に対して中央ぞろえにしたほうがいいなど気になる点はありますがカレンダーアプリとして満足のいくものが作れたかなと思います。(あとは、祝日データを自動更新できればもっといいですね。)