LibreOffice5(69)Javaの例:GUIをPythonにする その2

公開日: 2017年08月21日 更新日: 2019年05月11日

旧ブログ

t f B! P L
LibreOffice5(55)Javaの例:GUIをPythonにする その1の続きです。ダイアログは可能なものはLibreOffice5(58)モードレスダイアログの例をPythonに翻訳する:その4の3つ目の方法、つまりUnoControlDialogサービスで作成しそれぞれモダルとモードレス(ノンモダル)で動かせるようにしています。

前の関連記事:LibreOffice5(68)画像フィルターリストの作成


dialogdocument.py: ダイアログにWriterドキュメントのプレビューを表示する



GUI/dialogdocument.py at 2004a6fd843a3daa850299d4c8f3ed29d46c0d36 · p--q/GUI · GitHub
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
def macro():
    ctx = XSCRIPTCONTEXT.getComponentContext()  # コンポーネントコンテクストの取得。
    smgr = ctx.getServiceManager()  # サービスマネージャーの取得。
    doc = XSCRIPTCONTEXT.getDocument()  # マクロを起動した時のドキュメントのモデルを取得。  
    docframe = doc.getCurrentController().getFrame()  # モデル→コントローラ→フレーム、でドキュメントのフレームを取得。
    docwindow = docframe.getContainerWindow()  # ドキュメントのウィンドウ(コンテナウィンドウ=ピア)を取得。
    toolkit = docwindow.getToolkit()  # ピアからツールキットを取得。 
    dialog, addControl = dialogCreator(ctx, smgr, {"Name": "Dialog1", "PositionX": 102, "PositionY": 41, "Width": 300, "Height": 400, "Title": "Document-Dialog", "Moveable": True, "TabIndex": 0})  # UnoControlDialogを生成、とそれにコントロールを使いする関数addControl。
    addControl("FixedText", {"Name": "Headerlabel", "PositionX": 6, "PositionY": 6, "Width": 300, "Height": 8, "Label": "This code-sample demonstrates how to display an office document in a dialog window", "NoLabel": True})
    addControl("Button", {"PositionX": 126, "PositionY": 370, "Width": 50, "Height": 14, "Label": "~Close dialog", "PushButtonType": 1})  # PushButtonTypeの値はEnumではエラーになる。
    dialog.createPeer(toolkit, docwindow)  # ダイアログを描画。親ウィンドウを渡す。ノンモダルダイアログのときはNone(デスクトップ)ではフリーズする。
    dialogwindow = dialog.getPeer()  # ダイアログウィンドウ(=ピア)を取得。
    subwindow =  createWindow(toolkit, SHOW + BORDER, {"PositionX": 40, "PositionY": 50, "Width": 420, "Height": 550, "ParentIndex": 1, "Parent": dialogwindow, "WindowServiceName": "dockingwindow", "Type": SIMPLE})  # ツールキットを使ってドキュメントウィンドウの上にウィンドウを作成する。3番目の引数サービス名はcom.sun.star.awt.WindowDescriptorで定義されている。
    subframe = smgr.createInstanceWithContext("com.sun.star.frame.Frame", ctx)  # 新しいフレームを生成。
    subframe.initialize(subwindow)  # フレームにコンテナウィンドウを入れる。 
    nodes = PropertyValue(Name = "Preview", Value = True), PropertyValue(Name = "ReadOnly", Value = True# com.sun.star.document.MediaDescriptor
    subframe.loadComponentFromURL("private:factory/swriter", "_self", 2, nodes) # フレームのコンポーネントウィンドウにWriterドキュメントをロード。
    # ノンモダルダイアログにするとき。
#  showModelessly(ctx, smgr, docframe, dialog) 
    # モダルダイアログにする。フレームに追加するとエラーになる。
    dialog.execute() 
    dialog.dispose()
マクロセレクターに表示させている関数macro()だけ抜き出したものです。

実行にはモジュールのインポートの部分と下で説明している汎用関数が必要です。

UnoControlDialogサービスで作成したウィンドウを親として、ツールキットのcreateWindow()メソッドで"dockingwindow"サービスのウィンドウを作成しています。

そしてそのウィンドウを新たにインスタンス化したフレームのコンテナウィンドウにしてフレームを初期化しています。

そしてその初期化したフレームにloadComponentFromURL()メソッドでWriterドキュメントをコンポーネントウィンドウにロードしています。

ドキュメントはReadOnlyをTrueにしてロードしていますが、入力はできてしまいます。

PreviewもTrueなので入力した内容の保存はされないようです。

フレーム、コンテナウィンドウ、コンポーネントウィンドウの関係はLibreOffice(32)デベロッパーガイド4:コンポーネントフレームワークで学習しました。

createWindow()メソッドで作成するウィンドウの種類はWindowServiceNameで指定しますが、62種類もあります。

dialog   LibreOffice5(33)モードレスダイアログの例をPythonに翻訳する:その1
splitter  LibreOffice5(57)モードレスダイアログの例をPythonに翻訳する:その3
dockingwindow  このページ

これらdialog、splitter、dockingwindowの3つについては使い方がわかりましたが、他の59種類は解説もみつけられず、使いどころがわかりません。

Windows10 64bitでdialogdocument.pyを実行する


Writerドキュメントのマクロセレクターからdialogdocumentのmacroを呼び出せばWindows10でも実行できました。

cd "C:\Users\pq\AppData\Roaming\LibreOffice\4\user\Scripts\python"
"C:\Program Files (x86)\LibreOffice 5\program\python.exe" dialogdocument.py
(pqはWindows10のログイン名)

コマンドプロンプトではこのコマンドでオートメーションで実行してみましたが動きませんでした。


LibreOfficeへの接続まではうまくいきますが、その後このメッセージがでてきてLibreOfficeがフリーズします。

これはオートメーション接続に使っているofficehelper.pyのエラーであって、LibreOffice5(1)officehelper.bootstrap()を使うの方法で一応解決できますが、これでdialogdocument.pyまで動作するかは確認していません。

先日の強制アップデートでデフォルトシェルになったPowerShellではもっと悲惨で上のコマンドでは引数のファイルすら認識されませんでした。

多分PowerShellではコマンドプロンプトとコマンドの形式が変更になったと推測しますが、いまさら覚える気にはならない、、、

dialogdocument.pyで使っている汎用関数


dialogdocument.pyでは関数macro()以外は他の例でも使えるように汎用的な関数にしています。

createWindow(toolkit, attr, props)
1
2
3
4
5
6
def createWindow(toolkit, attr, props):  # ウィンドウタイトルは変更できない。attrはcom.sun.star.awt.WindowAttributeの和。propsはPositionX, PositionY, Width, Height, ParentIndex, Parent, WindowServiceName, Type。
    aRect = Rectangle(X=props.pop("PositionX"), Y=props.pop("PositionY"), Width=props.pop("Width"), Height=props.pop("Height"))
    d = WindowDescriptor(Bounds=aRect, WindowAttributes=attr)
    for key, val in props.items():
        setattr(d, key, val)
return toolkit.createWindow(d) # ウィンドウピアを返す。
これはcreateWindow()でウィンドウを作成するときに使います。

attrには定数com.sun.star.awt.WindowAttributeの和を入れます。

propsにはWindowDescriptor Structの属性をキーとしてその値を入れた辞書を入れます。

showModelessly(ctx, smgr, parentframe, dialog)
1
2
3
4
5
6
7
def showModelessly(ctx, smgr, parentframe, dialog):  # ノンモダルダイアログにする。オートメーションではリスナー動かない。ノンモダルダイアログではフレームに追加しないと閉じるボタンが使えない。
    frame = smgr.createInstanceWithContext("com.sun.star.frame.Frame", ctx)  # 新しいフレームを生成。
    frame.initialize(dialog.getPeer())  # フレームにコンテナウィンドウを入れる。
    frame.setName(dialog.getModel().getPropertyValue("Name"))  # フレーム名をダイアログモデル名から取得(一致させる必要性はない)して設定。
    parentframe.getFrames().append(frame)  # 新しく作ったフレームを既存のフレームの階層に追加する。
    dialog.setVisible(True# ダイアログを見えるようにする。  
return frame # フレームにリスナーをつけるときのためにフレームを返す。
ウィンドウをモードレスにするときにexecute()に代わってこれを実行します。

ノンモダルダイアログで使うときはこの関数は使いません。

モードレスウィンドウにしたときは、ウィンドウをフレームのウィンドウにしてフレーム列に追加しないと閉じるボタンが使えません。

ctxはコンポーネントコンテクスト、smgrはサービスマネージャー、parentframeは親フレーム、dialogはUnoControlDialogです。

dialogをコンテナウィンドウにしたフレームを作成してダイアログと同じフレーム名にして親フレームから得たフレーム列に追加します。

フレームのCreatorは親フレームになります。

最後にdialogを見えるようにしています。

リスナーを付けるなどの目的で新しく作成したフレームを使いたいときがあるので新し苦作成したフレームを戻り値にしています。

オートメーションでモードレスダイアログを表示させるとウィンドウの中が描画されずウィンドウを動かすと残像が残ってLibreOfficeがフリーズするので、オートメーションではなくマクロセレクターから呼び出さないといけません。

dialogCreator(ctx, smgr, dialogprops)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
def dialogCreator(ctx, smgr, dialogprops):  # ダイアログと、それにコントロールを追加する関数を返す。まずダイアログモデルのプロパティを取得。
    dialog = smgr.createInstanceWithContext("com.sun.star.awt.UnoControlDialog", ctx)  # ダイアログの生成。
    if "PosSize" in dialogprops:  # コントロールモデルのプロパティの辞書にPosSizeキーがあるときはピクセル単位でコントロールに設定をする。
        dialog.setPosSize(dialogprops.pop("PositionX"), dialogprops.pop("PositionY"), dialogprops.pop("Width"), dialogprops.pop("Height"), dialogprops.pop("PosSize"))  # ダイアログモデルのプロパティで設定すると単位がMapAppになってしまうのでコントロールに設定。
    dialogmodel = smgr.createInstanceWithContext("com.sun.star.awt.UnoControlDialogModel", ctx)  # ダイアログモデルの生成。
    dialogmodel.setPropertyValues(tuple(dialogprops.keys()), tuple(dialogprops.values()))  # ダイアログモデルのプロパティを設定。
    dialog.setModel(dialogmodel)  # ダイアログにダイアログモデルを設定。
    dialog.setVisible(False# 描画中のものを表示しない。
    def addControl(controltype, props, attrs=None):  # props: コントロールモデルのプロパティ、attr: コントロールの属性。
        control = None
        items, currentitemid = None, None
        if controltype == "Roadmap"# Roadmapコントロールのとき、Itemsはダイアログモデルに追加してから設定する。そのときはCurrentItemIDもあとで設定する。
            if "Items" in props:  # Itemsはダイアログモデルに追加されてから設定する。
                items = props.pop("Items")
                if "CurrentItemID" in props:  # CurrentItemIDはItemsを追加されてから設定する。
                    currentitemid = props.pop("CurrentItemID")
        if "PosSize" in props:  # コントロールモデルのプロパティの辞書にPosSizeキーがあるときはピクセル単位でコントロールに設定をする。
            if controltype=="Grid":
                control = smgr.createInstanceWithContext("com.sun.star.awt.grid.UnoControl{}".format(controltype), ctx)  # コントロールを生成。
            else:
                control = smgr.createInstanceWithContext("com.sun.star.awt.UnoControl{}".format(controltype), ctx)  # コントロールを生成。
            control.setPosSize(props.pop("PositionX"), props.pop("PositionY"), props.pop("Width"), props.pop("Height"), props.pop("PosSize"))  # ピクセルで指定するために位置座標と大きさだけコントロールで設定。
            controlmodel = _createControlModel(controltype, props)  # コントロールモデルの生成。
            control.setModel(controlmodel)  # コントロールにコントロールモデルを設定。
            dialog.addControl(props["Name"], control)  # コントロールをコントロールコンテナに追加。
        else# Map AppFont (ma)のときはダイアログモデルにモデルを追加しないと正しくピクセルに変換されない。
            controlmodel = _createControlModel(controltype, props)  # コントロールモデルの生成。
            dialogmodel.insertByName(props["Name"], controlmodel)  # ダイアログモデルにモデルを追加するだけでコントロールも作成される。
        if items is not None# コントロールに追加されたRoadmapモデルにしかRoadmapアイテムは追加できない。
            for i, j in enumerate(items):  # 各Roadmapアイテムについて
                item = controlmodel.createInstance()
                item.setPropertyValues(("Label", "Enabled"), j)
                controlmodel.insertByIndex(i, item)  # IDは0から整数が自動追加される
            if currentitemid is not None#Roadmapアイテムを追加するとそれがCurrentItemIDになるので、Roadmapアイテムを追加してからCurrentIDを設定する。
                controlmodel.setPropertyValue("CurrentItemID", currentitemid)
        if control is None# コントロールがまだインスタンス化されていないとき
            control = dialog.getControl(props["Name"])  # コントロールコンテナに追加された後のコントロールを取得。
        if attrs is not None# Dialogに追加したあとでないと各コントロールへの属性は追加できない。
            for key, val in attrs.items():  # メソッドの引数がないときはvalをNoneにしている。
                if val is None:
                    getattr(control, key)()
                else:
                    getattr(control, key)(val)
        return control  # 追加したコントロールを返す。
    def _createControlModel(controltype, props):  # コントロールモデルの生成。
        if not "Name" in props:
            props["Name"] = _generateSequentialName(controltype)  # Nameがpropsになければ通し番号名を生成。
        if controltype=="Grid":
            controlmodel = dialogmodel.createInstance("com.sun.star.awt.grid.UnoControl{}Model".format(controltype))  # コントロールモデルを生成。UnoControlDialogElementサービスのためにUnoControlDialogModelからの作成が必要。
        else:
            controlmodel = dialogmodel.createInstance("com.sun.star.awt.UnoControl{}Model".format(controltype))  # コントロールモデルを生成。UnoControlDialogElementサービスのためにUnoControlDialogModelからの作成が必要。
        if props:
            values = props.values()  # プロパティの値がタプルの時にsetProperties()でエラーが出るのでその対応が必要。
            if any(map(isinstance, values, [tuple]*len(values))):
                [setattr(controlmodel, key, val) for key, val in props.items()]  # valはリストでもタプルでも対応可能。XMultiPropertySetのsetPropertyValues()では[]anyと判断されてタプルも使えない。
            else:
                controlmodel.setPropertyValues(tuple(props.keys()), tuple(values))
        return controlmodel
    def _generateSequentialName(controltype):  # コントロールの連番名の作成。
        i = 1
        flg = True
        while flg:
            name = "{}{}".format(controltype, i)
            flg = dialog.getControl(name)  # 同名のコントロールの有無を判断。
            i += 1
        return name
    return dialog, addControl  # コントロールコンテナとそのコントロールコンテナにコントロールを追加する関数を返す。
(2017.9.9追記LibreOffice5(81)Javaの例:GUIをPythonにする その7でaddControl()はコントロールを返すようにしました。)
(2017.10.21追記。46行目が間違っていたので修正しました。修正が大変なのでLibreOffice5(73)Javaの例:GUIをPythonにする その6までのGitHubへのリンクは修正していませんが、46行目は使っていないので動作は問題ないと思います。)
(2018.1.2追記。46行目のsetPropertyValue()はLibreOffice5.4から値がタプルの時にエラーがでるようになってしまいました。[]stringが必要なのに[]anyになっていると言われます。なので、プロパティではなくアトリビュートとして設定するように変更しました。)
(2018.1.28追記UnoControlGridサービスだけ他のコントロールと違ってcom.sun.star.awtモジュールではなく、com.sun.star.awt.gridモジュールになるのでそれに対応させました。)

ctxはコンポーネントコンテクスト、smgrはサービスマネージャー、dialogpropsはUnoControlDialogModelの属性をキーとする辞書になります。

dialogpropsのキーはUnoControlDialogModelの属性以外にPosSizeをキー、定数com.sun.star.awt.PosSizeを値に持つことができて、そのときはcom.sun.star.awt.PosSizeで指定された要素についてピクセル単位で大きさを指定できます。

通常は値をPOSSIZEにしてXY座標、幅高さを指定することになると思います。

PosSizeのキーがない時はすべてma単位での指定になります(LibreOffice5(62)Map AppFont (ma)とピクセル参照)。

作成したUnoControlDialogは非表示にしています。

戻り値は、UnoControlDialogのインスタンスとaddControlという関数のタプルです。

addControl(controltype, props, attrs=None)

この関数はdialogCreator()のスコープでクロージャを形成しています。

この関数によってaddControl()と同時に返されたUnoControlDialogにコントロールを追加できます。

controltypeはcom.sun.star.awt.UnoControlを前に付けて、コントロールサービスを表します。

propsはコントロールモデルの属性をキーとする辞書、attrsはコントロールのメソッドをキーとする辞書です。

辞書の値はそれぞれの属性の値かメソッドの引数になりますが、メソッドの引数がないときはNoneを値にします。

propsのキーにNameがないときはcontroltypeに1から連番のNameを入れます。

これもUnoControlDialogと同様にPosSizeをキーにすることでピクセル単位で位置と大きさを指定できます。

UnoControlRoadmapModelはdialogdocument.pyで使っていないコントロールですが、これだけはinsertByIndex()で項目を追加しないといけないことと、CurrentItemIDはその追加後に設定しないといけないので、途中で別処理にしています。

if __name__ == "__main__"以降

LibreOffice5(59)モードレスダイアログの例をPythonに翻訳する:その5で作成した、マクロをオートメーションでも実行するためのコードで、一部改変しています。

officehelper.pyを使ってLibreOfficeを起動してpipe接続します(LibreOffice5(1)officehelper.bootstrap()を使う参照)。

接続が成功したら、接続先のLibreOfficeのバージョンをTerminalに出力します。

macro()のコードを変更せずにオートメーションで使うためにグローバル変数XSCRIPTCONTEXTを作成しています。

XSCRIPTCONTEXTはXScriptContextインターフェイスを実装しています。

com.sun.star.frame.Desktopサービスが非推奨になっていることに気が付いたので代わりにtheDesktop Singletonを使っています。

最後にWriterドキュメントを新規作成してそれがロードされてからmacro()を実行するようにしています。
(2017.8.28追記。WriterとCalcを1行のコメントアウトで切り替えれるようにLibreOffice5(76)FilePickerのフィルターを作成: 失敗で作り変えました。)

これでマクロモードと同様にmacro()が実行できますが、Eclipse: PyDevメモ: LibreOfficeのPythonマクロのリスナーの問題点でやったようにオートメーションでは発火しないリスナーがあったり、マクロモードに比べていくつかの制約があります。

しかしオートメーションではPyDevのエディタで簡単にブレークポイントを設定できるので制約がありながらもスクリプト作成時には頻繁に使ってます。

次の関連記事:LibreOffice5(70)Javaの例:GUIをPythonにする その3

ブログ検索 by Blogger

Translate

«
Aug. 2017
»
Sun
Mon
Tue
Wed
Thu
Fri
Sat
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
Created by Calendar Gadget

QooQ