Fletにはタイトルバーのウィジェットが無く、かわりにAppBarがあります。しかし、このAppBarはデスクトップ向けのGUIとしてはかなり不格好。 (スマホ向けのデプロイも意識してのことだろうから仕方ないけども!)
そんなわけで、上記画像のようなタイトルバーを下記要件を満たす形で作ってみました。
- タイトルバー部分をドラッグすることでウィンドウを動かせる。
- ウィンドウはリサイズできる。
- 右上にコントロールボタンを設定する。
- 最大化ボタンは、ウィンドウ最大時と非最大時とでアイコンを分ける。
実装
内部はFlutterてことで、Dartっぽく書いてることもあり縦長ですがご愛嬌。
ポイントは最大化ボタンのところぐらいだろうか。ウィンドウの最大化タイミングは色々(画面上端にドロップ等)あるため、pageのon_window_event
に最大化アイコン変更の処理を持たせてます。
メソッドを上書いてますが、Pageを継承しても良しです。
一個注意なのが、Fletの標準アイコンによくある縮小ボタンぽいのが全然ないことです!限りなく近いicons.PHOTO_SIZE_SELECT_SMALL
を使っていますが若干苦し紛れです。
もっといいのあれば教えてください!
それにしても、Material系ライブラリはプライマリカラーの設定一つでガラッと雰囲気を変えられるので、デザインからっきしの自分はかなりテンションが上がりますな。
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) |