kivyMDチュートリアル其の弍什弍乃弍 Components - Menu続篇

ハロー、Qiita。いかがお過ごしでしょうか。

おぉ?、タイトルおかしくね?と思われる方もいらっしゃるかもしれませんが、
全くふざけるつもりはありません。いたって真剣です。まぁ、それは言い過ぎ
だけれども。

今日に至ってはおさらいというか以前動かなかったMenu篇を再度動かしてみる
という企画ですね。なんでこんなことしてるか分からない方のために少し振り
返ってみますと、以前のMenu篇で動かないものがあったんですよね。なので、
今日はバージョンアップもしたことだし、その続きとなります。なのでタイトル
に続篇という文言があるという。

そんなこと知らないよ、って方ともう忘れたよそんなことって方は以前のMenu
篇のページを見てもらえればと思います。今日は、以前に使用方法とかを扱って
いるので1からスタートするといったことをしません。

https://qiita.com/virty/items/5578d025d91fe626fa55

ということで今日は差分を取り扱っていく方向でやっていきたいと思います。
では今日も元気にえいえい、えいー。

## Menu

さて、どこから振り返るか迷うところがありますが、以前は全然Wrong節が触れ
られていませんでした。では、改めてそこからスタートするとします。

### Wrong

上側に記載されているWarningも一緒に見てみましょう。

:::note warn
Do not create the MDDropdownMenu object when you open 
the menu window. Because on a mobile device this one 
will be very slow!
:::

と同時にアンチパターンとなるコードも添えられています。
こちらも見てみましょう。

```python
menu = MDDropdownMenu(caller=self.screen.ids.button, items=menu_items)
menu.open()
```

要するに、クラス側とかでopenするなということでしょうか。それをした
とすると遅くなりますよ！と言われている気がします。あくまでkv側でon_re-
leaseプロパティのコールバックメソッドとして指定した方がよさそうですね。

というわけでここは変にすることなく、言われたことを忠実に守りましょう
(自戒含む)。

### Customization of menu item

お次はメニューアイテムのカスタム化ということになるでしょうか。なにやら、
概要が書かれていますね。見てみましょう。

> Menu items are created in the same way 
> as items for the RecycleView class.

ちなみにですが、RecycleViewクラスについてはIcon Definitions篇で取り
扱っているので、気になる方はこちらを参照いただければと思います。少々、
というか大分見にくいので、きっちりしたものを見たい方はマニュアル(kivy含む)
を参照してもらえればと思います。

https://qiita.com/virty/items/97499128a205113431d5

これからのコードは全て載っけるとデータ量が大幅に増加しますので、適宜、全容は
GitHubかもしくはマニュアルの方を見てもらえればと思います。といっても全く触れ
ないというのは少し寂しいので、必要なところを差分として掲載させることとします。
ということで必要差分を以下に記載します。

```python:xxii_ii/custom_menu.py
(略)

KV = '''
<RightContentCls>
    disabled: True
    adaptive_size: True
    pos_hint: {"center_y": .5}

    MDIconButton:
        icon: root.icon
        user_font_size: "16sp"
        md_bg_color_disabled: 0, 0, 0, 0

    MDLabel:
        text: root.text
        font_style: "Caption"
        adaptive_size: True
        pos_hint: {"center_y": .5}


<Item>

    IconLeftWidget:
        icon: root.left_icon

    RightContentCls:
        id: container
        icon: root.right_icon
        text: root.right_text


(略)
'''


class RightContentCls(IRightBodyTouch, MDBoxLayout):
    icon = StringProperty()
    text = StringProperty()


class Item(OneLineAvatarIconListItem):
    left_icon = StringProperty()
    right_icon = StringProperty()
    right_text = StringProperty()


class Test(MDApp):
(略)
        menu_items = [
            {
                "text": f"Item {i}",
                "right_text": f"R+{i}",
                "right_icon": "apple-keyboard-command",
                "left_icon": "git",
                "viewclass": "Item",
                "height": dp(54),
                "on_release": lambda x=f"Item {i}": self.menu_callback(x),
            } for i in range(5)
        ]
(略)
```

差分と言いながら、ほとんど載っけることになりました汗。細かく見てると
キリがないですが、重要なことはRightContentClsとItemクラス・レイ
アウトの2つということになりそうです。

#### RightContentCls

ということでこっちを見ていきます。全て見ていくと、3週間くらいの量となる
のでまずは概要だけ触れておきます。このクラスはIRightBodyTouchとMDBox-
Layoutのミックスインとなります。あとは、プロパティとして、iconとtext
プロパティを独自に持っています。

レイアウトとしては、それぞれのプロパティを持っていることからも分かるように、
横方向にアイコンとテキストが配置されることとなります。

少しさっぱりとした触れ込みになっていますが、なんかよく分からんぞという方は
以下の関連ページをご覧頂くか、関連するマニュアルのページをご覧頂ければと思い
ます。

https://qiita.com/virty/items/d5fa0a529912e6b0beb0

https://qiita.com/virty/items/29d792d5ecf4ad0f86be

https://qiita.com/virty/items/8475f11f314b76af21e1

結構前提知識がいるのは骨が折れるところですが、そこは頑張ってもらえればと思い
ます。1通り見終わったときには、景色がまた変わってくることでしょう。

#### Item

こちらのクラスとしては、left/right_iconとright_textをプロパティとして
持っていますね。もうすでに分かる方もいらっしゃると思いますが、right*プロ
パティはRightContentClsのプロパティと関連があります。あとは、OneLine-
AvatarIconListItemを継承していることもありで。

レイアウトはこれもプロパティ通りですが、左に配置させるアイコンとRightCon-
tentClsレイアウトの合体させたものになります。なぜこんなことできるの?という
方は以下のページもしくはマニュアルを見てもらえれば一目瞭然かと思われます。

https://qiita.com/virty/items/3871b85bc67179d67b30

あとは、設定したプロパティとかをTestクラス側でmenu_itemsリストに入れ込んで
いけばおしまいということになります。まぁ、省略したものを除けばという形にはなり
ますが。

### 結果

1つやっただけで結構大変w
少し省エネモードに入るかもです。

すみません、関係なかったですね。一応触れ込み自体は終わったので、ここで結果の
方を見てみたいと思います。

![160.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/82826/4c278b7b-d91c-7e98-0aa6-c4744fab7c7d.png)

以前は動かなかったものが、問題なく動かせることがわかりました。ボタンを押した
ときの挙動も問題ありませんね。

### Header Menu

この調子でサクサクいきたいと思います。「Customization of menu item」同様、
重要なところを抜き出してコードの抜粋を掲載したいと思います。

```python:xxii_ii/menu_with_header.py
(略)

KV = '''
<MenuHeader>
    orientation: "vertical"
    adaptive_size: True
    padding: "4dp"

    MDBoxLayout:
        spacing: "12dp"
        adaptive_size: True

        MDIconButton:
            icon: "gesture-tap-button"
            pos_hint: {"center_y": .5}

        MDLabel:
            text: "Actions"
            adaptive_size: True
            pos_hint: {"center_y": .5}


(略)
'''


class MenuHeader(MDBoxLayout):
    '''An instance of the class that will be added to the menu header.'''


class Test(MDApp):
(略)
        menu_items = [
            {
                "text": f"Item {i}",
                "viewclass": "OneLineListItem",
                "height": dp(56),
                "on_release": lambda x=f"Item {i}": self.menu_callback(x),
            } for i in range(5)
        ]
        self.menu = MDDropdownMenu(
            header_cls=MenuHeader(),
            caller=self.screen.ids.button,
            items=menu_items,
            width_mult=4,
        )

(略)
```

なるべく少なくと考えていましたが、思うほどコード量が減りませんね。。
まぁでも大事なことなので、肝心なことを放置するよりかは良いことです。

とまぁボヤいたところで、こちらも重要なところを触れていきます。

#### MenuHeader

触れるところというと、このサンプルコードからはこのMenuHeaderレイアウト・クラス
のみですね。まず、レイアウトは先ほどのRightContent-Clsとほとんど同じ構成となり
ます。なので、ここは特に触れることなく引用ページを見てもらえればすんなり理解頂ける
かと思います。

強いて触れるとなると(支離滅裂)、MenuHeaderレイアウトのすぐ下のorientationプロ
パティですが、デフォルトで水平方向なので縦積みをする(vertical)という設定がサンプル
同様に必要そうです。ですが、こちらは挙動がおかしくなるだろうと見て、コメントアウトして
みましたが特に変わった様子はありませんでした。ですが、余計な不具合を生み出さないように
もこの通りの設定はした方が無難かもしれません。

あとは、クラス側でも特にプロパティもないし、取り止めもありません。では、何が変わった
ことがあるかと言われると、Testクラス側でMDDropdownMenuオブジェクトを作るときに、
引数としてheader_clsプロパティにMenuHeaderをまたオブジェクト生成しているところに
なります。これをしないとヘッダーが設定されません。

### 結果

ということで、触れ込みも終わったのでこちらでも結果の方を見てみます。

![161.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/82826/6ffcb428-5ca7-bdf3-8e19-7789c362567f.png)

特に変わったこともありませんでしたね。ちゃんと機能がなされています。
動作的にも、これまで動かしてきたものとなんら変わりありません。

### Menu with MDToolbar

こちらはなにやら、仰々しい(失礼)入りで概要が書かれていますね。まずは見てみま
しょう。

> The MDDropdownMenu works well with the standard MDToolbar. 
> Since the buttons on the Toolbar are created by the MDToolbar 
> component, it is necessary to pass the button as an argument 
> to the callback using lambda x: app.callback(x).

少し、自信がないので依頼をしてみます。間違って伝えても意味ないし。

> MDDropdownMenuは、標準のMDToolbarと一緒に動作します。ツールバー上のボタンは、
> MDToolbarコンポーネントによって作成されるので、lambda x: app.callback(x)を
> 使用して、コールバックの引数としてボタンを渡す必要があります。

これで正確な結果が・・と思いましたが、「一緒に動作する」は〜と上手く機能するという
ことではないですかね・・?まぁ、でも色々多角的な視点を持つというのは大事なことです。
後半は結構大事なことを言っていますね。これだけでどのボタンが渡るかということを自動
でやってもらっています。

また、補足情報も記載がありますね。これも併せて見てみましょう。

:::note
This example uses drop down menus for both the righthand and 
lefthand menus (i.e both the ‘triple bar’ and ‘triple dot’ menus) 
to illustrate that it is possible. A better solution for the 
‘triple bar’ menu would probably have been MDNavigationDrawer.
:::

翻訳は、難しいは難しいですが要は今回両方異なるメニュー達で実現しているけど、
よりよい使い方は「トリプルバー(別名ハンバーガーメニュー)」はナビゲーションドロー
ワーを使うために置いておけよなということでしょうか。ナビゲーションドローワーは
また今度というか下手すると来週ですね、そのときに扱えればと思います。

で、あとは長らくお待たせしました。サンプルコードに入りましょう。こちらもこれまで
同様重要なところをピックアップしてみます。

```python:xxii_ii/menu_with_toolbar.py
(略)

KV = '''
MDBoxLayout:
    orientation: "vertical"

    MDToolbar:
        title: "MDToolbar"
        left_action_items: [["menu", lambda x: app.callback(x)]]
        right_action_items: [["dots-vertical", lambda x: app.callback(x)]]

    MDLabel:
        text: "Content"
        halign: "center"
'''


class Test(MDApp):
    def build(self):
        menu_items = [
            {
                "viewclass": "OneLineListItem",
                "text": f"Item {i}",
                "height": dp(56),
                "on_release": lambda x=f"Item {i}": self.menu_callback(x),
             } for i in range(5)
        ]
(略)

    def callback(self, button):
        self.menu.caller = button
        self.menu.open()

    def menu_callback(self, text_item):
        self.menu.dismiss()
        Snackbar(text=text_item).open()


Test().run()
```

ここでの注目ポイントは、クラス・レイアウト側どちらでもですが、MDToolbarになり
ますね。レイアウト側もこれまでと特に変わりないと思いますが、ここでもMDToolbar
が当然ですが、変化しているところになります。MDToolbarの詳細は以下もしくはマニュ
アルをご覧ください。

https://qiita.com/virty/items/3c726e2a7faa694fb54d

クラス側でも先ほどの概要でも記載の通りですが、callbackメソッドに関してはどちらの
メニュー("menu" or "dots-vertical")かということはleft/right_action_items
プロパティの中での無名関数の引数で送り出せることができます。

menu_callbackメソッドに関しても、menu_itemsの中でどのアイテムを渡すかということ
を今度は無名関数の引数を指定して、そのまま送り出せていることになります。

### 結果

ということで、こちらも結果の方を見てみましょう。

![162.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/82826/a9e5902e-1edf-a6c2-62f8-814983f58166.png)

![163.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/82826/b69d1894-4e2d-b36e-5978-cb2bc2cb93d4.png)

![164.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/82826/f040309e-247a-0ef6-0388-e6373e4e8791.png)

![165.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/82826/e10855df-f116-8992-750e-4aa52d92de4d.png)

1-2枚目は左メニューの中から「Item 2」を選んでみた動作になります。
3-4枚目は同様に右メニューから「Item 4」を選んでいます。

特に変わった動きなどは確認していません。問題はなさそうに見えました。

### Position menu - Bottom position

さて、今日のお題も最後となります。こちらはポジション決めに関するお題・プロパティに
あたりますが、Centerポジションに関しては位置決めの他に目新しいものは見当たらなか
ったので端折ります。ということでこちらも重要なところを抜き出します。

```python:xxii_ii/menu_position_bottom.py
(略)

MDScreen

    MDTextField:
(略)
        on_focus: if self.focus: app.menu.open()

(略)

        menu_items = [
            {
                "viewclass": "IconListItem",
                "icon": "git",
                "height": dp(56),
                "text": f"Item {i}",
                "on_release": lambda x=f"Item {i}": self.set_item(x),
            } for i in range(5)]
        self.menu = MDDropdownMenu(
            caller=self.screen.ids.field,
            items=menu_items,
            position="bottom",
            width_mult=4,
        )

    def set_item(self, text__item):
        self.screen.ids.field.text = text__item
        self.menu.dismiss()

(略)
```

もうほとんど、端折っています。。ですが、それほどこれまでに重要なところは触れて
いるということでもあります。見るべきポイントはMDDropdownMenuオブジェクトを
生成するときに指定するpositionプロパティだけになりますね。それ以外はもうすで
に出ているものになります。

このプロパティは言うまでもなく、メニューをどこに出すかということになりますね。
今回は下部と指定しています。

また、itemsプロパティで指定しているmenu_itemsの中にあるon_releaseプロパティ
のコールバックメソッドでset_itemメソッドを指定しています。あとで分かることですが、
メニューアイテムを選択したあとテキストフィールドに設定されるよう、このようなことを
やっています。

あとは、MDTextFieldでフォーカスされたときにself.menuがオープンされることも
必見ですね。

### 結果

いろいろと触れてきましたが、見た方が早いので結果を覗いてみましょう。

![166.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/82826/f74c5190-d2cc-3a78-d87a-70f4ddabaa20.png)

![167.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/82826/648c9c52-4d57-bb1c-adf4-cf282aefffa6.png)

フォーカスされたときにメニューが開かれて、なにかしら選択するとそれが取り込まれた
様子がわかるかと思われます。ユーザーに選択させてそれを自動で入力させたいときは、
重宝しそうですよね。

## API - kivymd.uix.menu.menu

はい、おしまい。
というわけにはいかないので、本日使ったAPIについておさらいをしておきます。
前回記載のあるところは端折って今回だけ使ったものを記載しておきます。

### class kivymd.uix.menu.menu.MDDropdownMenu(**kwargs)

`header_cls`

> An instance of the class (Kivy or KivyMD widget) that 
> will be added to the menu header.

> New in version 0.104.2.

> header_cls is a ObjectProperty and defaults to None.

バージョンアップをしたことでこちらが使えるようになりました。
なにかナビゲーションだとか補助的な用途で使いたいときにおすすめの
プロパティとなります。

`position`

> Menu window position relative to parent element. 
> Available options are: ‘auto’, ‘center’, ‘bottom’.

> position is a OptionProperty and defaults to ‘auto’.

最後あたりに出てきたものです。興味のある方はcenterも試してみると
視野が広がるかも。

`open(self)`

> Animate the opening of a menu window.

使い方次第ではパフォーマンスに影響が出るところです。注意書きを再度
確認した方がよいですね。

`dismiss(self)`

> Closes the menu.

見落としやすい(おそらく自分だけ)ですが、こちらのメソッドも注意が必要
です。他のウィジェットからメニューを呼び出して、アイテムを選んだあとに
メニュー自体を閉じたいときはこちらを指定する必要があります。

## まとめ

さて、いかがだったでしょうか。

結構見てきたサンプルの種類が長くなってしまい、要点が分かりにくくなりそう
ですが、使い方は一貫性があります。呼び出すウィジェットとMenuウィジェットは
分けておき、Menuウィジェットは関連するもの含めクラス側にて指定することが
セオリーとなります。

慣れてしまえば、お決まりのような使い方となりそうです。まぁ、ほとんどのウィ
ジェットは言ってしまえばそうなのですが。。分からなくなったときはこの形を
覚えておきましょう。

ということで、今日はこの辺としたいと思います。来週は今日ほんの少し出てきた
NavigationDrawer篇の予定です。ハンバーガーメニューと一緒に使ってくれよな
というところで触れましたね。こちらのウィジェットも強力なアイテムとなる予感
です。来週もお楽しみに。

それでは、ごきげんよう。

## 参照

> Components » Menu
> https://kivymd.readthedocs.io/en/latest/components/menu/

> DeepL 翻訳ツール
> https://www.deepl.com/ja/translator

## 番外編

とまぁ、終わったのになんか余計なものあるよ!と思われるかもしれません。
はい、今日のテーマとは関係はないかもしれないし、あるかもしれない、いや、
やっぱありませんね。

何をしたい・言いたいかというと、ツールバーを片付けておきたいのです。バージョン
アップ後の作業(ちゃんと動くか試す)リストにリストアップしてなかったしで。

では、どこかと言われると以下の部分です。以前ではツールバーのツールチップが表示
されていませんでした。誰が覚えているのってね、自分もド忘れしました。ちょうど、
ツールバーとメニューの組み合わせもあったのでちょうど良かったです。

https://kivymd.readthedocs.io/en/latest/components/toolbar/#tooltips

マニュアルとコードは変わらないしで、あとコードの全てはGitHubにもあるのでそちらを
ご参照ください。ここでは動いたよーという報告だけにしておきます。

![168.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/82826/740f31f8-64df-b832-10c7-160c5854ca1e.png)

問題ありませんね。ちなみにですが、右のケバブメニュー(言いたいだけ)もツールチップは
問題ありませんでした。はい、今度こそ以上です。お疲れ様でした。