Calc(70)クリックした位置に出現するモダルダイアログ

2018-02-05

旧ブログ

t f B! P L
ドキュメントのコンテナウィンドウやコンポーネントウィンドウに追加したXMouseListenerは発火せず座標の取得に使えず、XEnhancedMouseClickHandlerはシートを分割や固定をしていると原点の座標の取得が困難であったためXMouseClickHandlerを使います。

前の関連記事:Calc(69)XMouseClickHandlerで取得できる位置


(2018.2.6追記。「ウィンドウの分割」のシートでは正しく動作しませんが、動作が安定しているのでXEnhancedMouseClickHandlerを使うことにしました。Calc(73)クリックした位置に出現するモダルダイアログ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.util import MeasureUnit  # 定数
from com.sun.star.style.VerticalAlignment import MIDDLE  # enum
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()  # コントローラの取得。
 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)  # テクストボックスコントロールのカーソルの位置を変更。ピア作成後でないと反映されない。
     dialog.execute()  
     dialog.dispose() 
     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
 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, 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)
関数getStringAddressFromCellRange()(Calc(55)追加できるリスナー一覧: その7参照)とdialogCreator()(LibreOffice5(69)Javaの例:GUIをPythonにする その2参照)を使っていますがコードは略しています。

105行目のDocumentEventListenerはドキュメントを閉じるときにMouseClickHandlerを削除するのに使用しています。

このリスナーのdisposing()は呼ばれないので(Calc(55)追加できるリスナー一覧: その7)、MouseClickHandlerの削除に続いてDocumentEventListener自身も削除しています。

クリックした位置とダイアログを出現させる位置


コントロールダイアログはフレーム(ウィンドウ)の左上角を原点とする座標の点で指定する(Calc(68)XEnhancedMouseClickHandlerで取得できる位置)ので、クリックした位置も同じ座標で求めないといけません。

クリックした位置はXMouseClickHandlerの引数のMouseEvent StructのXYから取得できますが、この点はセルが表示されている枠の左上角を原点としています(Calc(69)XMouseClickHandlerで取得できる位置参照)。

このセルが表示されている枠の左上角の点は分割していないウィンドウではcontroller.getPropertyValue("VisibleAreaOnScreen")(controllerはコントローラ)で画面の左上角を原点とする座標でXYを求められます(Calc(65)フレームとコントローラの位置)が、シートを分割しているときは左上の枠以外は原点にならないのでこの方法は使えません。

その代わりMouseEvent StructのSourceでクリックした枠を取得できるので(Calc(69)XMouseClickHandlerで取得できる位置)、そのAccessibleContextのgetLocationOnScreen()メソッドから画面の左上角を原点とする座標の点を取得しています(36行目)。

34行目でフレーム(ウィンドウ)の左上角の画面の左上角を原点とする座標の点はコンテナウィンドウから取得しています(Calc(65)フレームとコントローラの位置)。

36行目で取得した点から34行目で取得した点を引くと、表示されている枠の左上角の点の、フレームに対する相対座標のXYが取得できるので、それにMouseEvent StructのXYを加えてコントロールダイアログの位置にします(37行目と38行目)。

コントロールダイアログではma単位を使っているので39行目でピクセルをmaに変換しています(LibreOffice5(136)Map AppFont (ma)とピクセルと1/100mmの変換)。

これでクリックした点を左上角とするコントロールダイアログを表示できました。

ただしWindows10では原点であるはずの画面の左上角の点がなぜか(-8, -8)になっている(つまり原点が画面の左上角の点から右下に8pxずつずれている)ので(Calc(65)フレームとコントローラの位置)、クリックした点より右下8pxずれた位置にコントロールダイアログの左上角がきてしまいます(まだ私のメインPC一台でしか確認していません)。

クリックした位置にモダルダイアログを出現させるマクロの実行結果


PopupModalDialog.ods

このマクロを埋め込んだCalcドキュメントです。


ダブルクリックするとモダルダイアログが出現します。

モダルダイアログなのでダイアログを閉じないとシートの操作はできません。

テキストボックスコントロールにはダブルクリックしたセルのアドレスが入っています。

テキストボックスコントロールでEnterキーを押すかEnterボタンをクリックするとテキストボックスコントロールの内容が選択セルに入力されて、ダイアログが閉じます。

ウィンドウの分割や行と列の固定をしていても同様にダイアログが出現します。

しかし、分割や固定をしていないシートと混じったドキュメントの場合は、XMouseListenerのメソッドが発火しないときがありました。

ドキュメントを開いたときのシートの分割や固定の状態と異なるときにXMouseListenerのメソッドが発火しないのかと思いましたが、何回か操作を繰り返していると、そうでもなく、いまいち規則性はわかりませんでしたが、メソッドが発火したいときがあるのは確かです。

XMouseListenerのメソッドが発火しない規則がはっきりしないので対策も難しいです。

PopupModalDialog.odsではSheet1は分割や固定はなし、Sheet2は行と列を固定、Sheet3はウィンドウの固定をしています。

ドキュメントはSheet1がアクティブシートとして開きます。

Sheet2をアクティブにして右下の枠をダブルクリックしてもダイアログが出現しません。

ところが右下の枠以外をダブルクリックするとすべての枠でダイアログが出現するようになります。

しかし今度はSheet1でダイアログが出現しなくなります。

でもずっとダイアログが出現しないわけではなく、Sheet2に切り替えたりしているとSheet1でダイアログが出現するようになります。

次の関連記事:Calc(71)クリックした位置に出現するノンモダルダイアログ

ブログ検索 by Blogger

Translate

最近のコメント

Created by Calendar Gadget

QooQ