LibreOffice5(90)ScriptingURLの動的取得

2017-11-01

旧ブログ

t f B! P L
同じマクロを、マクロとして呼び出したり、オートメーションで呼び出したり、ドキュメントに埋め込んで呼び出したりしたいときがあったので、それぞれに対応できるようにScriptingURLを動的に取得してみました。マクロを完成させたときは動的取得は単なる無駄なオーバーヘッドになります。

前の関連記事:LibreOffice5(89)マクロをオートメーションで実行するためのコード


ScriptingURLを動的に取得するマクロ

#!/opt/libreoffice5.4/program/python
# -*- coding: utf-8 -*-
import unohelper  # オートメーションには必須(必須なのはuno)。
import os
from com.sun.star.sheet import CellFlags as cf  # 定数
def macro():  
 doc = XSCRIPTCONTEXT.getDocument()  # ドキュメントのモデルを取得。 
 ctx = XSCRIPTCONTEXT.getComponentContext()  # コンポーネントコンテクストの取得。
 smgr = ctx.getServiceManager()  # サービスマネージャーの取得。 
 ucp = "vnd.sun.star.tdoc:"  # 埋め込みマクロのucp。
 if __file__.startswith(ucp):  # 埋め込みマクロの時。__file__はvnd.sun.star.tdoc:/4/Scripts/python/filename.pyというように返ってくる。
  txt = "Macro embedded in the document"
  filepath = __file__.replace(ucp, "")  #  ucpを除去。
  transientdocumentsdocumentcontentfactory = smgr.createInstanceWithContext("com.sun.star.frame.TransientDocumentsDocumentContentFactory", ctx)
  transientdocumentsdocumentcontent = transientdocumentsdocumentcontentfactory.createDocumentContent(doc)
  contentidentifierstring = transientdocumentsdocumentcontent.getIdentifier().getContentIdentifier()  # __file__の数値部分に該当。
  macrofolder = "{}/Scripts/python".format(contentidentifierstring.replace(ucp, ""))  #埋め込みマクロフォルダへのパス。 
  location = "document" 
 else:
  if __name__ == "__main__":  # オートメーションの時。__file__はシステムパスが返ってくる。
   txt = "Execution with automation"
   filepath = __file__
  else:  # マクロで実行した時。__file__はfileurlで返ってくる。
   txt = "Run from the macro selector"
   filepath = unohelper.fileUrlToSystemPath(__file__)  # fileurlをシステムパスに変換。
  pathsubstservice = smgr.createInstanceWithContext("com.sun.star.comp.framework.PathSubstitution", ctx)
  fileurl = pathsubstservice.substituteVariables("$(user)/Scripts/python", True)  # $(user)を変換する。fileurlが返ってくる。
  macrofolder =  unohelper.fileUrlToSystemPath(fileurl)  # fileurlをシステムパスに変換する。マイマクロフォルダへのパス。 
  location = "user"
 relpath = os.path.relpath(filepath, start=macrofolder)  # パス区切りがOS依存で返ってくる。
 baseurl = "vnd.sun.star.script:{}${}?language=Python&location={}".format(relpath.replace(os.sep, "|"), "{}", location)  # ScriptingURLのbaseurlを取得。
 scriptingurl = baseurl.format(macro.__name__)  # 関数名を渡してScriptingURLを完成させる。  
 # シートに結果を出力。
 sheet = doc.getSheets()[0]
 sheet.clearContents(cf.VALUE+cf.DATETIME+cf.STRING+cf.ANNOTATION+cf.FORMULA+cf.STYLES+cf.HARDATTR)  # セルの内容を削除。cf.STYLES+cf.HARDATTRでセルの結合も解除される。
 datarows = (txt, ""),\
    ("__file__", __file__),\
    ("Path of macro folder", macrofolder),\
    ("Relative path from macro folder", relpath),\
    ("ScriptingURL", scriptingurl)
 r, c = len(datarows), len(datarows[0])  
 sheet[:r, :c].setDataArray(datarows)
 sheet[0, :2].merge(True)
 sheet[0, :c].getColumns().setPropertyValue("OptimalWidth", True)  # 列幅を最適化する。
g_exportedScripts = macro, #マクロセレクターに限定表示させる関数をタプルで指定。  
if __name__ == "__main__":  # オートメーションで実行するとき
 def automation():  # オートメーションのためにglobalに出すのはこの関数のみにする。
  import officehelper
  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.", file=sys.stderr)
     sys.exit()
    print("Connected to a running office ...")
    smgr = ctx.getServiceManager()  # サービスマネジャーの取得。
    print("Using {} {}".format(*_getLOVersion(ctx, smgr)))  # LibreOfficeのバージョンを出力。
    return func(ctx, smgr)  # 引数の関数の実行。
   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  # createXSCRIPTCONTEXTの引数にctxとsmgrを渡すデコレータ。
  def createXSCRIPTCONTEXT(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 ctx.getByName('/singletons/com.sun.star.frame.theDesktop')  # com.sun.star.frame.Desktopはdeprecatedになっている。
    def getDocument(self):
     return self.getDesktop().getCurrentComponent()
   return ScriptContext(ctx)  
  XSCRIPTCONTEXT = createXSCRIPTCONTEXT()  # XSCRIPTCONTEXTの取得。
  doc = XSCRIPTCONTEXT.getDocument()  # 現在開いているドキュメントを取得。
  doctype = "scalc", "com.sun.star.sheet.SpreadsheetDocument"  # Calcドキュメントを開くとき。
 #  doctype = "swriter", "com.sun.star.text.TextDocument"  # Writerドキュメントを開くとき。
  if (doc is None) or (not doc.supportsService(doctype[1])):  # ドキュメントが取得できなかった時またはCalcドキュメントではない時
   XSCRIPTCONTEXT.getDesktop().loadComponentFromURL("private:factory/{}".format(doctype[0]), "_blank", 0, ())  # ドキュメントを開く。ここでdocに代入してもドキュメントが開く前にmacro()が呼ばれてしまう。
  flg = True
  while flg:
   doc = XSCRIPTCONTEXT.getDocument()  # 現在開いているドキュメントを取得。
   if doc is not None:
    flg = (not doc.supportsService(doctype[1]))  # ドキュメントタイプが確認できたらwhileを抜ける。
  return XSCRIPTCONTEXT
 XSCRIPTCONTEXT = automation()  # XSCRIPTCONTEXTを取得。 
 macro()  # マクロの実行。
これでドキュメントに埋め込んで実行したとき、オートメーションとして実行したとき、マクロセレクターから実行したときの、3パターンに対応できます。

作成したScriptingURLはCalcのシートに出力するのでCalcで実行する必要があります。

ドキュメントに埋め込んで実行した結果


CalcExamples - p--qのGenerateScriptingURL.odsはこのマクロを埋め込んだCalcドキュメントです。


ドキュメントに埋め込んだとマクロからはオートメーションで実行できないので、46行目以降は不要です。

GenerateScriptingURLはドキュメントのマクロフォルダにpyファイルをいれたものです。


__file__はvnd.sun.star.tdoc:/7/Scripts/python/GenerateScriptingURL.pyが入っています。

このパスに含まれている数値はcontentidentifierといって、ファイルを開く順か何かで変化するので、TransientDocumentsDocumentContentFactoryサービスから取得しています。

このドキュメントのマクロフォルダは/7/Scripts/pythonなので、ScriptingURLはvnd.sun.star.script:GenerateScriptingURL.py$macro?language=Python&location=document
になります。

LibraryフォルダにいれたModule.pyにコピペして入れた同じマクロを実行してみます。


vnd.sun.star.script:Library|Module.py$macro?language=Python&location=document

ScriptingURLはパス区切りを|にして、ドキュメントのマクロフォルダからの相対パスを入れます。

オートメーションで実行した結果

Execution with automation 
__file__ /home/pq/.config/libreoffice/4/user/Scripts/python/SpreadSheetExample/SpreadSheetExample/src/contextmenu/GenerateScriptingURL.py
Path of macro folder /home/pq/.config/libreoffice/4/user/Scripts/python
Relative path from macro folder SpreadSheetExample/SpreadSheetExample/src/contextmenu/GenerateScriptingURL.py
ScriptingURL vnd.sun.star.script:SpreadSheetExample|SpreadSheetExample|src|contextmenu|GenerateScriptingURL.py$macro?language=Python&location=user
キャプチャ画像は大きすぎるのでシートの内容をペーストしました。

Eclipseのプロジェクト内にマクロファイルを入れてあるので深い階層になっています。

(2017.11.1追記。オートメーションで実行した時はグローバル変数を渡せないことがわかりました。Calc(36)コンテクストメニューをカスタマイズする: その5参照。)

マクロセレクターから実行した結果

Run from the macro selector 
__file__ file:///home/pq/.config/libreoffice/4/user/Scripts/python/SpreadSheetExample/SpreadSheetExample/src/contextmenu/GenerateScriptingURL.py
Path of macro folder /home/pq/.config/libreoffice/4/user/Scripts/python
Relative path from macro folder SpreadSheetExample/SpreadSheetExample/src/contextmenu/GenerateScriptingURL.py
ScriptingURL vnd.sun.star.script:SpreadSheetExample|SpreadSheetExample|src|contextmenu|GenerateScriptingURL.py$macro?language=Python&location=user
マクロファイルの場所は同じなので、ScriptingURLはオートメーションで実行した結果と同じです。 マクロセレクターから実行したときは__file__がfileurlで返ってきています。

「オートメーションで実行」以外はWindows10でも実行を確認できました。

汎用関数 getBaseURL(ctx, smgr, doc)

def getBaseURL(ctx, smgr, doc):  # 埋め込みマクロ、オートメーション、マクロセレクターに対応してScriptingURLのbaseurlを返す。
 modulepath = __file__  # ScriptingURLにするマクロがあるモジュールのパスを取得。ファイルのパスで場合分け。
 ucp = "vnd.sun.star.tdoc:"  # 埋め込みマクロのucp。
 if modulepath.startswith(ucp):  # 埋め込みマクロの時。__file__はvnd.sun.star.tdoc:/4/Scripts/python/filename.pyというように返ってくる。
  filepath = modulepath.replace(ucp, "")  #  ucpを除去。
  transientdocumentsdocumentcontentfactory = smgr.createInstanceWithContext("com.sun.star.frame.TransientDocumentsDocumentContentFactory", ctx)
  transientdocumentsdocumentcontent = transientdocumentsdocumentcontentfactory.createDocumentContent(doc)
  contentidentifierstring = transientdocumentsdocumentcontent.getIdentifier().getContentIdentifier()  # __file__の数値部分に該当。
  macrofolder = "{}/Scripts/python".format(contentidentifierstring.replace(ucp, ""))  #埋め込みマクロフォルダへのパス。 
  location = "document"  # マクロの場所。  
 else:
  filepath = unohelper.fileUrlToSystemPath(modulepath) if modulepath.startswith("file://") else modulepath # オートメーションの時__file__はシステムパスだが、マクロセレクターから実行するとfileurlが返ってくる。
  pathsubstservice = smgr.createInstanceWithContext("com.sun.star.comp.framework.PathSubstitution", ctx)
  fileurl = pathsubstservice.substituteVariables("$(user)/Scripts/python", True)  # $(user)を変換する。fileurlが返ってくる。
  macrofolder =  unohelper.fileUrlToSystemPath(fileurl)  # fileurlをシステムパスに変換する。マイマクロフォルダへのパス。 
  location = "user"  # マクロの場所。 
 relpath = os.path.relpath(filepath, start=macrofolder)  #  マクロフォルダからの相対パスを取得。パス区切りがOS依存で返ってくる。
 return "vnd.sun.star.script:{}${}?language=Python&location={}".format(relpath.replace(os.sep, "|"), "{}", location)  # ScriptingURLのbaseurlを取得。Windowsのためにos.sepでパス区切りを置換。
Calc(36)コンテクストメニューをカスタマイズする: その5でマクロ内で使えるように汎用関数getBaseURL()にしました。

引数はコンポーネントコンテクスト、サービスマネージャ、ドキュメントモデル、です。

マクロの呼び出され方は__file__のucpで判断しています。

modulepathも引数にすれば、カレントスクリプトファイル以外のマクロにも応用できます。

ただ、Calc(36)コンテクストメニューをカスタマイズする: その5で、オートメーションではScriptingURLにグローバル変数が渡せないとわかりましたので、オートメーションでの使う機会は少なそうです。

(2018.8.2追記。ドキュメント内ファイルのContentIdentifierはドキュメントを開き直すと更新されるのですが、__file__で取得できるパスのContentIdentifierは更新されないとわかりました。
def getBaseURL(xscriptcontext):  # 埋め込みマクロのScriptingURLのbaseurlを返す。__file__はvnd.sun.star.tdoc:/4/Scripts/python/filename.pyというように返ってくる。
 modulepath = __file__  # ScriptingURLにするマクロがあるモジュールのパスを取得。ファイルのパスで場合分け。sys.path[0]は__main__の位置が返るので不可。
 ucp = "vnd.sun.star.tdoc:"  # 埋め込みマクロのucp。
 filepath = modulepath.replace(ucp, "")  #  ucpを除去。ドキュメントを一旦閉じて開き直してもContentIdentifierが更新されない。
 filepath = os.path.join(*filepath.split("/")[2:])  # ex. Scripts/python/pythonpath/indoc/commons.py。ContentIdentifierを除く。
 macrofolder = "Scripts/python"
 location = "document"  # マクロの場所。 
 relpath = os.path.relpath(filepath, start=macrofolder)  # マクロフォルダからの相対パスを取得。パス区切りがOS依存で返ってくる。
 return "vnd.sun.star.script:{}${}?language=Python&location={}".format(relpath.replace(os.sep, "|"), "{}", location)  # ScriptingURLのbaseurlを取得。Windowsのためにos.sepでパス区切りを置換。
ContentIdentifier以下のパスで相対パスを取得するようにしました。

引数のxscriptcontextは将来的な汎用性のために与えているだけで、この関数内では使用していません。)

参考にしたサイト


OOoSF/Memo - ...?
ContentIdentifierを取得するBasicの例。

OOoBasic/Generic/UCB - ...?
UCPの一覧。

次の関連記事:LibreOffice5(91)コンポーネントデータノードをルートまでたどる

ブログ検索 by Blogger

Translate

最近のコメント

Created by Calendar Gadget

QooQ