【2日目】Fletでソリティアサンプルアプリ作成 ~UI構築の実践編①~

この記事は広告を含みます。

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での描画、クラスでの制御などを学習できたかと思います。