kivyのButtonの色を変更する

kivyのButtonの色を変更する方法あれこれ

概要

Pythonのマルチプラットフォーム向けGUIライブラリkivyで ボタンButtonの色を変更しようとすると、なかなか大変なので色々試してみたメモ。

画像ファイルを使用して変更する

これが通常の方法。
プロパティbackground_normalbackground_downbackground_disabled_normalbackground_disabled_downに それぞれに表示する画像ファイルを指定する。
単色で表示するなら1✕1pixelの画像ファイルでかまわない。

ソース

ソースはこんな感じ。
別途画像ファイルlightgray.png'red.pnggray.pngをカレントディレクトリに用意しておく。

ソースは↓の「開く」をクリックすると表示されます。

if __name__ == '__main__':
    from kivy.app import App
    from kivy.uix.button import Button
    from kivy.lang import Builder

    # GUIlレイアウト
    Layout = Builder.load_string(
'''
# この中はインデントに意味があるので余計なインデントを入れてはいけない
BoxLayout:
    orientation:'horizontal'
    
    BoxLayout:
        orientation     : 'vertical'
        size_hint       : (0.3, 0.1)
    Button:
        id              : button_test
        text            : "TEST"
        size_hint       : (0.2, 0.1)
        pos_hint        : {'center_x': 0.5, 'center_y': 0.5}
        on_press        : app.on_test()
        background_normal: 'lightgray.png'
        background_down: 'red.png'
        background_disabled_normal: 'gray.png'
        background_disabled_down: 'gray.png'

    Button:
        id              : button_ctrl
        text            : "Ena/Dis"
        size_hint       : (0.2, 0.1)
        pos_hint        : {'center_x': 0.5, 'center_y': 0.5}
        on_press        : app.on_ctrl()
    BoxLayout:
        orientation     : 'vertical'
        size_hint       : (0.3, 0.1)
'''
    )

    class MyApp(App):
        def __init__(self):
            super().__init__()
            
            self.button_test = Layout.ids.button_test
            self.button_ctrl = Layout.ids.button_ctrl
        
        def build(self):
            return Layout
        
        def on_test(self) :
            print('pressed')
        
        def on_ctrl(self) :
            self.button_test.disabled = not self.button_test.disabled
            
    MyApp().run()

動作

実行すると、Buttonが2つ表示され、TESTボタンが指定されたファイルのイメージで表示される。
Ena/DisボタンをクリックするとTESTボタンのDisable/Enableが切り替えらる。

Base64エンコードデータで指定

画像ファイルを使用すると、使用する色の分だけ画像ファイルを用意し、処理を流用する度に 忘れずにすべてのファイルをコピーしないといけない。
そこで、画像ファイルをbase64エンコードした文字列をpyファイル(またはkvファイル)に保存する方法を試してみる。

まず、上で用意したpngファイルを以下のコマンドでbase64エンコード(前に特定の文字列を付加)する。

echo "data:image/png;base64,`base64 -w 0 ≪pngファイル≫`"

付加されているdata:image/png;base64,は続くデータがpngイメージであることを示している。

出力された文字列を以下のようにファイル名の代わりに記載する。

        background_normal           : 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAGHaVRYdFhNTDpjb20uYWRvYmUueG1wAAAAAAA8P3hwYWNrZXQgYmVnaW49J++7vycgaWQ9J1c1TTBNcENlaGlIenJlU3pOVGN6a2M5ZCc/Pg0KPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyI+PHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj48cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0idXVpZDpmYWY1YmRkNS1iYTNkLTExZGEtYWQzMS1kMzNkNzUxODJmMWIiIHhtbG5zOnRpZmY9Imh0dHA6Ly9ucy5hZG9iZS5jb20vdGlmZi8xLjAvIj48dGlmZjpPcmllbnRhdGlvbj4xPC90aWZmOk9yaWVudGF0aW9uPjwvcmRmOkRlc2NyaXB0aW9uPjwvcmRmOlJERj48L3g6eG1wbWV0YT4NCjw/eHBhY2tldCBlbmQ9J3cnPz4slJgLAAAADUlEQVQYV2M4fPjwfwAH3wNJzT7giwAAAABJRU5ErkJggg=='
        background_down             : 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAANSURBVBhXY3gro/IfAAVUAi3GPZKdAAAAAElFTkSuQmCC'
        background_disabled_normal  : 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAGHaVRYdFhNTDpjb20uYWRvYmUueG1wAAAAAAA8P3hwYWNrZXQgYmVnaW49J++7vycgaWQ9J1c1TTBNcENlaGlIenJlU3pOVGN6a2M5ZCc/Pg0KPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyI+PHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj48cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0idXVpZDpmYWY1YmRkNS1iYTNkLTExZGEtYWQzMS1kMzNkNzUxODJmMWIiIHhtbG5zOnRpZmY9Imh0dHA6Ly9ucy5hZG9iZS5jb20vdGlmZi8xLjAvIj48dGlmZjpPcmllbnRhdGlvbj4xPC90aWZmOk9yaWVudGF0aW9uPjwvcmRmOkRlc2NyaXB0aW9uPjwvcmRmOlJERj48L3g6eG1wbWV0YT4NCjw/eHBhY2tldCBlbmQ9J3cnPz4slJgLAAAADUlEQVQYV2Oor6//DwAFewJ9z0FqqgAAAABJRU5ErkJggg=='
        background_disabled_down    : 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAGHaVRYdFhNTDpjb20uYWRvYmUueG1wAAAAAAA8P3hwYWNrZXQgYmVnaW49J++7vycgaWQ9J1c1TTBNcENlaGlIenJlU3pOVGN6a2M5ZCc/Pg0KPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyI+PHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj48cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0idXVpZDpmYWY1YmRkNS1iYTNkLTExZGEtYWQzMS1kMzNkNzUxODJmMWIiIHhtbG5zOnRpZmY9Imh0dHA6Ly9ucy5hZG9iZS5jb20vdGlmZi8xLjAvIj48dGlmZjpPcmllbnRhdGlvbj4xPC90aWZmOk9yaWVudGF0aW9uPjwvcmRmOkRlc2NyaXB0aW9uPjwvcmRmOlJERj48L3g6eG1wbWV0YT4NCjw/eHBhY2tldCBlbmQ9J3cnPz4slJgLAAAADUlEQVQYV2Oor6//DwAFewJ9z0FqqgAAAABJRU5ErkJggg=='

これにより、pngファイルを削除しても動作するようになる。

ただし、pyファイル(またはkvファイル)が大きくなってしまうことと、一目で指定されている色が把握できないというデメリットがある。

canvas.befor で指定する

画像ファイルを用意すること自体面倒なので、RGBA値で指定する方法はないかと考えてみる。
Labelと同様にcanvas.beforeで背景色を指定してみることを試してみたが、うまくいかなかった。

    Button:
        ・・・
        canvas.before:
            Color:
                rgba    : (1,0,0,1)
            Rectangle:
                pos     : self.pos
                size    : self.size

これはButtoncanvas.beforeをサポートしていないわけではなく、 ちゃんと指定通りに描画しているが、 その前面にButtonのBackground_XXXが表示されているためらしい。

それならば、Buttonの表示を透明にしてやればcanvas.beforeの表示が見えるはずである。
以下のように変更して試してみる。
background_color : (0, 0, 0, 0)が透明色にしている部分で、
background_XXXをヌル文字列にしているのはすべてのピクセルを(255,255,255,255)にするためであるが、 透明になるのであまり関係ないかもしれない。

    Button:
        ・・・
        background_normal           : ''
        background_down             : ''
        background_disabled_normal  : ''
        background_disabled_down    : ''
        background_color            : (0, 0, 0, 0) # 透明
        canvas.before:
            Color:
                rgba    : (1,0,0,1)
            Rectangle:
                pos     : self.pos
                size    : self.size

これでボタンが赤色になった。
しかし、これでばボタンを押下した時やDisableにした時に色が変化しない。

そこで、さらに以下のようにして押下した時やDisableにした時に色が変わるようにしてみた。
canvas.before.Color.rgbaself.disabledself.stateによって変更している。
下にGUIリソース定義部分全体を記載しておく。

# 分かりやすいようにグローバル変数で定義しておく
#:set BG_COLOR_NORMAL   (0.75, 0.75, 0.75, 1.0)
#:set BG_COLOR_DOWN     (1.00, 0.00, 0.00, 1.0)
#:set BG_COLOR_DISABLED (0.50, 0.50, 0.50, 1.0)

BoxLayout:
    orientation:'horizontal'
    
    BoxLayout:
        orientation     : 'vertical'
        size_hint       : (0.3, 0.1)
    Button:
        id              : button_test
        text            : "TEST"
        size_hint       : (0.2, 0.1)
        pos_hint        : {'center_x': 0.5, 'center_y': 0.5}
        on_press        : app.on_test()
        background_normal           : ''
        background_down             : ''
        background_disabled_normal  : ''
        background_disabled_down    : ''
        background_color            : (0, 0, 0, 0) # 透明
        canvas.before:
            Color:
                rgba    : BG_COLOR_DISABLED if self.disabled else BG_COLOR_NORMAL if self.state == 'normal' else BG_COLOR_DOWN
            Rectangle:
                pos     : self.pos
                size    : self.size
    Button:
        id              : button_ctrl
        text            : "Ena/Dis"
        size_hint       : (0.2, 0.1)
        pos_hint        : {'center_x': 0.5, 'center_y': 0.5}
        on_press        : app.on_ctrl()
    BoxLayout:
        orientation     : 'vertical'
        size_hint       : (0.3, 0.1)

カスタムボタンウィジェットを作成する

上でとりあえず目的は達成されたが、ボタンを配置する度に上記のような設定を書くのは面倒なので、 カスタムボタンウィジェットを作成してみる。

ソース

ソースは↓の「開く」をクリックすると表示されます。
ダウンロードしたいときは こちら からどうぞ。

解説

上の「canvas.befor で指定する」の方法をkv言語を使用せずpythonで定義している。
それぞれの色を自由に変更できるようにプロパティを用意した。

また、そのままだとボタンを並べて表示したときに境界が分からなくなるので、枠線を描画するため、以下のプロパティを用意した。
デフォルトは黒で幅1。

初期化時にボタン本体の背景を透明にし、canvas.beforeに描画命令を追加して背景の描画(背景色と枠線)することと、 状態、位置、プロパティが変化したときに描画パラメータを再設定する処理をbindしている。
bindした処理では状態や位置に合わせてcanvas.beforeの処理のパラメータを変更している。

枠線を描画するためのLineはwidth×2の幅で描画されるようなので、本来の矩形のwidthの半分だけ内側に描画されるようにしている。

GUIレイアウトを定義するときは、通常のButtonと同様のプロパティ設定で配置できる。
もちろん、上の通常時の色、押下時の色、無効時の色を変更したい場合は追加で指定すれば良い。