LibreOffice5(72)Javaの例:GUIをPythonにする その5

2017-08-23

旧ブログ

t f B! P L
今度はUnoMenu.javaをLibreOffice5(58)モードレスダイアログの例をPythonに翻訳する:その4の3つのウィンドウ作成方法のうちTaskCreatorサービスを使って作成したウィンドウにメニューバーをつけます。

前の関連記事:LibreOffice5(71)Javaの例:GUIをPythonにする その4


unomenu_taskcreator.py: チェックのあるメニュー項目を表示する


GUI/unomenu_taskcreator.py at 5aaeb1f890c668bbbdeb010320aa8d53f45e8264 · p--q/GUI
(2017.8.31追記。コントロールがUnoControlDialogElementサービスをサポートしていないことへの対応のために書き換えました。)

GUI/unomenu_taskcreator.py at develop · p--q/GUI · GitHub
(2018.2.17追記。createMenuの区切り線挿入時のItemPosが間違っていたので修正しました。)


メニューの項目にチェックが入っているものをウィンドウ内に表示する機能も追加しました。

TaskCreatorで作成したウィンドウの背景色は灰色ですが、UnoControlContainerを描画するとその背景は白色になります。
def macro():  # オートメーションではリスナーが呼ばれない、閉じるボタンでウィンドウを閉じるとLibreOfficeがクラッシュする。
 ctx = XSCRIPTCONTEXT.getComponentContext()  # コンポーネントコンテクストの取得。
 smgr = ctx.getServiceManager()  # サービスマネージャーの取得。
 doc = XSCRIPTCONTEXT.getDocument()  # マクロを起動した時のドキュメントのモデルを取得。
 docframe = doc.getCurrentController().getFrame()  # モデル→コントローラ→フレーム、でドキュメントのフレームを取得。
 docwindow = docframe.getContainerWindow()  # ドキュメントのウィンドウ(コンテナウィンドウ=ピア)を取得。
 toolkit = docwindow.getToolkit()  # ツールキットを取得。
 taskcreator = smgr.createInstanceWithContext('com.sun.star.frame.TaskCreator', ctx)
 args = NamedValue("PosSize", Rectangle(100, 100, 500, 500)), NamedValue("FrameName", "NewFrame")  # , NamedValue("MakeVisible", True)  # TaskCreatorで作成するフレームのコンテナウィンドウのプロパティ。
 frame = taskcreator.createInstanceWithArguments(args)  # コンテナウィンドウ付きの新しいフレームの取得。
 window = frame.getContainerWindow()  # 新しいコンテナウィンドウを新しいフレームから取得。
 frame.setTitle("MenuBar Example")  # フレームのタイトルを設定。
 docframe.getFrames().append(frame)  # 新しく作ったフレームを既存のフレームの階層に追加する。
 createMenu = menuCreator(ctx, smgr)
 items = ("~First MenuBar Item", 0),\
   ("~Second MenuBar Item", 0)  # メニューバーの項目。
 menubar = createMenu("MenuBar", items)  # メニューバーの作成。
 window.setMenuBar(menubar)
 menulistener = MenuListener(window, menubar)
 items = ("First Entry", CHECKABLE+AUTOCHECK, {"checkItem": True}),\
   ("First Radio Entry", RADIOCHECK+AUTOCHECK, {"enableItem": False}),\
   ("Second Radio Entry", RADIOCHECK+AUTOCHECK),\
   ("Third Radio Entry", RADIOCHECK+AUTOCHECK, {"checkItem": True}),\
   (),\
   ("Fourth Entry", CHECKABLE+AUTOCHECK, {"checkItem": True}),\
   ("Fifth Entry", CHECKABLE+AUTOCHECK),\
   ("Sixth Entry", 0),\
   ("~Close", 0, {"setCommand": "close"})  # 追加するメニュー項目。空のタプルは区切り線。
 popupmenu =  createMenu("PopupMenu", items, {"addMenuListener": menulistener})  # ポップアップメニューの作成。
 menubar.setPopupMenu(1, popupmenu)
 items = ("First Entry", CHECKABLE+AUTOCHECK, {"checkItem": True}),\
   ("Second Entry", 0)
 popupmenu =  createMenu("PopupMenu", items, {"addMenuListener": menulistener})  # ポップアップメニューの作成。
 menubar.setPopupMenu(2, popupmenu)
 controlcontainer, addControl = controlcontainerCreator(ctx, smgr, {"PositionX": 0, "PositionY": 0, "Width": 500, "Height": 500, "PosSize": POSSIZE})  # ウィンドウに表示させるコントロールコンテナを取得。
 addControl("FixedText", {"PositionX": 20, "PositionY": 20, "Width": 460, "Height": 460, "PosSize": POSSIZE, "Label": "\n".join(getStatus(menubar)), "NoLabel": True, "MultiLine": True})
 menulistener.control = controlcontainer.getControl("FixedText1")  # メニューリスナーにFiexedTextコントロールを渡す。
 controlcontainer.createPeer(toolkit, window)  # ウィンドウにコントロールを描画。
 controlcontainer.setVisible(True)  # コントロールの表示。
 window.setVisible(True)  # ウィンドウの表示。
TaskCreatorで作成したウィンドウはすでにフレームのコンテナウィンドウになっている上、ウィンドウタイトルの変更もできます。

LibreOffice5(71)Javaの例:GUIをPythonにする その4で使った汎用関数createPopupMenu()に代わってメニューバーも作成できる汎用関数menuCreator()を使っています。

menuCreator()については下で解説します。

これでメニューバーもポップアップメニューも同じ形式で作成できるようになりました。

メニューバーでもポップアップメニューでもXMenuインターフェイスのinsertItem()メソッドで同じようにメニュー項目が追加できるのですが、メニューバーの項目にMenuItemStyleを設定してもどう反映されているのかわからなかったのでメニューバーに追加する項目のスタイルはすべて0にしています。
def getStatus(menubar):  # メニューバーについているすべてのポップアップメニューの項目について走査し、チェックがついている項目名を取得。
 t = "   "  # スペース4個にするとタブに置換してしまうときあり。
 txts = []
 c = menubar.getItemCount()
 for i in range(c):
  flg = True
  popup = menubar.getPopupMenu(i+1)
  pc = popup.getItemCount()
  for j in range(pc):
   if popup.isItemChecked(j+1):
    if flg:  # Trueのときはメニューバーの項目名を表示する。
     txts.append(menubar.getItemText(i+1).replace("~", ""))  # ショートカットキーを表す~は削る。
     flg = False
    txts.append("{}{} is checked.".format(t, popup.getItemText(j+1).replace("~", "")))
  txts.append("")
 return txts
この関数getStatus()でメニューバーについているポップアップメニューの全項目を走査してisItemEnabled()がTrueになっている項目を取得しています。

この「チェックされている項目」に対して「チェックされていない項目」も取得したかったのですが、メニューのオブジェクトからMenuItemStyleの状態を取得する方法がわからなかったのでFalseの項目の取得はしていません。

どの項目がチェックボックスやラジオボタンなのかを控えておけばisItemEnabled()がFalseであればチェックがついていないと判断できます。

2行目おインデントのスペースの数を4個ではなくて、3個にしているのは、Eclipse: PyDevメモ: Mixed Indentation: Spaces foundへの対応の走査をしたときにスペースが4個だとタブに置換されてしまうからです。

タブではインデントされませんでした。
class MenuListener(unohelper.Base, XMenuListener):
 def __init__(self, window, menubar):
  self.window = window
  self.menubar = menubar
  self.control = None
 def itemHighlighted(self, menuevent):
  pass
 def itemSelected(self, menuevent):  # PopupMenuの項目がクリックされた時。
  cmd = menuevent.Source.getCommand(menuevent.MenuId)  # メニューIDを元にメニューからメニュー項目のコマンドを取得。
  if cmd == "close":
   self.window.dispose()  # ウィンドウを閉じる。
  else:  # com::sun::star::awt::MenuItemStyleを知る方法がみつからない。uncheckedを知る方法もない。
   self.control.getModel().setPropertyValue("Label", "\n".join(getStatus(self.menubar)))
 def itemActivated(self, menuevent):
  pass
 def itemDeactivated(self, menuevent):
  pass
 def disposing(self, eventobject):
  pass
複数のポップアップメニューに同じリスナーのインスタンスをつけたので発火させたメニュー項目の判別はコマンドでしています。

といってもコマンドはclose一つでしかなく、それ以外のときはすべてgetStatus()を呼び出してウィンドウの表示を更新しています。

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


もう新たにでてきたものだけについて書きます。

menuCreator(ctx, smgr)
def menuCreator(ctx, smgr):  #  メニューバーまたはポップアップメニューを作成する関数を返す。
 def createMenu(menutype, items, attr=None):  # menutypeはMenuBarまたはPopupMenu、itemsは各メニュー項目の項目名、スタイル、適用するメソッドのタプルのタプル、attrは各項目に適用する以外のメソッド。
  if attr is None:
   attr = {}
  menu = smgr.createInstanceWithContext("com.sun.star.awt.{}".format(menutype), ctx)
  for i, item in enumerate(items, start=1):  # 各メニュー項目について。
   if item:
    if len(item) > 2:  # タプルの要素が3以上のときは3番目の要素は適用するメソッドの辞書と考える。
     item = list(item)
     attr[i] = item.pop()  # メニュー項目のIDをキーとしてメソッド辞書に付け替える。
    menu.insertItem(i, *item, i-1)  # ItemId, Text, ItemSytle, ItemPos。ItemIdは1から始まり区切り線(欠番)は含まない。ItemPosは0から始まり区切り線を含む。
   else:  # 空のタプルの時は区切り線と考える。
    menu.insertSeparator(i-1)  # ItemPos
  if attr:  # メソッドの適用。
   for key, val in attr.items():  # keyはメソッド名あるいはメニュー項目のID。
    if isinstance(val, dict):  # valが辞書の時はkeyは項目ID。valはcreateMenu()の引数のitemsであり、itemsの3番目の要素にキーをメソッド名とする辞書が入っている。
     for method, arg in val.items():  # 辞書valのキーはメソッド名、値はメソッドの引数。
      if method in ("checkItem", "enableItem", "setCommand", "setHelpCommand", "setHelpText", "setTipHelpText"):  # 第1引数にIDを必要するメソッド。
       getattr(menu, method)(key, arg)
      else:
       getattr(menu, method)(arg)
    else:
     getattr(menu, key)(val)
  return menu
 return createMenu
LibreOffice5(71)Javaの例:GUIをPythonにする その4で使った汎用関数createPopupMenu()に代わってメニューバーも作成できる汎用関数です。
(2018.2.17追記。区切り線挿入時のItemPosが間違っていたので修正しました。)

コンポーネントコンテクストとサービスマネージャーはクロージャに持たせています。

createMenu(menutype, items, attr=None)

menuCreator()の戻り値のこの関数createMenu()でメニューバーやポップアップメニューを作成します。

menutypeに文字列MenuBarを入れるとメニューバー、文字列PopupMenuを入れるとポップアップメニューが返ってきます。

itemsは各メニュー項目の項目名、スタイル、適用するメソッドのタプルのタプルです。

itemsの3番目の要素の辞書で指定するメソッドは引数に各項目のIDが必要なので、各項目を設定するタプルの要素にしています。

これら以外に適用するメソッドは辞書attrに入れます。

IDと位置番号はitemsの順にそれぞれ0からと1から自動採番します。

(2018.2.17追記。ItemIDは1から始まり区切り線には割り当てられません。ItemPosは0から始まり区切り線を含みます。)

itemsの要素で空のタプルは区切り線と判断します。

区切り線には位置番号を割り当てますがIDは欠番とします。

controlcontainerCreator(ctx, smgr, containerprops)
def controlcontainerCreator(ctx, smgr, containerprops):  # コントロールコンテナと、それにコントロールを追加する関数を返す。まずコントロールコンテナモデルのプロパティを取得。UnoControlDialogElementサービスのプロパティは使えない。propsのキーにPosSize、値にPOSSIZEが必要。
 container = smgr.createInstanceWithContext("com.sun.star.awt.UnoControlContainer", ctx)  # コントロールコンテナの生成。
 container.setPosSize(containerprops.pop("PositionX"), containerprops.pop("PositionY"), containerprops.pop("Width"), containerprops.pop("Height"), containerprops.pop("PosSize"))
 containermodel = smgr.createInstanceWithContext("com.sun.star.awt.UnoControlContainerModel", ctx)  # コンテナモデルの生成。
 containermodel.setPropertyValues(tuple(containerprops.keys()), tuple(containerprops.values()))  # コンテナモデルのプロパティを設定。存在しないプロパティに設定してもエラーはでない。
 container.setModel(containermodel)  # コンテナにコンテナモデルを設定。
 container.setVisible(False)  # 描画中のものを表示しない。
 def addControl(controltype, props, attrs=None):  # props: コントロールモデルのプロパティ、キーにPosSize、値にPOSSIZEが必要。attr: コントロールの属性。
  name = props.pop("Name") if "Name" in props else _generateSequentialName(controltype)  # サービスマネージャーからインスタンス化したコントロールはNameプロパティがないので、コントロールコンテナのaddControl()で名前を使うのみ。
  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)  # コントロールにコントロールモデルを設定。
  container.addControl(name, control)  # コントロールをコントロールコンテナに追加。
  if attrs is not None:  # Dialogに追加したあとでないと各コントロールへの属性は追加できない。
   control = container.getControl(name)  # コントロールコンテナに追加された後のコントロールを取得。
   for key, val in attrs.items():  # メソッドの引数がないときはvalをNoneにしている。
    if val is None:
     getattr(control, key)()
    else:
     getattr(control, key)(val)
 def _createControlModel(controltype, props):  # コントロールモデルの生成。
  controlmodel = smgr.createInstanceWithContext("com.sun.star.awt.UnoControl{}Model".format(controltype), ctx)  # コントロールモデルを生成。
  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 = container.getControl(name)  # 同名のコントロールの有無を判断。
   i += 1
  return name
 return container, addControl  # コントロールコンテナとそのコントロールコンテナにコントロールを追加する関数を返す。
(2017.9.9追記LibreOffice5(81)Javaの例:GUIをPythonにする その7でaddControl()はコントロールを返すように変更しました。)

LibreOffice5(69)Javaの例:GUIをPythonにする その2でてきたdialogCreator()と使い方は同じですが、controlcontainerCreator()ではUnoControlDialogではなくUnoControlContainerを返してそれにaddControl()でコントロールを追加していきます。

UnoControlRoadmapのためのコードは省いています。

UnoControlContainerModelXMultiServiceFactoryサービスをサポートしていないので、コントロールをインスタンス化できず、コントロールはサービスマネジャーからインスタンス化するように変更しています。

 ma単位が使えるコントロールはUnoControlDialogModelのcreateInstance()メソッドでインスタンス化したものだけでなので、ピクセル単位しか使えません。

さらにはサービスマネージャーのcreateInstanceWithContext()でインスタンス化したコントロールはUnoControlDialogElementサービスをサポートしていないので、Height、Name、PositionX、PositionY、Step、TabIndex、Tag、 Widthのプロパティが設定できません。

setPropertyValues()メソッドでは設定できないプロパティは無視されるだけでエラーがでませんでした。

コントロールの位置と大きさはcontrolcontainerCreator()ではプロパティの辞書propsのキーにPosSize、値に定数com.sun.star.awt.PosSizeのPOSSIZEを指定することで、コントロールのsetPosSize()メソッドでPositionX、PositionY、Width、Heightをピクセル単位で指定するようにしています。

Step、TabIndex、Tagの3つのプロパティは使えません。

Nameはコントロールモデルには設定できませんが、コントロールのaddControl()メソッドでコントロールモデルをコントロールに追加するときの名前にNameプロパティの代わりにNameの値を使っています。

これによってコントロールコンテナからgetControl()メソッドでNameでコントロールを取得できます。

コントロールコンテナからは名前でコントロールを取得できるのに、コントロール自身は名前を知らないという状態です。

createWindow()やTaskCreatorでウィンドウを作成するときはma単位しか使えないのでコントロールをピクセル単位でしか設定できないのはそんなに問題ではありません。

しかしコントール自体に名前が設定できないので、リスナーの引数のEventObject StructのSourceからはどのコントロールから発火したのかNameでは判断できません。

この対策としてはUnoControlButtonであればsetActionCommand()メソッドでコマンドを設定することで対応できます。

UnoControlEditもリスナーでよく使いそうですが、予め使用するコントロールをリスナーオブジェクトに持たせておく以外のいい対策はなさそうです。

オートメーションはデバッグ用でしかないことはunomenu_createWindow.pyのときと同様です。

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

ブログ検索 by Blogger

Translate

最近のコメント

Created by Calendar Gadget

QooQ