要掌握 Compose,就得了解其元件庫有哪些元件可使用,以及各元件的使用方式。因此從今天開始,耕讀筆記將綜覽常用 UI 元件,了解各元件的名稱、可傳入的參數及實際顯示的結果。
在寫 Desktop UI 時,最外層的 UI 應該就屬 Window 了吧?今天將從這個元件下手,也會提一下怎麼設定 App Icon,讓您的 App 看起來更有個性!
在 application()
Lambda 裡呼叫的 Window()
(視窗)元件,就是應用程式 UI 的最外層,在這個「框」的裡面,才是一般使用者認知到、可操作的 UI。不過嚴格說來,Window()
本身就是一個 UI 元件,使用者可以透過視窗上的三個按鈕分別關閉應用程式、最大化及最小化,視窗上方的 Bar 可以讓使用者用拖曳的方式調整在整個桌面上的位置,也可以從視窗邊緣調整視窗大小,甚至一個 App 也能有不只一個 Window。而使用者可以如何跟視窗互動,取決於我們在宣告 Window()
元件時傳入的參數。
在使用 Compose 元件時,所有的設定都是透過函式參數傳入元件。因此要知道一個元件有哪些設定值可以調整,可以透過 IDE 的參數提示來了解,以下就將 Window()
元件的常用參數整理如下:
Window(
onCloseRequest = ...,
title = ...,
state = ...
icon = ...,
visible = ...,
undecorated = ...,
transparent = ...,
resizable = ...,
alwaysOnTop = ...,
) {
// ...
}
onCloseRequest
:設定當 Window 被關閉時的 Handler,若 App 只有一個 Window,一般來說會直接傳入 ::exitApplication
來讓整個 App 關閉。title
:設定 Window 的名稱,一般會顯示在 Window 的 Title Bar 位置上。state
:傳入一個 WindowState
實例來設定 Window 的狀態,包括 position
(位置)及 size
(尺寸)。icon
:可將 resources 裡的圖片透過 Painter
傳入,做為 Window 的圖示,依不同作業系統會有顯示上的差異(比方說在 macOS 上 Window 沒有圖示的設計)。visible
:可透過這個屬性顯示或不顯示 Window,預設為 true
。undecorated
:可設定是否顯示 Window 的 Title Bar 區域,預設為 false
。transparent
:設定 Window 是否為透明,預設為 false
。resizable
:設定 Window 是否能調整大小,若為 true
則滑鼠碰到 Window 邊緣時會變為調整寬度、高度或斜角的圖示,若為 false
則滑鼠碰到 Window 邊緣時沒反應,預設為 true
。alwaysOnTop
:設定 Window 是不是永遠在所有視窗的最上層,預設為 false
。Window
狀態(position
及 size
)Window()
元件的 state
參數可以讓我們設定其狀態,這個狀態包括 position
(位置)及 size
(尺寸)。要設定其狀態,需傳入一個 WindowState
實例,並設定其參數即可。
設定 position
時,若想直接以座標指定,可將 X 及 Y 值以 dp 設定後,以 WindowPosition
回傳:
Window(
state = WindowState(position = WindowPosition(500.dp, 100.dp))
) {
// ...
}
若想依照使用者的桌面大小動態置中,則可搭配 Alignment
設定 Window 的對齊方式:
Window(
state = WindowState(position = WindowPosition.Aligned(Alignment.Center)),
) {
// ...
}
若要設定 Window 的長寬,則用 DpSize
設定 size
屬性:
Window(
state = WindowState(size = DpSize(1440.dp, 768.dp)),
) {
// ...
}
雖然 Window 可以設定顯示時尺寸,但由於我們無法預測使用者顯示設備,有可能我們設定的尺寸大於顯示設備的尺寸。因此,在設定尺寸時,可以先偵測一下顯示設備的能力,再動態的依結果來設定。下以是筆者在 Compose for Desktop 官方範例裡發現這段程式碼:
fun getPreferredWindowSize(desiredWidth: Int, desiredHeight: Int): DpSize {
val screenSize: Dimension = Toolkit.getDefaultToolkit().screenSize
val preferredWidth: Int = (screenSize.width * 0.8f).toInt()
val preferredHeight: Int = (screenSize.height * 0.8f).toInt()
val width: Int = if (desiredWidth < preferredWidth) desiredWidth else preferredWidth
val height: Int = if (desiredHeight < preferredHeight) desiredHeight else preferredHeight
return DpSize(width.dp, height.dp)
}
Window(
state = WindowState(
position = WindowPosition.Aligned(Alignment.Center),
size = getPreferredWindowSize(500, 500)
)
) {
// ...
}
有了這個函式,就可以傳入預期的 Window 尺寸,函式會自動依照顯示設備能力做判斷,選擇最適合的尺寸顯示。
Window
顯示狀態(placement
)若有需要讓 Window
最大化或甚至是全螢幕,在 WindowState
裡還有一個參數是 placement
,可以指定 Window
的顯示狀態。想要讓 Window
最大化顯示,使用 WindowPlacement.Maximized
即可:
Window(
state = WindowState(
placement = WindowPlacement.Maximized
)
) {
// ...
}
或是讓 Window
全螢幕(WindowPlacement.Fullscreen
)也是可以的:
Window(
state = WindowState(
placement = WindowPlacement.Fullscreen
)
) {
// ...
}
Window
Title Bar 的 Theme由於現代人使用螢幕的時間都很長,各平台作業系統都推出 Light(亮)及 Dark(暗)兩種 Theme,甚至還能依照日光時間動態切換以保護眼睛。 既然作業系統內建的 Window 有不同的 Theme,那我們自行開發 App 的 Window 也得與作業系統同步才行!
可惜的是,我們無法直接從 Compose 程式碼設定 Window 的 Title Bar Theme,只能透過 JVM 參數在編譯時設定。打開專案的 Gradle Build Script,找到 nativeDistributions
一段,加上 jvmArgs("-Dapple.awt.application.appearance=system")
即可讓 Window 依作業系統的 Theme 來顯示。
compose.desktop {
// ...
application {
// ...
nativeDistributions {
// ...
jvmArgs(
"-Dapple.awt.application.appearance=system"
)
}
}
}
要注意必需從 Gradle Task 運行 App 才能看到這段設定的效果,若是從 main()
函式點擊綠色播放鍵運行的話,記得也要把這個 JVM 參數設定到對應的 Run Configuration 裡,才會生效喔!
另外,若想要指定 Theme 的話,則把這個參數的值從 system
換成 light
或 dark
即可。
預設的 Title Bar 其實沒什麼空間可以讓開發者做客製化,因此市面上看起來很炫的 Title Bar 作法可能是把原本的 Title Bar 藏起來,然後自己刻一個長得很像、操作行為也一致的 Title Bar 來偷天換日。
要在 Compose 裡做到這種效果,參考官方範例的作法,先將 undecorated
參數設為 true
將原生的 Title Bar 隱藏起來。接著在 Window 裡宣告一個 WindowDraggableArea()
元件,並在元件內繪製 Title Bar 的 UI,這樣就可以「模擬」出 Title Bar 的客製化效果。
Window(
onCloseRequest = ::exitApplication,
undecorated = true
) {
WindowDraggableArea {
Box(Modifier.fillMaxWidth()
.height(48.dp)
.background(Color.DarkGray))
}
}
不過使用這種作法時,要注意 Title Bar 得要畫得跟原生的一樣,客製化功能也不能太突兀,才不會破壞各作業系統的操作慣例與使用者的操作習慣。另外也要注意其他 UI 與 WindowDraggableArea()
的排版間距,元件間才不會彼此影響。
Window
圖示(icon
)若 App 的運行平台是 Windows,則支援在 Window 上顯示圖示。要設定 Window 的圖示,首先要將圖示的圖片放到專案 src/jvmMain/resources
資料夾底下,Compose for Desktop 的官方範例是使用 192 x 192 pixel、32 bit color 的 PNG 圖檔,接著以 painterResource()
函式指定圖示路徑,並設定到 Window()
元件的 icon
參數:
Window(
icon = painterResource("window-icon.png")
) {
// ...
}
由於每個作業系統特色不同,Window 的圖示不見得會顯示出來,因此開發者更在意的應該是 App 的圖示。
App 圖示也不是由 Compose 來設定,必需透過 Gradle 在構建過程將圖示素材依不同平台一併設定打包。首先將各平台的 App 圖示放到 src/jvmMain/resources
資料夾底下,開啟專案 Build Script,在 nativeDistributions
區塊,依官方範例輸入圖示相關的設定:
compose.desktop {
// ...
application {
// ...
nativeDistributions {
// ...
val iconsRoot = project.file("./src/jvmMain/resources")
macOS {
iconFile.set(iconsRoot.resolve("icon-mac.icns"))
}
windows {
iconFile.set(iconsRoot.resolve("icon-windows.ico"))
}
linux {
iconFile.set(iconsRoot.resolve("icon-linux.png"))
}
}
}
}
完成後再運行 App 時,應該就可以看到圖示不再是預設的 Java Duke 了!
透過今天對 Window()
元件的筆記整理,應該對視窗控制及設定有更多的認識,明天筆者將研究跟 Window 有密切相關的 MenuBar 元件。