WxPythonを使用してノードインターフェイスを作成する例。 パート2:マウスイベントの処理

短いシリーズの記事では、WxPythonを使用して、ユーザーインターフェイスを開発するという非常に具体的なタスクを解決する方法と、このソリューションをユニバーサルにする方法について説明します。 このチュートリアルは、すでにこのライブラリの研究を始めており、最も単純な例よりも複雑で全体的なものを見たい人向けに設計されています(ただし、比較的単純なものから始まります)。

最後の部分では、タスクについて話し、実装プロセス、またはオブジェクトのレンダリングについて説明し始めました。 次に、ユーザーインタラクションを実装します。

パート1:描画の学習
パート2:マウスイベントの処理
パート3:機能の追加とキーボード処理の継続
パート4:ドラッグアンドドロップの実装
パート5:ノードの接続

誰も気にせず、猫の下で歓迎…


前回、キャンバスに単純なノードを描画する単純なプログラム(テキスト付きの長方形)を取得したことを思い出してください。 ノードを移動可能にします。

4.オブジェクトにカーソルを合わせたときにオブジェクトを強調表示する


ただし、ノードの移動を実装する前に、1つの便利な機能を作成します。オブジェクトの上にマウスを移動すると、オブジェクトが強調表示されます。 その後、この機能の一部は、残りの機能を実装する際に役立ちます。 実装するには、3つのアクションを実行する必要があります。
1)カーソルの動きを追跡する
2)カーソルの下の一番上のオブジェクトを見つけて保存する
3)このオブジェクトの選択をレンダリングする

カーソルの動きを追跡するには、対応するイベントハンドラーをキャンバスクラスに追加する必要があります。
self.Bind(wx.EVT_MOTION, self.OnMouseMotion) 

これで、カーソルを移動すると、メソッドが呼び出されます:
  def OnMouseMotion(self, evt): pos = self.CalcUnscrolledPosition(evt.GetPosition()).Get() self._objectUnderCursor = self.FindObjectUnderPoint(pos) self.Render() 

ここでは3つのアクションが行われます:ウィンドウに対するカーソルの座標が与えられるため、最初にそれらをキャンバスの座標に変換する必要があります(スクロールがあるため)。次に、対応するオブジェクトを見つけ、ユーザーがカーソルを移動するオブジェクトのハイライトが表示されるように画像を更新する必要があります。 カーソルの下のオブジェクトを検索する方法は次のとおりです。
  def FindObjectUnderPoint(self, pos): #Check all objects on a canvas. Some objects may have multiple components and connections. for obj in reversed(self._canvasObjects): objUnderCursor = obj.ReturnObjectUnderCursor(pos) if objUnderCursor: return objUnderCursor return None 

ここにあるものはすべて些細なことであり、それほどではありません。 一方では、すべてのオブジェクトを調べて、カーソルの下にあるオブジェクトを探します。 そして、最高のものを取得したいので、これを逆の順序で実行しています。 最後に追加されたオブジェクト。 一方、「ReturnObjectUnderCursor」メソッドを使用して、オブジェクトを返しますが、どのオブジェクトをチェックしているかはわかっているようです。 これは将来に余裕を持って行われるため、他のオブジェクトを含むノードを作成できます(たとえば、他のノードへの接続またはノードのサイズを変更するための角度)。 これまでのところ、ノード上のこのメソッドは、カーソルが長方形内にあるかどうかを確認するだけです。
  def ReturnObjectUnderCursor(self, pos): if pos[0] < self.position[0]: return None if pos[1] < self.position[1]: return None if pos[0] > self.position[0]+self.boundingBoxDimensions[0]: return None if pos[1] > self.position[1]+self.boundingBoxDimensions[1]: return None return self 

そのため、どのオブジェクトがカーソルの下にあるかを常に把握しており、レンダリング中に何らかの方法でオブジェクトを選択し、レンダリング中にこのコードを実行します。
  if self._objectUnderCursor: gc.PushState() self._objectUnderCursor.RenderHighlighting(gc) gc.PopState() 

ノードにハイライトをレンダリングするためのコードを追加することは残ります。
  def RenderHighlighting(self, gc): gc.SetBrush(wx.Brush('#888888', wx.TRANSPARENT)) gc.SetPen(wx.Pen('#888888', 3, wx.DOT)) gc.DrawRectangle(self.position[0]-3, self.position[1]-3, self.boundingBoxDimensions[0]+6, self.boundingBoxDimensions[1]+6) 

ここでは、透明なブラシを使用して、レンダリング時に以前にレンダリングされたもの(ノード自体)が上書きされないようにします。
結果はこの写真です:

カーソルは事後に終了する必要があったため、少し伝統的ではありません:)
ここに興味のある人にすべてのコードを提供するわけではありません。GitHubのこのコミットには含まれています。

5.小さなリファクタリングとインターフェースの追加


繰り返しますが、今回は少しリファクタリングするために、オブジェクトの移動の実装を長く延期しません。 このフレームワークは普遍的である必要があるため、ここではノードは移動不可能なものを含め、あらゆる種類のものになり得ることを意味します(たとえば、オブジェクト自体またはノードの一部のコンポーネントによって定義されるオブジェクト間の接続。 そのため、ノードで可能なこととできないことを説明する何らかの汎用的な方法が必要です。 とにかく、ノード用のある種のユニバーサルインターフェイスを紹介したいと思います。 確かに、今のところabc、zope.interfaceなどは使用せず、キャンバス上のオブジェクトの基本クラスを作成します。
 class CanvasObject(object): def __init__(self): #Supported operations self.clonable = False self.movable = False self.connectable = False self.deletable = False self.selectable = False def Render(self, gc): """ Rendering method should draw an object. gc: GraphicsContext object that should be used for drawing. """ raise NotImplementedError() def RenderHighlighting(self, gc): """ RenderHighlighting method should draw an object with a highlighting border around it. gc: GraphicsContext object that should be used for drawing. """ raise NotImplementedError() def ReturnObjectUnderCursor(self, pos): """ ReturnObjectUnderCursor method returns a top component of this object at a given position or None if position is outside of all objects. pos: tested position as a list of x, y coordinates such as [100, 200] """ raise NotImplementedError() 

ご覧のとおり、デフォルトではサポートされていない多くの標準アクションがあります。 ただし、キャンバス上のオブジェクトには3つのメソッドが必要です。 これは論理的であり、なぜキャンバス上に表示できないオブジェクトが必要なのか(レンダリング)、また、表示されているように、カーソルでオブジェクトをつつく(ReturnObjectUnderCursor、RenderHighlighting)。 そしてここで、ノードを移動したい、つまり それらは移動可能でなければならず、このために特別なクラスがあります:
 from MoveMe.Canvas.Objects.Base.CanvasObject import CanvasObject class MovableObject(CanvasObject): def __init__(self, position): super(MovableObject, self).__init__() self.position = position self.movable = True 

ここではすべてがシンプルで、このクラスは移動を許可し、位置などの有用なプロパティも追加するため、この位置を持たずに何かをある位置から別の位置に移動することは困難です。 これで、ノードの定義は新しいクラスの相続人になったため、もう少し複雑になりましたが、一般に、これはまだ同じ古いノードです:
 from MoveMe.Canvas.Objects.Base.CanvasObject import CanvasObject from MoveMe.Canvas.Objects.Base.MovableObject import MovableObject class SimpleBoxNode(MovableObject, CanvasObject): ........... 

6.ノードの移動


そこで、待望の移動ノードの実装に到達しました。 これを行うには、2つの基本的な手順を実行する必要があります:ユーザーがドラッグを開始したオブジェクト(つまり、マウスの左ボタンクリック時にカーソルの下にあったオブジェクト)を記憶し、ユーザーがマウスボタンを離すまでカーソルを移動するときにオブジェクトの位置を更新します。
最初のアクションは次で実行されます。
  def OnMouseLeftDown(self, evt): if not self._objectUnderCursor: return if self._objectUnderCursor.movable: self._lastDraggingPosition = self.CalcUnscrolledPosition(evt.GetPosition()).Get() self._draggingObject = self._objectUnderCursor self.Render() 

カーソルの位置と、カーソルの下の現在のオブジェクトが移動をサポートしている場合は、移動されていることを覚えているだけです。 カーソルの下にオブジェクトが存在するかどうかのチェックがまだ行われない限り、ボイドを移動しても意味がありません。 第二部はもう少し面白いです
  def OnMouseMotion(self, evt): pos = self.CalcUnscrolledPosition(evt.GetPosition()).Get() self._objectUnderCursor = self.FindObjectUnderPoint(pos) if not evt.LeftIsDown(): self._draggingObject = None if evt.LeftIsDown() and evt.Dragging() and self._draggingObject: dx = pos[0]-self._lastDraggingPosition[0] dy = pos[1]-self._lastDraggingPosition[1] newX = self._draggingObject.position[0]+dx newY = self._draggingObject.position[1]+dy #Check canvas boundaries newX = min(newX, self.canvasDimensions[0]-self._draggingObject.boundingBoxDimensions[0]) newY = min(newY, self.canvasDimensions[1]-self._draggingObject.boundingBoxDimensions[1]) newX = max(newX, 0) newY = max(newY, 0) self._draggingObject.position = [newX, newY] #Cursor will be at a border of a node if it goes out of canvas self._lastDraggingPosition = [min(pos[0], self.canvasDimensions[0]), min(pos[1], self.canvasDimensions[1])] self.Render() 

最初のチェックでは、ユーザーが左ボタンを放したままマウスを使って運転する場合、それは間違いなく何も動かないことを保証します。 これは、ボタンのリリースイベントの移動を停止するよりも優れています。カーソルがウィンドウの外にあり、このイベントを受け取らないためです。 次に、実際に何かを引いていることを確認し、オブジェクトの相対的な変位を考慮し始めます。 現時点では、キーボードで何が起こっているかについては考えていません(Ctrlが押されているかどうかは、後で行われます)。 キャンバスを超えて行くためのチェックがまだあります。 この検証では、すべてが非常に単純で明確ではありません。 一方で、キャンバスのサイズが固定されている場合、そのサイズに固定する必要があります。また、一方で、キャンバスを途中まで引き伸ばすことは良いことです(ただし、これは理想的なソリューションではありません)。 一般に、現時点では、キャンバスのサイズは固定され、ノードはキャンバスの境界に接します。
以上で、オブジェクトをキャンバス内で移動できるようになりました。 コードはGitHubのこのコミットに含まれています 。 そして、それはこのように見えます:


PS:誤字については個人で書いてください。

Source: https://habr.com/ru/post/J201538/


All Articles