I Can Has Cheezburger?

A graphical interface to get some fast food

avatar

Sébastien Boisgérault
Associate Professor, ITN Mines Paris – PSL

Photo by sk on Unsplash

At the beginning …

Flet is a Python library for building graphical user interfaces.

Implement the example given in its documentation to get familiar with it.

Flet counter

Graphical Architecture

Develop the graphical architecture of a menu ordering application, represented in the figure below:

Graphical user interface for menu order

You will have to explore the documentation of the components offered by flet.

At first:

  • Nothing is “functional” in your interface: all numeric values are coded “hard-coded”, nothing happens when you click on a button, etc.

  • Don’t worry if the appearance of your interface is not exactly the same as the one given in the example. It will always be time to come back later to refine this representation.

You can use https://emojipedia.org to find the emojis you need (🍔 hamburger, 🍟 fries, etc.)

Solution
from flet import app, icons
from flet import MainAxisAlignment
from flet import (
    Card,
    Column,
    Container,
    Divider,
    FilledButton,
    IconButton,
    Markdown,
    Row,
    Text,
    TextField,
)


def main(page):
    page.title = "I Can Has Cheezburger?"
    page.window_width = 400
    page.window_height = 430
    page.add(
        Column(
            alignment=MainAxisAlignment.CENTER,
            controls=[
                Row(
                    [
                        Text("🍔", size=50),
                        Text("5.95 €"),
                        Container(
                            width=100,
                            content=TextField(
                                value="0", read_only=True
                            ),
                        ),
                        IconButton(icon=icons.ADD),
                        IconButton(icon=icons.REMOVE),
                    ],
                    alignment=MainAxisAlignment.CENTER,
                ),
                Row(
                    [
                        Text("🍟", size=50),
                        Text("3.60 €"),
                        Container(
                            width=100,
                            content=TextField(value="0"),
                        ),
                        IconButton(icon=icons.ADD),
                        IconButton(icon=icons.REMOVE),
                    ],
                    alignment=MainAxisAlignment.CENTER,
                ),
                Row(
                    [
                        Text("🥗", size=50),
                        Text("8.30 €"),
                        Container(
                            width=100,
                            content=TextField(value="0"),
                        ),
                        IconButton(icon=icons.ADD),
                        IconButton(icon=icons.REMOVE),
                    ],
                    alignment=MainAxisAlignment.CENTER,
                ),
                Row(
                    [
                        Text("🥤", size=50),
                        Text("2.60 €"),
                        Container(
                            width=100,
                            content=TextField(value="0"),
                        ),
                        IconButton(icon=icons.ADD),
                        IconButton(icon=icons.REMOVE),
                    ],
                    alignment=MainAxisAlignment.CENTER,
                ),
                Divider(),
                Row(
                    [
                        Card(
                            Container(
                                Markdown(
                                    "**TOTAL:** 0.00 €"
                                ),
                                padding=10,
                            )
                        ),
                        FilledButton(
                            text="Buy", icon=icons.PAYMENT
                        ),
                    ],
                    alignment=MainAxisAlignment.SPACE_BETWEEN,
                ),
            ],
        )
    )


app(target=main)

Custom Component

The flet documentation explains how you can create your own components. Used wisely, this possibility should allow you to make the architecture of your command application more readable.

Ideally, we would like to have a Product component that takes care of the representation of a product, the display of its price as well as the counting of the number of units that the customer wishes to order. The resulting application could then take the following form:

from flet import app, icons
from flet import MainAxisAlignment
from flet import (
    Card,
    Column,
    Container,
    Divider,
    FilledButton,
    IconButton,
    Markdown,
    Row,
    Text,
    TextField,
)

from product import Product


def main(page):
    page.title = "I Can Has Cheezburger?"
    page.window_width = 400
    page.window_height = 430
    page.add(
        Column(
            alignment=MainAxisAlignment.CENTER,
            controls=[
                Product("🍔", 5.95),
                Product("🍟", 3.60),
                Product("🥗", 8.30),
                Product("🥤", 2.60),
                Divider(),
                Row(
                    [
                        Card(
                            Container(
                                Markdown(
                                    "**TOTAL:** 0.00 €"
                                ),
                                padding=10,
                            )
                        ),
                        FilledButton(
                            text="Buy", icon=icons.PAYMENT
                        ),
                    ],
                    alignment=MainAxisAlignment.SPACE_BETWEEN,
                ),
            ],
        )
    )


app(target=main)

Develop a class Product in a file product.py so that this new program works (as before).

Solution
from flet import icons
from flet import MainAxisAlignment
from flet import (
    IconButton,
    Container,
    Row,
    Text,
    TextField,
)


class Product(Row):
    def __init__(self, emoji, price):
        self.price = price
        self.emoji = emoji
        super().__init__(
            [
                Text(self.emoji, size=50),
                Text(f"{self.price:.2f} €"),
                Container(width=100, content=TextField(value="0")),
                IconButton(icon=icons.ADD),
                IconButton(icon=icons.REMOVE),
            ],
            alignment=MainAxisAlignment.CENTER,
        )

Locally Functional Component

Make sure that the Product component is functional locally. That is to say, that the buttons + and - of a product only affect the quantity of this product and not the quantity of another product. Do not worry about the total of the order for the moment. However, make sure that the quantity of a product cannot be negative.

Solution
from flet import icons
from flet import MainAxisAlignment
from flet import (
    IconButton,
    Container,
    Row,
    Text,
    TextField,
)

class Product(Row):
    def __init__(self, emoji, price):
        self.price = price
        self.emoji = emoji
        self.quantity = 0
        more = IconButton(
            icon=icons.ADD, on_click=self.add_one
        )
        less = IconButton(
            icon=icons.REMOVE, on_click=self.remove_one
        )
        self.price_field = TextField(
            value=str(self.quantity), read_only=True
        )
        super().__init__(
            [
                Text(self.emoji, size=50),
                Text(f"{self.price:.2f} €"),
                Container(width=100, content=self.price_field),
                more,
                less,
            ],
            alignment=MainAxisAlignment.CENTER,
        )

    def add_one(self, event):
        self.quantity += 1
        self.price_field.value = str(self.quantity)
        self.update()

    def remove_one(self, event):
        self.quantity -= 1
        self.quantity = max(self.quantity, 0)
        self.price_field.value = str(self.quantity)
        self.update()

Fully Functional Component

Two things are missing from our product component:

  • An attribute (or property) total that allows to know how much the chosen number of units of this component will cost.

    hamburgers = Product("🍔", 5.95)
    hamburgers.total  # 0.0 initially
  • A (optional) hook to signal to the user of the component that the number of units (and therefore the cost) of this product has changed. This hook will take the form of a callback function that we provide to the product when it is constructed:

    def print_hamburgers_total(event):
        print(hamburgers.total)
    
    hamburgers = Product("🍔", 5.95, on_change=print_hamburgers_total)

Make the necessary changes to the Product component so that it is fully functional.

Solution
from flet import icons
from flet import MainAxisAlignment
from flet import (
    IconButton,
    Container,
    Row,
    Text,
    TextField,
)

def do_nothing(event):
    pass

class Product(Row):
    def __init__(self, emoji, price, on_change=None):
        self.price = price
        self.emoji = emoji
        self.quantity = 0
        self.on_change = on_change if on_change else do_nothing
        more = IconButton(
            icon=icons.ADD, on_click=self.add_one
        )
        less = IconButton(
            icon=icons.REMOVE, on_click=self.remove_one
        )
        self.price_field = TextField(
            value=str(self.quantity), read_only=True
        )
        super().__init__(
            [
                Text(self.emoji, size=50),
                Text(f"{self.price:.2f} €"),
                Container(width=100, content=self.price_field),
                more,
                less,
            ],
            alignment=MainAxisAlignment.CENTER,
        )

    def get_total(self):
        return self.price * self.quantity

    total = property(get_total)

    def add_one(self, event):
        self.quantity += 1
        self.price_field.value = str(self.quantity)
        self.on_change(event)
        self.update()

    def remove_one(self, event):
        self.quantity -= 1
        self.quantity = max(self.quantity, 0)
        self.price_field.value = str(self.quantity)
        self.on_change(event)
        self.update()

Integration

Complete your application so that the total of the order is always up to date.

Solution
from flet import app, icons
from flet import MainAxisAlignment
from flet import (
    Card,
    Column,
    Container,
    Divider,
    FilledButton,
    Markdown,
    Row,
)

from product import Product


def main(page):
    page.title = "I Can Has Cheezburger?"
    page.window_width = 400
    page.window_height = 430

    total_markdown = Markdown("**Total:** 0.0 €")

    def on_change(event):
        total = sum([p.total for p in products])
        total_markdown.value = f"**Total:** {total:.2f} €"
        page.update()

    products = [
        Product("🍔", 5.95, on_change=on_change),
        Product("🍟", 3.60, on_change=on_change),
        Product("🥗", 8.30, on_change=on_change),
        Product("🥤", 2.60, on_change=on_change),
    ]

    page.add(
        Column(
            alignment=MainAxisAlignment.CENTER,
            controls=[
                *products,
                Divider(),
                Row(
                    [
                        Card(
                            Container(
                                total_markdown, padding=10
                            )
                        ),
                        FilledButton(
                            text="Buy", icon=icons.PAYMENT
                        ),
                    ],
                    alignment=MainAxisAlignment.SPACE_BETWEEN,
                ),
            ],
        )
    )


app(target=main)