Creating a DockablePanel-Controlmanager Using C#, Part 5

Creating a DockablePanel-Controlmanager Using C#, Part 5

Actions in a Docked condition

In Parts 1-4, you have learned how to create all the parts that are needed to create a DockingManager; you now are able to manage DockableForms on the screen. You have laid out the design and learned how the different parts will work together. You decided to use DockingButtons to offer the user the ability to dock the DockableForms. Apart from that, you have learned that, by using the given design, you are faking a bit, because you really do not dock the forms but instead dock a Panel named DockablePanel. You have come to the point where you are able to show these certain buttons on the screen when you start to drag your DockablePanel. You can already dock them with your Basic Docking methods.

If you haven't yet read the previous parts of this article series, you will need to do so before you can start with this one, or you will not fully understand the structure and logic of this design pattern. Here are the preceding articles:

Note: Because you have to add some parts here and there, the code becomes more and more complex now and, to be sure where you are, I have added the regions where the code is to be found or where it should be created, so that it is easier for you to follow. If you still haven't created all the regions by now, you really should do this organizational work beforehand; it will save you hours of work.

Being able to do some basic docking actions after some hours of work is good progress, but there are still some actions needed; a well-designed DockingManager should be able to handle those actions. So, one of the points you still aren't able to do is to size the DockableForms during the docking process. If you take your VS 2005 IDE as an example of being able to size docked panes, you will notice that if you need the panel a bit wider or narrower, you can drag on its pane and it will become wider or narrower. When you undock the pane, it snaps back to its original size. Now, when you want to achieve this, you will need to be able to size the DockingControler. This will be the way you will arrange it.

Sizing the DockingControler

If you can recall, you have already added a padding zone to your DockingControler for this reason. In designing the sizing mechanism, you have to be aware that sizing the DockingControler also means that you need to size the DockablePanel; this is followed by resizing the DockableForm which, in its inheritance, in your DockingControlTestApplication is named TechForm. You will get a cascade of resizing Events and this, as I have seen when testing on a 2 GHz Dual Core machine+, has led to lots of flicker. So, I decided to reduce the number of resizing Events by the use of a Timer that I called SizerTick. This is added to the DockingControler now simply by opening the Designer View of the DockingControler and double-clicking on the Timer-control in the Toolbox.

Property Value
(Name) sizerTick
Enabled False
Interval 5

Create the Tick-Event Delegate of this Timer Control Named SizerTick_Tick

First, see the code and then have a closer look at how this works. It's standard that, when your mouse moves over the borders of any undocked Form, the cursor changes its appearance to some arrows that show you sizing will be possible. When you have the Form docked, you are not able to size the Form wherever you want. So, the cursor's appearance should be changed only when you are hovering on a border, where sizing is possible. So, for the cursor's Shape, you have the following SetBorderCursor() method.

#region private methods ( DockingControler )
private BorderRange SetBorderCursor() {
   Point pos = Cursor.Position;
   BorderRange inRange = GetBorderRange(pos);
   switch (inRange) {
      case BorderRange.Right:
      case BorderRange.Left:
         this.Cursor = Cursors.VSplit;
         break;
      case BorderRange.Top:
      case BorderRange.Bottom:
         this.Cursor = Cursors.HSplit;
         break;
      default:
         this.Cursor = Cursors.Default;
         break;
   }
   return inRange;
}

private BorderRange GetBorderRange(Point posOnScreen) {
   Point pt = PointToClient(posOnScreen);
   if (this.Dock == DockStyle.Left && pt.X > (this.Width - 3) 
      && pt.X < this.Width && pt.Y > 0 && pt.Y <= this.Height) {
      return BorderRange.Right;
   }
   if (this.Dock == DockStyle.Right && pt.X >= 0 && pt.X < 3
      && pt.Y > 0 && pt.Y < this.Height) {
      return BorderRange.Left;
   }
   if (this.Dock == DockStyle.Bottom && pt.Y >= 0 && pt.Y < 3
      && pt.X > 0 && pt.X < this.Width) {
      return BorderRange.Top;
   }
   if (this.Dock == DockStyle.Top && pt.Y > (this.Height - 3)
      && pt.Y <= this.Height && pt.X > 0 && pt.X <= this.Width) {
      return BorderRange.Bottom;
   }
   return BorderRange.None;
}
//. . . 
#endregion

Depending on where your DockingControler has been docked, you must allow only the opposite side of the DockingControler to be sized. All other attempts should return BorderRange.None; this means that you are not hovering on any Border of the DockingControler that is sizeable.

The full sizing action will need the following fields to be added in your code.

#region Fields (DockingControler)
// needed private Fields for Sizing the Controler
private bool _controlerSizing;
private BorderRange _inRange;
private bool _sizeNow;
private Point _lastMousePos;
private Point _actMousePos;
//. . . 
#endregion

The mouse events of the DockingControler need to be overridden in the following way.

#region overrides (DockingControler)
protected override void OnMouseDown(MouseEventArgs e) {
   if (_inRange != BorderRange.None) {
      _lastMousePos = this.PointToScreen(e.Location);
      //At this time the actual Mouse position is also the last one
      _actMousePos = _lastMousePos;
      _controlerSizing = true;
   }
   base.OnMouseDown(e);
}
//. . . 
#endregion

#region overrides (DockingControler)
protected override void OnMouseMove(MouseEventArgs e) {
   if (_controlerSizing) {
      // We get a new actual mouse position
      _actMousePos = this.PointToScreen(e.Location);
      if (_sizeNow) {
         // The Timer isn't allowed to be restarted during
         // all the sizing evets are done
         SizerTick.Enabled = false;
      }
      else {
         // If we aren't sizing we start the Sizing Timer
         SizerTick.Enabled = true;
      }
   }
   else {
      _inRange = SetBorderCursor();
   }
   base.OnMouseMove(e);
}

This is the sizing method itself; it sizes the Control on exactly that border, where sizing is possible. The change of the size depends on the difference in the cursor's position between two cycles, where this method is called.

#region private Methods (DockingControler)
private void SizeAccordingRange() {
   switch (_inRange) {
      case BorderRange.Right:
         int deltaX = _lastMousePos.X - _actMousePos.X;
         this.Width -= deltaX;
         break;
      case BorderRange.Left:
         deltaX = _lastMousePos.X - _actMousePos.X;
         this.Width += deltaX;
         this.Left -= deltaX;
         break;
      case BorderRange.Bottom:
         int deltaY = _lastMousePos.Y - _actMousePos.Y;
         this.Height -= deltaY;
         break;
      case BorderRange.Top:
         deltaY = _lastMousePos.Y - _actMousePos.Y;
         this.Height += deltaY;
         this.Top -= deltaY;
         break;
   }
   //Now store the actual Mouseposition as the last one
_lastMousePos = _actMousePos;
}
//. . .
#endregion

#region overrides (DockingControler)
protected override void OnMouseUp(MouseEventArgs e) {
   _controlerSizing = false;
   SizerTick.Enabled = false;
   _sizeNow = false;
   base.OnMouseUp(e);
}
//. . .
#endregion
#region delegates (DockingControler)
private void SizerTick_Tick(object sender, EventArgs e) {
   _sizeNow = true;
   SizerTick.Enabled = false;
   SizeAccordingRange();
   _sizeNow = false;
}
//. . .
#endregion

Figure 1: Time diagram Sizing the DockingControler

Figure 1 demonstrates the way it works. When the LeftMouseButton has been pressed and you are in the range of a Border of the DockingControler that could be used for sizing it, _controlelSizing is set to true (See OnMouseDown() method). This causes the Timer to start ticking and fires the Tick event. In the corresponding delegate, _sizeNow is set to true, and then the sizing Method SizeAccordingRange() is called. This is done in the Timer event so it isn't directly coupled anymore to the mousemove event. But, you have to know there is a Windows.Foms Timer used, so this is no separated thread; it's all done in the applications main thread. You have to know, basically the event is fired means it's added to the message queue and will be done in order when the message is to be done there.

Creating a DockablePanel-Controlmanager Using C#, Part 5

After _sizeNow is set to true, SizerTick.Enabled is set to false so that the Timer cannot respond a second time, even when a new MouseMove event gets executed in between. The point is that, even when a MouseMove event could occur, the SizerTick.Enabled gets set to false, but doesn't call the resize event again. When sizing is done, _sizeNow is set to false again and the next mousemove event will set the SizerTick timer to enabled again. The next sizing action starts. To better explain this process, have a look at the following flowchart in Figure 2.

[SizeAccordingRange.JPG]

Figure 2: Flowchart Mouse events in context with DockingControler Sizing actions

The amount of the size change depends on the difference of the mouse positions that occurred from one sizing action to the next; this is the difference between _lastMousePos and _actMousePos X or Y values. There is one thing still missing here. What am I talking about? If you compile it, it seems to run, but I'm talking about an aspect you haven't learned about until now. You have to differentiate between the two different sizing methods—sizing the form when it is undocked and sizing the form when it is docked. This is not only by the fact that sizing in a docked state needs to also resize the controler; it also means that undocking the Form should bring it back to the original size you have given it in an undocked state. So, the form needs to memorize which Size it had before it was docked; you need to create a sort of memory effect. To do this, you first have to add the Load event to the DockablePanels Control. The simplest way is to create it using the designer and in the properties page is to go to events and double-click the Load event line. It creates the stub for the delegate and adds it to the forms events delegates.

There, you need to add the following code.

#region Delegates (DockablePanels)
private void DockablePanel_Load(object sender, EventArgs e) {
   filler.Location =
      new Point(SystemInformation.FrameBorderSize.Width/2,
      HEAD_VERT);
   filler.Width =
      this.Width -  SystemInformation.FrameBorderSize.Width;
   filler.Height =
      this.Height - SystemInformation.FrameBorderSize.Height/2 -
      HEAD_VERT;
   _undockedSize = new Size(this.Size.Width, this.Size.Height);
   HeaderBox.PinVisible = false;
}
// . . .
#endregion

This way, you ensure that the filler panel has its correct size and that the size is stored in the _undockedSize Field, from the beginning. You haven't seen this missing part because, until now, all sizing was done only in the resize delegate and there was no need to store the values. You want the form to remember the last size you have chosen before docking. But, maybe the Form has never changed its original state before docking. In storing the starting values of the Form, it wouldn't forget its original state and after undocking, it will be able to regain its original size.

But, if the Size is changed before you dock, these changes also need to be stored. You simply use the resize delegate to do this.

#region Delegates (DockablePanels)
private void DockablePanel_Resize(object sender, EventArgs e) {
   if (_noResize ) return;
   if (this.Width < MIN_WIDTH) {
      this.Width = MIN_WIDTH;
   }
   if (this.Height < MIN_HEIGHT) {
      this.Height = MIN_HEIGHT;
   }
   HeaderBox.Width = this.Width;
   filler.Width = this.Width
      - SystemInformation.FrameBorderSize.Width;
   filler.Height =
      this.Height - SystemInformation.FrameBorderSize.Height / 2
      - HEAD_VERT;
   filler.Left = SystemInformation.FrameBorderSize.Width/2;
   filler.Top = HEAD_VERT;
   picGripTriangle.Left = filler.Width - picGripTriangle.Width
      - GRIP_PADDING;
   picGripTriangle.Top = filler.Height - picGripTriangle.Height
      - GRIP_PADDING;
   // when the size is changed in an undocked state drawing on
   // forms border
   if (_formSizing) {
      // change the sizeValues and store it in the form  itself
      _undockedSize = this.Size;
      // also change the value in the admin Class
      _admin.ActDockPanelSize = this.Size;
   }
   if (_dockingType != DockType.None){
      // if we are docked we are not attached to the carrier
      // so we need to size the carrierForm 
      _carrierForm.Size = new Size(this.Width, this.Height);
   }
}
//. . . 
#endregion

Most of this was done much earlier when I talked about resizing the Form in the undocked state, but now you need to store these values. You will also notice that I have eliminated some of my constants in this method by using the SystemInformation.FrameBorderSize values instead. But, that's only cosmetics. The new code segment that does the work in the above resize delegate looks like the following:

   if (_formSizing) {
      // change the sizeValues and store it in the form itself
      _undockedSize = this.Size;
      // also change the value in the admin Class
      _admin.ActDockPanelSize = this.Size;
   }

Another point you will need to concider is the following. If you want to undock, you need to click to the headerbox using the left mousebutton and dragging the panel. It may happen that someone clicks and then changes his mind or his finger slips from the button or whatever, without dragging. If someone does this, nothing should happen.

But, you may be thinking that this will crash. This is because you are already docked, but the next MouseUp event will fire the DockControlActivities message again on the DockingManager and, because you still have your HitState stored just as it was the first time you docked, the panel will be Docked now as the Controler is second time, which causes a crash. To handle this, you need to add some code. At first, get the HitState value handled. You need it as long the panel isn't docked. The best way I think of is to set this state to none every time the mouse gets pressed. This also prevents problems that might happen when you click on your panel and the DockingButton is just in the same position where our mousecursor is, which would lead to unpredictable results. So, in LeftMouseButtonPressed, you add the following lines to handle this.

#region private Methods (DockablePanel)
private void UndockPanel(MouseEventArgs e) {
//. . .
// just after the following line
_admin.ClientScreenPosition = new Point(TopLeft.x, TopLeft.y);
// we add the needed reset of the Hitstate value.
_hitted = HitState.none;
//. . . 

Now, every time you press the mouse button down, the Hitstate is none. As you know, you need to move the mouse to recalculate it. In case you do this with a docked panel, the panel will undock, so you will never get a problem of having the same panel docked twice onto a controller. On the other hand, if you aren't moving and only doing a short click on the panel without moving, nothing will happen because the HitState stays none in this case.

In some cases, the MouseUp event might occur but the mouse down event wasn't fired. I have had this in extensive testing. To get secure also in this case, you will have to change the MouseUp event also. Here is the changed code.

private void HeaderBox_MouseUp(object sender, MouseEventArgs e) {
   if (e.Button == MouseButtons.Left) {
      _isMouseDown = false;
      if (_hitted == HitState.none) {
         // if we are not starting to dock
         // save position where we dropped without docking
         _lastX = this.Left;
         _lastY = this.Top;
      }
      if (_draggingState != DraggingState.Dropped &&
         _dockingType == DockType.None) {
         _draggingState = DraggingState.Dropped;
         ControlDockedEventArgs de = new ControlDockedEventArgs();
         // informs about dropping ( mouse Up )
         de.DockControlState = _draggingState;
         // where we are docking ( if we are docking )
         de.Hitted = _hitted;
         // needed to identify the DockingForm
         de.DockingFormKey = _key;
         OnDockControlActivities(de);
      }
   }
   OnMouseUp(e);
}

As you can see, I have added checking the _draggingState and also the _dockingType if the HitState is still none.

You are only saving the actual mouse position on screen and you are only firing the DockCotrolActivities on the DockingManager when you havne't docked yet. Now, it's also impossible that clicking on a docked panel causes some flicker on the screen by activating and deactivating the transparent form.

Creating a DockablePanel-Controlmanager Using C#, Part 5

Undocking the Panel

Now, looking at the basic docking methods, almost all is done. What's to be done? There is one very necessary point missing. You need to be able to undock your DockablePanel again. Have a look at this. Basically, this shouldn't be a big action. There are only a few things you have to take into consideration.

The point you may unintentionally be ignoring is the fact that you don't know where in the Captionbar the user will catch the window. And, you don't want to get the window jumping around on the screen; you want to make the undocking procedure very smooth.

Basically, the cursor itself during undocking moves only a very small portion, maybe only a few pixels, so for your purposes it is your reference point. The Left as well as the Top position of the DockablePanel, related to the DockingControler, is zero. This is of minor interest because, just after you have undocked the DockablePanel, it is part of the DockableForm that is no MDI Child but an independent Form on the screen. So, when you undock the form and you didn't allow too much flicker, nor jumping the Form around on the screen, you need to calculate where you have to place it. At the moment of undocking, you want to have the Form exactly in the same position where it was just before you clicked and MouseDown() fired. This is why you need to calculate its screen Positions. Look at Figure 3.

[UndockMeasures.jpg]

Figure 3: Calculating the screen position of the DockableForm using the cursor's position

You may think that's simple. Get the position of the cursor on the Captionbar and simply calculate the DockableForms Position for the first moment of undocking by this formula:

DockableForm.Left = CursorScreenPosition.Left - delta_X;
DockableForm.Top  = CursorScreenPosition.Top  - delta_Y;

delta_X and delta_y are the X and Y values of the actual cursor position. They are related to the DockablePanels Captionbar, the HeaderBox, and you get them in the MouseEventArgs, so no problem.

[UndockLeftFits.JPG]

Figure 4: Undocking a DockablePanel docked to the left seems to fit

But, this calculation is not the 'state of the art' solution. Look at the following images to see which troubles arise by using this formula:

[UndockTop.JPG]

Figure 5: Undocking a DockablePanel docked to the Top and the panel jumps to the left

This is why you need to have another solution for this mechanism. The same calculation as before now keeps the top left corner of your DockableForm at the place where the Top-Left Corner of the docked Panel had been. Because the size of the Form changes back to its original size that it had before you started to dock, this now will cause the form to jump to the left and the Mouse Cursor will still stay on the same place where it was. You are no longer dragging your Form in the middle of its Captionbar; you are in distance to your form but still dragging it around.

It looks a bit funny, but nobody can work with such habits of a Form. Your calculation obviously needs to be influenced by:

  • The DockType. If it's docked left, right, top, bottom, or whatever you will have later on in this article.
  • Changes of the width of the Form between docked and undocked state. This isn't only occurring in DockType.Top or DockType.Bottom docking. Because you can resize your DockingControler, the width of the Form may change during undocking also when you simply have docked left or right.

[UndockedWidthChanges.JPG]

Figure 6: The Mouse Cursor seems to jump toward the right corner of the Form caused by Changes of the Form's Width

Knowing all that, you now will have to adapt your design. Show the Form as if it were taken in the middle of its Captionbar, independent of where you really have taken it. But, there are two small exceptions in this rule. If the panel is docked to the left and you are taking it in the left half of the caption bar, you grasp the panel as it is because this way you will not move the panel's Left-Top position. (When I say 'left half', I'm talking about panel size in its undocked state.)

Now, after this examination you are ready to start coding this functionality. Clicking only on the Captionbar of a DockablePanel shouldn't change its docking state. You should need to have to have the left Mouse button already pressed down to the Captionbar of a docked Panel and this way movE the mouse over it. Only in this case, your docked Panel should become undocked again. So, add the following in your MouseMoves() method. The undock Cycle should not happen in every MouseMove event, so you need to check whether the panel is docked before you undock the panel.

#region private Methods (DockablePanel)
private void MouseMoves(MouseEventArgs e){
   WinAPI.POINT mousePos;
   mousePos.x = (short)e.Location.X;
   mousePos.y = (short)e.Location.Y;
   APICall.ClientToScreen(HeaderBox.Handle, ref mousePos);
   // we Check if we are docked and undock the panel 
   if (this.DockingType != DockType.None ) {
      UndockPanel(e);
   }else {
      Moving(mousePos);
      // . . . code as before

The MouseEventArgs are handled by the UndockPanel() method as its argument. Here, you calculate the new position of the DockableForm on the screen. In taking care of the positioning situation of the shown pictures, you have three different situations:

  • Docked to the Left and the cursor position is in the first half of the undocked form's width
  • Docked to the Right and the cursor position is in the last half of the undocked form's width
  • All other states of Docking (UndockPanel isn't called if you have DockTye.none, so this cannot happen)

Figure 7 is a construction drawing for easier understanding of the calculation.

[CalculateUndockedPosition.JPG]

Figure 7: Calculating when the panel is docked to the right

Creating a DockablePanel-Controlmanager Using C#, Part 5

The previous example shows a panel that was expanded when docked and snaps back to the original size when undocked. You can do similar drawings for all other docking positions.

#region private Methods (DockablePanel)
private void UndockPanel(MouseEventArgs e)
{
   // User tries to move the control, lets undock the Panel
   // at first we will calculate the new position of the 
   // DockableForm on the screen
   int posX = 0;
   int deltaWidth = this.Width - this._undockedSize.Width;
   if (_dockingType == DockType.Left && e.X
      < _undockedSize.Width / 2) {
      // only if we are left docked and the mouse is in 
      // the left part of the captionbar
      posX = _lastX;
   }
   else if (_dockingType == DockType.Right && e.X - deltaWidth >
      _undockedSize.Width /2) {
      // or we are right docked and the mouse is near to 
      //the right corner
      int curDistRight = this.Width - e.X;
      int newCurDistLeft = _undockedSize.Width - curDistRight;
      posX = _lastX + e.X - newCurDistLeft;
   } else {
      // all other dockingTypes we do
      // set mousepoint to medium position
      posX = _lastX + e.X - _undockedSize.Width / 2;
   }
   // now we inform the DockingControler and the DockingManager
   OnUndock();
   // we correct the Size to the original values
   this.Height = _undockedSize.Height;
   this.Width = _undockedSize.Width;
   // now we change Position to the calculated one.
   _carrierForm.Left = posX;
   _carrierForm.Top = _lastY;
   // the panel is added to ist carrier the DockableForm again
   _carrierForm.Controls.Add(this);
   // now we get it visible
   this.Visible = true;
   _carrierForm.Visible = true;
   // and as a Form on Top of all other Forms
   _carrierForm.TopMost = true;
   _carrierAttached = true;
   // we dont allow resizing during docking to the carrier
   _noResize = true;
   this.Dock = DockStyle.Fill;
   _noResize = false;
   // now we set the dockingType to none, so this method isn't
   // called again in the next MouseMove event delegate.
   _dockingType = DockType.None;
   HeaderBox.PinVisible  = false;
   // now we bring the DockingButtons on Top 
   _dockingManager. DockingButtonsToTop();
}
// The standard method to fire the event we do like the following
private void OnUndock() {
   if (Undock != null) {
      Undock(this);
   }
}//. . . 
#endregion

Understanding this code shouldn't be a big problem for you by now. There is only one point you shouldn't forget. On one side, you are setting the DockableForm to be the Topmost form so that it's impossible to slip underneath the MDI Form. On the other hand, the DockingButtons needs to be on top of them, so they can be seen when your DockableForm is moved over the MDI window. So, after setting the DockableForm to be Topmost, you have to set all the DockingButtons (that are all derived from Form) to Topmost too, so they are to be seen on top of the DockableForms. This way, you are correcting the Z-order of your Forms. This is done by the DockingManager.

#region Internal Methods (DockingManager)
internal void DockingButtonsToTop() {
   LeftButton.TopMost = true;
   RightButton.TopMost = true;
   TopButton.TopMost = true;
   BottomButton.TopMost = true;
}
#endregion

Now, you need to create the delegate declaration so you are able to fire an Undock event. You add it on top of DockablePanel.cs.

#region Enumerations & Delegatedeclarations (DockablePanel.cs)
internal delegate void
   DockedFormUndockEventDelegate(DockablePanel panel);

And, in the DockablePanels events you add the needed Undock event. It informs the DockingControler that the panel is leaving.

#region events (DockablePanel)
internal event DockedFormUndockEventDelegate Undock;
//. . .
#endregion

To receive the messages, you need to create a delegate in the DockingControler class, too. For easy and controlled adding and removing of the delegate, you only create one specific _undockPanel delegate in every DockingControler. You add a Field for the delegate in the DockingControler and you initialize it in the Constructor.

#region Fields ( DockingControler )
private DockedFormUndockEventDelegate _undockPanel;
#endregion
#region Constructor 
internal DockingControler() {
   InitializeComponent();
   //. . .
   _undockPanel =
      new DockedFormUndockEventDelegate(dockPanel_Undock);
   //. . . 
#endregion

The best way to add the needed delegate is the same way you did it with the _prepareClosing event delegate already, in the method where you add the panel to the controller during the docking process. You have to add the following after the existing code.

#region internal Methods (DockingControler )
internal void AddPanel(DockablePanel dockPanel){
   //. . . and in the last line we add
   dockPanel.Undock += _undockPanel;
}

And now, you have do undock the DockablePanel from the controller. Basically, you need to do the following to handle this:

  • Remove the panel from the DockedPanels List in the DockAdministration class because it is no longer docked. This way, it is no longer included in calculations for the InnerClientRectangle and all the mathematical calculations of the Preview windows position and size.
  • Undock it from the SplitContainers panel.
  • Remove it from the SplitContainer.
  • Remove all delegates that are placed in this DockingControler and are connected to the DockablePanel.
  • If there is no other panel docked to this DockingControler, you have at last to remove this DockingControler.
  • From the DockControlers List in the DockAdministration too, and to Dispose this DockingControler.

This you can see in the following method:

#region Delegates (DockingControler )
private void dockPanel_Undock(DockablePanel dockPanel) {
   this._dockingManager.Admin.RemoveDockedPanel(dockPanel.Key);
   dockPanel.Visible = false;
   // we undock the panel from the SplitContainer.
   dockPanel.Dock = DockStyle.None;
   switch (dockPanel.DockingType) {
      case DockType.Left:
      case DockType.Right:
      case DockType.Top:
      case DockType.Bottom:
         if (this.DockSplitContainer.Panel1.Controls.
             Contains(dockPanel)) {
         //we remove the panel from the SplitContainer
         this.DockSplitContainer.Panel1.Controls.Remove(dockPanel);
            _dockedPanelsCount = 0;
         }
         break;
      default:
         return;
   }
   // remove delegate so it doesn't react anymore in this Control
   RemoveDelegates(dockPanel);
   if (_dockedPanelsCount == 0) {
      this.Visible = false;
      // this Controler is closed and disposed so we can remove
      // it from the collection
      this._dockingManager.Admin.RemoveDockControler(this._key);
      this.Dispose();
   }

}
// . . .
#endregion

As explained before, you need to remove the delegtes from the DockablePanel.

At the moment, the DockingControler has only two delegates attached to this panel, so you only have to remove these delegates. You have to check whether the delegates exist and then subtract them from the delegates List of the DockableForm. Maybe you are asking why you should do this, because, as you see, the DockingControler itself is disposed after the panel is undocked. This is correct, but think of a case with advanced docking, where different controls are docked to the DockingControler and the controller isn't deleted. Maybe you are docking left, and then changing and docking the same panel to the top. You would have an _undockPanel delegate in the Left DockingControler and a second one in the Top DockingControler. Undocking from this panel then would first try to undock from the Left DockingControler because the delegate isn't removed. But, this panel is no longer docked here and the DockingControler maybe still exists or is disposed by another panel in between. So, you will get into unpredictable conditions that would end in a crash.

To get a mechanical picture of what would happen, think that each delegate connects the dockingControler and the DockAblePanel with a thin wire. Then, move the panel from one dockingControler to the next without cutting the wires. If you move some panels to and from different Controlers, you will end up in a tangle of wires. You have to remove these delegates very carefully. Adding additional delegates will need to extend this methods too.

#region private Methods (DockingControler )
private void RemoveDelegates(DockablePanel dockPanel) {
   if (dockPanel.DelegateStatus("ClosingWhileDocked")) {
      {
         dockPanel.PrepareClosing -= _prepareClosing;
      }
   }
   if (dockPanel.DelegateStatus("Undock")) {
      dockPanel.Undock -= _undockPanel;
   }
}
// . . .
#endregion

#region internal Methods (DockablePanel)
internal bool DelegateStatus(string type) {
   switch (type) {
      case "ClosingWhileDocked":
         if (this.PrepareClosing != null) {
            return true;
         }
         break;
      case "Undock":
         if (this.Undock != null) {
            return true;
         }
         break;
      default:
         return false;
   }
   return false;
}
   // . . .
#endregion

If you were to compile and run it now, the undocking should work.

Creating a DockablePanel-Controlmanager Using C#, Part 5

Advanced Docking Methods

You are ready now to proceed to add some more Advanced Docking Methods to your project. You will have to extend the different methods you already have for simple docking. The concept behind it is the following: As soon as you have docked at least one panel, you have two different areas on your screen, as shown in Figure 8.

[TwoAreas.JPG]

Figure 8: The different areas of docking space.

The innerClientScreenRectangle is the area where your docking can occur. Both of the areas shown in Figure 8 may be docking areas; it only depends in which area your mouse hovers, and which area is chosen to be the docking area. This way you always have only one innerClientScreenRectangle but you may have different DockingControler areas depending on how many panels you have docked using basic docking methods. If you are doing advanced docking, one of these DockingControlers areas is your actual innerClientScreenRectangle. Docking into the Basic Docking area, your innerClientScreenRectangle is exactly what you have had until now, but docking to an existing DockingControler area always leads to an advanced docking action. Quite simple, isn't it? Your program logic requires you to find out where your Mouse Cursor hovers and if you are in the Base Docking area or in the DockingControler area, you need to show different docking Buttons on different places on the screen. Because there can be more than one DockingControler on the screen when hovering one of them, you also need to decide which one it is. This can easily be done by checking its key.

Extending the Show DockingButtons Procedure

To find out where your mouse is hovering, create a method that returns the key of the DockingControler you are currently hovering on, or, if you are hovering in a space where no DockingControler is docked, call FreeSpace.

#region internal Methods (DockAdministration )
//. . . 
internal Hover GetHoverBase(WinAPI.POINT pt) {
   APICall.ScreenToClient(_dockManager.Parent.Handle, ref pt);
   DockStyle dockingStyle = InSidePanel(pt);
   switch (dockingStyle) {
      case DockStyle.Left:
         return Hover.LeftPanel;
      case DockStyle.Right:
         return Hover.RightPanel;
      case DockStyle.Top:
         return Hover.TopPanel;
      case DockStyle.Bottom:
         return Hover.BottomPanel;
      default:
         return Hover.FreeSpace;
   }
}
#endregion

The InsidePanel() method simply establishes whether your Cursor Coordiantes are inside one of the DockingControlers. It is used to calculate GetHoverBase() but needs to get the coordinates related to the MDI window so that you can translate the actual global cursor position to MDI-related values.

#region private Methods (DockAdministration )
//. . .
private DockStyle InSidePanel(WinAPI.POINT pt) {
   foreach (KeyValuePair<string, DockingControler>
      kvp in DockControlers) {
      DockingControler ctl = kvp.Value;
      if (ctl != null && ctl.Visible == true) {
         int left = ctl.Left;
         int right = ctl.Left + ctl.Width;
         int top = ctl.Top;
         int bottom = ctl.Top + ctl.Height;
         if (left < pt.x && right > pt.x && top < pt.y &&
            bottom > pt.y) {
            // it is inside the panel
            _hoverBaseKey = ctl.Key;
            // we get the amount of panels docked on a specific
            // Controler we are just hovering on it there
            _actDockedPanelsCount = ctl.DockedPanelsCount;
            return ctl.Dock;
            }
        }
   }
   _hoverBaseKey = String.Empty;
   return DockStyle.None;
}
#endregion

Basically, there are three things you need to know about that DockingControler you are hovering upon.

  • The way it is docked, so you need to know whether is this a controller that is docked to the left or the right, to the top, or to the bottom. You need this information, for example, when you are showing the DockingButtons because they are different in their layout depending on the fact that if the DockingControler is orientated horizontally (top or bottom docked) or vertically (left or right docked).
  • The Key of it. By knowing this DockingControler key, you know the DockingControler and you can access its position on the screen, so you are able to show the transparent DockingPreviewRectangle.
  • The quantity of DockablePanels already docked to this DockingControler. You need to know this because, if there are already two or more DockablePanels docked to this DockingControler, you must allow docking only to a Tab Control. GetHoverBase() is called the whole time the mouse moves while dragging around a DockableForm. This way, you always have actual Values in the private Fields _hoverBaseKey and _actDockedPanelsCount of the DockAdministration class.

So, you need add a HoverBaseKey and an ActDockedPanelsCount Property to the DockAdministration class to get informed about these values.

#region Fields (DockAdministration )
//. . .
private string _hoverBaseKey;
private int _actDockedPanelsCount;
#endregion

#region Properties (DockAdministration )

//. . .
internal string HoverBaseKey {
   get {
      return _hoverBaseKey;
   }
}

internal int ActDockedPanelsCount {
      // the Amount of docked Panels of a Form we are
      // just hovering on with another panel
      get {
         return _actDockedPanelsCount;
      }
   }
#endregion

You are calculating these actual values in the DockAdministration class. Additionally, you are storing the last calculated values in the DockablePanel you were just dragging around. This way, you are able to compare if any change has occurred.

#region Fields (DockablePanel)
//. . .
// which sort of panel we are hovering
private Hover _hoverBase;
// The key of the Panel where we just are hovering
private string _hoverBaseKey;
#endregion

It may happen that someone drags a DockableForm around, stops, and then takes another Form and then goes on dragging. Although by dragging the first DockaleForm, he may have stopped dragging while hovering in the space of one specific DockingControler but the other DockableForm hovers over a totally other DockingControler or is in the free space of the innerClientRectangle. This is why you instantly recalculate when the mouse goes down and a new DockableForm is picked up to drag it around.

So, you have to add to your LeftMouseButtonPressed() method in the DockablePanel class.

#region private Methods
//. . . 
private void LeftMouseButtonPressed(MouseEventArgs e) {
   //. . . we have this already
   _admin.ClientRectangle = _clientRectangle;
   // and just here we add
   _hoverBase = _admin.GetHoverBase(_mousePos);
   // Key of the panel over which we just are;
   _hoverBaseKey = _admin.HoverBaseKey;
   // the remaining part of the method follows here
   _lastX = _mousePos.x - e.X;
   //. . .
}
//. . .
#endregion

In the mouseMoves method, _hoverBaseKey needs to be added too, but there is also a need to find out when your mouse changes its actual area where it was hovering, from one DockingControler to another one or if it is changing to the area where no DockingControlers are placed. I'll explain all the needed actions in the code, as you will see.

private void MouseMoves(MouseEventArgs e) {
   WinAPI.POINT mousePos;
   mousePos.x = (short)e.Location.X;
   mousePos.y = (short)e.Location.Y;
   int actDockedPanelsCount = 0;
   APICall.ClientToScreen(HeaderBox.Handle, ref mousePos);
   // is the panel Docked ? then we undock it now 
   if (this.DockingType != DockType.None) { 
      UndockPanel(e);
   }
   else {
      // the moving action itself changes the DockableForms position
      Moving(mousePos);
      // where are we hovering ?
      Hover isOver = _admin.GetHoverBase(_mousePos);
      bool insidePanel;
      if (isOver == Hover.FreeSpace) {
         insidePanel = false;
         actDockedPanelsCount = 0;
      }
      else {
         insidePanel = true;
         actDockedPanelsCount = _admin.ActDockedPanelsCount;
      }
      if (_draggingState == DraggingState.Captured ||
         _hoverBaseKey != _admin.HoverBaseKey) {
         // Only if it is Captured before or if the 
         // _hoverBaseKey has just changed, that is
         // when the location changes from one panel
         // to another or to free space
         // Now we use the prepared data to fire them
         // to the docking manager
         ShowControlEventArgs ds = new ShowControlEventArgs();
         ds.InsidePanel = insidePanel;
         // Key of he actual Controler we are hovering,
         // thus we can decide which buttons to show
         _hoverBaseKey = _admin.HoverBaseKey;
         // preparing data to fire ShowDockControls
         ds.HoverBaseKey = _hoverBaseKey;
         ds.IsOver = isOver;
         _admin.ActDockPanelSize = this.Size;
         Console.WriteLine("Width= " + this.Size.Width.ToString());
         // In this case we show our DockingButtons
         OnShowDockControls(ds);
            _draggingState = DraggingState.Moving;
      }
      // we are only calling this event when the draggingState
      // is 'Moving' so we are sure LeftMouseButton is down, 
      // and DockablePanel is moving captured by the mouse.
      if (_draggingState == DraggingState.Moving) {
         ControlDockedEventArgs de = new ControlDockedEventArgs();
         HitState hitted = HitTest(e);
         de.DockControlState = _draggingState;
         // Only when the HitState changes we show or hide
         // the DockingPreviewRectangle
         if (_hitted != hitted) {
            de.Hitted = hitted;
            de.HoverBaseKey = _admin.HoverBaseKey;
            _hitted = hitted;    // remember last state
            // In this case we show the TransparentForm
            // as the DockingPreviewRectangle 
            OnDockControlActivities(de);
         }
      }
   }
}

Creating a DockablePanel-Controlmanager Using C#, Part 5

In the HeaderBox_MouseUp() delegate, you only have to add the information which DockingControler is to be used for docking. You haven't needed that until now because in simple docking all the time you used a new DockingControler. Now, in advanced docking, you need to exactly inform the DockingManager which DockingControler is to be used or if a new one is to be created. You send this information by adding the HoverBaseKey just before firing the DockControlActivities event.

#region Delegates (DockablePanel )
//. . .
private void HeaderBox_MouseUp(object sender, MouseEventArgs e){
   //. . .
   de.Hitted = _hitted;
   // just after the above we add this line
   de.HoverBaseKey = _admin.HoverBaseKey;
   // and we add the remaining code here
   de.DockingFormKey = _key;
   OnDockControlActivities(de);
   //. . .
}
#endregion

Now, your DockingManager gets all the information it needs to show and hide the correct DockingButtons or to do the correct desired docking action. You only need to adapt the dockPanel_ShowDockControls and the dockPanel_Messages delegate. When this is done, you need to get your DockingControler to accept the DockablePanels and to add them according to the method you have chosen for docking. Start with the DockingManagers delegates. Both delegates already exists and are connected to the originating DockablePanel, so you only need to extend their built-in architecture to improve their capabilities. First, extend the dockPanel_ShowDockControls delegate. Until this point, you had:

#region Delegates (DockingManager)
//. . . 
private void dockPanel_ShowDockControls(ShowControlEventArgs e) {
   _admin.AnalyseMDIParentControls();
   ShowSinglePanelDockingControls(e);
}
#endregion

Now, you have to do a bit more. Because the DockingButtons that are to be used are changing, you have to hide the buttons that already exist on the screen. Then, before showing the DockingButtons as they have to be now (which and where on the screen), you need to do a new analysis of the MDI and its Controls on the screen. Maybe the user has docked new buttonstrips or added new menus in between, so you have to do a new analysis of the existing MDI controls. If you are inside a DockingControlers area, you have to perform advanced docking and to show the appropritiate DockingButtons for this, so you call ShowAdvancedDockingFeaturesControls() in any other case you show the known DockingButtons within the innerClientScreenRectangle, so you call the already existing method ShowSinglePanelDockingControls(). That's how you will do it.

#region Delegates (DockingManager)
//. . . 

private void dockPanel_ShowDockControls(ShowControlEventArgs e){
   // if any Docking Panels are just active hide them.
   HideDockingButtons();
   _admin.AnalyseMDIParentControls(); 
   if (e.InsidePanel){
      ShowAdvancedDockingFeaturesControls(e);
   }else{
      ShowSinglePanelDockingControls(e);
   }
}
#endregion

#region Private Methods (DockingManager)
//. . . 
private void
   ShowAdvancedDockingFeaturesControls(ShowControlEventArgs e){
   // Get the hovered DockingControler using its key
   DockingControler dockCtrl = GetDockingControler(e.HoverBaseKey);
   // define the new innerClientRectangle;
   _admin.CalcInnerClientRectangle(e);
   switch (e.IsOver){
      case Hover.LeftPanel:
      case Hover.RightPanel:
         int dockedPanelsCount = dockCtrl.DockedPanelsCount;
         if (dockedPanelsCount < 2 &&
            _admin.InnerClientScreenRectangle.Height >
            _admin.GetMinVertSize()){
            // if we have more then two we only allow tabbed controls
            _admin.SetVertDockButtonsSize();
            _admin.CalculateDockingButtonPositions(e);
            _innerClientScreenRectangle =
               _admin.InnerClientScreenRectangle;
            UpperButton.Location = _admin.UpperButtonPosition;
            LowerButton.Location = _admin.LowerButtonPosition;
            UpperButton.Show();
            LowerButton.Show();
         }else {
            _admin.SetOnlyCenterButtonSize();
            _admin.CalculateDockingButtonPositions(e);
            _innerClientScreenRectangle =
               _admin.InnerClientScreenRectangle;
         }
         CenterButton.Location = _admin.CenterButtonPosition;
         CenterButton.Show();
         PanelButtonsToTop();
         break;
      case Hover.TopPanel:
      case Hover.BottomPanel:
         dockedPanelsCount = dockCtrl.DockedPanelsCount;
         if (dockedPanelsCount < 2 && _admin.GetMinHorSize()
            < _admin.InnerClientScreenRectangle.Width) {
            // if we have more then two we only allow tabbed controls
            _admin.SetHorDockButtonsSize();
            _admin.CalculateDockingButtonPositions(e);
            _innerClientScreenRectangle =
               _admin.InnerClientScreenRectangle;
            LeftSideButton.Location = _admin.LeftSideButtonPosition;
            RightSideButton.Location =
               _admin.RightSideButtonPosition;
            LeftSideButton.Show();
            RightSideButton.Show();
         }else{
            _admin.SetOnlyCenterButtonSize();
            _admin.CalculateDockingButtonPositions(e);
            _innerClientScreenRectangle =
               _admin.InnerClientScreenRectangle;
         }
         CenterButton.Location = _admin.CenterButtonPosition;
         CenterButton.Show();
         PanelButtonsToTop();
         break;
   }
}
//. . . 
#endregion

Now, let me explain what's going on in this method. By using HoverBaseKey, you are informed which DockingControler was hovered so you can address the DockingControler and all needed data therein. Then, you calculate the InnerClientScreenRectangle calling CalcInnerClientRectangle() method of the DockAdministration class where all your calculations are done. Note that the name of this method is shortened as in fact it should be named as CalcInnerClientScreenRectangle() because this rectangle is always calculated in relation to its global position on the screen. As mentioned earlier, you have to handle two different situations:

  • A vertical orientated DockingControler when you have hovered a DockingControler docked to the left or to the right side. Here, you have to check whether the vertical Size of the DockingControler allows three DockingButtons to be shown on the screen.
  • A horizontal orientated DockingControler that's docked to the Top or to the Bottom. Here, you check whether the horizontal extension of the DockingControler allows three DockingButtons to be shown on the screen.
  • In both cases, you have to check whether there is only one panel docked. Then, you adjust the DockingButtons to the needed size and calculate its position. If the DockingControler size doesn't allow three DockingButtons on the screen, you show only the Center Button. This way, you don't allow the usage of the DockingControler SplitControler; you only allow docking to the TabControl of the DockingControler. That's what this code basically does. As you can see, there are some methods that you need to create to get the above method working.
#region Private Methods (DockingManager)
//. . . 
private DockingControler GetDockingControler(string hoverBaseKey) {
   try{
       DockingControler ctl =
          Admin.GetDockingControler(hoverBaseKey);
       return ctl;
   }catch{
       return null;
   }
}
//. . . 
#endregion

#region internal Methods (DockAdministration)
internal DockingControler GetDockingControler(string hoverBaseKey) {
      if (DockControlers.ContainsKey(hoverBaseKey)) {
         return DockControlers[hoverBaseKey];
      }
      return null;
   }
//. . . 
#endregion

All pointers of your DockingControls are stored in the SortedList DockControlers in the DockAdministration. So, you are checking whether the key exists in this list and then returning a pointer to the needed DockingControler.

#region internal Methods (DockAdministration)
internal void CalcInnerClientRectangle(ShowControlEventArgs e) {
   if (e.InsidePanel) {
      CalcInnerClientRect(e);
   }
}
//. . . 
#endregion

The real calculation is done in the CalcInnerClientRect() method, but you encapsulate this method to be sure it's only called when you are hovering inside a DockingControls area. The encapsulating method is named CalcInnerClientRectangle() This is the calculation itself.

#region private Methods (DockAdministration)
//. . . 
private DockingControler CalcInnerClientRect(ShowControlEventArgs e){
   string key = e.HoverBaseKey;
   // we find the panel where it is just over
   if (DockControlers.ContainsKey(key)) {
      DockingControler ctl = DockControlers[key];
      // and now the Parent where it is docked
      int innerWidth = ctl.Width;
      int innerHeight = ctl.Height;
      Point location = PanelScreenLocation(ctl);
      // what's the rectangle over the panel where we are just on
      _innerClientScreenRectangle.Size = new Size(innerWidth,
         innerHeight);
      // where is the rectangle on the screen
      _innerClientScreenRectangle.Location = location;
      return ctl;
   }
   return null;
}

private Point PanelScreenLocation(Control p) {
   WinAPI.POINT location;
   // The location is the left/top point of a panel
   // We are using the controls handle, so we use the controls
   // own reference frame  which starts at left /top with 0/0
   location.x = 0; 
   location.y = 0;
   // transfering this point to global screen coordinates
   APICall.ClientToScreen(p.Handle, ref location);
   return new Point(location.x, location.y);
}
//. . . 
#endregion

Creating a DockablePanel-Controlmanager Using C#, Part 5

When you are drawing three DockingButtons on the screen and they need to fit into a specific Rectangle area and you don't want to get the Buttons pictures overlapping each other, the minimum size of this rectangle is the result of all three DockingButtons and a small border that you will name BT_OFFSET. If the pictures are drawn from top to bottom, vertically arranged, you need to calculate the minimum vertical size that you will need to draw the buttons when they are arranged horizontally. Then, you need to get the minimum needed width of all these buttons together.

#region Fields (DockAdministration)
//. . . 
private const int BT_OFFSET = 6;
#endregion

#region internal Methods (DockAdministration)
internal int GetMinVertSize() {
   // 6 = top and Bottom distance
   return tabButtonSize.Height + 2 * vertButtonSize.Height + 2
      * BT_OFFSET;
}
internal int GetMinHorSize() {
   // 6 = left and right distance of buttons
   return tabButtonSize.Width + 2 * horButtonSize.Width + 2
      * BT_OFFSET;
}

Setting the different buttons' size additionally sets the invisible buttons' size to zero; when you are using the values for calculations, the invisible buttons don't influence the calculation. The following code depends on whether these buttons are arranged on the screen horizontally, vertically, or if only the Center Button is on the screen.

internal void SetVertDockButtonsSize() {
   _buttonRanges[(int)buttonType.Left].Size      = Size.Empty;
   _buttonRanges[(int)buttonType.Right].Size     = Size.Empty;
   _buttonRanges[(int)buttonType.Top].Size       = Size.Empty;
   _buttonRanges[(int)buttonType.Bottom].Size    = Size.Empty;
   _buttonRanges[(int)buttonType.Upper].Size     = vertButtonSize;
   _buttonRanges[(int)buttonType.Lower].Size     = vertButtonSize;
   _buttonRanges[(int)buttonType.LeftSide].Size  = Size.Empty;
   _buttonRanges[(int)buttonType.RightSide].Size = Size.Empty;
   _buttonRanges[(int)buttonType.Center].Size    = tabButtonSize;
}


internal void SetHorDockButtonsSize() {
   _buttonRanges[(int)buttonType.Left].Size      = Size.Empty;
   _buttonRanges[(int)buttonType.Right].Size     = Size.Empty;
   _buttonRanges[(int)buttonType.Top].Size       = Size.Empty;
   _buttonRanges[(int)buttonType.Bottom].Size    = Size.Empty;
   _buttonRanges[(int)buttonType.Upper].Size     = Size.Empty;
   _buttonRanges[(int)buttonType.Lower].Size     = Size.Empty;
   _buttonRanges[(int)buttonType.LeftSide].Size  = horButtonSize;
   _buttonRanges[(int)buttonType.RightSide].Size = horButtonSize;
   _buttonRanges[(int)buttonType.Center].Size    = tabButtonSize;
}

internal void SetOnlyCenterButtonSize() {
   _buttonRanges[(int)buttonType.Left].Size      = Size.Empty;
   _buttonRanges[(int)buttonType.Right].Size     = Size.Empty;
   _buttonRanges[(int)buttonType.Top].Size       = Size.Empty;
   _buttonRanges[(int)buttonType.Bottom].Size    = Size.Empty;
   _buttonRanges[(int)buttonType.Upper].Size     = Size.Empty;
   _buttonRanges[(int)buttonType.Lower].Size     = Size.Empty;
   _buttonRanges[(int)buttonType.LeftSide].Size  = Size.Empty;
   _buttonRanges[(int)buttonType.RightSide].Size = Size.Empty;
   _buttonRanges[(int)buttonType.Center].Size    = tabButtonSize;
}

The following Properties return the Positions of the different buttons that you have calculated in the earlier mentioned methods.

#region Properties (DockAdministration)
//. . . 
internal Point LeftSideButtonPosition {
   get {
      return _buttonRanges[(int)buttonType.LeftSide].Location;
   }
}
internal Point RightSideButtonPosition {
   get {
      return _buttonRanges[(int)buttonType.RightSide].Location;
   }
}
internal Point UpperButtonPosition {
   get {
      return _buttonRanges[(int)buttonType.Upper].Location;
   }
}
internal Point LowerButtonPosition {
   get {
      return _buttonRanges[(int)buttonType.Lower].Location;
   }
}
internal Point CenterButtonPosition {
   get {
      return _buttonRanges[(int)buttonType.Center].Location;
   }
}
//. . . 
#endregion

For the basic Docking actions, you had DockingButtonsToTop. In the advanced docking method, you call PanelButtonsToTop(); this corrects the z-order of these panels.

#region Private Methods (DockingManager)
//. . . 
private void PanelButtonsToTop() {
   UpperButton.TopMost     = true;
   LowerButton.TopMost     = true;
   CenterButton.TopMost    = true;
   LeftSideButton.TopMost  = true;
   RightSideButton.TopMost = true;
}
//. . . 
#endregion

Now, you need to add the missing code for the advanced docking feature.

If you look at the full code of this method now, I have shown you different times how such calculations could be understood by using a simple drawing. I think you will easily be able to follow the following calculations this way.

#region internal Methods (DockAdministration)
//. . . 
internal void
   CalculateDockingButtonPositions(ShowControlEventArgs e) {
   // now we need to get the actual size of the inner window 
   // which is the size reduced by visible menues and toolbars
   if (e.InsidePanel) {
      DockingControler ctl = CalcInnerClientRect(e);
      if (ctl != null) {
         int topButtsHorLeft = _innerClientScreenRectangle.Left +
            (_innerClientScreenRectangle.Width -
             _buttonRanges[(int)buttonType.Upper].Width -
             ctl.Padding.Right - ctl.Padding.Left) / 2;
         int botButtsHorLeft = topButtsHorLeft;
         int centButtsLeft = _innerClientScreenRectangle.Left +
            (_innerClientScreenRectangle.Width -
             _buttonRanges[(int)buttonType.Center].Width -
             ctl.Padding.Right - ctl.Padding.Left) / 2;
         int topButtsTop = _innerClientScreenRectangle.Top
            + BT_OFFSET;
         int rightButtsLeft = _innerClientScreenRectangle.Left +
            _innerClientScreenRectangle.Width -
            _buttonRanges[(int)buttonType.LeftSide].Width
               - BT_OFFSET;
         int leftButtsTop = _innerClientScreenRectangle.Top +
            (_innerClientScreenRectangle.Height -
             _buttonRanges[(int)buttonType.LeftSide].Height -
             ctl.Padding.Bottom - ctl.Padding.Top) / 2;
         int rightButtsTop = leftButtsTop;
         // from border to button = 6
         int leftButtsLeft = _innerClientScreenRectangle.Left
            + BT_OFFSET;
         int bottButtsTop = _innerClientScreenRectangle.Top +
            _innerClientScreenRectangle.Height -
            _buttonRanges[(int)buttonType.Upper].Height -
            ctl.Padding.Bottom - BT_OFFSET;
         int centButtsTop = _innerClientScreenRectangle.Top +
            (_innerClientScreenRectangle.Height -
             _buttonRanges[(int)buttonType.Center].Height -
             ctl.Padding.Bottom - ctl.Padding.Top) / 2;
         _buttonRanges[(int)buttonType.Upper].Location =
            new Point(topButtsHorLeft, topButtsTop);
         _buttonRanges[(int)buttonType.LeftSide].Location =
            new Point(leftButtsLeft, leftButtsTop);
         _buttonRanges[(int)buttonType.RightSide].Location =
            new Point(rightButtsLeft, rightButtsTop);
         _buttonRanges[(int)buttonType.Lower].Location =
            new Point(botButtsHorLeft, bottButtsTop);
         _buttonRanges[(int)buttonType.Center].Location =
            new Point(centButtsLeft, centButtsTop);
      }
   }else {
      CalculateInnerClientRectangle();
      //in every other case we have this 
      int topButtsHorLeft = _innerClientScreenRectangle.Left +
        (_innerClientScreenRectangle.Width -
         _buttonRanges[(int)buttonType.Top].Width) / 2;
      int topButtsTop = _innerClientScreenRectangle.Top + 6;
      int rightButtsLeft = _innerClientScreenRectangle.Left +
        _innerClientScreenRectangle.Width -
        _buttonRanges[(int)buttonType.Right].Width - 6;
      int rightButtsTop = _innerClientScreenRectangle.Top +
        (_innerClientScreenRectangle.Height -
         _buttonRanges[(int)buttonType.Right].Height) / 2;
      int leftButtsTop = rightButtsTop;
      int leftButtsLeft = _innerClientScreenRectangle.Left + 6;
      int botButtsHorLeft = topButtsHorLeft;
      int bottButtsTop = _innerClientScreenRectangle.Top +
        _innerClientScreenRectangle.Height -
        _buttonRanges[(int)buttonType.Bottom].Height - 6;

      _buttonRanges[(int)buttonType.Top].Location =
        new Point(topButtsHorLeft, topButtsTop);
      _buttonRanges[(int)buttonType.Left].Location =
        new Point(leftButtsLeft, leftButtsTop);
      _buttonRanges[(int)buttonType.Right].Location =
        new Point(rightButtsLeft, rightButtsTop);
      _buttonRanges[(int)buttonType.Bottom].Location =
        new Point(botButtsHorLeft, bottButtsTop);
   }
}

Compiling at this point allows you to show the advanced docking buttons on the screen.

Creating a DockablePanel-Controlmanager Using C#, Part 5

Next, you will have to add the needed code to finish your preview.

private void ShowHideDockingPreview(ControlDockedEventArgs e) {
   //. . . 
   switch (e.Hitted) {
      //. . . after the existing statements we add

      case HitState.hoverUpper:
         DockingPreviewRectangle = new TransparentForm();
         DockingPreviewRectangle.Width =
            _innerClientScreenRectangle.Width;
         DockingPreviewRectangle.Height =
            _innerClientScreenRectangle.Height/2;
         // define the right upper corner of the transparent Form
         frameLeft = _innerClientScreenRectangle.Left;
         frameTop = _innerClientScreenRectangle.Top;
         break;
      case HitState.hoverLower:
         DockingPreviewRectangle = new TransparentForm();
         DockingPreviewRectangle.Width =
            _innerClientScreenRectangle.Width;
         DockingPreviewRectangle.Height =
            _innerClientScreenRectangle.Height / 2;
         // define the right upper corner of the transparent Form
         frameLeft = _innerClientScreenRectangle.Left;
         frameTop = _innerClientScreenRectangle.Bottom -
            _innerClientScreenRectangle.Height / 2;
         break;
      case HitState.hoverCenter:
         DockingPreviewRectangle = new TransparentForm();
         DockingPreviewRectangle.Width =
            _innerClientScreenRectangle.Width;
         DockingPreviewRectangle.Height =
            _innerClientScreenRectangle.Height;
         // define the right upper corner of the transparent Form
         frameLeft = _innerClientScreenRectangle.Left;
         frameTop = _innerClientScreenRectangle.Top;
         break;
      case HitState.hoverLeftSide:
         DockingPreviewRectangle = new TransparentForm();
         DockingPreviewRectangle.Width =
            _innerClientScreenRectangle.Width / 2;
         DockingPreviewRectangle.Height =
            _innerClientScreenRectangle.Height;
         // define the right upper corner of the transparent Form
         frameLeft = _innerClientScreenRectangle.Left;
         frameTop = _innerClientScreenRectangle.Top;
         break;
      case HitState.hoverRightSide:
         DockingPreviewRectangle = new TransparentForm();
         DockingPreviewRectangle.Width =
            _innerClientScreenRectangle.Width / 2;
         DockingPreviewRectangle.Height =
            _innerClientScreenRectangle.Height;
         // define the right upper corner of the transparent Form
         frameLeft = _innerClientScreenRectangle.Left +
            _innerClientScreenRectangle.Width / 2;
         frameTop = _innerClientScreenRectangle.Top;
         break;
      //... followed by the already existing code
      default:
         // this includes hitState.none
         KillDockingPreview();
         return;
   }
   // we first need to show  the transparent form
   // afterwards we can move it to its position
   DockingPreviewRectangle.Show();
   DockingPreviewRectangle.Left = frameLeft;
   DockingPreviewRectangle.Top = frameTop;
   DockingPreviewRectangle.Visible = true;
   // for getting a current view of whats underlying the transparent
   // form without refreshing is may happen that some strange
   // background is shown in the transparent Form as it was created
   // in a position of -30000 /-30000
   // so we add here
   DockingPreviewRectangle.Refresh();
}

Conclusion

You now are able to show the docking preview Rectangle for basic docking as well as for advanced docking actions. Also, your buttons are shown depending on where your mouse is on the screen. Next time, you will do the methods needed to dock the panels in advanced docking mode and all the methods needed to change from one docking method to another one.

I hope you enjoyed this article, and you will see me again in the next article!

Credits

The flowchart in Figure 2 was done using Visustin V5 Flowchart generator to create the chart directly out of the given code. I'm sorry about the original high quality is reduced by compressing the screenshot.



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

Downloads

Comments

  • There are no comments yet. Be the first to comment!

Leave a Comment
  • Your email address will not be published. All fields are required.

Top White Papers and Webcasts

  • Live Event Date: December 11, 2014 @ 1:00 p.m. ET / 10:00 a.m. PT Market pressures to move more quickly and develop innovative applications are forcing organizations to rethink how they develop and release applications. The combination of public clouds and physical back-end infrastructures are a means to get applications out faster. However, these hybrid solutions complicate DevOps adoption, with application delivery pipelines that span across complex hybrid cloud and non-cloud environments. Check out this …

  • Relying on outside companies to manage your network and server environments for your business and applications to meet the needs and demands of your users can be stressful. This is especially true as many Managed Hosting organizations fail to meet their service level agreements. Read this Forrester total economic impact report and learn what makes INetU different and how they exceed their customers' managed hosting expectations.

Most Popular Programming Stories

More for Developers

RSS Feeds