Search code examples
vb.netwinformsmousegdi+hittest

How to Mouse Hittest using IsVisible with rotated and transformed paths


I am drawing scaleable vector graphics with GDI+ and I need to hitttest on mousemove. All the examples I've seen use model space = world space, with no transforms. Here's a simplified example of the problem:

Imports System.Drawing.Drawing2D

Public Class Form1

  Private myrect As New GraphicsPath

  Protected Overrides Sub OnPaint(ByVal e As System.Windows.Forms.PaintEventArgs)

    ' The rectangle's centre is at 30,10. Move to origin to rotate
    e.Graphics.TranslateTransform(-30, -10, Drawing2D.MatrixOrder.Append)
    e.Graphics.RotateTransform(45, Drawing2D.MatrixOrder.Append)

    ' Move it back, 50x50 away from the origin 
    ' (80,60 because we moved -30,-10 to rotate)
    e.Graphics.TranslateTransform(80, 60, Drawing2D.MatrixOrder.Append)
    e.Graphics.DrawPath(New Pen(Brushes.Black, 2), myrect)

    ' ...loads more painting, many paths, many varying transformations

  End Sub

  Private Sub Form1_Load(ByVal sender As Object, ByVal e As System.EventArgs) _
    Handles Me.Load

    ' Make a rectangle 60x20 with top-left corner at the origin
    myrect.AddLine(0, 0, 60, 0)
    myrect.AddLine(60, 0, 60, 20)
    myrect.AddLine(60, 20, 0, 20)
    myrect.CloseFigure()

    ' ...loads more shapes created here

  End Sub

  Private Sub Form1_MouseMove(ByVal sender As Object, ByVal e As System.Windows.Forms.MouseEventArgs) Handles Me.MouseMove

    ' Pretend that all the drawing stuff above happened some unspecified time ago, 
    ' in different assembly, written by a martian, in some other vile language.
    ' Obviously, his "e.graphics" has long since been garbage-collected.
    If myrect.IsVisible(e.Location) Then
        ' Works when moving over the path at the origin, ignores transforms.
        Debug.WriteLine("Over the rectangle at " & e.Location.ToString)
    End If

  End Sub

End Class

Which produces (mouse moving in red) enter image description here

IsVisible kicks in near the origin, where the rectangle was before the transforms.

I know that if I had the Graphics used in OnPaint, I could use it to hittest with the transforms, but the golden rule says "never save graphics".

Any suggestions would be most welcome.


Post mortem: For the record, here's the implementation of Vincent's answer, which works nicely:

Imports System.Drawing.Drawing2D

Public Class Form1

  Private myrect As New GraphicsPath
  Private mytransform As Matrix

  Protected Overrides Sub OnPaint(ByVal e As System.Windows.Forms.PaintEventArgs)

    ' The rectangle's centre is at 30,10. Move to origin to rotate
    e.Graphics.TranslateTransform(-30, -10, Drawing2D.MatrixOrder.Append)
    e.Graphics.RotateTransform(45, Drawing2D.MatrixOrder.Append)

    ' Move it back, 50x50 away from the origin 
    ' (80,60 because we moved -30,-10 to rotate)
    e.Graphics.TranslateTransform(80, 60, Drawing2D.MatrixOrder.Append)
    e.Graphics.DrawPath(New Pen(Brushes.Black, 2), myrect)
    mytransform = e.Graphics.Transform

    ' ...loads more painting, many paths, many varying transformations

  End Sub

  Private Sub Form1_Load(ByVal sender As Object, ByVal e As System.EventArgs) _
    Handles Me.Load

    ' Make a rectangle 60x20 with top-left corner at the origin
    myrect.AddLine(0, 0, 60, 0)
    myrect.AddLine(60, 0, 60, 20)
    myrect.AddLine(60, 20, 0, 20)
    myrect.CloseFigure()

    ' ...loads more shapes created here

  End Sub

  Private Sub Form1_MouseMove(ByVal sender As Object, ByVal e As System.Windows.Forms.MouseEventArgs) Handles Me.MouseMove

    mytransform.Invert()
    Dim mouseat() As Point = {e.Location}
    mytransform.TransformPoints(mouseat)
    If myrect.IsVisible(mouseat(0)) Then
        ' Works when moving over the path at the origin, ignores transforms.
        Debug.WriteLine("Over the rectangle at " & e.Location.ToString)
    End If

  End Sub

End Class

Solution

  • Save the Graphics object's world transform matrix (Graphics.Transform), invert it (so it goes from page to world coordinates - I think that in this case page coordinates are equal to device coordinates, but if not you'll have to do more work to account for the scaling), and use it to transform your point before doing hit testing.

    Edit: You could also factor the logic in OnPaint that you use to build the world transform into a separate method that doesn't need a Graphics object and returns a Matrix, which you can then use with Graphics.MultiplyTransform or invert and use to modify your input coordinates.