LibreOffice5(91)コンポーネントデータノードをルートまでたどる

2017-11-04

旧ブログ

t f B! P L
ディスパッチコマンドに対応するラベルをLibreOfficeのコンフィギュレーションから調べたいので、まずはそのためにコンポーネントデータノードの構造を調べます。

前の関連記事:LibreOffice5(90)ScriptingURLの動的取得


ディスパッチコマンドのラベル名を設定している個所を探す


まずDevelopment/DispatchCommands - The Document Foundation Wikiにある適当なディスパッチコマンドで、/opt/libreoffice5.2/share/registryにあるファイルの内容を検索してみます。

linuxBeansのPCManFMで「.uno:FormatCellDialog」で検索しても何もでてこなかったので、「FormatCellDialog」で検索しました。


calc.xcd、main.xcd、registry_ja.xcdの3つのxcdファイルがでてきました。

calc.xcdにはen-USのラベルが入っていました。

main.xcdにはラベル名の設定はありませんでした。

registry_ja.xcdには日本語のラベルが入っていました。

とりあえずregistry_ja.xcdを使って手掛かりをつかみます。

ノードを抽出するXPathを決める


Python(26)XPathでxmlの子要素からルートまで遡る方法と同様にしてディスパッチコマンドのラベルを定義しているノードからルートまでたどります。

まずXPathでノードを抽出するには、ルートノードまで遡りたい起点となる単一ノードを抽出できるXPath、名前空間、xmlデータを含んでいるファイル、の3つの情報が必要です。

今回はディスパッチコマンドのラベルを定義しているノードのルートまでのノードをたどりたいので、先ほど検索したregistry_ja.xcdファイルにあるディスパッチコマンド「.uno:FormatCellDialog」のノードを見ます。
<node oor:name=".uno:FormatCellDialog">
    <prop oor:name="Label" oor:type="xs:string">
        <value xml:lang="ja">セルの書式設定(~C)...</value>
    </prop>
registry_ja.xcdで.uno:FormatCellDialogのラベルを設定しているノードはこのようになっています。

ノードのタグはnode、属性はoor:name=".uno:FormatCellDialog"ということになります。

なのでXPathはとりあえず次のようにします。
xpath = './/node[@oor:name=".uno:FormatCellDialog"]' 
oorは名前空間なのでこれを設定しないといけません。

名前空間はだいたいファイルの一番最初のノード(つまりファイルのルートノード)に書いてあります。
<oor:data xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:oor="http://openoffice.org/2001/registry">
    <oor:component-data xmlns:install="http://openoffice.org/2004/installation" oor:name="FormWizard" oor:package="org.openoffice.Office">
registry_ja.xcdファイルの最初はこのようになっているので、xmlnsのところをみて、xs、xsi、oorという名前空間を設定します。
 namespaces = {"oor": "{http://openoffice.org/2001/registry}",\
    "xs": "{http://www.w3.org/2001/XMLSchema}",\
    "xsi": "{http://www.w3.org/2001/XMLSchema-instance}"}
registry_ja.xcdファイルのフルパスは次のようになります。
 filepath = "/opt/libreoffice5.2/share/registry/res/registry_ja.xcd"

XPathをチェックするためのマクロ


XPathは調べたいノードだけが抽出されるようにしないといけませんが、それはやってみないとわからないので、XPathで取得できるノードを出力するマクロを使います。
#!/opt/libreoffice5.4/program/python
# -*- coding: utf-8 -*-
import unohelper  # オートメーションには必須(必須なのはuno)。
from com.sun.star.sheet import CellFlags as cf # 定数
from xml.etree import ElementTree
def macro(documentevent=None):  # 引数はイベント駆動用。
 doc = XSCRIPTCONTEXT.getDocument() if documentevent is None else documentevent.Source  # ドキュメントのモデルを取得。 
 filepath = "/opt/libreoffice5.2/share/registry/res/registry_ja.xcd"  # xmlファイルへのパス。
 xpath = './/node[@oor:name=".uno:FormatCellDialog"]'  # XPath。1つのノードだけ選択する条件にしないといけない。
 namespaces = {"oor": "{http://openoffice.org/2001/registry}",\
    "xs": "{http://www.w3.org/2001/XMLSchema}",\
    "xsi": "{http://www.w3.org/2001/XMLSchema-instance}"}  # 名前空間の辞書。replace()で置換するのに使う。
 queryNodesWithXPath(filepath, xpath, namespaces, doc)
def queryNodesWithXPath(filepath, xpath, namespaces, doc): 
 replaceWithValue, replaceWithKey = createReplaceFunc(namespaces)  # 名前空間を置換する関数を取得。
 xpath = replaceWithValue(xpath)  # 名前空間の辞書のキーを値に変換。
 tree = ElementTree.parse(filepath)
 nodes = tree.findall(xpath)  # XPathで抽出されるノードのリストを取得する。
 datarows = [(replaceWithKey(formatNode(node)),) for node in nodes]  # Calcに出力するために行のリストにする。
 controller = doc.getCurrentController()  # コントローラーを取得。
 sheet = controller.getActiveSheet()  # アクティブなシートを取得。
 sheet.clearContents(cf.VALUE+cf.DATETIME+cf.STRING+cf.ANNOTATION+cf.FORMULA+cf.HARDATTR+cf.STYLES)  # セルの内容を削除。cf.HARDATTR+cf.STYLESでセル結合も解除。
 sheet[:len(datarows), :len(datarows[0])].setDataArray(datarows)  # シートに結果を出力する。
 cellcursor = sheet.createCursor()  # シート全体のセルカーサーを取得。
 cellcursor.gotoEndOfUsedArea(True)  # 使用範囲の右下のセルまでにセルカーサーのセル範囲を変更する。
 cellcursor.getColumns().setPropertyValue("OptimalWidth", True)  # セルカーサーのセル範囲の列幅を最適化する。
def formatNode(node):  # 引数はElement オブジェクト。タグ名と属性を出力する。属性の順番は保障されない。
 tag = node.tag  # タグ名を取得。
 attribs = []  # 属性をいれるリスト。
 for key, val in node.items():  # ノードの各属性について。
  attribs.append('{}="{}"'.format(key, val))  # =で結合。
 attrib = " ".join(attribs)  # すべての属性を結合。
 n = "{} {}".format(tag, attrib) if attrib else tag  # タグ名と属性を結合する。
 return "<{}>".format(n)  
def createReplaceFunc(namespaces):  # 引数はキー名前空間名、値は名前空間を波括弧がくくった文字列、の辞書。
 def replaceWithValue(txt):  # 名前空間の辞書のキーを値に置換する。
  for key, val in namespaces.items():
   txt = txt.replace("{}:".format(key), val)
  return txt
 def replaceWithKey(txt):  # 名前空間の辞書の値をキーに置換する。
  for key, val in namespaces.items():
   txt = txt.replace(val, "{}:".format(key))
  return txt
 return replaceWithValue, replaceWithKey
g_exportedScripts = macro, #マクロセレクターに限定表示させる関数をタプルで指定。
先ほど調べた情報をもとに、8行目でxmlデータのあるファイルのフルパス、9行目でノードを抽出するXPath、10行目で名前空間、を定義しています。


Calcでこのマクロを実行すると、狙っていたnode oor:name=".uno:FormatCellDialog"というノードだけが抽出されたのでこのXPathは合格です。

XPathで抽出したノードからルートまでノードをさかのぼるマクロ

#!/opt/libreoffice5.4/program/python
# -*- coding: utf-8 -*-
import unohelper  # オートメーションには必須(必須なのはuno)。
from xml.etree import ElementTree
from com.sun.star.sheet import CellFlags as cf # 定数
def macro(documentevent=None):  # 引数はイベント駆動用。
 doc = XSCRIPTCONTEXT.getDocument() if documentevent is None else documentevent.Source  # ドキュメントのモデルを取得。 
 filepath = "/opt/libreoffice5.2/share/registry/res/registry_ja.xcd"  # xmlファイルへのパス。
 xpath = './/node[@oor:name=".uno:FormatCellDialog"]'  # XPath。1つのノードだけ選択する条件にしないといけない。
 namespaces = {"oor": "{http://openoffice.org/2001/registry}",\
    "xs": "{http://www.w3.org/2001/XMLSchema}",\
    "xsi": "{http://www.w3.org/2001/XMLSchema-instance}"}  # 名前空間の辞書。replace()で置換するのに使う。
 traceToRoot(filepath, xpath, namespaces, doc)
def traceToRoot(filepath, xpath, namespaces, doc):  # xpathは子ノードを取得するXPath。1つのノードだけ選択する条件にしないといけない。
 tree = ElementTree.parse(filepath)  # xmlデータからElementTreeオブジェクト(xml.etree.ElementTree.ElementTree)を取得する。ElementTree.parse()のElementTreeはオブジェクト名ではなくてモジュール名。
 replaceWithValue, replaceWithKey = createReplaceFunc(namespaces)
 xpath = replaceWithValue(xpath)  # 名前空間の辞書のキーを値に変換。
 nodes = tree.findall(xpath)  # 起点となる子ノードを取得。
 outputs = []
 if len(nodes)==1:
  node = nodes[0]
  outputs.append(replaceWithKey(formatNode(node)))  # 名前空間の辞書の値をキーに変換して出力する。
  while node is not None:
   xpath ="{}..".format(xpath)  # 親ノードのxpathを取得。
   node = tree.find(xpath)  # 親ノードを取得。親はひとつのはずなのでfind()メソッドを使う。
   if node is not None:  # 親ノードが取得できたとき
    outputs.append(replaceWithKey(formatNode(node)))
 elif len(nodes)>1:  # 調べる子ノードが複数あるとき。
  for node in nodes:  # 各子ノードについて。
   outputs.append("\n{}".format(replaceWithKey(formatNode(node))))  # 名前空間の辞書の値をキーに変換して出力する。
   path = xpath  # 子ノードのxpathを取得。
   childnode = node  # 子ノードを取得。
   parentnodes = True
   while parentnodes:  # 親ノードのリストの要素があるときTrue。
    path ="{}..".format(path)  # 親ノードのxpathを取得。
    parentnodes = tree.findall(path)  # 親ノードのリストを取得。
    for parentnode in parentnodes:  # 各親ノードについて
     if childnode in list(parentnode):  # 親ノードに子ノードのオブジェクトが存在するとき。
      outputs.append(replaceWithKey(formatNode(parentnode)))  # 親ノードを出力。
      childnode = parentnode  # 親ノードを子ノードにする。
      break  # この階層を抜ける。
   outputs.append("")  # 空行を入れる。
 if outputs:
  datarows = [(i,) for i in outputs]  # Calcに出力するために行のリストにする。
  controller = doc.getCurrentController()  # コントローラーを取得。
  sheet = controller.getActiveSheet()  # アクティブなシートを取得。
  sheet.clearContents(cf.VALUE+cf.DATETIME+cf.STRING+cf.ANNOTATION+cf.FORMULA+cf.HARDATTR+cf.STYLES)  # セルの内容を削除。cf.HARDATTR+cf.STYLESでセル結合も解除。
  sheet[:len(datarows), :len(datarows[0])].setDataArray(datarows)  # シートに結果を出力する。
  cellcursor = sheet.createCursor()  # シート全体のセルカーサーを取得。
  cellcursor.gotoEndOfUsedArea(True)  # 使用範囲の右下のセルまでにセルカーサーのセル範囲を変更する。
  cellcursor.getColumns().setPropertyValue("OptimalWidth", True)  # セルカーサーのセル範囲の列幅を最適化する。
def formatNode(node):  # 引数はElement オブジェクト。タグ名と属性を出力する。属性の順番は保障されない。
 tag = node.tag  # タグ名を取得。
 attribs = []  # 属性をいれるリスト。
 for key, val in node.items():  # ノードの各属性について。
  attribs.append('{}="{}"'.format(key, val))  # =で結合。
 attrib = " ".join(attribs)  # すべての属性を結合。
 n = "{} {}".format(tag, attrib) if attrib else tag  # タグ名と属性を結合する。
 return "<{}>".format(n)  
def createReplaceFunc(namespaces):  # 引数はキー名前空間名、値は名前空間を波括弧がくくった文字列、の辞書。
 def replaceWithValue(txt):  # 名前空間の辞書のキーを値に置換する。
  for key, val in namespaces.items():
   txt = txt.replace("{}:".format(key), val)
  return txt
 def replaceWithKey(txt):  # 名前空間の辞書の値をキーに置換する。
  for key, val in namespaces.items():
   txt = txt.replace(val, "{}:".format(key))
  return txt
 return replaceWithValue, replaceWithKey
g_exportedScripts = macro, #マクロセレクターに限定表示させる関数をタプルで指定。  
8,9,10行目で設定しているファイルパス、XPath、名前空間は先ほどチェックしたものです。

複数のノードを抽出するXPathでも空行を挟んで出力するようになっています(Calc(38)ディスパッチコマンドのラベル一覧の取得参照)。


Calcで実行するとこのような結果が返ってきます。
<oor:data>
<oor:component-data oor:name="CalcCommands" oor:package="org.openoffice.Office.UI">
<node oor:name="UserInterface">
<node oor:name="Commands">
<node oor:name=".uno:FormatCellDialog">
つまりルートノードからディスパッチコマンド.uno:FormatCellDialogのラベルを定義しているノードまでのノードの階層はこのようになっているとわかりました。

ちなみにファイルをcalc.xcdで実行すると次ような結果になりました。


calc.xcdにはen-USのラベルが設定されていますが、デフォルト値になるせいかoor:op="replace"がついています。

main.xcdでは属性ではなく値として設定されているのでルートまではさかのぼれませんでした。

(2018.3.23追記。親ノードの取得方法を変更しました。まずxpathでrootまでたどって取得できるノードの子と親の辞書を作成しています。
#!/opt/libreoffice5.4/program/python
# -*- coding: utf-8 -*-
import unohelper  # オートメーションには必須(必須なのはuno)。
from xml.etree import ElementTree
from collections import ChainMap 
from com.sun.star.sheet import CellFlags as cf # 定数
def macro(documentevent=None):  # 引数はイベント駆動用。
 doc = XSCRIPTCONTEXT.getDocument() if documentevent is None else documentevent.Source  # ドキュメントのモデルを取得。 
 filepath = "/opt/libreoffice5.2/share/registry/res/registry_ja.xcd"  # xmlファイルへのパス。
 xpath = './/node[@oor:name=".uno:FormatCellDialog"]'  # XPath。1つのノードだけ選択する条件にしないといけない。
 namespaces = {"oor": "{http://openoffice.org/2001/registry}",\
    "xs": "{http://www.w3.org/2001/XMLSchema}",\
    "xsi": "{http://www.w3.org/2001/XMLSchema-instance}"}  # 名前空間の辞書。replace()で置換するのに使う。
 traceToRoot(filepath, xpath, namespaces, doc)
def traceToRoot(filepath, xpath, namespaces, doc):  # xpathは子ノードを取得するXPath。1つのノードだけ選択する条件にしないといけない。
 tree = ElementTree.parse(filepath)  # xmlデータからElementTreeオブジェクト(xml.etree.ElementTree.ElementTree)を取得する。ElementTree.parse()のElementTreeはオブジェクト名ではなくてモジュール名。
 replaceWithValue, replaceWithKey = createReplaceFunc(namespaces)
 xpath = replaceWithValue(xpath)  # 名前空間の辞書のキーを値に変換。
 nodes = tree.findall(xpath)  # 起点となる子ノードを取得。
 outputs = []
 maps = []  # 子ノードをキー、親ノードの値とする辞書のリスト。
 parentnodes = True
 while parentnodes:  # 親ノードのリストの要素があるときTrue。
  xpath = "/".join([xpath, ".."])  # 親のノードのxpathの取得。
  parentnodes = tree.findall(xpath)  # 親ノードのリストを取得。
  maps.append({c:p for p in parentnodes for c in p})  # 新たな辞書をリストに追加。
 parentmap = ChainMap(*maps)  # 辞書をChainMapにする。
 for c in nodes:  # 各子ノードについて。
  outputs.append("\n{}".format(replaceWithKey(formatNode(c))))  # 名前空間の辞書の値をキーに変換して出力する。
  while c in parentmap:  # 親ノードが存在する時。
   c = parentmap[c]  # 親ノードを取得。
   outputs.append(replaceWithKey(formatNode(c)))  # 親ノードを出力。
  outputs.append("")  # 空行を出力。
 if outputs:
  datarows = [(i,) for i in outputs]  # Calcに出力するために行のリストにする。
  controller = doc.getCurrentController()  # コントローラーを取得。
  sheet = controller.getActiveSheet()  # アクティブなシートを取得。
  sheet.clearContents(cf.VALUE+cf.DATETIME+cf.STRING+cf.ANNOTATION+cf.FORMULA+cf.HARDATTR+cf.STYLES)  # セルの内容を削除。cf.HARDATTR+cf.STYLESでセル結合も解除。
  sheet[:len(datarows), :len(datarows[0])].setDataArray(datarows)  # シートに結果を出力する。
  cellcursor = sheet.createCursor()  # シート全体のセルカーサーを取得。
  cellcursor.gotoEndOfUsedArea(True)  # 使用範囲の右下のセルまでにセルカーサーのセル範囲を変更する。
  cellcursor.getColumns().setPropertyValue("OptimalWidth", True)  # セルカーサーのセル範囲の列幅を最適化する。
def formatNode(node):  # 引数はElement オブジェクト。タグ名と属性を出力する。属性の順番は保障されない。
 tag = node.tag  # タグ名を取得。
 attribs = []  # 属性をいれるリスト。
 for key, val in node.items():  # ノードの各属性について。
  attribs.append('{}="{}"'.format(key, val))  # =で結合。
 attrib = " ".join(attribs)  # すべての属性を結合。
 n = "{} {}".format(tag, attrib) if attrib else tag  # タグ名と属性を結合する。
 return "<{}>".format(n)  
def createReplaceFunc(namespaces):  # 引数はキー名前空間名、値は名前空間を波括弧がくくった文字列、の辞書。
 def replaceWithValue(txt):  # 名前空間の辞書のキーを値に置換する。
  for key, val in namespaces.items():
   txt = txt.replace("{}:".format(key), val)
  return txt
 def replaceWithKey(txt):  # 名前空間の辞書の値をキーに置換する。
  for key, val in namespaces.items():
   txt = txt.replace(val, "{}:".format(key))
  return txt
 return replaceWithValue, replaceWithKey
g_exportedScripts = macro, #マクロセレクターに限定表示させる関数をタプルで指定。
)

参考にしたサイト


Development/DispatchCommands - The Document Foundation Wiki
ディスパッチコマンド一覧。

次の関連記事:LibreOffice5(92)全ディスパッチコマンドのラベルの取得

ブログ検索 by Blogger

Translate

最近のコメント

Created by Calendar Gadget

QooQ