skip to Main Content

I have a program where I can, with the mouse, draw a rectangle in any of four directions.

These rectangles are used on a pictureBox to crop parts of an image.

These rectangles must be drawn while maintaining the ratio of a given dimension for example 320 x 200.

I want this tool to behave pretty much exactly like the crop tool in Photoshop, or like in the crop example found here:
https://imageresize.org/

I have most elements working correctly I’m just struggling on a few geometric calculations.

See the “Bottom right” example in my code. This works perfectly and basically I just want to apply this exact formula to the other directions.

I have been playing with different calculations for hours and I just can’t seem to work it out.

Here is the working code:

 Protected Overrides Sub OnMouseMove(ByVal e As MouseEventArgs)
    'Draw rectangle keeping aspect ratio
    If e.Button = Windows.Forms.MouseButtons.Left Then
        If e.X > startPos.X And e.Y > startPos.Y Then
            'Bottom right
            mRect = New Rectangle(mRect.Left, mRect.Top, e.X - mRect.Left, e.Y - mRect.Top)
            mRect.Size = New Size(mRect.Width, mRect.Width / Ratio.Text)
            If e.Y < mRect.Bottom Then
                mRect = Rectangle.FromLTRB(startPos.X, startPos.Y, e.X, e.Y)
                mRect.Size = New Size(mRect.Height * Ratio.Text, mRect.Height)
            End If
            Me.Invalidate()
        ElseIf e.X < startPos.X And e.Y > startPos.Y Then
            'Bottom left
            mRect = New Rectangle(e.X, startPos.Y, startPos.X - e.X, e.Y - startPos.Y)
            mRect.Size = New Size(mRect.Width, mRect.Width / Ratio.Text)
            Me.Invalidate()
        ElseIf e.X > startPos.X And e.Y < startPos.Y Then
            'Top right
            mRect = New Rectangle(startPos.X, e.Y, e.X - startPos.X, startPos.Y - e.Y)
            mRect.Size = New Size(mRect.Height * 1.6, mRect.Height)
            Me.Invalidate()
        ElseIf e.X < startPos.X And e.Y < startPos.Y Then
            'Top left
            mRect = New Rectangle(e.X, e.Y, startPos.X - e.X, startPos.Y - e.Y)
            mRect.Size = New Size(mRect.Width, mRect.Width / Ratio.Text)
            Me.Invalidate()
        End If
    End If
End Sub

Any help would be hugely appreciated. Thanks!

Below is how things currently work, you can see things go funky when drawing in the north west region. I need to get the same behavior as the south east (or bottom right per the code) for all quadrants.

enter image description here

2

Answers


  1. You have sample rectangle S with given ratio and dimensions sw, sh (320×200 in your example)

    Mouse positions form new rectangle N with dimensions nw, nh (absolute values!)

    Your task is, as far as I understand, to inscribe rectangle with the same ratio as S into rectangle N, getting rectangle R with base point (rx0, ry0) and dimensions (rw, rh)

       nx0 = min(e.x, startpos.x) 
       ny0 = min(e.y, startpos.y) 
       nw = abs(e.x - startpos.x)
       nh = abs(e.y - startpos.y) 
    
       if nw * sh >= nh * sw then   //    if N is "too wide"
            rh = nh
            rw = rh * sw / sh
            ry0 = ny0                   
            rx0 = nx0 + (nw - rw) / 2
       else                        //      N is "too slim"
            rw = nw
            rh = rw * sh / sw
            rx0 = nx0
            ry0 = ny0 + (nh - rh) / 2
    

    then

      mRect = New Rectangle(rx0,  ry0, rx0 + rw, ry0 + rh)
    

    enter image description here

    Login or Signup to reply.
  2. I propose a slightly different method to calculate the current position of the cursor, when a Ratio is applied to the Rectangle dimensions.

    You need of course to save the starting position of the Rectangle, using the MouseDown event of a Control, then track the Mouse movements, using the MouseMove event.

    The current position of the Cursor is calculated as usual (swapping the current Cursor Location the and Starting Position when the Offsets are negative).

    The only difference is the Height of the Rectangle, when the overall size is subject to a Ratio constraint.
    In this case, the Rectangle.Location.Y is determined by the Rectangle.Width / Ratio measure. This becomes visible if the Cursor.Location.Y is above the starting location (Cursor.Location.Y <= StartingPosition.Y). Just like in the code you posted.

    For the example, I’m using a custom Rectangle class that holds all the information needed to draw a shape, with or without a specific Ratio applied to its dimensions.

    ▶ Note that the Ratio is hard-coded to 1.6: it’s just to for testing, of course it can be set to anything else.

    Visual sample of the results:

    Drawing Rectangle with ration

    Private DrawingRects As List(Of DrawingRectangle) = New List(Of DrawingRectangle)()
    
    Private Sub PicureBox1_MouseDown(sender As Object, e As MouseEventArgs) Handles PicureBox1.MouseDown
        If e.Button = MouseButtons.Left Then
            DrawingRects.Add(New DrawingRectangle() With {
                .DrawingcColor = Color.LightGreen,
                .Location = e.Location,
                .Owner = CType(sender, Control),
                .Ratio = 1.6,
                .Size = Size.Empty,
                .StartPosition = e.Location
            })
        End If
    End Sub
    
    Private Sub PicureBox1_MouseMove(sender As Object, e As MouseEventArgs) Handles PicureBox1.MouseMove
        If e.Button = MouseButtons.Left Then
            Dim rect As DrawingRectangle = DrawingRects.Last()
            If e.X < rect.StartPosition.X Then rect.Location = New Point(e.X, rect.Location.Y)
            If e.Y < rect.StartPosition.Y Then rect.Location = New Point(rect.Location.X, e.Y)
    
            Dim currentWidth As Integer = Math.Abs(rect.StartPosition.X - e.X)
    
            If rect.Ratio = 1.0F Then
                rect.Size = New Size(currentWidth, Math.Abs(rect.StartPosition.Y - e.Y))
            Else
                If rect.StartPosition.Y <= rect.Location.Y Then
                    rect.Size = New Size(currentWidth, CType(Math.Abs(rect.StartPosition.X - e.X) / rect.Ratio, Integer))
                Else
                    Dim currentHeight As Integer = CType(currentWidth / rect.Ratio, Integer)
                    rect.Location = New Point(rect.Location.X, rect.StartPosition.Y - currentHeight)
                    rect.Size = New Size(currentWidth, currentHeight)
                End If
            End If
            DrawingRects(DrawingRects.Count - 1) = rect
    
            DirectCast(sender, Control).Invalidate()
        End If
    End Sub
    
    Private Sub PicureBox1_Paint(sender As Object, e As PaintEventArgs) Handles PicureBox1.Paint
        Dim canvas As Control = DirectCast(sender, Control)
    
        If DrawingRects.Count > 0 Then
            e.Graphics.SmoothingMode = SmoothingMode.AntiAlias
            For Each rect As DrawingRectangle In DrawingRects
                If canvas IsNot rect.Owner Then Continue For
                Using pen As New Pen(rect.DrawingcColor, rect.PenSize)
                    e.Graphics.DrawRectangle(pen, New Rectangle(rect.Location, rect.Size))
                End Using
            Next
        End If
    End Sub
    

    The DrawingRectangle class:

    ▶ Note: the class has an Owner property, referencing the current Control where the shape is drawn: this allows to use a List(Of DrawingRectangle) with different controls at the same time.

    Public Class DrawingRectangle
        Private rectAspect As SizeF = SizeF.Empty
        Private rectRatio As Single = 0F
    
        Public Property Owner As Control
        Public Property Location As Point
        Public Property Size As Size
        Public Property StartPosition As Point
        Public Property DrawingcColor As Color
        Public Property PenSize As Single
    
        Public Property Aspect() As SizeF
            Get
                Return rectAspect
            End Get
            Set(ByVal value As SizeF)
                Me.rectAspect = value
                SetAspectRatio(value)
            End Set
        End Property
    
        Public Property Ratio As Single
            Get
                Return rectRatio
            End Get
            Set(ByVal value As Single)
                rectRatio = value
                SetAspectRatio(value)
            End Set
        End Property
    
        Private Sub SetAspectRatio(aspect As SizeF)
            Me.rectRatio = aspect.Width / aspect.Height
        End Sub
        Private Sub SetAspectRatio(ratio As Single)
            Me.rectAspect = New SizeF(100, 100 / ratio)
        End Sub
    End Class
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search