LibreOffice5(59)モードレスダイアログの例をPythonに翻訳する:その5

2017-07-13

旧ブログ

t f B! P L
OOoBasic/Dialog/Example10 - ...?のmodelessdialog-3.odtをPythonにします。これはWriterドキュメントのテキストを選択するとそれを感知してモードレスダイアログ上のEditコントロールにそのテキストを表示します。

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


ドキュメントとモードレスダイアログを連携する例



modelessdialog3macro.py

Writerドキュメントからこのマクロを呼び出すとダイアログが起動します。

Writerドキュメントに文字を入力してマウスで文字を選択するとすぐにダイアログのEditコントロールに反映されます。

Show SelectionボタンをクリックするとEditコントロールの中味がメッセージボックスに表示されます。

ダイアログとドキュメントはモードレスの関係なのでそれぞれ操作できます。

メッセージボックスはドキュメントに対してモダルなのでメッセージボックスを表示している間はドキュメントの操作はできません。

しかしダイアログとの関係はモードレスになっているのでShow Selectionボタンをクリックすると複数のメッセージボックスを表示できます。

25行目のButtonListenerをインスタンス化するときの第二引数に渡すウィンドウがメッセージボックスに対してモダルになっています。

OOoBasic/Dialog/Example10 - ...?の解説にはフレームを閉じるときはそのリスナーを削除しないとクラッシュするとあります。

30-33行目をコメントアウトしてリスナーを削除しないようにしてみましたが、それでもとりあえずはクラッシュはしませんでした。

オートメーションからダイアログを実行したときの問題点


LibreOffice5(57)モードレスダイアログの例をPythonに翻訳する:その3でやったように、モードレス(ノンモダル)ダイアログではリスナーが発火せずボタンなどが動きませんでした。

execute()を実行してモダルダイアログにするとこの問題は起こりません。

しかし、execute()でダイアログの表示をオートメーションから何回か、早ければ2回目から、実行してダイアログの閉じるボタンでダイアログを閉じるとLibreOfficeがクラッシュして、ドキュメントの回復ダイアログがでてきます。

    dialog.execute()  # モダルダイアログにする。
__main__.DisposedException: Binary URP bridge disposed during call


ダイアログを表示させている間にLibreOffice本体のプロセスとオートメーションのプロセスの接続が切れているのが原因のようです。

リスナーを除去していないのが原因かと思ってリスナーを付けないようにしてみましたが、結果は変わりませんでしたのでリスナーは関係ありません。

(2017.7.14追記。クラッシュする原因がはっきりしました。フレームのコンテナウィンドウにしたウィンドウをノンモダルダイアログにしたのが原因でした。execute()でノンモダルウィンドウにするときはフレームに追加しないようにしたらクラッシュしなくなりました。)

setVisible(True)でモードレスダイアログにしたときもたまにクラッシュするときがありますが、滅多にありません。

マクロモードで実行したときはいずれもクラッシュするときはありませんでした。

オートメーションにするとEclipseのエディタで簡単にブレークポイントを設定できるのでデバッグするには便利なのですが、ダイアログを扱うのにはオートメーションは不向きなようです。

オートメーションであってもリスナーのメソッドにはブレークポイントは設定できず、マクロと同じようようにデバッグコードを挿入してリモートデバッグしないといけません。

ということでマクロのコードに、オートメーションで実行するためのコードに加えてマクロでも実行するためのコードを追加することにしました。

マクロをオートメーションで実行するためのコード


(2017.10.27追記。このコードはグローバル変数が多くてPyDevでエラーが指摘されにくくなっていたのでLibreOffice5(89)マクロをオートメーションで実行するためのコードで変更しました。)
g_exportedScripts = macro, #マクロセレクターに限定表示させる関数をタプルで指定。
if __name__ == "__main__":  # オートメーションで実行するとき
    import officehelper
    import traceback
    from functools import wraps
    import sys
    from com.sun.star.beans import PropertyValue
    from com.sun.star.script.provider import XScriptContext  
    def connectOffice(func):  # funcの前後でOffice接続の処理
        @wraps(func)
        def wrapper():  # LibreOfficeをバックグラウンドで起動してコンポーネントテクストとサービスマネジャーを取得する。
            try:
                ctx = officehelper.bootstrap()  # コンポーネントコンテクストの取得。
            except:
                print("Could not establish a connection with a running office.")
                sys.exit()
            print("Connected to a running office ...")
            smgr = ctx.getServiceManager()  # サービスマネジャーの取得。
            print("Using {} {}".format(*_getLOVersion(ctx, smgr)))  # LibreOfficeのバージョンを出力。
            try:
                return func(ctx, smgr)  # 引数の関数の実行。
            except:
                traceback.print_exc()
        def _getLOVersion(ctx, smgr):  # LibreOfficeの名前とバージョンを返す。
            cp = smgr.createInstanceWithContext('com.sun.star.configuration.ConfigurationProvider', ctx)
            node = PropertyValue(Name = 'nodepath', Value = 'org.openoffice.Setup/Product' )  # share/registry/main.xcd内のノードパス。
            ca = cp.createInstanceWithArguments('com.sun.star.configuration.ConfigurationAccess', (node,))
            return ca.getPropertyValues(('ooName', 'ooSetupVersion'))  # LibreOfficeの名前とバージョンをタプルで返す。
        return wrapper
    @connectOffice  # mainの引数にctxとsmgrを渡すデコレータ。
    def main(ctx, smgr):  # XSCRIPTCONTEXTを生成。
        class ScriptContext(unohelper.Base, XScriptContext):
            def __init__(self, ctx):
                self.ctx = ctx
            def getComponentContext(self):
                return self.ctx
            def getDesktop(self):
                return self.ctx.getServiceManager().createInstanceWithContext("com.sun.star.frame.Desktop", self.ctx)
            def getDocument(self):
                return self.getDesktop().getCurrentComponent()
        return ScriptContext(ctx)  
    XSCRIPTCONTEXT = main()  # XSCRIPTCONTEXTを取得。
    doc = XSCRIPTCONTEXT.getDocument()  # ドキュメントを取得。
    if not hasattr(doc, "getCurrentController"):  # ドキュメント以外のとき。スタート画面も除外。
        XSCRIPTCONTEXT.getDesktop().loadComponentFromURL("private:factory/swriter", "_blank", 0, ())  # Writerのドキュメントを開く。
        while doc is None:  # ドキュメントのロード待ち。
            doc = XSCRIPTCONTEXT.getDocument()
    macro()
(2017.8.22追記LibreOffice5(69)Javaの例:GUIをPythonにする その2で一部変更しました。エラー出力はprint()の引数にfile=sys.stderrを付けるようにしました。Desktopサービスは非推奨になっているで、代わりにtheDesktop Singletonを使うようにしています。)

LibreOffice5(1)officehelper.bootstrap()を使う
で作ったunopyboot.pyに手を加えて、起動したLibreOfficeのバージョンをprint()する機能と、Writerドキュメントを開く機能を追加します。

モジュールにするとPYTHONPATHの設定が面倒なのでそのままマクロの後ろにif __name__ == "__main__":で追加することを想定しています。

マクロは関数macro()と想定していますのでマクロに合わせて適宜変更する必要があります。

これをLibreOfficeのバンドルPythonをインタープリターにして起動するとWriterドキュメントが開いて関数macro()が実行されます。

LibreOfficeとの接続はofficehelper.pyがランダムなパイプ名でpipe接続してくれています。

Writerに限らずLibreOfficeのドキュメントがすでに開いているときは新たなドキュメントは開かないようにしています。

GUI/GUI/src/macro at bf5de1b5f0dd1f55742cdf4450978188111d9c1e · p--q/GUI

modelessdialog-2.odtをPythonにした3つのパターンのそれぞれにこのオートメーションで実行するためのコードを追加しました。

(2017.7.14追記。上記に書いたようにオートメーションでモダイアログをフレームに追加するとダイアログを閉じるときにLibreOfficeがクラッシュするのでフレームに追加するコードの位置をまとめました。)
(2017.7.17追記。モデルの属性になるUnoControlDialogElementサービスはUnoControlDialogModelサービスのcreateInstance()メソッドでコントロールをインスタンス化しないといけないことに対応しました。modelessdialog2macro_createWindow.pyとmodelessdialog2macro_taskcreator.pyは未対応です。)

しかし、先に書いたようにオートメーションで起動したときはダイアログのボタンは反応しません。

オートメーションから同じスクリプトをマクロとして呼び出すことはできない


オートメーションからはモードレスダイアログではリスナーが動かないのと、かといってノンモダルダイアログではLibreOfficeがクラッシュするので、オートメーションから同じファイルに載っているマクロをマクロとして実行するコードも追加しようとしましたがそれは動きませんでした。

__main__.ScriptFrameworkErrorException: <class 'pythonscript.com.sun.star.ucb.InteractiveAugmentedIOException'>: an error occurred during file opening

ファイルが開けないと言われます。

開こうとしているファイルはすでに開いているのでさもありなんです。

LibreOffice5(47)拡張機能のソースをオートメーションでも実行するでも同じ目に遭ったのにこのエラーを見るまですっかり忘れていました。
        import re #正規表現モジュール。
        import os
        from pathlib import Path
        script_uri_abs = sys.path[0]  # マクロファイルへの絶対パスを取得。
        pat = os.path.join("user", "Scripts", "python")  # マイマクロフォルダの絶対パスを取得するための正規表現パターンの一部。
        reg = re.compile(r'.+\W{}$'.format(pat))  # マイマクロフォルダの絶対パスを取得するための正規表現オブジェクト。
        for path in sys.path:
            if reg.search(path):
                p = Path(script_uri_abs.replace(path, ""))
                break
        script_uri = "{}.py$macro".format("|".join(p.parts[1:]))  # マイマクロフォルダからマクロへの相対パス。
        script_uri = "vnd.sun.star.script:" + script_uri + "?language=Python&location=user"
        mspf = XSCRIPTCONTEXT.getComponentContext().getValueByName("/singletons/com.sun.star.script.provider.theMasterScriptProviderFactory")
        sp = mspf.createScriptProvider("")
        script = sp.getScript(script_uri)
        script.invoke((), (), ())
マクロまでのパスを自動取得するところまで作ってしまったので載せておきますが、これではマクロは起動できません。

わざわざマクロセレクターでマクロを選択して実行する手間が省けると思ったのですが、そうはいきませんでした。

参考にしたサイト


OOoBasic/Dialog/Example10 - ...?
ドキュメントとモードレスダイアログを連携させたBasicの例modelessdialog-3.odtをPythonにしました。

次の関連記事:LibreOffice5(60)ドラッグでリサイズできるsplitterコントロールの例

ブログ検索 by Blogger

Translate

最近のコメント

Created by Calendar Gadget

QooQ