使用Python和Flet创建纸牌接龙游戏-第一部分
在本教程中,我们将逐步介绍如何使用Python和Flet创建一个著名的Klondike纸牌接龙游戏。为了灵感,我们参考了这个在线游戏:https://www.solitr.com/
本教程面向初学者/中级水平的Python开发者,需要具备基本的Python和面向对象编程知识。
在这里,您可以看到使用Flet和本教程将要实现的最终结果: https://gallery.flet.dev/solitaire/

我们将游戏实现分解为以下步骤:
在第2部分中(将在下个教程中介绍),我们将添加一个Appbar,其中包含开始新游戏、查看游戏规则和更改游戏设置的选项。
使用Flet入门
要使用Flet创建一个web应用程序,您不需要了解HTML、CSS或JavaScript,但您需要基本的Python和面向对象编程的知识。
Flet需要Python 3.8或更高版本。要使用Flet在Python中创建一个web应用程序,您需要先安装flet
模块:
pip install flet
首先,让我们创 建一个简单的Hello World应用程序。
创建hello.py
并添加以下内容:
import flet as ft
def main(page: ft.Page):
page.add(ft.Text(value="Hello, world!"))
ft.app(target=main)
运行此应用程序,您将看到一个带有问候语的新窗口:

可拖动卡片的概念验证应用
在概念验证中,我们只使用了三种类型的控件:
- Stack - 用于绝对定位插槽和纸牌的父控件
- GestureDetector - 将在Stack中移动的纸牌
- Container - 将放置纸牌的插槽。同时也将用作GestureDetector的
content
。
我们将概念验证应用程序分解为四个简单的步骤,这样在每个步骤之后,您都有一个完整的短程序可以运行和测试。
第1步:拖动纸牌
在此步骤中,我们将创建一个Stack
(接龙游戏区域)和一个GestureDetector
(纸牌)。然后将纸牌添加到Stack的controls
列表中。GestureDetector的left
和top
属性用于在Stack中进行绝对定位纸牌。
import flet as ft
def main(page: ft.Page):
card = ft.GestureDetector(
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(target=main)
运行应用程序,您将看到已将纸牌添加到堆栈中:

为了能够移动纸牌,我们将创建一个drag
方法,并在GestureDetector的on_pan_update
事件中调用该方法,该事件在用户拖动纸牌时每隔drag_interval
触发一次。
为了显示纸牌的移动,我们将在drag
方法中在每次发生on_pan_update
事件时更新纸牌的top
和left
属性。
以下是在Stack中拖动GestureDetector的最简代码:
import flet as ft
def main(page: ft.Page):
card = ft.GestureDetector(
left=0,
top=0,
content=ft.Container(bgcolor=ft.colors.GREEN, width=70, height=100),
)
def drag(dx: int, dy: int):
card.left += dx
card.top += dy
card.on_pan_update = drag
page.add(ft.Stack(controls=[card], width=1000, height=500))
ft.app(target=main)
现在,您可以在Stack中拖动纸牌了:

在下一部分,我们将介绍如何添加扇形堆叠纸牌功能。
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()
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()
def bounce_back(game, card):
"""return card to its original position"""
card.top = game.start_top
card.left = game.start_left
page.update()
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()
card_1 = ft.GestureDetector(
mouse_cursor=ft.MouseCursor.MOVE,
drag_interval=5,
on_pan_update=drag,
on_pan_end=drop,
on_pan_start=start_drag,
left=0,
top=0,
content=ft.Container(bgcolor=ft.colors.GREEN, width=70, height=100),
)
card_2 = ft.GestureDetector(
mouse_cursor=ft.MouseCursor.MOVE,
drag_interval=5,
on_pan_update=drag,
on_pan_end=drop,
on_pan_start=start_drag,
left=100,
top=0,
content=ft.Container(bgcolor=ft.colors.BLUE, 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_1, card_2], width=1000, height=500))
ft.app(target=main)
上述代码中添加了第二张卡片,并且重构了之前的代码逻辑,将与拖动和放置有关的函数逻辑移到了 drop
函数中,并且对 start_drag
事件进行了定义。我们将保留之前的 Solitaire
类,用于存储第一张卡片的初始位置。
完整的代码可以在这里找到:step3.py。
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 = [slot, card1, card2]
page.add(ft.Stack(controls=controls, width=1000, height=500))
现在,如果你运行这个应用程序,你会注意到当你移动这些卡片时,黄色卡片(card2)正常移动,但是绿色卡片(card1)会被黄色卡片遮挡。这是因为card2被添加到stack的controls列表中card1后面。为了修复这个问题,我们需要在on_pan_start
事件中将可拖动的卡片移动到controls列表的顶部:
def move_on_top(card, controls):
"""将可拖动的卡片移动到堆栈的顶部"""
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
现在这两个卡片可以正常拖动了:
为了实现这个功能,我们需要获取卡槽中的卡片堆叠的信息,包括拖动的卡片所在的卡槽和目标卡槽。让我们重新构建我们的程序,为实现展开堆叠的功能做好准备。
卡槽、卡片和纸牌类
卡槽将具有pile
属性,用于存储放置在该位置的卡片列表。现在卡槽是一个Container
控件对象,我们不能向其添加任何新属性。让我们创建一个新的Slot
类,该类将继承自Container
类,并在其中添加一个pile
属性:
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)
类似于Slot
类,让我们创建一个新的Card
类,其中包含slot
属性,用于记住它所在的卡槽。它将继承自GestureDetector
类,并将所有与卡片相关的方法移到其中:
CARD_WIDTH = 70
CARD_HEIGTH = 100
DROP_PROXIMITY = 20
import flet as ft
class Card(ft.GestureDetector):
def __init__(self, solitaire, color):
super().__init__()
self.slot = None
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.color = color
self.content=ft.Container(bgcolor=self.color, width=CARD_WIDTH, height=CARD_HEIGTH)
def move_on_top(self):
"""将可拖动的卡片移到标准堆栈的顶部"""
self.solitaire.controls.remove(self)
self.solitaire.controls.append(self)
self.solitaire.update()
def bounce_back(self):
"""将卡片返回到其原始位置"""
self.top = self.slot.top
self.left = self.slot.left
self.update()
def place(self, slot):
"""将卡片放置到卡槽中"""
self.top = slot.top
self.left = slot.left
def start_drag(self, e: ft.DragStartEvent):
self.move_on_top()
self.update()
def drag(self, e: ft.DragUpdateEvent):
self.top = max(0, self.top + e.delta_y)
self.left = max(0, self.left + e.delta_x)
self.update()
def drop(self, e: ft.DragEndEvent):
for slot in self.solitaire.slots:
if (
abs(self.top - slot.top) < DROP_PROXIMITY
and abs(self.left - slot.left) < DROP_PROXIMITY
):
self.place(slot)
self.update()
return
self.bounce_back()
self.update()