LibreOffice5(55)Javaの例:GUIをPythonにする その1

MessageBox.javaをPythonにします。close()の引数のブーリアンの意味がわからなかったのでそれも調べました。

前の関連記事:LibreOffice5(54)Javaの例:GUIを実行する


MessageBox.javaをPythonに翻訳する


GUI/messagebox.py at 3141def010fe74b77a016bbcfe1ab0a66bc00be4 · p--q/GUI

LibreOfficeとの接続部分以外はJavaと似たような構造のままPythonに翻訳しました。

画面がハイコントラスモードかどうかをisHighContrastModeActivated()の戻り値でTrue/Falseを得ています。

ハイコントラストモードについて応用する機会はなさそうですし、そもそも計算に使っているXVclWindowPeerインターフェイスがdeprecatedなのでこれはわざわざ計算せずにbishighcontrast = Falseと決め打ちすることにします。

ハイコントラストモードかどうかを判断する方法はこのdeprecatedされたインターフェイスを使うしかないようです(Creating Menus - Apache OpenOffice Wiki)。

Javaの例ではやたらtry文やif文でのエラーチェックやオブジェクトのチェックがでてきます。

必要性がよくわからないのでこれらは削除することにしました。

GUI/messagebox.py at 044c71d5105fb11cb7a5bbec0ca734a866a95114 · p--q/GUI · GitHub
def main(ctx, smgr):  # ctx: コンポーネントコンテクスト、smgr: サービスマネジャー
    desktop = smgr.createInstanceWithContext("com.sun.star.frame.Desktop", ctx)
    with loadmodel(desktop) as model:
        peer = model.getCurrentController().getFrame().getContainerWindow()
        bishighcontrast = False
        msgbox = peer.getToolkit().createMessageBox(peer, ERRORBOX, BUTTONS_OK, "My Sampletitle", "HighContrastMode is enabled: {}".format(bishighcontrast))
        msgbox.execute()
        msgbox.dispose()
@contextmanager
def loadmodel(desktop):
    prop = PropertyValue(Name="Hidden", Value=True)
    model = desktop.loadComponentFromURL("private:factory/swriter", "_blank", 0, (prop,)) 
    yield model
    if hasattr(model, "close"):
        model.close(False)
    else:
        model.dispose()    
メッセージボックス表示に必要なコードのみに絞るとこんなにすっきりしたコードになりました。

ドキュメントの開閉はコンテキストマネージャにした関数を使いました(20行目)。

コンテキストマネージャの実装はPython Cookbookの例09.22.defining_context_managers_the_easy_wayに従いました。

メッセージボックスを作っている17行目のcreateMessageBox()メソッドの引数にメッセージボックスのタイプにERRORBOX、ボタンタイプにBUTTONS_OKを指定しています

Calc(2)課題2:選択範囲の行列番号をメッセージボックスに表示でマクロでやったときはデスクトップ→フレーム→コンテナウィンドウ、という順に取得できたものが、今回は peer = desktop.getCurrentFrame().getContainerWindow()としてもフレーム取得の時点でNoneが返ってきてしまいます。

LibreOffice(32)デベロッパーガイド4:コンポーネントフレームワークの図にある通りモデル→コントローラ→フレーム→コンテナウィンドウと取得しないといけませんでした。

Hiddenを付けずにWriterドキュメントを開くようにするとメッセージボックスはモダルダイアログとして開きます(モダルダイアログについてはLibreOffice5(20)Anaconda3のtkinterモジュールを使う参照)。

Hiddenを付けずに開いたドキュメントのフレームはデスクトップのFramesには入っていますがCurrentFrameには入っていませんでした。

画面に表示されているドキュメントのフレームがCurrentFrameに入ると思っていましたがそうではないようです。

CurrentFrameにするにはマウスで選択するなどしてアクティブにしないといけないようです。

XCloseableインターフェイスのclose()メソッドの引数のブーリアンDeliverOwnershipの意味


ドキュメントを閉じるにはXCloseableインターフェイスのclose()メソッドを使います。

このclose()メソッドの引数であるブーリアンDeliverOwnershipの意味が理解できなかったので調べてみました。

Closing Documents - Apache OpenOffice Wiki

デベロッパーガイドに解説がありますがなかなか理解が難しいです。

LibreOffice(32)デベロッパーガイド4:コンポーネントフレームワークの「オフィスコンポーネントのモデルは共有が可能」の図にあるように複数のフレームが一つのモデルを共有することが可能です。

このようなモデルやフレームなどのオブジェクトが依存関係にある状態で、各オブジェクトをdispose()で破棄してしまうとデットロックやクラッシュが起きてしまう場合があります。

そこでモデルやフレームなどのオブジェクトはXCloseableインターフェイスを実装することによって、dispose()の代りにclose()を使って、デッドロックやクラッシュが起こらないように調整してからオブジェクトを破棄するようにしています。

XCloseableインターフェイスを実装したモデルやフレームはリスナーXCloseListenerの登録を受けています。

モデルやフレームからclose()が呼び出されるとまずリスナーにdispose()してよいかの許可を求めます。

反対するリスナーがない場合は、許可を求めたモデルやリスナーは自らdispose()を呼び出して消滅します。

許可を求められたリスナーは通知してきたオブジェクトが破棄されると困るときは、理由を付けて例外CloseVetoExceptionを返します。

リスナーから反対が返ってきた場合のXCloseableの対応の仕方がDeliverOwnershipによって変わってきます。

DeliverOwnershipがFalseのときは、単にオブジェクトの破棄を諦めます。

オブジェクトを破棄したい場合は、リスナーの反対してこないことを見計らって再度close()を実行しないといけません。

DeliverOwnershipがTrueのときは、オブジェクトをdispose()する権限をdispose()を反対してきたリスナーに譲ります。

そしてそのリスナーがそのオブジェクトをdispose()します。

そのときにはまた最初と同じようにすべてのリスナーにdispose()してよいかを通知します。

再びそのdispose()を反対するリスナーがあったときは最初と同様にdispose()する権限をその反対するリスナーに譲ります。

そして反対するリスナーがいなくなったところでようやくdispose()されます。

dispose()する権限を譲ってしまった後にその権限を取り戻す方法はないので、DeliverOwnershipをTrueにしたときはdispose()するまでの流れを止める方法はありません。

ということで、GUIからの操作でダメならまたあとでボタンをクリックし直せばよいというときはDeliverOwnershipをFalse、もうあとでまたclose()を呼び出す機会がない場合はDeliverOwnershipをTrueにすればよいと思います。

ということでmessagebox.pyではclose(False)ではなくてclose(True)にするべきですね。

デッドロックやクラッシュが起こらないようにclose()を実装しているオブジェクトではdispose()ではなくclose()を使わないといけません。

LibreOffice APIのXCloseableインターフェイスのclose()メソッドの解説


LibreOffice: XCloseable Interface Reference

理解するのにこのLibreOffice APIの解説ざっくり日本語にしたので載せておきます。


void close ( [in] boolean   DeliverOwnership )
raises ( CloseVetoException
)

オブジェクトをcloseする。

close()はdispose()の前に実行しなければいけません。しかし走っている内部プロセスをまだキャンセルできないときはclose()を拒否できるので、close()を実行したからといって必ずオブジェクトを閉じることができるわけではありません。内部プロセスが終了するまでclose()を保留しておくことはできず、内部プロセスをキャンセルできなければ例外CloseVetoExceptionを発生させてそのオブジェクトがclose()することをキャンセルしないといけません。

いずれの内部プロセスもキャンセルする前に、登録されているすべてのリスナーXCloseListenerへ通知しないといけません。いずれのリスナーも例外CloseVetoExceptionを拒否できます。呼び出し元がこの情報を取得する必要があるため、呼び出されたclose()メソッドの内部でこの例外を受け取ることは禁じられています。

例外CloseVetoExceptionがどこかで拒否された場合、走っていた内部プロセスが終了した後にどの他のオブジェクトがそのオブジェクトを再びclose()するのかを決めておかなければなりません。そこで引数DeliverOwnershipがそれを采配します。引数DeliverOwnershipがFalseのときは、close()を呼び出したオブジェクトしかそのオブジェクトをclose()できず、登録済みのリスナーXCloseListenerでもそのオブジェクトをclose()できません。引数DeliverOwnershipがTrueのときは、例外CloseVetoExceptionを拒否したリスナーがclose()することになります。この変更はリスナーのqueryClosing()メソッドで他のリスナーに通知されます。内部のプロセスが終了したそのリスナーはオブジェクトをcloseしようとしないといけません。このリスナーによるclose()の引数がDeliverOwnershipがTrueで、再び他のリスナーに例外CloseVetoExceptionが拒否されたときはそれを拒否したリスナーが再びclose()することになります。close()する権限を取り戻す方法はありません。

close()メソッドがすでにそのオブジェクトに対して呼び出されていた場合、何も起こりません。すでにdispose()したオブジェトやclose()したオブジェクトに対してclose()を呼び出したとしても通常は発生させるような例外RuntimeExceptionや例外DisposedExceptionは発生させないでください。例外はCloseVetoExceptionのみを扱うようにしてください。

パラメーター
DeliverOwnership(所有権を引き渡す)
TRUE:  closeしようとしているこのオブジェクトの所有権を例外CloseVetoExceptionを発生させるオブジェクトの一つに譲ります。この新しい所有者は、その実行中のプロセスが終了すると譲り受けたオブジェクトを再度closeしなといけません。
FALSE: 所有権を他のオブジェクトに譲りません。例外CloseVetoExceptionsに対応してclose()を止めます。close()したければあとで再度close()しないといけません。これは、一般的なユーザーインターフェイス処理に役立ちます。
例外
CloseVetoException close()しようとするオブジェクト自身またはそれに登録されているリスナーがこのclose()を拒否することを示します。

close(True)でドキュメントを閉じることにした


ということでWriterドキュメントはclose()を実装しているのはわかっているのでdispose()する部分を削除し、close()の引数はTrueに変更することにしました。

GUI/messagebox.py at f4b7168e82a851819bd4669870634ca1676fa208 · p--q/GUI
def main(ctx, smgr):  # ctx: コンポーネントコンテクスト、smgr: サービスマネジャー
    desktop = smgr.createInstanceWithContext("com.sun.star.frame.Desktop", ctx)  # コンポーネントコンテクストからデスクトップをインスタンス化。
    with loadmodel(desktop) as model:  # Wirterドキュメントを隠し属性で開いてモデルを取得。処理が終了したらモデルを閉じる。
        peer = model.getCurrentController().getFrame().getContainerWindow()  # モデルからコントローラを取得して、コントローラからフレームを取得して、フレームからコンテナウィンドウ=ピアオブジェクトを取得。
        bishighcontrast = False  # メッセージボックスに表示させる文字列にする。
        msgbox = peer.getToolkit().createMessageBox(peer, ERRORBOX, BUTTONS_OK, "My Sampletitle", "HighContrastMode is enabled: {}".format(bishighcontrast))  # ピアオブジェクトからツールキットを取得して、peerを親ウィンドウにしてメッセージボックスを作成。
        msgbox.execute()  # メッセージボックスを表示。
        msgbox.dispose()  # メッセージボックスを破棄。
@contextmanager  # コンテクストマネージャーを作成。
def loadmodel(desktop):
    prop = PropertyValue(Name="Hidden", Value=True)  # 隠し属性を設定。
    model = desktop.loadComponentFromURL("private:factory/swriter", "_blank", 0, (prop,)) # 新規Writerドキュメントを新しいフレームに開く。
    yield model
    model.close(True)  # モデルを閉じる。
これでMessageBox.javaの翻訳は完了しました。

参考にしたサイト


Windows でハイ コントラスト モードをオンにする
Windowsのバージョンによって切り替え方法が異なるようです。

Creating Menus - Apache OpenOffice Wiki
ハイコントラストモードかを判別する関数が載っています。

Closing Documents - Apache OpenOffice Wiki
オブジェクトをclose()するときの解説。

LibreOffice: XCloseable Interface Reference
close()の解説。

次の関連記事:LibreOffice5(56)モードレスダイアログの例をPythonに翻訳する:その2

PR

0 件のコメント:

コメントを投稿