Works by

Ren's blog

@rennnosuke_rk 技術ブログです

PythonでMacOS Xデスクトップアプリを作るときの選択肢【rumps/PyObjC】

この記事は、python Advent Calendar 2017【22日目】の記事になります。 本記事では、pythonMacのデスクトップアプリを作成できるライブラリrumpsPyObjCを紹介します。

pythonMac固有の機能を持つアプリを実装する

デスクトップアプリケーションをpythonで作る場合、以下のようなGUIツールキットの使用が検討できると思います。

GUIツールキットを使うことで、MacだけでなくWindowsUbuntu等、複数OSをサポートする形でアプリを作成できます。しかし(通知やメニューバーアプリなどの)Mac固有の動作がアプリに求められる場合、上記のようなツールキットだけでは実装できない場合があります。
じゃあSwiftで書けよという話になってくるのですが、どうしてもpythonのモジュール群を利用しつつMac専用の機能を利用したアプリを作りたいという場合に使えるのがrumpsPyObjCです。

rumps

rumpsはMacのメニューバーアプリ作成に特化したモジュールです。Macでのメニューバーアイコン実装・通知実装であれば、rumpsでとても簡単に実装できます。

インストール

インストールは例によってpip。とっても簡単。

pip install rumps

実装例

#!/usr/bin/env python
# coding: utf-8

import rumps


class App(rumps.App):
    def __init__(self):
        super(App, self).__init__("App")
        # メニューリストを登録
        self.menu = ["Parent"]
        # メニューは入れ子にできる
        self.menu["Parent"].add("Child1")
        self.menu["Parent"].add("Child2")
        self.menu["Parent"]["Child2"].add("GrandChild1")
        self.menu["Parent"]["Child2"].add("GrandChild2")
        # メニューバーアイコンも設定可能
        # self.icon = "icon.png"

    # rumps.clickedデコレータでもメニューを追加できる
    # rumps.clicked()の第一引数はメニューラベルになる
    # デコレートされた関数/メソッドは引数senderをとる
    @rumps.clicked("Alert!")
    def alert(self, _):
        # アラートダイアログ
        rumps.alert("Hello!")

    @rumps.clicked("Notify!")
    def notification(self, _):
        # Macの通知
        rumps.notification(message="Hello World!",
                           title="Hello!",
                           subtitle="World!")

    @rumps.clicked("Off")
    def switch(self, sender):
        # メニューにチェックマークをつける
        sender.state = not sender.state
        sender.title = "On" if sender.state else "Off"

    @rumps.clicked("Show Window!")
    def window(self, _):
        # テキストエディットを含むウィンドウを表示
        rumps.Window(message="Showing Window!",
                     title="Window",
                     default_text="default text...",
                     ok="Submit!",
                     cancel="Cancel...").run()

    @rumps.clicked("Start Timer!")
    def timer(self, _):
        # 一定時間ごとに処理を実行するタイマー

        count = 0

        # callback関数は引数にTimerオブジェクトをとる
        def counter(t):
            nonlocal count
            count += 1
            print(count)
            if count >= 10:
                print("Stop Timer!")
                t.stop()

        # タイマーオブジェクト
        # 一定時間ごとにcallbackを呼び出す
        timer = rumps.Timer(callback=counter, interval=1)
        timer.start()


# クラスメンバでなくとも、clickedデコレータを関数に付加するとメニューは追加される
@rumps.clicked("outer")
def outer(_):
    pass


if __name__ == "__main__":
    App().run()

実行結果

f:id:rennnosukesann:20171222213042p:plain

1. アラートダイアログ

おなじみのアラートダイアログ。

f:id:rennnosukesann:20171222212004p:plain

2. 通知

Macの通知です。通知センターでも管理対象となります。

f:id:rennnosukesann:20171222212455p:plain

3. メニューにチェックマークをつける

sender.stateのbool値がTrueの場合、メニューにチェックマークが付きます。

f:id:rennnosukesann:20171222212738p:plain f:id:rennnosukesann:20171222212834p:plain

4. テキストエディットを含むウィンドウを表示

テキストエディット付きウィンドウ。

f:id:rennnosukesann:20171222214531p:plain

5. 一定時間ごとに処理を実行するタイマー

GUIコンポーネントの類ではありませんが、Timerクラスを使うことで一定時間ごとの非同期処理が簡単に実装できます。

f:id:rennnosukesann:20171222214423g:plain


上記のコードでrumpsが提供する機能の殆どは網羅できていると思います。 メニュー作成+クリックイベントの紐付けをデコレータで実装でき、通知やアラート等も一行で書けて簡単です。 個人的にはメニューバーを起点とするMacのアプリを作ることが最近多いので、非常に重宝しています。

一方、rumpsは非常にシンプルで使いやすいのですが、機能が不足している感も否めません。 おそらくrumpsをいじくるうちに

  • メニューバーアイコンをクリックした時点でウィンドウを開きたい
  • Macの通知センターを使いたい
  • なんならSwiftでできることをpythonでやりたい ...etc

といった欲求が湧いてくると思います。

pythonでより柔軟にMac上で動作するアプリを作れないかと調べた所、Objective-Cpython経由で操作するラッパーとしてPyObjCと言うものがある模様。こちらについて調べてみました。

PyObjC

PyObjCpythonObjective-Cの双方向ブリッジで、これを使うことでpythonからObjective-Cクラスライブラリにアクセスできるとのこと。 「双方向」ブリッジというだけあって、pythonからObjective-Cが使えるだけでなく、Objective-Cからもpythonが使えるそうです。

インストール

インストールは例によっt(ry

pip install pyobjc

何はともあれHello,World

#!/usr/bin/env python
# coding: utf-8

from Foundation import NSLog

if __name__ == '__main__':
    # Hello, World!
    NSLog('Hello, World!') #2017-12-22 22:43:18.523 python[5737:43506794] Hello, World!

Foundationモジュールは、Objective-Cの基本機能がまとめられたもののようです。
Foundationモジュール中のNSLogを使うと、pythonの標準出力とは異なる形式で文字列がコンソール上に出力されました。

アラートダイアログ

#!/usr/bin/env python
# coding: utf-8

from AppKit import NSAlert

if __name__ == '__main__':
    # Hello, World!
    alert = NSAlert.alloc().init()
    alert.setMessageText_(u'Hello, World!')
    alert.runModal()

f:id:rennnosukesann:20171222225528p:plain

NSAlertクラスでアラートを出現させることができます。 但し普通にNSAlertでそのままインスタンス化はできず、NSAlert.alloc().init()で領域確保→初期化からのインスタンス取得をする必要があります。
このあたりの記述はObjective-Cにバインドする都合上仕方ないのかもしれませんが、少し冗長になっている印象を受けました。

メニューバーアイコン+メニュー

#!/usr/bin/env python
# coding: utf-8

from Foundation import *
from AppKit import *
from PyObjCTools import AppHelper
import rumps

class MenuBarApp(NSObject):

    def applicationDidFinishLaunching_(self, notification):

        # ステータスバー
        statusbar = NSStatusBar.systemStatusBar()

        # ステータスアイテム(アイコン)
        self.statusitem = statusbar.statusItemWithLength_(NSVariableStatusItemLength)

        # アイコンの設定
        image = NSImage.alloc().initByReferencingFile_('icon.png')
        self.statusitem.setImage_(image)

        # メニュー
        self.menu = NSMenu.alloc().init()
        # メニューアイテムその1
        menuitem = NSMenuItem.alloc().initWithTitle_action_keyEquivalent_('Hello, World!', 'hello:', 'world!')
        self.menu.addItem_(menuitem)
        # メニューアイテムその2
        menuitem = NSMenuItem.alloc().initWithTitle_action_keyEquivalent_('Quit', 'terminate:', '')
        self.menu.addItem_(menuitem)

        # メニューをステータスアイテムに紐付け
        self.statusitem.setMenu_(self.menu)

    def hello_(self, notification):
        rumps.notification("Hello, World!", "hello!", "world!")


if __name__ == "__main__":
    app = NSApplication.sharedApplication()
    delegate = MenuBarApp.alloc().init()
    app.setDelegate_(delegate)
    AppHelper.runEventLoop()

f:id:rennnosukesann:20171222233409p:plain

f:id:rennnosukesann:20171222233455p:plain

f:id:rennnosukesann:20171222233442p:plain

NSStatusBar.systemStatusBar()からステータスバーオブジェクトを取得し、そこにステータスアイテム(アイコン)を登録していくみたいですね。 rumpsの方がObjective-Cクラスライブラリ周りの記述について知らなくても良くて楽です。

終わりに

rumpsは簡単なMacのアプリケーションを作る場合には楽ですが、複雑なアプリケーションの作成にはやや不向きです。

PyObjCの場合、Objective-Cの提供する機能が(おそらく)全て使えるので柔軟な分、Objective-Cの流儀に合わせていく必要があるところが難しいのではと感じました。

今回は時間の都合上、PyObjCを十分に深掘りすることができてません。。。
慣れていけばrumpsより複雑なことができると思うので、もっとさわっておこうと思います。