前の関連記事:Calc(70)クリックした位置に出現するモダルダイアログ
(2018.2.7追記。「ウィンドウの分割」のシートでは正しく動作しませんが、動作が安定しているのでXEnhancedMouseClickHandlerを使うことにしました。Calc(74)クリックした位置に出現するノンモダルダイアログ2参照。)
クリックした位置にノンモダルダイアログを出現させるマクロ
import unohelper # オートメーションには必須(必須なのはuno)。
from com.sun.star.awt import XActionListener
from com.sun.star.awt import XKeyListener
from com.sun.star.awt import XMouseClickHandler
from com.sun.star.awt import Key # 定数
from com.sun.star.awt import MouseButton # 定数
from com.sun.star.awt import Point # Struct
from com.sun.star.awt import Selection # Struct
from com.sun.star.document import XDocumentEventListener
from com.sun.star.frame import XFrameActionListener
from com.sun.star.frame.FrameAction import FRAME_UI_DEACTIVATING # enum
from com.sun.star.util import XCloseListener
from com.sun.star.util import MeasureUnit # 定数
from com.sun.star.style.VerticalAlignment import MIDDLE
def macro(documentevent=None): # 引数は文書のイベント駆動用。import pydevd; pydevd.settrace(stdoutToServer=True, stderrToServer=True)
doc = XSCRIPTCONTEXT.getDocument() # 現在開いているドキュメントを取得。
ctx = XSCRIPTCONTEXT.getComponentContext() # コンポーネントコンテクストの取得。
smgr = ctx.getServiceManager() # サービスマネージャーの取得。
controller = doc.getCurrentController() # コントローラの取得。
mouseclickhandler = MouseClickHandler(controller, ctx, smgr, doc) # MouseClickHandler。MouseClickHandlerではSubject(コントローラ)が取得できないのでコントローラを渡しておく。
controller.addMouseClickHandler(mouseclickhandler) # コントローラにMouseClickHandlerを追加。
doc.addDocumentEventListener(DocumentEventListener(controller, mouseclickhandler)) # ドキュメントにDocumentEventListenerを追加。コントローラに追加したMouseClickHandlerを除去する用。
class MouseClickHandler(unohelper.Base, XMouseClickHandler):
def __init__(self, subj, ctx, smgr, doc):
self.subj = subj # disposing()用。コントローラは取得し直さないと最新の画面の状態が反映されない。
self.args = ctx, smgr, doc
def mousePressed(self, mouseevent):
ctx, smgr, doc = self.args
target = doc.getCurrentSelection() # ターゲットのセルを取得。
if mouseevent.Buttons==MouseButton.LEFT: # 左ボタンのとき
if target.supportsService("com.sun.star.sheet.SheetCell"): # ターゲットがセルの時。
if mouseevent.ClickCount==2: # ダブルクリックの時
controller = doc.getCurrentController() # 現在のコントローラを取得。分割しているのとしていないシートで発火しないことがある問題はself.subjでも解決しない。
frame = controller.getFrame() # フレームを取得。
containerwindow = frame.getContainerWindow() # コンテナウィドウの取得。
framepointonscreen = containerwindow.getAccessibleContext().getAccessibleParent().getAccessibleContext().getLocationOnScreen() # フレームの左上角の点(画面の左上角が原点)。
componentwindow = frame.getComponentWindow() # コンポーネントウィンドウを取得。
sourcepointonscreen = mouseevent.Source.getAccessibleContext().getLocationOnScreen() # クリックした枠の左上の点(画面の左上角が原点)。
x = sourcepointonscreen.X + mouseevent.X - framepointonscreen.X # ウィンドウの左上角からの相対Xの取得。
y = sourcepointonscreen.Y + mouseevent.Y - framepointonscreen.Y # ウィンドウの左上角からの相対Yの取得。
dialogpoint = componentwindow.convertPointToLogic(Point(X=x, Y=y), MeasureUnit.APPFONT) # ピクセル単位をma単位に変換。
actionlistener = ActionListener(target) # ボタンコントロールに追加するActionListener。操作するためにtargetを渡す。
keylistener = KeyListener(target) # テクストボックスコントロールに追加するKeyListener。操作するためにtargetを渡す。
m = 6 # コントロール間の間隔
name = {"PositionX": m, "Width": 50, "Height": 12, "NoLabel": True, "Align": 0, "VerticalAlign": MIDDLE} # PositionYは後で設定。
address = {"PositionX": m, "Width": 50, "Height": name["Height"], "VerticalAlign": MIDDLE} # PositionYは後で設定。
controldialog = {"PositionX": dialogpoint.X, "PositionY": dialogpoint.Y, "Width": XWidth(address, m), "Title": "Popup Dialog", "Name": "PopupDialog", "Step": 0, "Moveable": True} # コントロールダイアログのプロパティ。幅は右端のコントロールから取得。高さは最後に設定する。
dialog, addControl = dialogCreator(ctx, smgr, controldialog) # コントロールダイアログの作成。
name["PositionY"] = m
name["Label"] = "Target Address"
addControl("FixedText", name) # ラベルフィールドコントロールの追加。
address["PositionY"] = YHeight(name, m)
stringaddress = getStringAddressFromCellRange(target) # 選択セルの文字列アドレスを取得。
address["Text"] = stringaddress # テキストボックスコントロールに文字列アドレスを入れる。
textlength = len(stringaddress) # 文字列アドレスの長さを取得。
edit1selection = Selection(Min=textlength, Max=textlength) # カーソルの位置を最後にする。指定しないと先頭になる。
edit1 = addControl("Edit", address, {"addKeyListener": keylistener}) # テキストボックスコントロールの追加。
button1 = {"PositionY": YHeight(address, m), "Width": 26, "Height": name["Height"]+2, "Label": "~Cancel", "PushButtonType": 2} # PositionXは後で設定。
button2 = {"PositionY": YHeight(address, m), "Width": 22, "Height": name["Height"]+2, "Label": "~Enter", "PushButtonType": 0} # PositionXは後で設定。
button2["PositionX"] = XWidth(address, -button2["Width"])
button1["PositionX"] = button2["PositionX"] - int(m/2) - button1["Width"]
addControl("Button", button1) # ボタンコントロールの追加。
addControl("Button", button2, {"setActionCommand": "enter" ,"addActionListener": actionlistener}) # ボタンコントロールの追加。
dialog.getModel().setPropertyValue("Height", YHeight(button1, m)) # コントロールダイアログの高さを設定。
toolkit = componentwindow.getToolkit() # ピアからツールキットを取得。
dialog.createPeer(toolkit, componentwindow) # ダイアログを描画。親ウィンドウを渡す。ノンモダルダイアログのときはNone(デスクトップ)ではフリーズする。Stepを使うときはRoadmap以外のコントロールが追加された後にピアを作成しないとStepが重なって表示される。
edit1.setSelection(edit1selection) # テクストボックスコントロールのカーソルの位置を変更。ピア作成後でないと反映されない。
dialogframe = showModelessly(ctx, smgr, frame, dialog) # ノンモダルダイアログとして表示。ダイアログのフレームを取得。
actionlistener.frame = dialogframe # ActionListenerにダイアログフレームを渡す。閉じるため。
keylistener.frame = dialogframe # KeyListenerにダイアログフレームを渡す。閉じるため。
frameactionlistener = FrameActionListener() # FrameActionListener。フレームがアクティブでなくなった時に閉じるため。
dialogframe.addFrameActionListener(frameactionlistener) # FrameActionListenerをダイアログフレームに追加。
dialogframe.addCloseListener(CloseListener(dialog, frameactionlistener, actionlistener, keylistener)) # CloseListener
return True # セル編集モードにしない。
return False # セル編集モードにする。
def mouseReleased(self, mouseevent):
return False
def disposing(self, eventobject):
self.subj.removeMouseClickHandler(self)
def XWidth(props, m=0): # 左隣のコントロールからPositionXを取得。mは間隔。
return props["PositionX"] + props["Width"] + m
def YHeight(props, m=0): # 上隣のコントロールからPositionYを取得。mは間隔。
return props["PositionY"] + props["Height"] + m
class KeyListener(unohelper.Base, XKeyListener):
def __init__(self, target):
self.args = target
self.frame = None # ダイアログフレームをあとで入れる。
def keyPressed(self, keyevent):
if keyevent.KeyCode==Key.RETURN: # リターンキーが押された時。
target = self.args
source = keyevent.Source # テキストボックスコントロールが返る。
context = source.getContext() # コントロールダイアログが返ってくる。
target.setString(context.getControl("Edit1").getText()) # テキストボックスコントロールの内容を選択セルに代入する。
self.frame.close(True) # ダイアログフレームを閉じる。
def keyReleased(self, keyevnet):
pass
def disposing(self, eventobject):
eventobject.Source.removeKeyListener(self)
class CloseListener(unohelper.Base, XCloseListener): # ノンモダルダイアログのリスナー削除用。
def __init__(self, dialog, frameactionlistener, actionlistener, keylistener):
self.args = dialog, frameactionlistener, actionlistener, keylistener
def queryClosing(self, eventobject, getsownership): # コントロールダイアログを閉じると直前。フレーム削除する。
dialog, frameactionlistener, actionlistener, keylistener = self.args
dialog.getControl("Button2").removeActionListener(actionlistener) # ボタンコントロールのActionListenerの削除。
dialog.getControl("Edit1").removeKeyListener(keylistener) # テキストボックスコントロールのKeyListenerの削除。
eventobject.Source.removeFrameActionListener(frameactionlistener) # ダイアログフレームのFrameActionListenerの削除。
eventobject.Source.removeCloseListener(self) # ダイアログフレームのCloseListenerの削除。
def notifyClosing(self, eventobject):
pass
def disposing(self, eventobject):
eventobject.Source.removeCloseListener(self)
class ActionListener(unohelper.Base, XActionListener):
def __init__(self, target):
self.args = target
self.frame = None # ダイアログフレームをあとで入れる。
def actionPerformed(self, actionevent):
target = self.args
cmd = actionevent.ActionCommand
source = actionevent.Source # ボタンコントロールが返る。
context = source.getContext() # コントロールダイアログが返ってくる。
if cmd == "enter":
target.setString(context.getControl("Edit1").getText()) # テキストボックスコントロールの内容を選択セルに代入する。
self.frame.close(True) # ダイアログフレームを閉じる。
def disposing(self, eventobject):
eventobject.Source.removeActionListener(self)
class FrameActionListener(unohelper.Base, XFrameActionListener):
def frameAction(self, frameactionevent):
if frameactionevent.Action==FRAME_UI_DEACTIVATING: # フレームがアクティブでなくなった時。TopWindowListenerのwindowDeactivated()だとウィンドウタイトルバーをクリックしただけで発火してしまう。
frameactionevent.Frame.removeFrameActionListener(self) # フレームにつけたリスナーを除去。
frameactionevent.Frame.close(True)
def disposing(self, eventobject):
eventobject.Source.removeFrameActionListener(self)
class DocumentEventListener(unohelper.Base, XDocumentEventListener):
def __init__(self, controller, mouseclickhandler):
self.args = controller, mouseclickhandler
def documentEventOccured(self, documentevent):
controller, mouseclickhandler = self.args
if documentevent.EventName=="OnUnload": # ドキュメントを閉じる時。リスナーを削除する。
controller.removeMouseClickHandler(mouseclickhandler) # コントローラのMouseClickHandlerの削除。
documentevent.Source.removeDocumentEventListener(self) # このリスナーをドキュメントから削除。
def disposing(self, eventobject):
eventobject.Source.removeDocumentEventListener(self)
showModelessly()(LibreOffice5(69)Javaの例:GUIをPythonにする その2)を使っていますがコードは略しています。ハイライトしている部分がモダルダイアログのマクロとの違う点です。
FrameActionListenerでノンモダルダイアログがアクティブでなくなったときに閉じるようにしています。
モダルダイアログのときと違って、リスナーを削除するためにノンモダルダイアログのフレームにCloseListenerを追加しています。
リスナーを削除しないままにノンモダルダイアログの入っているフレームをclose()メソッドで閉じるとLibreOfficeがクラッシュします。
クリックした位置にノンモダルダイアログを出現させるマクロの実行結果
PopupNonmodalDialog.ods
このマクロを埋め込んだCalcドキュメントです。
シートの分割や固定をしているかしていないかでXMouseListenerのメソッドが発火したりしなかったりする問題はモダルダイアログのときと同じでした。
(2018.6.16追記。TaskCreatorでノンモダルダイアログを作成するときに渡すRectangle StructのYはウィンドウタイトルバーを含まないため、ウィンドウタイトルバーの高さ分下の位置を指定しないといけません。)
0 件のコメント:
コメントを投稿