iT邦幫忙

2022 iThome 鐵人賽

DAY 8
0

若視 Window 為 UI 的最外層,那 MenuBar 應該就算是第二層吧?MenuBar 是 Desktop App 很重要的元素,一般來說整個 App 提供的功能幾乎都會整理在選單裡,也會依照不同作業系統配上快速鍵顯示,讓使用者在使用的過程中可以一併學習和記憶。今天就要筆記一下 MenuBar 、與之關聯的元件,還有各元件的設定。

了解 MenuBar 相關元件及階層關係

其實應用程式選單是一個「階層式」的架構,在 Compose for Desktop 裡,與選單相關的元件共有 MenuBarMenuItemSeparatorCheckboxItemRadioButtonItem 共六個,其相互的階層關係以樹狀圖的方式呈現如下:

MenuBar
└── Menu
    ├── Item
    ├── CheckboxItem
    ├── RadioButtonItem
    └── Separator

從上面的圖例就可以看出,MenuBar 指的是應用程式的整個選單區域,可以把它視為一個容器,裡面裝了多個 Menu 及其下不同型式的 ItemMenuBar 根據不同作業系統有不同的顯示方式,比方說在 Windows 上,MenuBar 是顯示在視窗內的最上方;但在 macOS 上則是顯示在視窗外,集中顯示在桌面上方的全域選單裡。

MenuBar 底下,我們可以宣告數個 Menu,一般來說,每個 Menu 會放相同類別的功能選項。以 IntelliJ IDEA 為例,跟檔案有關的功能都會放在 File 選單裡、跟編輯有關的操作都會放在 Edit 裡,以此類推。Menu 本身外顯的只有其選單的名字,至於選單裡有哪些功能可以點選,要再宣告下一層的 Item

使用者最後會點選到的功能,都是在 Item 這一層,Compose for Desktop 有四種 Item 可使用:Item 就是一般的文字功能選單,CheckboxItemRadioButtonItem 可以做出較複雜的選擇互動,至於 Separator 只是拿來做為選項間視覺上的間隔,顯示時是一條水平線,並沒有實質的功能。

接著,就來筆記一下各 MenuBar 元件的設定用法。

MenuBar 元件

MenuBar 元件並沒任何設定值,本身只做為容器,盛裝其他選單元件。要注意的是,MenuBar 是跟著 Window 元件走的,所以宣告時要寫在 Window 元件底下。宣告一個基本選單的方式大致如下:

fun main() = application {
    Window(
        // ...
    ) {
        MenuBar {
            // ...
        }
    }
}

Menu 元件

做為 MenuBar 的下層,Menu 是盛裝 Item 的容器,有三個參數可供設定:

MenuBar {
    Menu(
        text = "...",
        mnemonic = 'C',
        enabled = true,
    ) {
        // ...
    }
}     
  • text:設定選單的外顯文字,是使用者實際會點選到的範圍。
  • mnemonic:將選單對應到鍵盤上的字元,當使用者按下 Alt 及該字元時可打開選單,預設為 null
  • enabled:設定選單是否啟用,若關閉則以灰色字樣顯示,預設為 treu

Item 元件

Item 是選單元件裡最底層的元件,也是使用者在選取時的終點,元件本身提供豐富的設定讓開發者自訂其行為:

Menu(/* ... */) {
    Item(
        text = "...",
        onClick = { /* ... */ },
        icon = null,
        enabled = true,
        mnemonic = null,
        shortcut = null,
    )
}
  • text:設定顯示在選項裡的文字。
  • onClick:設定點擊選項時要做的行為。
  • icon:設定選項文字前的圖示,預設 null 代表沒有圖示。
  • enabled:設定選項是否啟用,預設為 true
  • mnemonic:設定選項對應到鍵盤上的字元,當使用者按下 Alt 及該字元時等於點擊該選項,預設為 null
  • shortcut:設定選項對應的鍵盤快速鍵,預設 null 代表沒有快速鍵對應。

CheckboxItem 元件

CheckboxItemItem 的差別,在於 Checkbox 多了狀態,也因此在參數上多了一個 Boolean 值來紀錄狀態值。

Menu(/* ... */) {
    CheckboxItem(
        text = "...",
        checked = false,
        onCheckedChange = { /* ... */ },
        icon = null,
        enabled = true,
        mnemonic = null,
        shortcut = null,
    )
}
  • text:設定顯示在選項裡的文字。
  • checked:設定該選項是否被選取。
  • onCheckedChange:設定當選項值改變時要做的行為。
  • icon:設定選項文字前的圖示,預設 null 代表沒有圖示。
  • enabled:設定選項是否啟用,預設為 true
  • mnemonic:設定選項對應到鍵盤上的字元,當使用者按下 Alt 及該字元時等於點擊該選項,預設為 null
  • shortcut:設定選項對應的鍵盤快速鍵,預設 null 代表沒有快速鍵對應。

RadioButtonItem 元件

RadioButtonItemCheckboxItem 的差別在於選取的型式,設定值都是類似的。

Menu(/* ... */) {
    RadioButtonItem(
        text: String,
        selected: Boolean,
        onClick: () -> Unit
        shortcut: KeyShortcut? = null,
        icon: Painter? = null,
        enabled: Boolean = true,
        mnemonic: Char? = null,
    )
}
  • text:設定顯示在選項裡的文字。
  • selected:設定該選項是否被選取。
  • onClick:設定點擊選項時要做的行為。
  • shortcut:設定選項對應的鍵盤快速鍵,預設 null 代表沒有快速鍵對應。
  • icon:設定選項文字前的圖示,預設 null 代表沒有圖示。
  • enabled:設定選項是否啟用,預設為 true
  • mnemonic:設定選項對應到鍵盤上的字元,當使用者按下 Alt 及該字元時等於點擊該選項,預設為 null

Separator 元件

Separator 元件是在選項間顯示一條水平線,用於區隔選單裡不同區域的選項,屬於視覺修飾用。使用上也不需任何參數,直接呼叫即可。

Menu(/* ... */) {
    Item(/* ... */)
    Separator()
    Item(/* ... */)
}

多階層選單

了解各選單元件的使用方式後,就可以將以上這些元件綜合運用,寫出多階層選單了。範例如下:

MenuBar {
    Menu(
        text = "Item",
    ) {
        (1..3).forEach { topLevel ->
            Menu("SubMenu $topLevel") {
                if (isSubmenuShowing) {
                    (1..3).forEach { secondLevel ->
                        Item(text = "SubItem $secondLevel", onClick = { })
                    }
                }
            }
        }
    }
    Menu("Setting") {
        CheckboxItem(
            text = "Generate submenu?",
            checked = isSubmenuShowing,
            onCheckedChange = { isSubmenuShowing = !isSubmenuShowing }
        )
    }
}

值得一提的是,由於 Compose 的語法是 100% 原生的 Kotlin 程式碼,因此在宣告 UI 時可混合任何 Kotlin 語法進行條件判斷,運用 for 或 Collection 技巧產生多筆資料等技巧都是可以的。

設定 Menu Icon

Item 級別的元件都可以設定文字旁的 Icon,只要傳入一個 Painter 實例到 Itemicon 參數即可:

MenuBar {
    Menu("Menu1", mnemonic = 'A') {
        Item(
            text = "ResourceIcon",
            icon = painterResource("window-icon.png"),
            onClick = { /* ... */ }
        )
    }
    Menu("Menu2", mnemonic = 'B') {
        Item(
            text = "PainterIcon",
            icon = PainterIcon,
            onClick = { /* ... */ }
        )
    }
}

object PainterIcon : Painter() {
    override val intrinsicSize = Size(5f, 5f)

    override fun DrawScope.onDraw() {
        drawOval(Color(0xFFFFA500))
    }
}

我們可以用兩種方式來繪製 Icon:一種方式是使用圖片做為 Painter 的來源進行繪製,另一種方式是使用 Painter 類別,以向量繪製的方式呈現 Icon。

MenuBar 事件互動

在解析 ItemCheckboxItemRadioButtonItem 時發現,這三個元件都可以設定 Event Handler,根據不同元件,共有兩種事件:onClick(當滑鼠點擊時)及 onCheckedChange(當選中狀態改變時)。

透過監聽事件,我們就能改變應用程式狀態,進而改變顯示的結果。由於 Declarative UI 的核心精神之一,就是要讓狀態統一儲存在單一來源(即所謂 Single Source of Truth),因此在 Event Handler 裡,我們只改變應用程式的狀態,而不改變元件的外觀。透過 Compose 的 State 管理,各元件會自動監聽 State,若有變化就會自動依照新的狀態重繪 UI。

fun main() = application {
    var action by remember { mutableStateOf("None") }

    Window(/* ... */) {
        MenuBar {
            Menu("Item") {
                Item(text = "Cut", onClick = { action = "Cut" })
                Item(text = "Copy", onClick = { action = "Copy" })
                Item(text = "Paste", onClick = { action = "Paste" })
            }
        }
        
        DisplayActionScreen(action)
    }
}

在上面的例子裡,我們先宣告 action 變數來儲存使用者點擊到的選單名稱,預設為 None。當使用者點擊不同的 Item 時,在 Event Handler 裡我們就只改變 action 變數的值,而監聽 action 值的 DisplayActionScreen() 元件就會自動依照 action 的內容更新顯示。

關於 Compose 裡的狀態管理機制,後續會再有更深入的研究筆記,現階段先有基本觀念即可。

設定鍵盤快速鍵

前面有提到,各 Item 型的元件支援綁定鍵盤快速鍵,當使用者按下指定的快速鍵組合時,等同於用滑鼠點擊選單。只要針對要綁定鍵盤快速鍵的 Item 元件設定 shortcut 參數即可:

@OptIn(ExperimentalComposeUiApi::class)
fun main() = application {
    var action by remember { mutableStateOf("None") }

    Window(
        onCloseRequest = ::exitApplication,
        title = "MenuBar Keyboard Shortcut Demo",
    ) {
        MenuBar {
            Menu("File", mnemonic = 'F') {
                Item(
                    text = "Cut",
                    onClick = { action = "Cut" },
                    shortcut = KeyShortcut(Key.X, meta = true)
                )
                Item(
                    text = "Copy",
                    onClick = { action = "Copy" },
                    shortcut = KeyShortcut(Key.C, meta = true)
                )
                Item(
                    text = "Paste",
                    onClick = { action = "Paste" },
                    shortcut = KeyShortcut(Key.V, meta = true)
                )
            }
        }

        DisplayActionScreen(action)
    }
}

從上面的範例可以看到,shortcut 參數接受一個 KeyShortcut 物件,其封裝了鍵盤按鈕與輔助鍵的組合,以 Cut 為例,我們想要將 macOS 鍵盤的 ⌘ + x 綁定到這個選單,首先宣告 Key.X 綁定鍵盤的 x 鍵,再設定 meta = true 表示要同時按下 macOS 鍵盤的 ⌘ 鍵(⌘ 在程式裡被稱為 Meta 鍵)做組合。程式運行後按下 ⌘ + x,就可以在中間的 DisplayActionScreen() 元件看到 Last action: Cut 的字樣。

要注意的是,在設計鍵盤快速鍵時,要考量各作業系統的操作習慣。比方說在 macOS 上,使用者習慣用 ⌘ + x 做剪下的指令,但在 Windows 或 Linux 上,因為沒有 ⌘ 鍵,所以一般會用 Ctrl 鍵。因此比較好的作法,是在設定鍵盤快速鍵時,依不同作業系統設定 meta = truectrl = true

另外,由於 Key 類別還在實驗階段(Experimental),因此在使用前,需要在函式前加上 @OptIn(ExperimentalComposeUiApi::class) Annotation 的標記。

參考資料


上一篇
第 7 天:常用 UI 元件之 Window
下一篇
第 9 天:常用 UI 元件之 Text
系列文
傳教士的 Compose for Desktop 耕讀筆記30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言