Fletでいい感じなフレームレスウィンドウを作る

フレームレスウィンドウ

Fletにはタイトルバーのウィジェットが無く、かわりにAppBarがあります。しかし、このAppBarはデスクトップ向けのGUIとしてはかなり不格好。 (スマホ向けのデプロイも意識してのことだろうから仕方ないけども!)

そんなわけで、上記画像のようなタイトルバーを下記要件を満たす形で作ってみました。

  • タイトルバー部分をドラッグすることでウィンドウを動かせる。
  • ウィンドウはリサイズできる。
  • 右上にコントロールボタンを設定する。
  • 最大化ボタンは、ウィンドウ最大時と非最大時とでアイコンを分ける。

実装

内部はFlutterてことで、Dartっぽく書いてることもあり縦長ですがご愛嬌。

ポイントは最大化ボタンのところぐらいだろうか。ウィンドウの最大化タイミングは色々(画面上端にドロップ等)あるため、pageのon_window_eventに最大化アイコン変更の処理を持たせてます。 メソッドを上書いてますが、Pageを継承しても良しです。

一個注意なのが、Fletの標準アイコンによくある縮小ボタンぽいのが全然ないことです!限りなく近いicons.PHOTO_SIZE_SELECT_SMALLを使っていますが若干苦し紛れです。 もっといいのあれば教えてください!

それにしても、Material系ライブラリはプライマリカラーの設定一つでガラッと雰囲気を変えられるので、デザインからっきしの自分はかなりテンションが上がりますな。

import flet as ft
from flet import (
ButtonStyle,
Column,
Container,
Icon,
IconButton,
Page,
RoundedRectangleBorder,
Row,
Text,
Theme,
UserControl,
WindowDragArea,
colors,
icons,
padding,
)
class WindowControlButton(IconButton):
def __init__(self, *args, icon_size: int, **kwargs):
super().__init__(*args, icon_size=icon_size, **kwargs)
self.height = 30
self.width = 40
self.style = ButtonStyle(
shape=RoundedRectangleBorder(),
color=colors.ON_BACKGROUND,
overlay_color=colors.SURFACE_VARIANT,
)
class WindowTitleBar(UserControl):
def __init__(self, title: str, page: Page) -> None:
super().__init__()
self.page = page
self.title = title
self.maximize_button = WindowControlButton(
icons.SQUARE_OUTLINED,
icon_size=15,
on_click=self.maximized_button_clicked,
)
self.minimize_button = WindowControlButton(
icons.MINIMIZE_OUTLINED,
icon_size=13,
on_click=self.minimize_button_clicked,
)
self.close_button = WindowControlButton(
icons.CLOSE,
icon_size=13,
on_click=lambda _: self.page.window_close(),
)
def minimize_button_clicked(self, e):
self.page.window_minimized = True
self.page.update()
def maximized_button_clicked(self, e):
self.page.window_maximized = not self.page.window_maximized
self.page.update()
def change_maximized_button_icon(self):
# 最大化ボタンのアイコン変更
if self.page.window_maximized:
self.maximize_button.icon = icons.PHOTO_SIZE_SELECT_SMALL
else:
self.maximize_button.icon = icons.SQUARE_OUTLINED
self.update()
def build(self):
return Container(
Row(
[
WindowDragArea(
Row(
[
Container(
# ここにアプリアイコン
Icon(icons.CRISIS_ALERT, color=colors.SECONDARY),
padding=padding.only(5, 2, 0, 2),
),
Text(
self.title,
color=colors.SECONDARY,
),
],
height=30,
),
expand=True,
),
self.minimize_button,
self.maximize_button,
self.close_button,
],
spacing=0,
),
bgcolor=colors.PRIMARY_CONTAINER,
)
class MainWindow(Container):
def __init__(self, page: Page, title: str):
super().__init__()
page.window_title_bar_hidden = True
page.window_title_bar_buttons_hidden = True
page.theme_mode = "dark"
page.padding = 0
page.theme = Theme(color_scheme_seed=colors.CYAN)
page.on_window_event = self.page_window_event
self.page = page
self.title_bar = WindowTitleBar(title, self.page)
self.content = Column(
[
self.title_bar,
Container(
Text(value="Hello, world!"),
padding=padding.all(10),
),
],
spacing=0,
)
self.bgcolor = colors.BACKGROUND
def page_window_event(self, e):
# pageのウィンドウイベント内で最大化アイコンの変更
self.title_bar.change_maximized_button_icon()
def main(page: Page):
app = MainWindow(page, "Main window")
page.add(app)
if __name__ == "__main__":
ft.app(main)