6. マウス操作

(2022.11.15. 公開)

 マウスを使った画面の拡大・縮小をやってみます。また、表示位置を平行移動させる機能と、拡大・縮小をリセットする機能も同時に実装しましょう。

6-1. マウスイベントハンドラの実装

 マウスイベントには多数の種類がありますが、ここでは以下のものを使います。

 frame.panel 上でこれらのイベントが発生したら、つかまえるようにします。それぞれにハンドラを書くこともできますが、同じハンドラを呼び出して、ハンドラ内で処理を分岐しても構いません。

  --  イベントハンドラを接続
  frame.button:Connect(wx.wxEVT_BUTTON, OnClick)
  frame.panel:Connect(wx.wxEVT_PAINT, OnPaint)
  frame.panel:Connect(wx.wxEVT_LEFT_DOWN, OnMouse)
  frame.panel:Connect(wx.wxEVT_LEFT_UP, OnMouse)
  frame.panel:Connect(wx.wxEVT_MOTION, OnMouse)

 マウスイベントハンドラの実装には、決まった型があります。最初に、マウスイベントから「イベントが起きた時のマウスの位置」を取得します。

function OnMouse(event)
  --  マウスポインタの位置
  local pt = event:GetPosition()

 今回は、「画面内のドラッグ」を実装しようとしています。この場合、「最初にマウスボタンが押された位置」を記録しておきます。「最初」かどうかは、event:LeftDown()(左マウスボタンが押された)か、または event:Dragging()(ドラッグされた)でかつ「まだマウスボタンの位置が記録されていない」場合として判断します。(wxWidgets が正しく動作していれば event:LeftDown() だけでよいはずですが、念のために event:Dragging() のロジックも書いておく、というスタンスです。)

  if event:LeftDown() or (event:Dragging() and frame.save == nil) then
    --  現在の状態を保存
    frame.save = { x0 = frame.x0, y0 = frame.y0, scale = frame.scale, pt = wx.wxPoint(pt) }

 その後、マウスボタンが押された状態で wxEVT_MOTION が来た時に、ドラッグの処理をします。変数 active_id は、「拡大縮小」か、「平行移動」か、どちらかを選ぶものです。後から、これを切り替えるためのボタンも実装します。

  elseif event:Dragging() then
    --  ドラッグ
    if active_id == EXPAND_ID then
      local height = frame.panel:GetClientSize():GetHeight()
      --  開始点と現在の点の (1, 1) 方向への射影を求める
      local r1 = (frame.save.pt.x - frame.ax_width) + frame.save.pt.y
      local r2 = (pt.x - frame.ax_width) + pt.y
      r = math.max(r1, 2.0) / math.max(r2, 2.0)
      --  左上を起点に r 倍に拡大(縮小)
      frame.scale = frame.save.scale * r
      frame.y0 = frame.save.y0 + (height - frame.ax_height) * (frame.save.scale - frame.scale)
    elseif active_id == MOVE_ID then
      local x0 = frame.save.x0 - (pt.x - frame.save.pt.x) * frame.scale
      local y0 = frame.save.y0 + (pt.y - frame.save.pt.y) * frame.scale
      frame.x0 = x0
      frame.y0 = y0
    end
    frame:Refresh()

 マウスボタンが離された時に、「最初にマウスボタンが押された位置」の情報をクリアしておきます。この情報があるかどうかで「ドラッグが始まったかどうか」を判断しているので、この処理を忘れるとおかしな動作をします。

  elseif event:LeftUp() then
    --  ドラッグ終了
    frame.save = nil
  end

6-2. 拡大・平行移動・ホームボタンの実装

 拡大、平行移動の切り替えをボタンでできるようにします。拡大をリセットするホームボタンも実装します。

 ボタンのアイコンは PNG か何かで用意すればよいのですが、別ファイルになるのも面倒なので、テキストで記述する XPM 形式にしてしまいました。

local expand_xpm = {
  -- width height ncolors cpp
  "16 16 6 1",
  -- colors
  "A c #000000",
  "B c #131313",
  "C c #383838",
  "D c #676767",
  "E c #C0C0C0",
  ". c None",
  -- pixels
  "................",
  ".AAAAAD.........",
  ".ACE............",
  ".AEBE...........",
  ".A.EBE..........",
  ".A..EBE.........",
  ".D...EBE........",
  "......EBE.......",
  ".......EBE......",
  "........EBE...D.",
  ".........EBE..A.",
  "..........EBE.A.",
  "...........EBEA.",
  "............ECA.",
  ".........DAAAAA.",
  "................"
}

 ボタンは次のように作ります。

  --  「移動」「拡大縮小」ボタンを作成
  local move_bitmap = wx.wxBitmap(cross_xpm)
  frame.move_button = wx.wxBitmapToggleButton(frame.panel, MOVE_ID, move_bitmap, wx.wxPoint(0, 0), wx.wxSize(22, 22), TOGGLEBUTTON_STYLE)
  local expand_bitmap = wx.wxBitmap(expand_xpm)
  frame.expand_button = wx.wxBitmapToggleButton(frame.panel, EXPAND_ID, expand_bitmap, wx.wxPoint(0, 0), wx.wxSize(22, 22), TOGGLEBUTTON_STYLE)

 wxBitmapToggleButton のスタイル指定は、Mac と Windows で変えたほうがよいようです。TOGGLEBUTTON_STYLE という値を下のように初期化しておきます。wxWidgets は、各プラットフォームのネイティブな GUI 部品を使うことがメリットなのですが、特定のプラットフォーム上で GUI を作り込むと、他のプラットフォームで実行した時に画面が崩れることがよくあります。プラットフォーム依存だけじゃなくて、OS のバージョンにも依存する場合がありますね。このあたりは、わりときめ細かく対応していかないといけません。

--  wxBitmapButton のスタイル(プラットフォーム依存)
if string.find(wx.wxPlatformInfo.Get():GetOperatingSystemFamilyName(), "Mac") then
  TOGGLEBUTTON_STYLE = wx.wxBORDER_SIMPLE
else
  TOGGLEBUTTON_STYLE = 0
end

 「拡大」と「平行移動」は状態を選択するトグルボタンですが、「拡大のリセット」は動作を指定するプッシュボタンです。プッシュボタンには wxBitmapButton を使います。

  local home_bitmap = wx.wxBitmap(home_xpm)
  frame.home_button = wx.wxBitmapButton(frame.panel, HOME_ID, home_bitmap, wx.wxPoint(0, 0), wx.wxSize(22, 22), TOGGLEBUTTON_STYLE) -- これはトグルボタンではなくプッシュボタン

 イベントハンドラはボタンごとに書いたほうが単純ですが、同じようなコードが続くのも格好がよくないので、トグルボタン2つは共通にしてみました。

  frame.move_button:Connect(wx.wxEVT_TOGGLEBUTTON, OnClickIconButton)
  frame.expand_button:Connect(wx.wxEVT_TOGGLEBUTTON, OnClickIconButton)
  frame.home_button:Connect(wx.wxEVT_BUTTON, OnClickHomeButton)

 実装はこんな感じです。OnClickIconButton の方は、ちょっと無駄なコードが多かったかも。

--  「拡大縮小」または「移動」ボタンがクリックされた
function OnClickIconButton(event)
  local button = event:GetEventObject():DynamicCast("wxToggleButton")
  local id = button:GetId()
  active_id = id  --  MOVE_ID または EXPAND_ID
  for i = 1, LAST_ID do
    local b = frame.panel:FindWindow(i):DynamicCast("wxToggleButton")
    b:SetValue(i == id)
  end
end

--  ホームボタンがクリックされた
function OnClickHomeButton(event)
  frame.x0 = -2
  frame.y0 = -2
  frame.scale = 0.01
  frame:Refresh()
end

 今回は一応これで完成とします。エラー処理とか、単体で動くアプリにするとか、積み残したことがいろいろありますが、必要があれば続編を書くことにしたいと思います。

 本章のプログラム: [graphcalc06.wx.lua]

目次