前の関連記事:Calc(77)コントローラのロックとアクションのロックとリスナー
グリッドコントロールの行をマウスでコピペできるようにしたマクロ
import unohelper # オートメーションには必須(必須なのはuno)。 from datetime import datetime from com.sun.star.accessibility import AccessibleRole # 定数 from com.sun.star.awt import XActionListener from com.sun.star.awt import XEnhancedMouseClickHandler from com.sun.star.awt import XMenuListener from com.sun.star.awt import XMouseListener from com.sun.star.awt import MouseButton # 定数 from com.sun.star.awt import PopupMenuDirection # 定数 from com.sun.star.awt import Rectangle # Struct from com.sun.star.awt import ScrollBarOrientation # 定数 from com.sun.star.document import XDocumentEventListener from com.sun.star.style.VerticalAlignment import MIDDLE # enum from com.sun.star.util import XCloseListener from com.sun.star.view.SelectionType import MULTI # enum def macro(documentevent=None): # 引数は文書のイベント駆動用。import pydevd; pydevd.settrace(stdoutToServer=True, stderrToServer=True) ctx = XSCRIPTCONTEXT.getComponentContext() # コンポーネントコンテクストの取得。 smgr = ctx.getServiceManager() # サービスマネージャーの取得。 doc = XSCRIPTCONTEXT.getDocument() # マクロを起動した時のドキュメントのモデルを取得。 controller = doc.getCurrentController() # コントローラの取得。 enhancedmouseclickhandler = EnhancedMouseClickHandler(controller, ctx, smgr, doc) controller.addEnhancedMouseClickHandler(enhancedmouseclickhandler) # EnhancedMouseClickHandler doc.addDocumentEventListener(DocumentEventListener(enhancedmouseclickhandler)) # DocumentEventListener。ドキュメントのリスナーの削除のため。 class DocumentEventListener(unohelper.Base, XDocumentEventListener): def __init__(self, enhancedmouseclickhandler): self.args = enhancedmouseclickhandler def documentEventOccured(self, documentevent): # ドキュメントのリスナーを削除する。 enhancedmouseclickhandler = self.args if documentevent.EventName=="OnUnload": source = documentevent.Source source.removeEnhancedMouseClickHandler(enhancedmouseclickhandler) source.removeDocumentEventListener(self) def disposing(self, eventobject): eventobject.Source.removeDocumentEventListener(self) class EnhancedMouseClickHandler(unohelper.Base, XEnhancedMouseClickHandler): def __init__(self, subj, ctx, smgr, doc): self.subj = subj 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: # ダブルクリックの時 cellbackcolor = target.getPropertyValue("CellBackColor") # セルの背景色を取得。 if cellbackcolor==0x6666FF: # 背景が青色の時。 createDialog(ctx, smgr, doc, True) # ノンモダルダイアログにする。 return False # セル編集モードにしない。 elif cellbackcolor==0xFFFF99: # 背景が黄色の時。 createDialog(ctx, smgr, doc, False) # モダルダイアログにする。 return False # セル編集モードにしない。 return True # セル編集モードにする。 def mouseReleased(self, enhancedmouseevent): return True # シングルクリックでFalseを返すとセル選択範囲の決定の状態になってどうしようもなくなる。 def disposing(self, eventobject): # ドキュメントを閉じる時でも呼ばれない。 self.subj.removeEnhancedMouseClickHandler(self) def createDialog(ctx, smgr, doc, flg): frame = doc.getCurrentController().getFrame() # モデル→コントローラ→フレーム、でドキュメントのフレームを取得。 containerwindow = frame.getContainerWindow() # ドキュメントのウィンドウ(コンテナウィンドウ=ピア)を取得。 toolkit = containerwindow.getToolkit() # ピアからツールキットを取得。 m = 6 # コントロール間の間隔 grid = {"PositionX": m, "PositionY": m, "Width": 118, "Height": 100, "ShowColumnHeader": True, "ShowRowHeader": True, "SelectionModel": MULTI, "VScroll": True, "ShowRowHeader": False} # グリッドコントロールの基本プロパティ。 label = {"Height": 12, "NoLabel": True, "Align": 2, "VerticalAlign": MIDDLE} # ラベルフィールドコントロールの基本プロパティ。 textbox = {"Height": label["Height"], "VerticalAlign": MIDDLE} # テクストボックスコントロールの基本プロパティ。 button = {"Width": 30, "Height": label["Height"]+2, "PushButtonType": 0} # ボタンの基本プロパティ。PushButtonTypeの値はEnumではエラーになる。 controldialog = {"PositionX": 100, "PositionY": 40, "Width": grid["PositionX"]+grid["Width"]+m, "Title": "Grid Example", "Name": "controldialog", "Step": 0, "Moveable": True} # コントロールダイアログの基本プロパティ。幅は右端のコントロールから取得。高さは最後に設定する。 dialog, addControl = dialogCreator(ctx, smgr, controldialog) # コントロールダイアログの作成。 actionlistener = ActionListener() grid1 = addControl("Grid", grid) # グリッドコントロールの取得。マウスリスナーはメニューリスナー作成後に追加する。 menulistener = MenuListener(grid1) # ポップアップメニューにつけるメニューリスナーを取得。 items = ("~Cut", 0, {"setCommand": "cut"}),\ ("Cop~y", 0, {"setCommand": "copy"}),\ ("~Paste Above", 0, {"setCommand": "pasteabove"}),\ ("P~aste Below", 0, {"setCommand": "pastebelow"}),\ (),\ ("~Delete Selected Rows", 0, {"setCommand": "delete"}) # グリッドコントロールにつける右クリックメニュー。 popupmenu = menuCreator(ctx, smgr)("PopupMenu", items, {"addMenuListener": menulistener}) # 右クリックでまず呼び出すポップアップメニュー。 mouselister = MouseListener(doc, popupmenu, menulistener) grid1.addMouseListener(mouselister) gridmodel = grid1.getModel() # グリッドコントロールモデルの取得。 gridcolumn = gridmodel.getPropertyValue("ColumnModel") # DefaultGridColumnModel column0 = gridcolumn.createColumn() # 列の作成。 column0.Title = "Date" # 列ヘッダー。 column0.ColumnWidth = 60 # 列幅。 gridcolumn.addColumn(column0) # 列を追加。 column1 = gridcolumn.createColumn() # 列の作成。 column1.Title = "Time" # 列ヘッダー。 column1.ColumnWidth = grid["Width"] - column0.ColumnWidth # 列幅。列の合計がグリッドコントロールの幅に一致するようにする。 gridcolumn.addColumn(column1) # 列を追加。 griddata = gridmodel.getPropertyValue("GridDataModel") # GridDataModel now = datetime.now() # 現在の日時を取得。 d = now.date().isoformat() t = now.time().isoformat().split(".")[0] griddata.addRow(0, (d, t)) # グリッドに行を挿入。 label["PositionY"] = textbox["PositionY"] = YHeight(grid, m) label1, label2 = [label.copy() for dummy in range(2)] textbox1, textbox2 = [textbox.copy() for dummy in range(2)] label1["Label"] = "Date: " label1["PositionX"] = m label1["Width"] = 18 textbox1["PositionX"] = XWidth(label1) textbox1["Width"] = 42 textbox1["Text"] = d label2["Label"] = "Time: " label2["PositionX"] = XWidth(textbox1, m) label2["Width"] = 18 textbox2["PositionX"] = XWidth(label2) textbox2["Width"] = 34 textbox2["Text"] = t addControl("FixedText", label1) addControl("Edit", textbox1) addControl("FixedText", label2) addControl("Edit", textbox2) button["PositionY"] = YHeight(label, m) button1, button2, button3 = [button.copy() for dummy in range(3)] button1["Label"] = "~Now" button1["PositionX"] = controldialog["Width"] - (button["Width"] + m) * 3 button2["Label"] = "~Insert" button2["PositionX"] = XWidth(button1, m) button3["Label"] = "~Close" button3["PositionX"] = XWidth(button2, m) button3["PushButtonType"] = 2 # CANCEL addControl("Button", button1, {"setActionCommand": "now" ,"addActionListener": actionlistener}) addControl("Button", button2, {"setActionCommand": "insert" ,"addActionListener": actionlistener}) addControl("Button", button3) dialog.getModel().setPropertyValue("Height", YHeight(button1, m)) # コントロールダイアログの高さを設定。 dialog.createPeer(toolkit, containerwindow) # ダイアログを描画。親ウィンドウを渡す。ノンモダルダイアログのときはNone(デスクトップ)ではフリーズする。Stepを使うときはRoadmap以外のコントロールが追加された後にピアを作成しないとStepが重なって表示される。 if flg: # ノンモダルダイアログにするとき。オートメーションでは動かない。 dialogframe = showModelessly(ctx, smgr, frame, dialog) args = popupmenu, menulistener, dialog, mouselister, actionlistener dialogframe.addCloseListener(CloseListener(args)) # CloseListener。ノンモダルダイアログのリスナー削除用。 else: # モダルダイアログにする。フレームに追加するとエラーになる。 dialog.execute() dialog.dispose() 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 CloseListener(unohelper.Base, XCloseListener): # ノンモダルダイアログのリスナー削除用。 def __init__(self, args): self.args = args def queryClosing(self, eventobject, getsownership): popupmenu, menulistener, dialog, mouselister, actionlistener = self.args popupmenu.removeMenuListener(menulistener) dialog.getControl("Grid1").removeMouseListener(mouselister) dialog.getControl("Button1").removeActionListener(actionlistener) eventobject.Source.removeCloseListener(self) def notifyClosing(self, eventobject): pass def disposing(self, eventobject): eventobject.Source.removeCloseListener(self) class ActionListener(unohelper.Base, XActionListener): def actionPerformed(self, actionevent): cmd = actionevent.ActionCommand source = actionevent.Source # ボタンコントロールが返る。 context = source.getContext() # コントロールダイアログが返ってくる。 if cmd == "now": now = datetime.now() # 現在の日時を取得。 context.getControl("Edit1").setText(now.date().isoformat()) # テキストボックスコントロールに入力。 context.getControl("Edit2").setText(now.time().isoformat().split(".")[0]) # テキストボックスコントロールに入力。 elif cmd=="insert": d = context.getControl("Edit1").getText() t = context.getControl("Edit2").getText() gridcontrol = context.getControl("Grid1") # グリッドコントロールを取得。 griddata = gridcontrol.getModel().getPropertyValue("GridDataModel") # グリッドコントロールモデルからDefaultGridDataModelを取得。 selectedrows = gridcontrol.getSelectedRows() # 選択行インデックスのタプルを取得。 if not selectedrows: # 選択行がない時。 selectedrows = griddata.RowCount-1, # 最終行インデックスを選択していることにする。 insertRows(gridcontrol, griddata, selectedrows, 1, ((d, t),)) # 選択行の下に行を挿入する。 def disposing(self, eventobject): eventobject.Source.removeActionListener(self) class MouseListener(unohelper.Base, XMouseListener): def __init__(self, doc, popupmenu, menulistener): self.args = doc, popupmenu, menulistener def mousePressed(self, mouseevent): # グリッドコントロールをクリックした時。 doc, popupmenu, menulistener = self.args gridcontrol = mouseevent.Source # グリッドコントロールを取得。 if mouseevent.Buttons==MouseButton.LEFT and mouseevent.ClickCount==2: # ダブルクリックの時。 selection = doc.getCurrentSelection() # シート上で選択しているオブジェクトを取得。 if selection.supportsService("com.sun.star.sheet.SheetCell"): # 選択オブジェクトがセルの時。 griddata = gridcontrol.getModel().getPropertyValue("GridDataModel") # GridDataModelを取得。 rowdata = griddata.getRowData(gridcontrol.getCurrentRow()) # グリッドコントロールで選択している行のすべての列をタプルで取得。 selection.setString(" ".join(rowdata)) # 選択セルに書き込む。 elif mouseevent.PopupTrigger: # 右クリックの時。 rowindex = gridcontrol.getRowAtPoint(mouseevent.X, mouseevent.Y) # クリックした位置の行インデックスを取得。該当行がない時は-1が返ってくる。 if rowindex>-1: # クリックした位置に行が存在する時。 popupmenu.enableItem(3, True) # Pasteメニューを表示する。 popupmenu.enableItem(4, True) # Pasteメニューを表示する。 if not gridcontrol.isRowSelected(rowindex): # クリックした位置の行が選択状態でない時。 gridcontrol.deselectAllRows() # 行の選択状態をすべて解除する。 gridcontrol.selectRow(rowindex) # 右クリックしたところの行を選択する。 rows = gridcontrol.getSelectedRows() # 選択行インデックスを取得。 rowcount = len(rows) # 選択行数を取得。 if rowcount>1 or not menulistener.rowdata: # 複数行を選択している時または保存データがない時 。 popupmenu.enableItem(3, False) # Pasteメニューを表示しない。 popupmenu.enableItem(4, False) # Pasteメニューを表示しない。 pos = Rectangle(mouseevent.X, mouseevent.Y, 0, 0) # ポップアップメニューを表示させる起点。 popupmenu.execute(gridcontrol.getPeer(), pos, PopupMenuDirection.EXECUTE_DEFAULT) # ポップアップメニューを表示させる。引数は親ピア、位置、方向 def mouseReleased(self, mouseevent): pass def mouseEntered(self, mouseevent): pass def mouseExited(self, mouseevent): pass def disposing(self, eventobject): eventobject.Source.removeMouseListener(self) class MenuListener(unohelper.Base, XMenuListener): def __init__(self, gridcontrol): self.gridcontrol = gridcontrol self.rowdata = None def itemHighlighted(self, menuevent): pass def itemSelected(self, menuevent): # PopupMenuの項目がクリックされた時。 gridcontrol = self.gridcontrol griddata = gridcontrol.getModel().getPropertyValue("GridDataModel") # GridDataModelを取得。 selectedrows = gridcontrol.getSelectedRows() # 選択状態の行インデックスのタプルを取得。 cmd = menuevent.Source.getCommand(menuevent.MenuId) if cmd=="cut": # 選択行のデータを取得してその行を削除する。 self.rowdata = [griddata.getRowData(r) for r in selectedrows] # 選択行のデータを取得。 [griddata.removeRow(r) for r in selectedrows] # 選択行を削除。 elif cmd=="copy": # 選択行のデータを取得する。 self.rowdata = [griddata.getRowData(r) for r in selectedrows] # 選択行のデータを取得。 elif cmd=="pasteabove": # 行を選択行の上に挿入。 insertRows(gridcontrol, griddata, selectedrows, 0, self.rowdata) elif cmd=="pastebelow": # 空行を選択行の下に挿入。 insertRows(gridcontrol, griddata, selectedrows, 1, self.rowdata) elif cmd=="delete": # 選択行を削除する。 [griddata.removeRow(r) for r in selectedrows] # 選択行を削除。 def itemActivated(self, menuevent): pass def itemDeactivated(self, menuevent): pass def disposing(self, eventobject): eventobject.Source.removeMenuListener(self) def insertRows(gridcontrol, griddata, selectedrows, position, datarows): # positionは0の時は選択行の上に挿入、1で下に挿入。 c = len(datarows) # 行数を取得。 griddata.insertRows(selectedrows[0]+position, ("", )*c, datarows) # 行を挿入。 gridcontrol.deselectAllRows() # 行の選択状態をすべて解除する。 gridcontrol.selectRow(selectedrows[0]+position) # 挿入した行の最初の行を選択する。LibreOffice5(72)Javaの例:GUIをPythonにする その5のmenuCreator()、LibreOffice5(69)Javaの例:GUIをPythonにする その2のshowModelessly()とdialogCreator()を使っています。
XGridDataModelインターフェイスのgetRowData()メソッドで1行の列の値がタプルで取得できるので、切り取りやコピーはそのタプルをリストにして取得しています(219行目と222行目)。
ペーストはXMutableGridDataModelインターフェイスのinsertRows()メソッドを使って複数行を一度に挿入しています。
1行ずつ行を挿入するinsertRow()メソッドもありますが、insertRows()メソッドでも1行を挿入できるのでinsertRows()メソッドだけで事足ります。
addRow()やaddRows()メソッドで行を追加すると行ヘッダに1から順に空き番号が自動的に振られますが、insertRow()やinsertRows()メソッドでは行ヘッダが自動的に割り当てられません。
なのでこのマクロでは行ヘッダを空文字にして行を挿入し、グリッドコントロールモデルのShowRowHeaderプロパティをFalseにして行ヘッダを非表示にしています。
複数行を選択できるようにするためにはSelectionModelプロパティにenum SelectionTypeにMULTIを指定しないといけません。
Calcドキュメントのシートと違ってダブルクリックするとセルの編集モードになるというのはないので、Insertボタンでテキストボックスコントロールからデータを取得して新規行に挿入します。
GridControl2.ods
このマクロを埋め込んだCalcドキュメントです。
GridControl2.odsの実行結果
青色のセルをダブルクリックするとノンモダルダイアログ、黄色のセルをダブルクリックするとモダルダイアログとしてコントロールダイアログが出現します。
NowボタンをクリックするとテキストボックスコントロールのDateとTimeに現在の日付と時刻が入力されます。
InsertボタンをクリックするとテキストボックスコントロールのDateとTimeの文字列をグリッドコントロールの最下行に追加します。
NowボタンとInsertボタンを交互にクリックするとNowボタンをクリックしたときの日時がグリッドコントロールに追加されていきます。
グリッドコントロールの行を右クリックするとコンテクストメニューが出現します。
行をCutかCopyをしたあとに右クリックするとPaste Above、Paste Belowの項目が追加されます。
Paste Aboveは選択行の上にペースト、Paste Belowは選択行の下にペーストします。
ShiftキーやCtrlキーで複数行を選択すると複数行について切り取りやコピー、削除ができます。
複数行を切り取りやコピーしたときはまとめてペーストします。
0 件のコメント:
コメントを投稿