前の関連記事:Calc(72)リスナーの削除をしているメソッドの発火の確認をするためのマクロ
クリックした位置に出現するモダルダイアログ2
import unohelper # オートメーションには必須(必須なのはuno)。
from com.sun.star.awt import XActionListener
from com.sun.star.awt import XKeyListener
from com.sun.star.awt import XEnhancedMouseClickHandler
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.util import MeasureUnit # 定数
from com.sun.star.style.VerticalAlignment import MIDDLE
from com.sun.star.ui.dialogs import ExecutableDialogResults # 定数
def macro(documentevent=None): # 引数は文書のイベント駆動用。import pydevd; pydevd.settrace(stdoutToServer=True, stderrToServer=True)
doc = XSCRIPTCONTEXT.getDocument() # 現在開いているドキュメントを取得。
ctx = XSCRIPTCONTEXT.getComponentContext() # コンポーネントコンテクストの取得。
smgr = ctx.getServiceManager() # サービスマネージャーの取得。
controller = doc.getCurrentController() # コントローラの取得。
enhancedmouseclickhandler = EnhancedMouseClickHandler(controller, ctx, smgr, doc) # EnhancedMouseClickHandler。EnhancedMouseClickHandlerではSubject(コントローラ)が取得できないのでコントローラを渡しておく。
controller.addEnhancedMouseClickHandler(enhancedmouseclickhandler) # コントローラにEnhancedMouseClickHandlerを追加。
doc.addDocumentEventListener(DocumentEventListener(controller, enhancedmouseclickhandler)) # ドキュメントにDocumentEventListenerを追加。コントローラに追加したEnhancedMouseClickHandlerを除去する用。
class EnhancedMouseClickHandler(unohelper.Base, XEnhancedMouseClickHandler):
def __init__(self, subj, ctx, smgr, doc):
self.subj = subj # disposing()用。コントローラは取得し直さないと最新の画面の状態が反映されない。
self.args = ctx, smgr, doc
def mousePressed(self, enhancedmouseevent):
ctx, smgr, doc = self.args
target = enhancedmouseevent.Target # ターゲットのセルを取得。
if enhancedmouseevent.Buttons==MouseButton.LEFT: # 左ボタンのとき
if target.supportsService("com.sun.star.sheet.SheetCell"): # ターゲットがセルの時。
if enhancedmouseevent.ClickCount==2: # ダブルクリックの時
controller = doc.getCurrentController() # 現在のコントローラを取得。分割しているのとしていないシートで発火しないことがある問題はself.subjでも解決しない。
frame = controller.getFrame() # フレームを取得。
containerwindow = frame.getContainerWindow() # コンテナウィドウの取得。
framepointonscreen = containerwindow.getAccessibleContext().getAccessibleParent().getAccessibleContext().getLocationOnScreen() # フレームの左上角の点(画面の左上角が原点)。
componentwindow = frame.getComponentWindow() # コンポーネントウィンドウを取得。
border = controller.getBorder() # 行ヘッダの幅と列ヘッダの高さの取得のため。
accessiblecontext = componentwindow.getAccessibleContext() # コンポーネントウィンドウのAccessibleContextを取得。
for i in range(accessiblecontext.getAccessibleChildCount()): # 子AccessibleContextについて。
childaccessiblecontext = accessiblecontext.getAccessibleChild(i).getAccessibleContext() # 子AccessibleContextのAccessibleContext。
if childaccessiblecontext.getAccessibleRole()==51: # SCROLL_PANEの時。
for j in range(childaccessiblecontext.getAccessibleChildCount()): # 孫AccessibleContextについて。
grandchildaccessiblecontext = childaccessiblecontext.getAccessibleChild(j).getAccessibleContext() # 孫AccessibleContextのAccessibleContext。
if grandchildaccessiblecontext.getAccessibleRole()==84: # DOCUMENT_SPREADSHEETの時。これが枠。
bounds = grandchildaccessiblecontext.getBounds() # 枠の位置と大きさを取得(SCROLL_PANEの左上角が原点)。
if bounds.X==border.Left and bounds.Y==border.Top: # SCROLL_PANEに対する相対座標が行ヘッダと列ヘッダと一致する時は左上枠。
for k, subcontroller in enumerate(controller): # 各枠のコントローラについて。インデックスも取得する。
cellrange = subcontroller.getReferredCells() # 見えているセル範囲を取得。
if len(cellrange.queryIntersection(target.getRangeAddress())): # ターゲットが含まれるセル範囲コレクションが返る時その枠がクリックした枠。「ウィンドウの分割」では正しいiは必ずしも取得できない。
sourcepointonscreen = grandchildaccessiblecontext.getLocationOnScreen() # 左上枠の左上角の点を取得(画面の左上角が原点)。
if k==1: # 左下枠の時。
sourcepointonscreen = Point(X=sourcepointonscreen.X, Y=sourcepointonscreen.Y+bounds.Height)
elif k==2: # 右上枠の時。
sourcepointonscreen = Point(X=sourcepointonscreen.X+bounds.Width, Y=sourcepointonscreen.Y)
elif k==3: # 右下枠の時。
sourcepointonscreen = Point(X=sourcepointonscreen.X+bounds.Width, Y=sourcepointonscreen.Y+bounds.Height)
x = sourcepointonscreen.X + enhancedmouseevent.X - framepointonscreen.X # ウィンドウの左上角からの相対Xの取得。
y = sourcepointonscreen.Y + enhancedmouseevent.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) # テクストボックスコントロールのカーソルの位置を変更。ピア作成後でないと反映されない。
dialog.execute()
dialog.dispose()
return False # セル編集モードにしない。
return True # セル編集モードにする。
def mouseReleased(self, enhancedmouseevent):
return True # シングルクリックでFalseを返すとセル選択範囲の決定の状態になってどうしようもなくなる。
def disposing(self, eventobject):
self.subj.removeEnhancedMouseClickHandler(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
def keyPressed(self, keyevent):
if keyevent.KeyCode==Key.RETURN: # リターンキーが押された時。
target = self.args
source = keyevent.Source # テキストボックスコントロールが返る。
context = source.getContext() # コントロールダイアログが返ってくる。
target.setString(context.getControl("Edit1").getText()) # テキストボックスコントロールの内容を選択セルに代入する。
context.endDialog(ExecutableDialogResults.OK) # ダイアログフレームを閉じる。
def keyReleased(self, keyevnet):
pass
def disposing(self, eventobject):
eventobject.Source.removeKeyListener(self)
class ActionListener(unohelper.Base, XActionListener):
def __init__(self, target):
self.args = target
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()) # テキストボックスコントロールの内容を選択セルに代入する。
context.endDialog(ExecutableDialogResults.OK) # ダイアログフレームを閉じる。
def disposing(self, eventobject):
eventobject.Source.removeActionListener(self)
class DocumentEventListener(unohelper.Base, XDocumentEventListener):
def __init__(self, controller, enhancedmouseclickhandler):
self.args = controller, enhancedmouseclickhandler
def documentEventOccured(self, documentevent):
controller, enhancedmouseclickhandler = self.args
if documentevent.EventName=="OnUnload": # ドキュメントを閉じる時。リスナーを削除する。
controller.removeEnhancedMouseClickHandler(enhancedmouseclickhandler) # コントローラのEnhancedMouseClickHandlerの削除。
documentevent.Source.removeDocumentEventListener(self) # このリスナーをドキュメントから削除。
def disposing(self, eventobject):
eventobject.Source.removeDocumentEventListener(self)
ハイライトした36行目から55行目が、Calc(70)クリックした位置に出現するモダルダイアログの36行目のsourcepointscreenを置き換えている部分です。まずコンポーネントウィンドウのAccessibleContextを取得してその子AccessibleContextからSCROLL_PANE=51を探し出します(40行目)。
SCROLL_PANE=51は1つしかないはずです(Calc(67)AccessibleWindowの位置と大きさを取得するマクロ)。
さらにその子AccessibleContextからDOCUMENT_SPREADSHEET=84を探します(43行目)。
このDOCUMENT_SPREADSHEET=84は通常は1つですが、シートのウィンドウの分割や行や列の固定をしているときは4つまで増えます。
SCROLL_PANE=51に対するXが行ヘッダの幅、Yが列ヘッダの高さに一致しているDOCUMENT_SPREADSHEET=84が左上の枠と判断します(45行目)。
(2019.1.26追記。この条件は行番号が400ぐらい以上になると成立しませんでした。なので、XYともに最小のものを左上枠と判断するようにしないといけません。)
AccessibleContextからは画面の左上角を原点とする座標のXYを取得できる(49行目)ので、左上枠の大きさからすべての枠の左上角の画面に対するXYを算出できます。
EnhancedMouseEvent StructのXYはクリックした枠の左上角を原点とする座標の点なので、これで画面の左上角を原点とする座標のXYが算出できます。
しかし、問題はXEnhancedMouseClickHandlerのメソッドの引数のEnhancedMouseEvent StructのSourceアトリビュートはNoneしか返ってこないので、XMouseListenerのようにクリックしている枠がどれか判定できないことです。
EnhancedMouseEvent StructからはTargetでクリックしたセルが取得できるだけです。
セルのアドレスでは、「ウィンドウの分割」でスクロールしてどの枠にも同じセルを表示させているときはどの枠かは判定できません。
なので、XEnhancedMouseClickHandlerを使った今回のマクロでは「ウィンドウの分割」では正しくクリックした位置にダイアログを出現させることはできません。
「行と列の固定」のときは左上の枠がスクロールすることがなく、各枠で同じセルが表示されることはないのでTargetのアドレスでどの枠を判定できます。
なので、46行目でコントローラから各枠のSpreadsheetViewPaneを取得して、Targetがそれに含まれるかを判断しています(48行目)(Calc(63)セルの固定、ウィンドウの分割、の境界を取得する)。
これでどの枠をクリックしたのかが判定できるので、左上枠のとき(49行目)、左下枠のとき(50行目)、右上枠のとき(52行目)、右下枠のとき(54行目)で場合分けして、EnhancedMouseEvent StructのXYの原点を決定しています。
「ウィンドウの分割」や「行と列の固定」をしていないときは枠が一つの場合なので同じアルゴリズムで処理できます。
クリックした位置にモダルダイアログを出現させるCalcドキュメント
PopupModalDialog2.ods
動作はCalc(70)クリックした位置に出現するモダルダイアログのPopupModalDialog.odsと同じですが、Sheet3の「ウィンドウの分割」の場合はクリックした位置に正しくダイアログが出現しません。
右下枠のB8セルをダブルクリックしていますが、左上枠にもB8セルがあるために左上枠をクリックしたと判定してしまって、左上枠の左上角を原点としてダイアログを出現させてしまっています。

0 件のコメント:
コメントを投稿