[筆記] 一套打天下!|Flet 套件使用雜記

2024/03/07閱讀時間約 18 分鐘

文前碎碎唸

在著手開發一套程式時,會讓人覺得煩躁的考量點有哪些呢?

除了最基礎的功能性、目標平台、程式語言之外,另一個讓人頭痛的,應該就是 UI 的設計跟串接了吧?

尤其有些情況下,還必須被迫遷移到自己不熟悉的程式語言去,完全就是一場災難。

最近在做專案時,也有遇到相同的困擾,不過也因為這樣的需求,讓我在因緣際會下,接觸到了這款名為 Flet 的 Python 套件。

而在實際使用後發現,這個套件雖然好用,但可惜的是,不確定是因為本身相對年輕,還是大家普遍有更好的解方,它在網路上的討論度似乎沒那麼高,參考資料也相對少。


於是,在自己埋頭硬幹與撞牆的循環下,便萌生了寫下這些血淚經歷的念頭。

Flet 是啥咪

這裡再簡單說明一下,Flet 是一個以 Flutter 為基礎的介面框架,並再以不同程式語言進行包裝,讓開發者可以使用自己熟悉方式進行撰寫

raw-image


目前此套件已支援 Python 語言,而根據官網說明,後續應該會陸續堆出 Go 語言以及 C# 的版本。

raw-image


而 Flutter 則是一套由 Google 開發的跨平台邏輯架構,以開放語言開放語言 Dart 為核心,具有跨平台支援的特性,可以透過它開發多平台通用,且接近原生使用體驗的操作介面。

說起來,這個套件已經不是第一次出現在我的專案中了,在我不久前的《[突發專案] 攻防定番?|vocus 自製工具復活與試用 Flet 套件開發新介面》中,就有實際用它來為過去的程式重新撰寫簡單的操作介面,對於此套件基本使用方式有興趣的朋友,也可以先前往該篇做參考。

至於本篇的內容,由於屬於雜項筆記性質而非專門教學,因此會採用段落分隔主題的方式撰寫,如果不想逐一瀏覽內容,可以善用標題來尋找有興趣的部分喔。

本筆記撰寫時,最新 Flet 為 2024.2.14 釋出的 0.20.0 版本,由於此套件更新仍頻繁,撰文時也曾數次遇到因套件調整而造成輸出專案出錯的狀況,因此要參考的話請依實際情況斟酌服用。

撞牆注意

認識今天的主角後,接著來分享近期開發專案時,所領悟出的一些事項吧。


一刀斬天下?

首先就是這個看到「跨平台」三個字應該都普遍會有的疑問。

既然有多平台的支援性,那我是否就能一台電腦加上 Python 就打遍天下了呢?

以開發出的成品來看的話,這題的廣義答案為「是」,但很遺憾的還是有一定的但書

雖然在開發時可以單純使用 Python 進行,但就算這個套件再強大,有些系統面的限制依然是銅牆鐵壁無法輕易突破的。

像是我們依然無法在 Mac 以外的系統中,輸出 Apple 生態系中能夠使用的原生程式,而想要輸出 Android 程式的話,也還是要在電腦上安裝 Android  相關的 SDK 以及支援套件。


尷尬的網頁

網頁版應用雖然是一個例外的選擇,但缺點就是無法使用到較完整的系統特性。

目前要 Host 由 Flet 開發的網頁程式的話,也有其他套件需配合的條件。

raw-image

像是想要以 Python 方式搭建伺服器,就必須要使用 Fastapi 來建立。

而相對廣泛使用的 Flask 架構目前並沒有完整的官方支援,因此就開發自由度上還是有所限制。


行動版地雷

如果想開發行動設備的 APP 時,不確定否為系統限制,不論是 iOS 或 Android 平台,在專案中所使用的第三方套件,僅能是以「純粹 Python」所撰寫的才能使用。

像是 Pandas、Numpy、Pillow 等平常開發時常見也常用的套件,會因為其本身底層核心,或是依賴的內容包含了其他語言所開發的部分,而導致包裝時出現錯誤。

圖形處理套件 Pillow 無法直接打包,需要自行編譯

圖形處理套件 Pillow 無法直接打包,需要自行編譯

另外,在 Android 系統上的資料權限取得部分,除了預設程式本身的資料目錄外,似乎沒辦法在撰寫程式的時候輕易地進行設定。

直接寫入會出現權限錯誤

直接寫入會出現權限錯誤

就算使用 Flet 中的「File / Path Picker」也只能取得讀取路徑的權限,而無法進行資料寫入。

註:這部分還在釐清中,由於不是很熟悉 Android 開發生態,因此尚未確認是否為路徑選擇問題。

卡卡卡輸出

這就要說回到我在前一版本時的輸出經驗,如果在建立安裝檔時,沒有在參數中加入輸出詳細進度的選項,而建立過程又出現部分套件無法包裝的情況時,整個建立流程就會直接卡住,不會有錯誤訊息也不會自動終止。

表面上看起來還在執行,但已經卡住了

表面上看起來還在執行,但已經卡住了

因此這裡建議在輸出時,記得在參數部份加上「-vv」以將流程完整顯示,避免流程卡死在奇怪的地方了還在呆呆地等待,此時你的視窗已經像變心的情人一樣,不會再回應你了。

flet build apk -vv

另外在 Windows 桌面版的建立中,不同於過去 Pyinstaller 的包裝模式,以 build 指令輸出的 Windows 版本程式,並不會把專案包裝成單一執行檔,其中輸出資料夾中只要任一相關檔案遺失,裡面的 EXE 執行檔就無法運作。

將 EXE 拉出來單獨執行就會出錯

將 EXE 拉出來單獨執行就會出錯

不過「build」這個指令,也是在去年 2023 年末的更新中才加入的,而且官方也有在 Blog 中提到,預計在 2024 年第一季的 Roadmap 中,會著重改善這個建立流程,因此可以繼續期待未來的更新。

也建議在這個階段,想要穩定輸出正常運作專案的話,可以暫時將 Requirements 文件中的 Flet 版本固定住,待必要時再升級,以避免輸出後出現無法預期的錯誤。


奇異特性開發

雷點與注意事項看完後,接著來點輕鬆的項目,來看看一些特別的小技巧與特性吧!

這部分記錄的是一些專案開發過程中,因為想達成某些較特別效果而去嘗試出來的小特性,就依照自己的需求作參考囉。


無框之窗

不知道大家以前有沒有玩過桌面小寵物,或是其他沒有傳統「實體」視窗,有點類似 Widget 的浮動小程式呢?

桌面小寵物 Desktop Goose ,絕對不是桌布 XD

桌面小寵物 Desktop Goose ,絕對不是桌布 XD

其實這個形式在 Flet 中也辦得到,而且方式也十分簡單,這裡只要將視窗參數中的

  • window_title_bar_hidden
  • window_frameless

兩個項目都啟用,並且把視窗背景顏色參數

  • window_bgcolor
  • wbgcolor

調整為透明,就可以達到浮動效果。

page.window_title_bar_hidden = True
page.window_frameless = True
page.window_bgcolor = ft.colors.TRANSPARENT
page.bgcolor = ft.colors.TRANSPARENT

不過要注意的是,在這個模式下,程式的控制條整個會被隱藏,因此你無法直接對它進行拖曳或關閉,這時就必須再加上一個

  • ft.WindowDragArea

才能把控制選項放回來。

# Add Drag Area to Main Page
page.add(
# Create Drag Area Stack
ft.Row(
[
# Drag Area Item
ft.WindowDragArea(
# Container for Content in Drag Area
ft.Container(
# Title Text in Drag Area
ft.Text(
"Drag Move Maximize and restore application window."
),
# Settings For Drag Area
bgcolor=ft.colors.AMBER_300,
padding=10
),
expand=True
),
# Windows Close Button (Icon Button)
ft.IconButton(
ft.icons.CLOSE,
on_click=lambda _: page.window_close()
)
]
)
)

由於我自己開發的部分程式碼比較雜亂,這裡就只放改自官網的部分程式碼,接著來看一下效果吧!

桌面小 Widget 形式執行

桌面小 Widget 形式執行

另外如果想要讓它維持在所有視窗的最上層的話,再加上一個

  • window_always_on_top

的參數設定就可以了。

page.window_always_on_top = True

執行後就可以看到不論你開多少其他視窗,都不會把你的小工具壓在底下囉~


隔空傳輸

雖然不確定這個特性的實際應用價值,但既然驗證後可行,就一起記錄吧!

在 Flet 套件本身中,其實就有內建同網域下兩個 Session 互相溝通的功能,背後使用的是「PubSub」的溝通模式。

也就是其中一個 Session 對特定主題發出的訊息,可以同時傳送到所有訂閱該主題的 Session 視窗中。

透過這個功能,就可以在 Flet 中建立類似即時聊天室的應用程式。

而在自己的專案中,我則是想將這種特性應用在不同設備間的指令傳輸,也就是我能直接在電腦上,透過網頁或是另外的傳送程式,將指令直接發到手機上的 APP 中。

但遺憾的是,不管怎麼嘗試,我都無法順利將 PubSub 的溝通方式建立在兩個不同的應用程式間,也導致我原本對這個解決方案想像的破滅。

無計可施之際,我決定嘗試最原始的方案,直接在 APP 中建立一個 Flask 伺服器來放控制用的網頁,並且透過後端邏輯,直接與 Flet 視窗進行互動與觸發畫面更新。

自行開發的測試範例

自行開發的測試範例

這樣一來,只要兩台裝置都位於同樣的網路環境下的話,就能直接透過 IP 連線開啟介面,並直接透過瀏覽器對程式進行控制了。

其實這個方案的寫法不難,就直接將一個最基礎的 Flask 後端邏輯包裝進專案中,並設定在程式啟動時一併開起即可。

唯一的地雷點,大概就是 Flask 後端與 Flet 介面,兩者都是啟動後便會持續運作的,因此勢必得讓其中一者以 Threads 的方式在背景執行後,再執行另一個。

而由於我的網頁會直接控制介面更動,因此我選擇將 Flask 邏輯直接包在 Flet 的主要 Function 中,並在 UI 啟動前預先執行,以下是簡化後的大致架構。

# Flet
import flet as ft

# Server Related
import threading
import socket
from flask import Flask

# Start Server Function
port = 3000
app = Flask(__name__)

def web():
app.run(debug=False, host='0.0.0.0', port=port)

# Get Current IP
def get_ip():
try:
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.connect(("8.8.8.8", 80))
ip = s.getsockname()[0]
s.close()
except:
ip = "No Connection"
return ip

# Main Flet Page
def main(page:ft.Page):
#### Some GUI Stuff You Want
page.add(
ft.Text(
"Connect to : "+get_ip()+":"+str(port)
)
)
### Web Control (Connect to your device IP with browser to see this page)
@app.route('/')
def infopg():
info = "This is the page you will see while connected to same wifi."
return info

### Start Server as Thread
if __name__ == '__main__':
stat = False
try:
threading.Thread(target=web, daemon=True).start()
stat = True
except Exception as e:
stat = str(e)
page.add(
ft.Text("Server State : "+str(stat)))
# Update Page Content
page.update()

# Start Flet GUI
ft.app(target=main, assets_dir="assets")

執行後結果如下圖。

用瀏覽器連線到 APP 視窗的 IP 與 Port 即可看到網頁

用瀏覽器連線到 APP 視窗的 IP 與 Port 即可看到網頁

這樣就可以連線成功了,接著就是修改網頁接收到請求時,所觸發的動作即可。

而且由於網頁邏輯是包在 Main Function 中,因此可以直接的針對視窗中的內容進行更動及重整。


縮放自如

雖然大部分的情況下,我們應該不用針對畫面中的項目大小進行自動調整,大不了加一個滑動條讓使用者調整就好了。

但如果今天是特殊情況,像是單純手機由直向轉為橫向,想要直接讓畫面自動適應的話,該如何處理呢?

其實也不難,只要記得我們可以動態取得視窗的長寬,以及可以偵測到視窗大小變動事件,兩個主要特性就可搞定,那就直接上範例!

# Get Window Size and Return Calculated Text Size
def getsiz():
# Get Current Windows Size
pw = page.width
ph = page.height

# Get the Smaller Side as Base
fs = pw if ph > pw else ph

# Calculate New Text Size
siz = (fs/12)

return {"WSZ":{"W":pw,"H":ph},"SIZ":siz}

# Text for Window Size
winsiz = ft.Text(
str(round(pw))+":"+str(round(ph)),
size = getsiz()["SIZ"]
)

# Add Text to Page
page.add(winsiz)

# Resize Textdef
presiz(e):
siz = getsiz()

# Update Dispaly Value
winsiz.value = str(round(siz["WSZ"]["W"]))+":"+str(round(siz["WSZ"]["H"]))

# Update Size
winsiz.size = siz["SIZ"]
page.update()

# Detect Page Resize Action
page.on_resize=presiz

實際執行效果如下,注意視窗比例,以及文字大小的變化。

字體大小會依視窗比例更動

字體大小會依視窗比例更動

這樣一來,就能在視窗大小出現變化時,自動調整元件大小囉。


好啦,為了避免篇幅爆表,這次就先分享三個特別寫法,其他的之後有機會再記錄吧!

確定可使用套件

最後來列一下目前我專案中有使用,且確定可以包裝到手機 APP 版本中的套件。

  • flet> Flet Main Package [LINK]
  • flask> For Hosting Web Server
  • jieba> For Splitting Text
  • pyotp> For Generating OTP (One Time Password) Code
  • gtts> Google Text to Speech Package
  • feedparser> For Parsing RSS Feed
  • python-dateutil> Enhance Datetime Package
  • python-dotenv> Environment Variables 
  • requests> Https Request
  • opencc-python-reimplemented> TRD / SIM Chinese Translate
  • wikipedia> Wiki Searching Package
  • beautifulSoup4> Get Information from Webpage
  • natsort> Sorting
  • pypinyin> Transform Chinese into Pinyin Style

紀錄結束!


結語

以上,大概就是近期在嘗試使用新套件時,特別想記錄的一些事情啦!

其實除了記錄經驗外,還有一部分是在開發過程中,實在太難找到所需的資料了,常常搜尋解果翻了數頁後,就開始出現不相關的內容,又或是找到最後又回到攏統的官方文件。

雖然自己的火候依舊不足,只能邊摸索邊前進,但還是希望這份筆記能幫助到有需要的人,也歡迎有使用過這個套件的朋友補充交流喔。


最後還是再次提醒一下,這個套件更新十分頻繁,所以很可能今天所出的問題,隔了數天就被解決,又或是原本沒問題的部分,在某天就突然炸裂,所以就斟酌使用吧!

全文。待續

25會員
173內容數
偽命名並非無名,是為了意識的生存,取得身份的代號,成為數位生命的新載具。
留言0
查看全部
發表第一個留言支持創作者!