Creating a Dockable Panel-Controlmanager Using C#, Part 2

WEBINAR:On-Demand

Creating a Dockable Panel-Controlmanager Using C#, Part 2

Fly Panels, Fly!

Goals of this Article

What you want to achieve in this article is to get a borderless Form to be moved around on the screen by capturing it with the mouse and dragging it around, and to be able to resize the form, the same way as a normal sizeable Form could be sized by dragging its edges.

Note: To understand the design and construction of this DockablePanel Controler, you need to follow the steps in Part one of this article series. So, if you haven't read Part One yet, please first read and download Part 1 of this article series.

This article will teach you how to work with the base class 'Control' to pass Mouse events from controls that are placed on a Form to the Panel's Mouse events in a manner that you will have the Panel's MouseMove event thrown independent if the Form is covered with lots of different controls or there is only one very big control set on the Form, which covers it totally. You will be able to have the MouseMove event thrown wherever and whenever the mouse hovers over the Panel, This will be done without any usage of hooking. The funny trick on this is that, for the user, all this events seems to be the Forms mouse events, because at every time he never sees the original Form the programmer has created at Designtime. Let me give you a short review to fully get the point.

In the first part of this article series, you created the DockableForm. Remember, this Form in reality is created as a borderless Form. The surface you see on the screen is the DockablePanel where all the Controls you had on the Form will be added now with this article.

Figure 1: The DockableForm is fully covered with the DockablePanel control.

If you were to try to move the Form using the Mouse by dragging it around, nothing happens because:

A borderless Form has no Titlebar that you can use to move it around.

You never will reach the Form with any mouse action because the Form's surface is totally covered by the Panel, and what you see as the Form's Titlebar is actually your GradientPanel class, which is used here to build the Headerbox of your new Control.

To achieve this, you need to implement two operations; you need to get the Panel moving and you need to be able to resize the panel using the mouse by dragging its borders during runtime, just as a normal resizable Form acts.

To move a normal Form around, you move the mouse cursor over the Titlebar, press the left mouse button down, and keep the left mouse button depressed while moving the mouse around. When the left mouse button is released, the Form is no longer captured so you can stop moving the form. All this works only when the mouse is over the title of the Form; in this case, this is represented by the HeaderBox of the DockablePanel.

So, the first thing, obviously, you need to do is to create the MouseUp, MouseDown, and MouseMove delegates for the HeaderBox. To do this, select the DockablePanel; in the Designer View, select the HeaderBox; in the Properties Window choose 'Events', and double-Click on the needed events. This will create the following code (add the #region markings so you keep a neat and tidy code).

Now, you need to add code to these Delegates. To get the global Screen coordinates of your Mouse, you will use the API call ClientToScreen(), so add the necessary namespace on top of the DockablePanel codepage.

And in the MouseDown Delegate, you check whether the left Button was pressed when the MouseDown is fired.

private void HeaderBox_MouseDown(object sender, MouseEventArgs e) {
if (e.Button == MouseButtons.Left) {
LeftMouseButtonPressed(e);
}
}
private void LeftMouseButtonPressed(MouseEventArgs e) {
// store the actual mouseposition
_mousePos.x = e.X;
_mousePos.y = e.Y;
// change it to global-Screen Coordinates so we have
// the real position of the mouse instead of
// its position in the header
APICall.ClientToScreen(HeaderBox.Handle, ref _mousePos);
// the internal notification that the mouse is just down
_isMouseDown = true;
// the global position of the DockablePanels Left-Top
// corner is the global Position of the mouse
// reduced by the position in the header, so we get
_lastX = _mousePos.x - e.X;
_lastY = _mousePos.y - e.Y;
}

This way, you get the start position of your DockableForm where it is placed on the screen, upon starting the Form move. The following picture demonstrates the coordinate calculations.

Figure 2: Calculation of Global Screen Coordinates of the Panels Left Top Corner

In the MouseUp Delegate, you simply set the internal notification (_isMouseDown) to false because you have released the button; it is not pressed down any more. At the moment, there is nothing else to do with this delegate.

With the MouseMove delegate, you need to check the value of the internal boolean (_isMouseDown) to determine whether the form should still move or not. If the mousebutton was released, _isMouseDown will be false, meaning you are no longer moving. This is done with the next code segment.

With the Moving procedure (which follows next), you need to ensure that you are attaching and detaching the panel to and from the carrier form, depending whether it is moved or docked. Basically, with this method you are using the difference between the previous position and the current position.

If you were to compile and run the project, you would be able to move the panel in the same way as you can move a form—through its Titlebar.

A Note before compiling the project: Remember that, back in Part 1 when you created the buttonstrip, you made them as visible. Now, because they are in the correct place, you should hide them because you want them hidden when the program starts.

Edit CreateAllElements in the DockingManager class, to look like the following, to hide the Buttonstrips when the program starts:

WEBINAR:On-Demand

Sizing the DockablePanels

The user changes its size by dragging its borders when the panel is in an undocked mode.

[SizingUndocked.JPG]

Figure 3: Possibilities of Sizing a Form in its undocked state.

With code during the docking procedure to change Panels size according to the size needed for docking. Some examples are shown here:

[SizingDuringDocking1.JPG]

Figure 4: Some of the Sizing done by your code during docking

The third and last type of sizing occurs when the panel is docked and the user manually changes its width or height to a size he wants to have the docked window look like.

[ManualSizingDuringDocking.JPG]

Figure 5: Manual Sizing by the user during the Panels are docked

Remember though, that both the last two resizing possibilities must not change the window's size in the undocked state, so when you undock again you want the window to have the size you drew before docking. That's the fundamental condition you need to fulfill with your code. The general rule is:As long as you are docked in any way, any sizing done will not influence the undocked Size of the DockableForm, only the DockablePanels. Remember also, that when the Panel is docked, your DockablePanel is detached from the DockableForm and this Form is invisible during that time.

With the first resizing option, you may that think it is not necessary to change the Size of the DockableForm when you are docked as well. Don't forget that when changing the size of the form in any way, you need to change the size of your Controls (which you drew on your TechForm that is derived from DockableForm) as well.

Your DockableForm needs to throw the correct resize event every time the DockablePanel is resized, so you need to size DockableForm in any case when DockablePanel is sized, even when the Form is invisible. You need to keep an eye on storing and restoring the size of the undocked panel when docking or undocking the panel. That's what you basically have to consider about sizing.

Start your coding to achieve this.

First, you need to create some constants and fields to control what's going on. Add the following to your DockablePanels private Fields:

While moving the mouse over the DockablePanel, you have two different things that should happen. The first is that the Mouse cursor gets changed when you are near enough to the border. This should be done in every case as long as your form isn't docked. There is no need to press down the mouse button for this.

The second is, when the Left mouse button is pressed down, you need the mouse doing the sizing work. But, keep in mind how you are doing sizing: You first move to the borders of a Form then, when near enough and you see the changed cursor, you press down the mouse button. So basically, when the cursor changes you are informed that you are in a range where you can cause sizing by pressing the mouse button down and dragging it in the needed direction. So, what you want to code now is manual sizing when the DockablePanel isn't docked. The Field _sizeable is set when the user decides the Borderstyle to Fixed (false) or Sizeable (true). (Look back to Property BorderStyle - Set Method.)

protected override void OnMouseDown(MouseEventArgs e) {
// is it a sizeable Form and is our Cursor in a range
// where sizing could be done
if (_sizeable && _inRange != BorderRange.None) {
// where is our mouse located in Screenposition
// as we are calculating the position of this panel
// we can use the standard Method
_lastMousePos = this.PointToScreen(e.Location);
// we set the _formsizing boolean to true to keep
// information that we are just in the sizing prozess.
_formSizing = true;
}
// now simple caling the base procedure to throw the event
base.OnMouseDown(e);
}

In the MouseUp, you only need to cancel your _formSizing boolean, to inform the DockablePanel class (where you need it) that there is actually no sizing done.

The most complex Method now is that which has to do all the work—measuring Cursor position and changing the Form.

protected override void OnMouseMove(MouseEventArgs e) {
// only if the Borderstyle of the form is a sizeable
// one and only when we are not docked
// the form shows its sizing arrows when mouse hovers
// over the border
if (e.Button == MouseButtons.Left && _sizeable &&
_dockingType == DockType.None) {
if (_formSizing) { // else means:mouse is only Moving
SizeAccordingRange(e);
} else {
_inRange = SetBorderCursor();
}
} else {
this.Cursor = Cursors.Default;
}
base.OnMouseMove(e);
}

Creating a Dockable Panel-Controlmanager Using C#, Part 2

WEBINAR:On-Demand

How It Works

When the mouse is moving on the screen and entering the Panel area, OnMouseMove() is called by the Panel and MouseMove events are thrown in the base method of OnMouseMove(). You can see that, as long as the _formSizing Field is false, you are only calculating the range where the mouse currently is. If you are docked or if the form is not sizeable, the cursor is set to its default and the MouseMove event is thrown; nothing else occurs.

But, if you are in a border's range, _inRange is any value but not BorderRange.None. At this time, pressing down the left mouse button will call OnMouseDown() and _formSizing will be set to true.

OnMouseMove set the points, so instead of checking Borderrange again, now the method SizeAccordingRange() is called; this sizes the form depending on the range where the mouse was dragged on the Panel just before.

To get this to work easily, you have reduced the problem of a seemingly complicate method to two other methods (what you did in earlier situations, too), which could be understood easily. Okay, the following method calculates the Range and decides which Cursor will to be set and Changes the cursor when needed, depending on its position relative to the panel's Border.

Top and Bottomrange need a NorthSouth cursor arrows Cursor, Left and Right border needs the EastWest arrows Cursor, Left-Top and Right-Bottom corners need NorthWest to SouthEast arrows Cursor and at least Right-Top and Left-Bottom corner needs a NorthEast to SouthWest arrow Cursor. Basically, that's all this method does. The real problem is to find out if and in which BorderRange the cursor is; this is done in another method. Generally, I would say if the code gets too unclear or seems to be confusingly long, extract a method out of it and give it a self-explaining name and your code will get shorter methods that are easy to understand and at last, some years later when you might need to do some service on them, or a customer wants some specific features built into it, you will easily orientate yourself, what was written there, even if you have written thousands of other code lines in between. If code is too complex, simplify it as much as possible, extracting methods wherever it makes sense. That's my private rule.

To understand the method to check the BorderRanges where the different Cursors occur, look at the following drawing.

And now, the method that sizes the Panel. As explained earlier, where the sizing is done depends on the border. An exact observation of how this is works on a Form will show you the following situation. Looking at the next table, keep in mind that the position of a Form is given by the Top-Left position. Moving a Form is normally the same as changing the Top-Left position. But, when moving a Form, you have no change in size.

Where It's Drawn

Effect Caused

Right Border

The width changes; the Left Border doesn't move

Bottom Border

The Height is changed; TopBorder doesn't move

Left Border

The Right Border keeps still, the width changes, the Left position of the Form changes in the same amount as the width changes, so when the Width increases, the Left position value gets reduced by the same amount

Top Border

The Bottom keeps still, the Height changes, the Top position of the Form changes in the same amount, as the height changes, so when the Height increases, the Top position is reduced

WEBINAR:On-Demand

when the actual mouseposition is less than the last mouseposition you had previously, you get a deltaX, which is positive. So, when dragging on the left Border, you see the following.

_carrierForm.Width += deltaX;
_carrierForm.Left -= deltaX;

The width of the carrierForm needs to be increased by this amount and the left position of the Form needs to be reduced in the same way. Remember, because you are not docked, the Dockable Panel is still attached to its carrierForm the DockableForm. So, changing the Form also changes the size of the DockablePanel.

Now, compile the project for testing reasons and you will see that you basically can change the size of the DockablePanel now, but different small problems occur.

The Top Border doesn't set N-S arrows and could not be drawn; N-W, N-E are not working too

Resizing the DockablePanel doesn't resize the controls on the TechForm

A once-changed Mousepointer moving across the DockableForm doesn't change back to Default Cursor.

This is all quite normal in this state of coding because you haven't programmed all that's necessary for sizing. You can handle them one by one.

Point a) is caused because the HeaderBox catches all the mouse events, so no event will reach the DockableForm in this area. You need to get the HeaderBox mouse events causing DockableForm mouse events too.

In the HeaderBox_MouseDown delegate, you simply add OnMouseDown(e); So now, this delegate looks like the following:

In the HeaderBox_MouseMove event, the MouseMove Method that is the main method for moving the Form should only be called when the mouse is down ,and if you are not just sizing it! So, you do the following:

Compile it and you will see that this problem is handled now. You are able to size N-S , N-W, and N-E now without problems.

Point b) is caused because the resizing of the listcontrol in the TechForm simply hasn't been coded yet. This basically doesn't mean the Control wouldn't work properly. The truth is that in the TechForm, which is built in to test the Control, the resize delegate isn't coded. So, it's time to code it.

In the TechForm class, you add the following constants and the TechForm_Resize delegate. This delegate contains the code to resize the list control lstTechnicans every time the Form is resizing.

Point c) The problem with the mousepointer is that it needs a very exact look to your construction. Refer to Figure 6 to understand what's going on. I have inserted the filler Panel and the listTechnicans into the drawing now.

[Overlapping.JPG]

Figure 7: Ranges on the DockablePanel overlapped by other controls

What you can see in your picture is that the filler panel (red rectangle) covers the DockablePanel control in a wide range and overlaps the area where the Bordersizing could occur. When the mouse is in the area of the filler panel, all mouse events are going to this panel, so your DockablePanel doesn't get mousemove events any more. Compare Figures 6 and 7 and you will see that the area of the DockablePanel where the mouse turns back to default cursor is fully covered by the filler panel, so the default cursor doesn't occur in most cases. You handle this this way:

Creating a Dockable Panel-Controlmanager Using C#, Part 2

WEBINAR:On-Demand

What This Means

This is drawn because there is an additional problem coming up, when you handle the filler panels mouse events. You don't know, where the programmer will put other controls. In your case specifically, you have made a listview control on your TechForm (which is derived from DockableForm). This listview is set with a small border of three pixels. Hovering slowly over the form will get the mouse back to the default before it enters your control. But, if you hover very quickly, the cursor stays as it has been before. So, you also need to catch every controls' mouse event and use it to get your cursor reset to default.

Remember that you added all controls that had been on the DockableForm at design time to your DockablePanel using the AddFormControl method you had coded in Part 1 of the article series.

So, it's easy here to add an event to every control that is added on the form. Change the code of this method to the following:

internal void AddFormControl(Control c) {
if (c != null) {
this.filler.Controls.Add(c);
// set a handler to the control to catch the
// MouseMove event of the control
c.MouseMove += new MouseEventHandler(c_MouseMove);
}
}

And the delegate now also sets the Cursor back to default. There is one small point I want to call to your attention. The input of the c_mouseMove delegate contains all data related to the control that throws the event, so when you want to get a correct MouseMove event thrown from the DockablePanel before calling base.OnMouseMove(..) method, you need to correct the Location data of the MouseEventArgs to Positions related to the DockablePanel instead of being related to the control that originated the MouseMove event. Because MouseEventArgs class properties are read only, you need to set the values in the constructor of this class. Simply create a new MouseEventArgs object using the properties of the origin but changed in Location. Because all controls that could be drawn to a DockableForm (or to a Form inherited of DockableForm) could only be located on the filler panel (see AddFormControl), their position is relative to the filler panel. This leads to the formula to calculate the correct mouse position on the DockablePanel, which you see in the following code.

Closing the DockablePanel

Closing DockableForm and closing a DockablePanel isn't just possible by disposing it. There are different points you have to know. Remember in Part 1 of this series, for administration of the docking cycles you added the DockablePanels as well as the DockableForms to SortedLists in the DockingManager class. Now, when closing a DockablePanel, you might think that you only need to dispose of the DockableForm. But that wouldn't work, and I want to explain to you why. As you will see later in this project, the DockablePanel is only attached to the DockableForm as its carrier, as long as it isn't docked. In starting to dock the DockingPanel, the carrier form (DockableForm) is detached from the DockablePanel. The panel stays visible, the carrier Form gets invisible, and the panel is added to another Usercontrol called DockingControler that you will meet in the next part of this series. To get the correct carrier back on the DockablePanel when undocking the panel again, you need to store the DockablePanels and the DockableForms in two different SortedLists. Both have Keys that let us combine them together again in a correct way. This takes care of the point, that you may have more then one panel docked and when undocking them, you need to bring together exactly those pairs of Forms and Panels that belong together.

When you are disposeing the DockableForm, this would only dispose the DockablePanel as long as it isn't just docked. When you are docked, the DockablePanel, as explained earlier, is detached from the Form; when deleting the DockableForm, this wouldn't necessarily dispose the DockablePanel too. And in every case, you need to delete the DockableForm and the DockablePanel from the Sorted Lists where they are all added.

You need to create a delegate that informs the DockingManager of closing the DockablePanel, and in the delegate you need to remove both the panel and its carrier from their sorted lists.

In the Enumerations & DelegateDeclarations region in DockablePanel.cs (you'll find this on top of the namespace DockingControls.Forms), you add the new delegate Definition. On Top of our DockablePanelClass, you add a new region called events and there you declare your event. This looks like the following now.

In Part 1, you added the HeaderBox_ExitClick() delegate. Now, you remove the reminder that we had added there and add a method to close the panel instead.

private void HeaderBox_ExitClick() {
//Console.Writeline (""Exit was Clicked");
this.Close();
}
// And the method to close the panel is defined like the following
public void Close() {
// throwing the event
OnClosingPanel();
// disposing Panel if it wasn't disposed
// together with the carrier Form
if (!this.IsDisposed) {
this.Dispose();
}
}
// And here we have throwing the Closing Event
private void OnClosingPanel() {
// in every way inform the manager to close the carrier
if (this.PanelClosing != null) {
this.PanelClosing(_key);
}
}

In the DockingManager, you need to make sure the delegate is attached. This could easily be done in the AddPanel method where every DockablePanel is added to the DockingManager. You add the delegate here, for each DockablePanel one delegate. Add Panel now looks like the following:

internal void AddPanel(DockablePanel dockPanel) {
string key = "P" + _count.ToString();
// each DockablePanel gets a new number
// independing of some are maybe closed again.
// we start with "P0" p for panel
_count++;
dockPanel.Key = key;
// add dockPanel to the SortedList
AllDockPanels.Add(key, dockPanel);
// we attach the PanelClosing delegate to the Panel
dockPanel.PanelClosing +=new DockedFormClosingEventDelegate
(dockPanel_PanelClosing);
// now we attach the carrier to the DockablePanel;
AttachCarrier(dockPanel);
}

Now, you only need to remove the DockablePanel and the carrier Form from the sortedLists as explained earlier.

#region Delegates
private void dockPanel_PanelClosing(string key) {
// Its closed so we remove it from AllDockPanels List too
AllDockPanels.Remove(key);
// now we delete its carrier ( DockableForm ) too
// read out the Form from our SortedList
Form carrierForm = AllCarriers[key];
// Remove it from the SortedList
AllCarriers.Remove(key);
// close the carrier ( DockableForm )
carrierForm.Close();
}
#endregion

DockableForm moving, sizing, and closing now work. In the next article, you will see how to calculate positions of the docking buttons, the indication-rectangle that shows where you will be docking and the size of the DockablePanel, hiding and showing docking Buttons, changing their face when hovering over them, and some other very interesting material needed to go forward in your project. Dont miss it; it will be released in approximatly a month.

About the Author

Johann Schwarz

I first encountered computers in 1968, since then, I've written several programs in various Languages such as FORTRAN, C, C++, VB 6, and C#. Mostly I am a hobbiest, self taught programmer, but my computer and my programming is my second life

Advertiser Disclosure:
Some of the products that appear on this site are from companies from which QuinStreet receives compensation. This compensation may impact how and where products appear on this site including, for example, the order in which they appear. QuinStreet does not include all companies or all types of products available in the marketplace.

Thanks for your registration, follow us on our social networks to keep up-to-date