Click here to Skip to main content
15,868,010 members
Articles / Desktop Programming / Windows Forms

Owner Drawn Resizable Control

Rate me:
Please Sign up or sign in to vote.
4.70/5 (16 votes)
30 Jan 2007CPOL14 min read 105.3K   2.7K   40   30
Creating a custom control that you can resize at runtime from all four corners.

Sample Image - article.gif

Introduction

Have you ever wanted to create a control that you could move as well as resize at run-time? And not just resize the lazy way, but the same way that it is done in a real designer? This article should help you get on your way.

What the article provides

This article will show you the basic idea behind my implementation of creating a movable and resizable control. My intent here (if it does not become obvious by the end of the article) is to help some others along that may be banging their heads against the table trying to do what I needed to do.

What the article DOES NOT provide

This article should not be considered a primer or BKM (Best Known Method) that outlines all the details on creating an owner drawn control. I do not get into that aspect of the process above and beyond my straight implementation here. It was not my intention as I am still learning the entire 'owner draw' thing myself. I have been a past creator of composite controls, and my recent project is requiring that I learn how to draw my own control interface.

Background

My need

I have started a project recently that is going to be a diagramming application where the user will be able to add objects to a drawing surface, manipulate their properties, and then serialize them to disk. I began this project as I always do, scouring the Internet for any scraps of 'prior art' that I can find since I little want to do any wheel inventing unless I really have to.

I found many bits of sample code and examples showing different ways to do the basics of what I wanted to do. There were a wide range of ways, everything from hosting a .NET designer surface to simply drawing objects onto a simple container. What I noticed was that none of them (aside from the hosted designer surface that just seemed really overkill to me) showed one basic thing, how to resize an object on the screen.

Many of the examples were centered around creating an instance of a custom control dynamically, placing that control onto a container surface, and then allowing the user to do things like move it around and alter the controls properties. None of them seemed to show the user how to create a control to be placed on the form that could be resized the way that the VS.NET designer allowed.

Ways to resize

There are really two ways to resize a control. I will refer to them as the easy way and the hard way.

  1. The easy way simply allows the user to use the mouse to drag the lower left hand corner to achieve the size they want. I see many things out there that implement object resizing in this manner, and I have two words to say about them. Cop out!
  2. The hard way is, well, harder to implement, but, in my opinion, provides (wait for it, wait for it) a much richer user experience (man, I hate using marketing-speak) than the easy way. This manner of resizing allows the user to use the mouse to drag any of the four corners to resize the control.

The code

OK, time for the show. Time to see the code that had me pulling my hair out for about 3 days.

Theory

The first thing you need to understand is the basic structure and need of what it is that you want to do. I have defined my requirements as follows:

  1. I need to be able to select any one of the corners of the control and drag it to a new size.
  2. When I drag a corner to resize, the rest of the control must remain locked in place.
  3. I also need to be able to move the control around, by dragging it in addition to resizing it by dragging.
  4. I wanted my hot-spots to be shown on the control surface. This may or may not be a requirement for you.

So, let's get started...

Step #1 - Setup the form to draw the hot-spots

To help keep things straight in my head as well as in the code, I decided on a name scheme first off that would help me associate the various variables I would use, with the locations of their meaning in the graphical surface of the control. To help with this, I decided to make use of the typical compass directions, as shown below:

compass.gif

Figure 1 - The basic control compass directions

The intent here is to create four rectangle regions on the form and draw them in place as needed, then allow the control to track when the mouse is moved over them and when a user holds down the left mouse button when the pointer is inside the control.

To do this, I set up four module scoped Rectangle objects as follows:

VB
'Hotspot rectangles 
Private mNWRect As Rectangle 
Private mNERect As Rectangle 
Private mSWRect As Rectangle 
Private mSERect As Rectangle 

Also, since we need to be able to track when the mouse is being held down and know if it is in a specific hotspot, I created a set of flags to track the up and down state of the mouse button as follows:

VB
'Hotspot mousedown flags
Private mNWSelected As Boolean = False
Private mNESelected As Boolean = False
Private mSWSelected As Boolean = False
Private mSESelected As Boolean = False

Lastly, I needed a way to store the difference between the x,y coordinates where the user clicked on the control and the x,y coordinates of the physical corner they are dragging. This will help me properly size the control later on. I created them as follows:

VB
'Individual offset variables
Private mNWxOffset As Integer
Private mNWyOffset As Integer
Private mNExOffset As Integer
Private mNEyOffset As Integer
Private mSExOffset As Integer
Private mSEyOffset As Integer
Private mSWxOffset As Integer
Private mSWyOffset As Integer
Private mCenteralXOffset As Integer
Private mCentralYOffset As Integer

I decided to create them as individual Integers instead of pairs of Points simply because when I tried to do that, the code really became difficult to read. This method seems to work OK for me while keeping the code a bit more readable. My thoughts were also that these numbers are really not coordinates in their own right but more of a difference between two sets of coordinates, so the Point nomenclature really did not sit well with me.

You might also note that some of these variables are set but never used in this implementation. Sorry, that's just the way I am. I think it is better to have them there for completeness sake, and if the compiler is worth anything, they should get optimized out if they are never used.

Now, once we have all this work done, we can setup the functionality that draws the control's interface. To do this, I simply created an overloaded OnPaint procedure and built the code to draw the hotspots.

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

    'Build each hotspot needed based upon the size of the control
    mNWRect = New Rectangle(0, 0, 10, 10)
    mNERect = New Rectangle(MyBase.Width - 11, 0, 10, 10)
    mSWRect = New Rectangle(0, MyBase.Height - 11, 10, 10)
    mSERect = New Rectangle(MyBase.Width - 11, MyBase.Height - 11, 10, 10)

    'Draw each rect onto the surface of the control
    e.Graphics.DrawRectangle(Pens.Black, mNWRect)
    e.Graphics.DrawRectangle(Pens.Black, mNERect)
    e.Graphics.DrawRectangle(Pens.Black, mSWRect)
    e.Graphics.DrawRectangle(Pens.Black, mSERect)

End Sub

Step #2 - Build the code to react to the user's mouse actions

In this step, we need to handle a few mouse button actions from the user. Specifically, we will be handling the MouseDown and MouseUp events.

Below is the code to handle the MouseDown event:

VB
Private Sub UserControl1_MouseDown(ByVal sender As Object, _
        ByVal e As System.Windows.Forms.MouseEventArgs) Handles Me.MouseDown

    MyBase.Parent.SuspendLayout()

    'When the user presses down on the left mouse button store 
    'some critical numbers for use during the mouse move.
    If (mNWRect.Contains(e.Location)) Then
        mNWSelected = True
        mNWxOffset = e.X
        mNWyOffset = e.Y
        storedOposite.X = (MyBase.Location.X + MyBase.Width)
        storedOposite.Y = (MyBase.Location.Y + MyBase.Height)

    ElseIf (mNERect.Contains(e.Location)) Then
        mNESelected = True
        mNExOffset = (MyBase.Width - e.X)
        mNEyOffset = e.Y
        storedOposite.X = MyBase.Location.X
        storedOposite.Y = (MyBase.Location.Y + MyBase.Height)

    ElseIf (mSWRect.Contains(e.Location)) Then
        mSWSelected = True
        mSWxOffset = e.X
        mSWyOffset = (MyBase.Height - e.Y)
        storedOposite.X = (MyBase.Location.X + MyBase.Width)
        storedOposite.Y = MyBase.Location.Y

    ElseIf (mSERect.Contains(e.Location)) Then
        mSESelected = True
        mSExOffset = (MyBase.Width - e.X)
        mSEyOffset = (MyBase.Height - e.Y)

    Else 'clicked on anyting BUT the handles
        mCenteralXOffset = e.X
        mCentralYOffset = e.Y

    End If

End Sub

In the code above, we first check to see if the mouse pointer location (passed to the event handler as e.Location) is within the bounds of any of our hot spots we have defined. We do this by simply checking to see if the e.Location coordinates are contained within the various Rectangle objects we have setup to denote each hotspot.

Note here that we also handle the case where the user has not held down the mouse button in any hotspot, but rather over the general body of the control. We will use this later when we write the code that will allow the entire control to be dragged by the user.

Once we have determined that the mouse pointer is currently inside of a specific hotspot, I set the appropriate flag value for that hotspot location to true. This will be used as a signal later on in the MouseMove handler to know what direction the user is resizing. In addition to this, we set our offset values so the code knows how many pixels away for the corner we are resizing the mouse pointer is at the time they held down the button. We also make sure we know the coordinates of the corner that is directly opposite from the one we are selecting, since we want to use that corner as the new anchor point for the control.

Below is the code to handle the MouseUp event:

VB
Private Sub UserControl1_MouseUp(ByVal sender As Object, _
        ByVal e As System.Windows.Forms.MouseEventArgs) Handles Me.MouseUp

    mNWSelected = False
    mNESelected = False
    mSWSelected = False
    mSESelected = False

    MyBase.Parent.ResumeLayout()

End Sub

In the code above, we are simply clearing all the hot spot flags so the code knows that we are no longer holding down any mouse buttons. Nothing special here, but odd things happen when you don't do this :)

Step #3 - Build the code to react to the user's mouse dragging

In this step, we simply react when the user moves the mouse inside the bounds of the control, and act accordingly, moving the various parts of the control around as needed as long as the button is pressed. This is where the real magic happens, so watch close.

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

    'Set the proper mouse pointer for each hotspot
    If (mNWRect.Contains(e.Location)) Then
        MyBase.Cursor = Cursors.PanNW

    ElseIf (mNERect.Contains(e.Location)) Then
        MyBase.Cursor = Cursors.PanNE

    ElseIf (mSWRect.Contains(e.Location)) Then
        MyBase.Cursor = Cursors.PanSW

    ElseIf (mSERect.Contains(e.Location)) Then
        MyBase.Cursor = Cursors.PanSE

    Else
        MyBase.Cursor = Cursors.SizeAll

    End If

    'React to the movement only when the left button is held down.
    If (e.Button = Windows.Forms.MouseButtons.Left) Then
        If (mNWSelected = True) Then
            Dim clientPosition As Point = _
                MyBase.Parent.PointToClient(System.Windows.Forms.Cursor.Position)
            Dim adjustedLocation As New _
                Point(clientPosition.X - mNWxOffset, clientPosition.Y - mNWyOffset)
            Dim width As Integer = (storedOposite.X - adjustedLocation.X)
            Dim height As Integer = (storedOposite.Y - adjustedLocation.Y)

            If ((width > 25) And (height > 25)) Then
                MyBase.Location = adjustedLocation
                MyBase.Width = width
                MyBase.Height = height

                MyBase.Invalidate()

            End If

        ElseIf (mNESelected = True) Then
            Dim clientPosition As Point = _
                MyBase.Parent.PointToClient(System.Windows.Forms.Cursor.Position)
            Dim adjustedLocation As New _
                Point(MyBase.Location.X, (clientPosition.Y - mNEyOffset))
            Dim width As Integer = ((clientPosition.X + mNExOffset) - MyBase.Location.X)
            Dim height As Integer = (storedOposite.Y - adjustedLocation.Y)

            If ((width > 25) And (height > 25)) Then
                MyBase.Location = adjustedLocation
                MyBase.Width = width
                MyBase.Height = height

                MyBase.Invalidate()

            End If

        ElseIf (mSWSelected = True) Then
            Dim clientPosition As Point = _
                MyBase.Parent.PointToClient(System.Windows.Forms.Cursor.Position)
            Dim adjustedLocation As New _
                Point(clientPosition.X - mSWxOffset, MyBase.Location.Y)
            Dim width As Integer = ((storedOposite.X + mSWxOffset) - clientPosition.X)
            Dim height As Integer = ((clientPosition.Y + mSWyOffset) - MyBase.Location.Y)

            If ((width > 25) And (height > 25)) Then
                MyBase.Location = adjustedLocation
                MyBase.Width = width
                MyBase.Height = height

                MyBase.Invalidate()

            End If

        ElseIf (mSESelected = True) Then
            Dim width As Integer = (e.X + mSExOffset)
            Dim height As Integer = (e.Y + mSEyOffset)

            If ((width > 25) And (height > 25)) Then
                MyBase.Width = width
                MyBase.Height = height

                MyBase.Invalidate()

            End If

        Else
            Dim clientPosition As Point = _
                MyBase.Parent.PointToClient(System.Windows.Forms.Cursor.Position)
            Dim adjustedLocation As New _
                Point(clientPosition.X - mCenteralXOffset, clientPosition.Y - mCentralYOffset)
            MyBase.Location = adjustedLocation

        End If

    End If

End Sub

The first section of this code performs the simple act of setting the correct cursor image when the user holds the mouse over specific locations within the bounds of the control's surface. Notice that it is simple to match up the correct cursor image with the correct hotspot, because I used names that match between both. Score one for some forethought here :)

The next section of code was the bear. It acts on the user's mouse movement based upon the hotspot selected, and takes the appropriate actions to resize the control from that corner's perspective, while maintaining an anchor point based upon the corner opposite of the one being dragged. Since the actual algorithm used varies depending upon what corner we are moving around, I will have to address each one individually.

One important thing to note here is the use of the PointToClient method. This method provides you with a way to translate the current mouse coordinates of the user control to the mouse coordinates of the parent container. This is important because at some point, you are going to need to change the control's location within the parent as part of the resize action, so you are going to need to use the coordinate values of the container. This method does the translation for you, as shown below:

pointtoclient.gif

Figure #2 - Shows the relations between the various places you can click on a control

As shown in Figure #2, when you move your mouse over a form that has controls on it, you end up passing through a different coordinate scheme. There is one coordinate scheme to track your mouse pointer over the entire form (shown here in black), but as your mouse passes over other controls, they each have their own coordinates within their own graphics regions (shown here in red).

When the user clicks within the bounds of a control as shown in the example as point B, the MouseDown event handler passes to that procedure the current mouse coordinates as they appear within that control's graphics region. Here, we show that when the user clicks the mouse, they are going to be given the X,Y coordinates of 50,50.

This is fine if you are going to be using these coordinates just within the bounds of the control's graphics region, but if you are going to use these to help position the control on the parent, then you will need these coordinates translated to ones that are meaningful to the parent's coordinate system. The PointToClient method does this for you, translating the local coordinates to the ones meaningful to the parent's drawing region, in this case 100,100. You can then use these coordinates along with the offset data to correctly place the control and adjust its size.

Enough about that, now on to the gory details of resizing the control.

mNWSelected = True

This one is not too bad. All we need to do is calculate a new location based upon the pointer's current client location and the pointer's offset, but then we also need to use this new location to calculate a new size in order to keep the opposite corner locked in place, making it appears as if it is the new origin. Once that is done, we just apply the new values and call the Invalidate method to force a redraw of the control, and we are all set.

mNESelected = True

This one gets a bit odd simply because not only are you adjusting the size, but you also have to adjust the Y location as well. You will notice in this section of code that the adjustedLocation structure keeps the X component unchanged while only adjusting the Y component as needed. The new size is also calculated based upon the opposite corner coordinates and the new location values.

mSWSelected = True

This one is really just the opposite of the NE code segment, but in this case, it is the Y component of the location that we are keeping unchanged, and we are only adjusting the X component as the size of the control based upon that.

mSWSelected = True

This is the simplest one of the bunch, because all we need to do here is adjust the overall size of the control itself; we do not need to muck with the location at all. Simply calculating the adjusted location based off of the pointer's current client location and the pointer's offset is enough here.

Default

This is the code that is triggered if the user is dragging the control around by any point other than one of the hotspots. This will result in a simple move of the control without any resizing at all.

Points of interest

First of all, I learned that this was a lot tougher than it looked. The entire time I was doing this, I was saying to myself 'their has to be a simpler way'. If anyone out there knows of one, or finds one, please let me know.

Second, I would like to point people to some very good sources of information that I have looked through in my travels towards a solution:

  • CodeProject article: Runtime Movable Controls. Author: Dave Kreskowiak. Link: http://www.codeproject.com/vb/net/RuntimeMovableControls.asp.
  • CodeProject article: Build Your Own Visual Studio: An Application Framework for Editing Objects at Runtime. Author: Salysle. Link: http://www.codeproject.com/vb/net/ObjectManagerBundle.asp
  • Both of these are very well written articles and were a tremendous help, not only for learning about drawing and resizing controls, but also in helping me get closer to my end application. Thanks to you both.

  • CodeProject Article: Runtime Control Resizer. Author: Seth Rowe. Link: http://www.codeproject.com/useritems/rtcontrolresizer.asp
  • While this one came after mine (apparently inspired by a question I had posted on Usenet while working on this problem), it showed a very good example of how two people working on the same problem can come up with very different solutions.

Third, I wanted to point out a recent change that I had to make to overcome a problem when this control was placed inside of a flow layout panel. I had to add the Parent.SuspendLayout and Parent.ResumeLayout function calls to the MouseDown and MouseUp event handlers to force the parent container control to stop trying to refresh the screen when dragging or adjusting the size. For some reason, this was causing some very odd behavior in the flow layout panel. You can see below how the flow layout panel now behaves.

Sample screenshot

TODO:

Well, there is a small listing of things that could be called 'TODO' items I guess. Not all of them are for me though.

  • Add support for dragging by the N, S, E, and W compass points.
  • Add support for dynamically setting the hotspots on control selection. I am not sure how to accomplish this as I am not sure how I would go about drawing the grab handles outside the boundaries of the control and then allow the user to drag by them.
  • See if there is a better way to stop the flicker when I redraw the control. I have the DoubleBuffer property of the base control set, but I am not sure this is enough.
  • See why when you place this control onto a flow layout panel you seemingly can no longer drag the control around. I have to admit at this point that I am a bit stumped on this one.

History

  • 1/30/2007 - Fixed a bug dealing with placing these controls in the flow layout panel and trying to resize and drag them around. Thanks Sacha Barber.
  • 1/17/2007 - First release.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer (Senior)
United States United States
Ray spends his time between running a private software company (www.enterprocity.com) and working during the day as a Software Architect for Independent Health (www.independenthealth.com).

His second love, falling only below his wife and child, is programming. His language of choice is VB.NET but he can work in C# and C/C++ enough to get by. Unfortunately his current day job @ IH has him buried deep in solutions using Java (UGH).

He has also recently started a teaching career at a local community college.

Comments and Discussions

 
GeneralRe: First Article Pin
Ray Cassick17-Jan-07 18:54
Ray Cassick17-Jan-07 18:54 
GeneralRe: First Article Pin
Pezza24-Jan-07 0:39
Pezza24-Jan-07 0:39 
GeneralRe: First Article Pin
Ray Cassick24-Jan-07 2:35
Ray Cassick24-Jan-07 2:35 
GeneralRe: First Article Pin
dl4gbe8-Aug-10 6:25
dl4gbe8-Aug-10 6:25 
GeneralRe: First Article Pin
Ray Cassick8-Aug-10 16:02
Ray Cassick8-Aug-10 16:02 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.