この記事は広告を含みます。
1. はじめに
Fletの1日目の記事で環境構築とプロジェクト初期化について解説しました。今回は、その環境を活用して、実践的なサンプルアプリとしてソリティアゲームのUIを作成していきます。 なぜソリティアなのか?というと、公式がだしているからです。ソースコードを。 それで、カードの配置や状態管理、クリックなどのイベント処理が必要なため、Fletの多彩な機能(レイアウト、リアルタイム更新、イベントハンドリング)を確認するのに最適な題材かなと思い。。。サンプルコードを見ながらやっていきたいかと。。。
2. サンプルコードの詳細解説
ドラッグ
import flet as ft # Use of GestureDetector for with on_pan_update event for dragging card # Absolute positioning of controls within stack def main(page: ft.Page): def drag(e: ft.DragUpdateEvent): e.control.top = max(0, e.control.top + e.delta_y) e.control.left = max(0, e.control.left + e.delta_x) e.control.update() card = ft.GestureDetector( mouse_cursor=ft.MouseCursor.MOVE, drag_interval=5, on_pan_update=drag, left=0, top=0, content=ft.Container(bgcolor=ft.Colors.GREEN, width=70, height=100), ) page.add(ft.Stack(controls=[card], width=1000, height=500)) ft.app(main)
このような感じで公式かいていました。。
ドロップ
import flet as ft # Use of GestureDetector for with on_pan_update event for dragging card # Absolute positioning of controls within stack def main(page: ft.Page): class Solitaire: def __init__(self): self.start_top = 0 self.start_left = 0 solitaire = Solitaire() def start_drag(e: ft.DragStartEvent): solitaire.start_top = e.control.top solitaire.start_left = e.control.left e.control.update() def drag(e: ft.DragUpdateEvent): e.control.top = max(0, e.control.top + e.delta_y) e.control.left = max(0, e.control.left + e.delta_x) e.control.update() def bounce_back(game, card): """return card to its original position""" card.top = game.start_top card.left = game.start_left page.update() def drop(e: ft.DragEndEvent): if ( abs(e.control.top - slot.top) < 20 and abs(e.control.left - slot.left) < 20 ): place(e.control, slot) else: bounce_back(solitaire, e.control) e.control.update() def place(card, slot): """place card to the slot""" card.top = slot.top card.left = slot.left page.update() card = ft.GestureDetector( mouse_cursor=ft.MouseCursor.MOVE, drag_interval=5, on_pan_update=drag, on_pan_end=drop, left=0, top=0, content=ft.Container(bgcolor=ft.Colors.GREEN, width=70, height=100), ) slot = ft.Container( width=70, height=100, left=200, top=0, border=ft.border.all(1) ) page.add(ft.Stack(controls = [slot, card], width=1000, height=500)) ft.app(main)[f:id:kunio-ud:20250304221842p:plain]
カード追加
import flet as ft # Adding 2 more slots # Deal cards before the beginning of the game # On_pan_end, go through slots list to find slot in proximity if possible class Solitaire: def __init__(self): self.start_top = 0 self.start_left = 0 def main(page: ft.Page): def place(card, slot): """place card to the slot""" card.top = slot.top card.left = slot.left def bounce_back(game, card): """return card to its original position""" card.top = game.start_top card.left = game.start_left page.update() def move_on_top(card, controls): """Moves draggable card to the top of the stack""" controls.remove(card) controls.append(card) page.update() def start_drag(e: ft.DragStartEvent): move_on_top(e.control, controls) solitaire.start_top = e.control.top solitaire.start_left = e.control.left def drag(e: ft.DragUpdateEvent): e.control.top = max(0, e.control.top + e.delta_y) e.control.left = max(0, e.control.left + e.delta_x) e.control.update() def drop(e: ft.DragEndEvent): for slot in slots: if ( abs(e.control.top - slot.top) < 20 and abs(e.control.left - slot.left) < 20 ): place(e.control, slot) e.control.update() return bounce_back(solitaire, e.control) e.control.update() slot0 = ft.Container( width=70, height=100, left=0, top=0, border=ft.border.all(1) ) slot1 = ft.Container( width=70, height=100, left=200, top=0, border=ft.border.all(1) ) slot2 = ft.Container( width=70, height=100, left=300, top=0, border=ft.border.all(1) ) slots = [slot0, slot1, slot2] card1 = ft.GestureDetector( mouse_cursor=ft.MouseCursor.MOVE, drag_interval=5, on_pan_start=start_drag, on_pan_update=drag, on_pan_end=drop, left=0, top=0, content=ft.Container(bgcolor=ft.colors.GREEN, width=70, height=100), ) card2 = ft.GestureDetector( mouse_cursor=ft.MouseCursor.MOVE, drag_interval=5, on_pan_start=start_drag, on_pan_update=drag, on_pan_end=drop, left=100, top=0, content=ft.Container(bgcolor=ft.colors.YELLOW, width=70, height=100), ) controls = [slot0, slot1, slot2, card1, card2] # deal cards place(card1, slot0) place(card2, slot0) solitaire = Solitaire() page.add(ft.Stack(controls=controls, width=1000, height=500)) ft.app(target=main)
山を作る
- ここからは、クラスを分けます。
main.pyでソリティアクラスを呼び出します。
import flet as ft from solitaire import Solitaire def main(page: ft.Page): solitaire = Solitaire() page.add(solitaire) ft.app(target=main)
カードとスロットクラスを読み込んで、 カードとスロットの生成管理してますね。
solitaire.py
# CARD_OFFSET = 20 SOLITAIRE_WIDTH = 1000 SOLITAIRE_HEIGHT = 500 import flet as ft from card import Card from slot import Slot class Solitaire(ft.Stack): def __init__(self): super().__init__() # self.start_top = 0 # self.start_left = 0 self.controls = [] self.slots = [] # self.card_offset = CARD_OFFSET self.width = SOLITAIRE_WIDTH self.height = SOLITAIRE_HEIGHT def did_mount(self): self.create_card_deck() self.create_slots() self.deal_cards() def create_card_deck(self): card1 = Card(self, color="GREEN") card2 = Card(self, color="YELLOW") card3 = Card(self, color="RED") card4 = Card(self, color="BLUE") self.cards = [card1, card2, card3, card4] def create_slots(self): self.slots.append(Slot(top=0, left=0)) self.slots.append(Slot(top=0, left=200)) self.slots.append(Slot(top=0, left=300)) self.controls.extend(self.slots) self.update() def deal_cards(self): self.controls.extend(self.cards) for card in self.cards: card.place(self.slots[0]) self.update()
カードクラス内は、カードの操作ですね。
card.py
CARD_WIDTH = 70 CARD_HEIGTH = 100 DROP_PROXIMITY = 30 CARD_OFFSET = 20 import flet as ft class Card(ft.GestureDetector): def __init__(self, solitaire, color): super().__init__() self.mouse_cursor = ft.MouseCursor.MOVE self.drag_interval = 5 self.on_pan_start = self.start_drag self.on_pan_update = self.drag self.on_pan_end = self.drop self.left = None self.top = None self.solitaire = solitaire self.slot = None self.card_offset = CARD_OFFSET self.color = color self.content = ft.Container( bgcolor=self.color, width=CARD_WIDTH, height=CARD_HEIGTH ) self.draggable_pile = [self] def move_on_top(self): """Brings draggable card pile to the top of the stack""" # for card in self.get_draggable_pile(): for card in self.draggable_pile: self.solitaire.controls.remove(card) self.solitaire.controls.append(card) self.solitaire.update() def bounce_back(self): """Returns draggable pile to its original position""" for card in self.draggable_pile: card.top = card.slot.top + card.slot.pile.index(card) * CARD_OFFSET card.left = card.slot.left self.solitaire.update() def place(self, slot): """Place draggable pile to the slot""" for card in self.draggable_pile: card.top = slot.top + len(slot.pile) * CARD_OFFSET card.left = slot.left # remove card from it's original slot, if it exists if card.slot is not None: card.slot.pile.remove(card) # change card's slot to a new slot card.slot = slot # add card to the new slot's pile slot.pile.append(card) self.solitaire.update() def get_draggable_pile(self): """returns list of cards that will be dragged together, starting with the current card""" if self.slot is not None: self.draggable_pile = self.slot.pile[self.slot.pile.index(self) :] else: # slot == None when the cards are dealed and need to be place in slot for the first time self.draggable_pile = [self] def start_drag(self, e: ft.DragStartEvent): self.get_draggable_pile() self.move_on_top() self.solitaire.update() def drag(self, e: ft.DragUpdateEvent): for card in self.draggable_pile: card.top = ( max(0, self.top + e.delta_y) + self.draggable_pile.index(card) * CARD_OFFSET ) card.left = max(0, self.left + e.delta_x) self.solitaire.update() def drop(self, e: ft.DragEndEvent): for slot in self.solitaire.slots: if ( abs(self.top - (slot.top + len(slot.pile) * CARD_OFFSET)) < DROP_PROXIMITY and abs(self.left - slot.left) < DROP_PROXIMITY ): self.place(slot) self.solitaire.update() return self.bounce_back()
スロットクラスは、枠線とスロットサイズだけですね。
slot.py
SLOT_WIDTH = 70 SLOT_HEIGHT = 100 import flet as ft class Slot(ft.Container): def __init__(self, top, left): super().__init__() self.pile=[] self.width=SLOT_WIDTH self.height=SLOT_HEIGHT self.left=left self.top=top self.border=ft.border.all(1)
実行結果がコチラ。
ふぅ。。CardクラスはCardクラスですが、PlayerControllerっていうイメージですね。 業務でReactを使っているのですが、、、Reactをpython風によくifを書いてしまいます。笑
ここで、おおよそ半分まで写経できたかと思います。 続きは、3日目にします。
Fletでの描画、クラスでの制御などを学習できたかと思います。