Introduction
The questions have been posted more than a few times: "How can I make my own controls?" or "How do I make a label control that's moveable at runtime?" The answer to both is pretty easy! I'm going to walk you through making your own control, a label control that's movable at runtime, not just design time. I'll show you how to take an existing Label
control and build upon it. I'll also show you how to build an application to test your new control and use it to watch the control being built up. But, before we dig into the details, we need some specifications. After all, we can't hang any code on this new MovableLabel
control without knowing what we want it to do, right? So, what do we want, and better yet, what do we "need" this thing to do?
Some Specifications
- First, we want all of the functionality the standard
Label
control offers. We're just adding the ability to move it with the mouse at runtime. Rule #1 of creating your own custom controls is, if another control already exists with most of the functionality you need, inherit from it!
- We need to move our
Label
control when the mouse button is being held down on it.
- We need some kind of indicator to show that our control is movable when the mouse is over it.
- We also need to change the mouse cursor when the mouse is being held down to show the user that we're in the "moving" mode.
Starting the Code
Inheriting from Label
The first thing on our list is getting all of the functionality of the standard Label
control into our MovableLabel
control. Start by creating a new Windows Class Library project, and call it Movable Controls Library. In the Solution Explorer, rename the file Class1.vb to MovableLabel.vb. This will allow you to create your own library of runtime movable controls. All you need to do is add another class to the Movable Controls Library project for each control that you are going to make.
Now, since we'll be creating a Windows Forms control, we need to import a couple of things. The System.Windows.Forms
namespace gives us access to the standard Windows Forms Label
control and all the little things we need to interact with Windows Forms. To add this, go to the "Project" menu, and click "Add Reference...". Make sure the ".NET" tab is selected, and scroll down its list to find the System.Windows.Forms.dll file. Double-click it to add the reference.
The mouse coordinates we get from the mouse event arguments will all be passed in as Point
structures. So, we'll need the System.Drawing
namespace to get access to the Point
structure. Go through the same procedure to add a reference to the System.Drawing.dll file.
With the references out of the way, we just need to change the code a bit to import the namespaces these two DLL files expose, and tell our MovableLabel
control which control class it's inheriting from:
Imports System.Windows.Forms
Imports System.Drawing
Public Class MovableLabel
Inherits System.Windows.Forms.Label
End Class
That's all there is to it! Now we have a class that does the exact same thing as the standard Label
control. But, we can't test it like this. If you try to hit [F5] to start the project, you'll get an error that says "A project with an Output Type of Class Library cannot be started directly..." A class library needs something to instantiate it in order to test it. We'll have to add a second project to our solution to test our new control in.
Creating a Sandbox
What's a "sandbox"? That's what I call a small application that is written to test the functionality of a class library or some other code idea. Since we're creating a new Windows Forms control, we'll need a Windows Forms application to test our control in. Go to the "File" menu and click "Add...", then click "New Project...". Select "VB.NET Windows Application", and call it "Movable Controls Sandbox". This will add a second project to our solution. Notice, in the Solution Explorer, that the new project name is bolded, compared to our other project name. This means that this is the startup project. When you hit [F5] to start the debugger running your code, this is the project that will launch first. If, for any reason, you need to change the startup project, just right-click the project name in the Solution Explorer and click "Set as Startup Project" in the context menu that pops up.
The Sandbox project needs a little bit of modification so we know what we're looking at when we run it. In the Solution Explorer, find the file called "Form1.vb" and rename it to "MainForm.vb". Then double the new filename to open the form in the Designer. In the Properties pane below the Solution Explorer, find the Text
property, under the Appearance section, and change it to read "Movable Controls Sandbox".
Now, we need to drop an instance of our new control on the form so we can see that it works so far. If you haven't already done so, hit [Ctrl]+[Shift]+B to build the solution. This will check to see that all of the projects in the solution are compiled into executable code and up to date with the latest changes to the source code. It will also add our new control to the Toolbox on the left side of the Designer window. Open the Toolbox, and find and expand the section called "Movable Controls Library". In it, you should find the MovableLabel
control. Double-click the MovableLabel
control in the Toolbox. The Designer should add a new instance of the control to the form. Move the control to about the middle of the form. Then, look at the Properties pane under the Solution Explorer. Notice that all of the properties you would find in a normal Label
control are here. We've got a working Label
control, just with a different class name!
To make things a little easier later on, find the Text
property and change it to read "This is a runtime movable label control". Then, find the AutoSize
property, near the bottom of the Properties list, and change it to "False
". We'll resize the control on the form to something a little bigger to click on and drag around. So grab one of the corner handles on our new control, and drag it so that the outline of the control appears roughly square and you can read the complete text in the box. Now, make the MovableLabel
a little prettier, by adding some color and centering the text in the box.
Find the BackColor
property, and type "Green
" in its value box, then find ForeColor
and change it to "White
". Change the BorderStyle
to FixedSingle
and the TextAlign
property to MiddleCenter
. You should end up with something that looks like this:
Now hit [F5] to run the Sandbox app. You'll find that our MovableLabel
looks and feels exactly like any other Label
control - static and boring! Let's start adding some new functionality to it! Click on the little "X" on the form to close the Sandbox application and stop the debugger.
User Feedback
Static Mode
Our MovableLabel
needs to be able to give some kind of feedback to the user. Since our control has two modes, Static
and Movable
, we should give the user some feedback to let them know what mode the control is in.
Static
mode is where the control is sitting stationary, waiting for the mouse to do something we're interested in. In this case, Static
mode should be looking for a MouseDown
event. The firing of this event is what will trigger the control to switch to Movable
mode. While we're waiting for this to happen, we should give the user a clue that we're in Static
mode and waiting. We can do this by setting the cursor to something like a pointing hand when the mouse pointer passes over our control. Since the Label
control we inherited from already exposes a Cursor
property that does this, all we have to do is set the value of Cursor
in our control's constructor. This way, each and every instance of MovableLabel
will initialize itself to show this Hand cursor from the moment it's dropped on the form. Click on the "MovableLabel.vb" tab in the editor window, and add the following code between the Inherits
line and End Class
:
Public Sub New()
MyBase.New()
Me.Cursor = Cursors.Hand
End Sub
Now hit [F5] to see the results. You should now see that whenever the mouse passes over the MovableLabel
, the cursor changes to a Hand pointer and changes back when the mouse moves off our control.
The way that this is implemented does not take away the ability of the user to change the Cursor
property to whatever cursor they feel like while in Static
mode. It merely sets up the default cursor used if one is not specified in design mode. Try it. All you have to do is drag another MovableLabel
control onto the form and look at the Cursor
property in the Properties pane. Notice, it doesn't say "Default" anymore. Let's move on...
A Little Inheritance Background
What about that MyBase.New()
line? That line is there because we are using all of the code, properties, and methods of the Label
class we inherited from. The Label
class has kind of become our slave. We're wrapping the Label
class inside a layer of our own code, and manipulating the Label
through that layer of code. But, it still is a separate class that has its own rules about how it functions and what it needs to function. So, all Windows Forms controls have an InitializeComponent()
method that is called when they are instantiated. This call nearly always appears in the control's constructor, or New
method. Since the Label
class is doing nearly all of the work we need to have done, we have to tell the Label
to initialize itself properly so the rest of the control will work. All we do to facilitate this is call the constructor of the class we inherited from, or what's called the "base class". In VB.NET syntax, MyBase
always refers to the base class we inherit from. So, to call the base class' constructor, we use MyBase.New()
.
Movable Mode
Here's where we really start to hang some code on this thing. We need to add support to change the mouse pointer when the left mouse button is held down on our control, and we need to change it back to what it was when the mouse button is released. This is easily done by handling the MouseDown
and MouseUp
events exposed by the Label
class.
First, we need to add a field to our class that keeps track of what the current Cursor
value is of our Label
control. We'll set this value when the MouseDown
event fires, then use it to restore the Cursor
when the MouseUp
event fires. Add the following code between the Inherits
line and the Public Sub New()
line:
Private m_CurrentCursor As Cursor
Now we have to add code to change the cursor at the appropriate times. Remember, we're controlling a wrapped-up Label
class, so it's going to be raising all the events we see through the MyBase
identifier. We'll need to save the current cursor state when the mouse button is held down, change the cursor to signify that we've entered Movable
mode, then change it back to what we found when the mouse button is released.
To add the MouseDown
and MouseUp
event handlers, it's recommended that you pick them from the two dropdown lists at the top of the Editor window. This is to ensure that you get the event argument signatures correct. If they don't match, you'll run into problems when you compile the code. In the left-side drop down, pick "(MovableLabel
Events)", with the little lightning bolt next to it. This will show only our class' available events in the right-side dropdown. In that dropdown, pick MouseDown
. This will automatically generate an empty event handler, with the appropriate argument signature for that event. Do the same for the MouseUp
. Fill in the empty handlers with the following code:
Private Sub MovableLabel_MouseDown( _
ByVal sender As Object, _
ByVal e As System.Windows.Forms.MouseEventArgs) _
Handles MyBase.MouseDown
If e.Button = Windows.Forms.MouseButtons.Left Then
m_CurrentCursor = MyBase.Cursor
MyBase.Cursor = Cursors.SizeAll
End If
End Sub
Private Sub MovableLabel_MouseUp( _
ByVal sender As Object, _
ByVal e As System.Windows.Forms.MouseEventArgs) _
Handles MyBase.MouseUp
MyBase.Cursor = m_CurrentCursor
End Sub
Hit [F5] to see what you've done. Be sure to hold the mouse button down on the MovableLabel
control, and watch what happens.
Tracking the Mouse
OK. Now, we get down to actually making the MovableLabel
move! I'll start with a little explanation of how we're going to calculate the new position of the label based on the mouse position.
When the user presses the mouse button down on our control, it raises the MouseDown
event. In the MouseEventArgs
, we'll get the position of the mouse (point 3) relative to the top-left corner of our control (point 2). Since we want the mouse pointer to maintain this position inside our control while it's moving, we need to store the position we get here. We'll use it later to maintain this relationship between the control and the mouse position.
When we calculate the position of the control, we need the position of the mouse relative to the parent container of our control, in our case, the MainForm
client area (point 1). In the MouseEventArgs
that we're going to get, we'll still get the position of the mouse inside the control the mouse is moving in, but that doesn't do us any good. We need to convert that location to a point inside the parent control. We do this by calling the PointToClient
method of the parent container of our control. The Label
class, along with any other class that inherits from Control
, exposes a Parent
property that returns a reference to whatever control container is holding our control, or Parent
. The PointToClient
method will convert any point (point 3) relative to the inside of any of its child controls (point 2) to the same point relative to the top-left corner of itself (point 1).
Now we've got the two pieces of information we need to calculate the new position of our control (point 2). Point2 = (Point3 relative to Point1) - (Point 3 relative to Point 2), or Point2 = (Point3 relative to Point1) - (saved coordinates from the MouseDown
event).
Add a MoveMove
event handler to the code. Now, so long as the mouse is over our control and moves so much as a single pixel, this event is raised, whether the mouse button is down or not. We don't want to do anything if we're in Static
mode, so we need a flag to check in this event handler to see what mode we're in. If we're in Movable
mode, we need to calculate a new position for our control. If we're in Static
mode, we do not do anything and let the event handler terminate. So let's add this flag to our class' fields. Change the field code near the top of the class code to read:
Private m_CurrentCursor As Cursor
Private m_CursorOffset As Point
Private m_Moving As Boolean
The flag to signal moving has been setup, but now we need to manage the status of that flag in the appropriate places. Since our Movable
mode can only be entered inside the MouseDown
event, the m_Moving
flag should be set to True
in that event. Modify the MouseDown
event code as follows. We'll also be adding the code to save the mouse position inside our control when the event is raised.
Private Sub MovableLabel_MouseDown( _
ByVal sender As Object, _
ByVal e As System.Windows.Forms.MouseEventArgs) _
Handles MyBase.MouseDown
If e.Button = Windows.Forms.MouseButtons.Left Then
m_CurrentCursor = MyBase.Cursor
MyBase.Cursor = Cursors.SizeAll
m_CursorOffset = e.Location
m_Moving = True
End If
End Sub
Modify the MouseUp
event handler to look like the following:
Private Sub MovableLabel_MouseUp( _
ByVal sender As Object, _
ByVal e As System.Windows.Forms.MouseEventArgs) _
Handles MyBase.MouseUp
m_Moving = False
MyBase.Cursor = m_CurrentCursor
End Sub
Almost done! Fill in the MouseMove
event handler:
Private Sub MovableLabel_MouseMove( _
ByVal sender As Object, _
ByVal e As System.Windows.Forms.MouseEventArgs) _
Handles MyBase.MouseMove
If m_Moving Then
Dim clientPosition As Point = _
MyBase.Parent.PointToClient
(System.Windows.Forms.Cursor.Position)
Dim adjustedLocation As New Point( _
clientPosition.X - m_CursorOffset.X, _
clientPosition.Y - m_CursorOffset.Y)
MyBase.Location = adjustedLocation
End If
End Sub
Hit [F5] to run it, and see if you can move your new MovableLabel
control around!
Future Expansion
The code built in this article is a simplified version of the solution that is supplied in the download at the top of the article. The MovableLabel
class in the download has a slew of additions, such as:
- Support for staying within the bounds of the parent container
- Maintaining the bounds restriction during resizing of the parent container
- Adding a property to turn the Parent Container Bounds support on and off
- Exposing a
ValueChanged
event and maintaining a list of subscribers to that event
- Support for moving the control from one parent to another at design time
- Designer Serialization support for maintaining the values of properties at design time
- Setting new property default values
The complete MovableControl.vb file as written in this article:
Imports System.Windows.Forms
Imports System.Drawing
Public Class MovableLabel
Inherits System.Windows.Forms.Label
Private m_CurrentCursor As Cursor
Private m_CursorOffset As Point
Private m_Moving As Boolean
Public Sub New()
MyBase.New()
Me.Cursor = Cursors.Hand
End Sub
Private Sub MovableLabel_MouseDown( _
ByVal sender As Object, _
ByVal e As System.Windows.Forms.MouseEventArgs) _
Handles MyBase.MouseDown
If e.Button = Windows.Forms.MouseButtons.Left Then
m_CurrentCursor = MyBase.Cursor
MyBase.Cursor = Cursors.SizeAll
m_CursorOffset = e.Location
m_Moving = True
End If
End Sub
Private Sub MovableLabel_MouseMove( _
ByVal sender As Object, _
ByVal e As System.Windows.Forms.MouseEventArgs) _
Handles MyBase.MouseMove
If m_Moving Then
Dim clientPosition As Point = _
MyBase.Parent.PointToClient
(System.Windows.Forms.Cursor.Position)
Dim adjustedLocation As New Point( _
clientPosition.X - m_CursorOffset.X, _
clientPosition.Y - m_CursorOffset.Y)
MyBase.Location = adjustedLocation
End If
End Sub
Private Sub MovableLabel_MouseUp( _
ByVal sender As Object, _
ByVal e As System.Windows.Forms.MouseEventArgs) _
Handles MyBase.MouseUp
m_Moving = False
MyBase.Cursor = m_CurrentCursor
End Sub
End Class
This article was written expressly for The Code Project. If you find this article, in part or in whole, on any other website, it is a blatent copyright violation. Please inform the CodeProject staff of this at webmaster@codeproject.com. Thank you.