Docking Control in C# That Can Be Dragged and Resized

Environment: .NET Beta 2 SDK

Introduction

One of the first features of C# that took my interest was the ability to dock a control onto the edge of a form. Now I could attach a control (or more likely a composite control by deriving from UserControl) onto a Form edge and quickly construct a useful looking application. But there is one crucial factor missing from this scenario. The user has no discretion over the size or positioning of this docked control. I want the user to be able to drag the control to a different edge and be able to resize the control so that they can customise the application area to suit their own preferences.

Composite Control for Docking

To solve this problem we need to create a new composite control DockingControl that is able to take a caller supplied control and manage its position and sizing. Our composite control will need a resizing bar; a grab handle area that can be used to move its docking position and a place for the caller supplied control to be displayed.

class DockingControl : UserControl
{
  private Form _form;
  private DockingResize _resize;
  private DockingHandle _handle;
  private BorderControl _wrapper;
  
  public DockingControl( Form form, 
                         DockStyle ds, 
                         Control userControl)
  {
     // Remember the form we are hosted on  
     _form = form;

     // Create the resizing bar, gripper handle and 
     // border control
     _resize = new DockingResize(ds);
     _handle = new DockingHandle(this, ds);
     _wrapper = new BorderControl(userControl);

     // Wrapper should always fill remaining area
     _wrapper.Dock = DockStyle.Fill;

     // Define our own initial docking position for 
     // when we are added to host form
     this.Dock = ds;

     Controls.AddRange(new Control[]{ _wrapper, 
                                      _handle, 
                                      _resize});
  }

  public Form HostForm { get { return _form; } }

The final line of code in the instance constructor adds the three child controls _wrapper, _handle and _resize. The order of the controls in the initializer list is absolutely crucial because when the DockingControl (or any other Control) has its Dock style changed this ordering determines the position and size of the child controls. Calculations are made starting with the last control added (which equates to last entry in the initializer list) back towards the first, which is the exact opposite of what I would have expected.

The _resize bar is first to be positioned (and so last in initializer list) as it should always be shown spanning the entire length of the docking control. Next is the _handle as it should be positioned under the sizing bar and finally the _wrapper control, this is last because it always has a Dock style of Fill and we want it to take up whatever space is leftover when all the other controls have finished being laid out.

Change of docking position

When the docking position of our composite control is changed we need to ensure that our child controls are also correctly positioned for the new docking style. So we override the inherited Dock property and recalculate the correct size and positions as appropriate.

  // Override the base class property to allow extra work
  public override DockStyle Dock
  {
     get { return base.Dock; }

     set
     {
        // Our size before docking position is changed
        Size size = this.ClientSize;

        // Remember the current docking position
        DockStyle dsOldResize = _resize.Dock;

        // New handle size is dependant on the orientation
        // of the new docking position
        _handle.SizeToOrientation(value);

        // Modify docking position of child controls
        // based on our new docking position
        _resize.Dock = 
              DockingControl.ResizeStyleFromControlStyle(value);
        _handle.Dock = 
              DockingControl.HandleStyleFromControlStyle(value);

        // Now safe to update ourself through base class
        base.Dock = value;

        // Change in orientation occured?
        if (dsOldResize != _resize.Dock)
        {
          // Must update our client size to ensure the 
          // correct size is used when the docking position 
          // changes.  We have to transfer the value that 
          // determines the vector of the control to the 
          // opposite dimension
          if ((this.Dock == DockStyle.Top) || 
              (this.Dock == DockStyle.Bottom))
              size.Height = size.Width;
          else
              size.Width = size.Height;

          this.ClientSize = size;
        }

        // Repaint our controls 
        _handle.Invalidate();
        _resize.Invalidate();
     }
  }

Two static functions (ResizeStyleFromControlStyle and HandleStyleFromControlStyle) are used to find the correct docking style for the _resize and _handle controls dependant on the new Dock style. Of special note is the code that checks for a change in docking orientation and then changes the Width or Height of the control. Remember that when our control is docked to the top or bottom of the form then the Width of the control is calculated for us by the form and the Height determines how far inwards the docking control extends. When the orientation moves to be left or right then we need to update the Width of the control to reflect how far from the edge we want the control to extend. So the new Width should be the old Height, otherwise the Width will remain the same as the entire width of the form and so it would fill the entire client area.

The rest of the DockingControl class follows and consists of static properties for recovering GDI+ objects (to be used by the child controls for drawing) and the previously mentioned static methods used for calculating the new docking position of each child control based on the new position of the DockingControl.

  // Static variables defining colors for drawing
  private static Pen _lightPen = 
      new Pen(Color.FromKnownColor(KnownColor.ControlLightLight));
  private static Pen _darkPen = 
      new Pen(Color.FromKnownColor(KnownColor.ControlDark));
  private static Brush _plainBrush = Brushes.LightGray;

  // Static properties for read-only access to drawing colors
  public static Pen LightPen     { get { return _lightPen;  } }
  public static Pen DarkPen     { get { return _darkPen;  } }
  public static Brush PlainBrush  { get { return _plainBrush; } }

  public static DockStyle ResizeStyleFromControlStyle(DockStyle ds)
  {
     switch(ds)
     {
     case DockStyle.Left:
        return DockStyle.Right;
     case DockStyle.Top:
        return DockStyle.Bottom;
     case DockStyle.Right:
        return DockStyle.Left;
     case DockStyle.Bottom:
        return DockStyle.Top;
     default:
        // Should never happen!
        throw new ApplicationException("Invalid DockStyle argument");
     }
  }

  public static DockStyle HandleStyleFromControlStyle(DockStyle ds)
  {
    switch(ds)
    {
     case DockStyle.Left:
        return DockStyle.Top;
     case DockStyle.Top:
        return DockStyle.Left;
     case DockStyle.Right:
        return DockStyle.Top;
     case DockStyle.Bottom:
        return DockStyle.Left;
     default:
        // Should never happen!
        throw new ApplicationException("Invalid DockStyle argument");
    }
  }
}

Resizing

Our first child control is called DockingResize and provides an area of the docking control that the user can drag for resizing. Notice that when the mouse is clicked the OnMouseDown remembers the current size of the parent DockingControl and the screen position of the mouse. This is necessary so that when the OnMouseMove is received it can calculate how far the mouse has been moved since it was pressed and so the new size of DockingControl. Also notice that it will set the cursor to indicate a resizing operation is allowed.

// A bar used to resize the parent DockingControl
class DockingResize : UserControl
{
  // Class constants
  private const int _fixedLength = 4;

  // Instance variables
  private Point _pointStart;
  private Point _pointLast;
  private Size _size;

  public DockingResize(DockStyle ds)
  {
     this.Dock = DockingControl.ResizeStyleFromControlStyle(ds);
     this.Size = new Size(_fixedLength, _fixedLength);
  }  

  protected override void OnMouseDown(MouseEventArgs e)
  {
     // Remember the mouse position and client size when
     // capture occured
     _pointStart = 
            _pointLast = PointToScreen(new Point(e.X, e.Y));
     _size = Parent.ClientSize;

     // Ensure delegates are called
     base.OnMouseDown(e);
  }

  protected override void OnMouseMove(MouseEventArgs e)
  {
     // Cursor depends on if we a vertical or horizontal resize
     if ((this.Dock == DockStyle.Top) || 
        (this.Dock == DockStyle.Bottom))
        this.Cursor = Cursors.HSplit;
     else
        this.Cursor = Cursors.VSplit;

     // Can only resize if we have captured the mouse
     if (this.Capture)
     {
        // Find the new mouse position
        Point point = PointToScreen(new Point(e.X, e.Y));

        // Have we actually moved the mouse?
        if (point != _pointLast)
        {
          // Update the last processed mouse position
          _pointLast = point;

          // Find delta from original position
          int xDelta = _pointLast.X - _pointStart.X;
          int yDelta = _pointLast.Y - _pointStart.Y;

          // Resizing from bottom or right of form means 
          // inverse movements
          if ((this.Dock == DockStyle.Top) || 
             (this.Dock == DockStyle.Left))
          {
             xDelta = -xDelta;
             yDelta = -yDelta;
          }

          // New size is original size plus delta
          if ((this.Dock == DockStyle.Top) || 
             (this.Dock == DockStyle.Bottom))
             Parent.ClientSize = new Size(_size.Width,
                                          _size.Height + yDelta);
          else
             Parent.ClientSize = new Size(_size.Width + xDelta,
                                          _size.Height);

          // Force a repaint of parent so we can see 
          // changed appearance
          Parent.Refresh();
        }
     }

     // Ensure delegates are called
     base.OnMouseMove(e);
  }

The only other work needed in this class is the override of OnPaint that is used to draw the 3D appearance of the resizing bar itself. It uses static methods from the DockingControl to recover the correct GDI+ objects to use.

  protected override void OnPaint(PaintEventArgs pe)
  {
     // Create objects used for drawing
     Point[] ptLight = new Point[2];
     Point[] ptDark = new Point[2];
     Rectangle rectMiddle = new Rectangle();

     // Drawing is relative to client area
     Size sizeClient = this.ClientSize;

     // Painting depends on orientation
     if ((this.Dock == DockStyle.Top) || 
        (this.Dock == DockStyle.Bottom))
     {
        // Draw as a horizontal bar
        ptDark[1].Y = ptDark[0].Y = sizeClient.Height - 1;
        ptLight[1].X = ptDark[1].X = sizeClient.Width;
        rectMiddle.Width = sizeClient.Width;
        rectMiddle.Height = sizeClient.Height - 2;
        rectMiddle.X = 0;
        rectMiddle.Y = 1;
     }
     else if ((this.Dock == DockStyle.Left) || 
           (this.Dock == DockStyle.Right))
     {
        // Draw as a vertical bar
        ptDark[1].X = ptDark[0].X = sizeClient.Width - 1;
        ptLight[1].Y = ptDark[1].Y = sizeClient.Height;
        rectMiddle.Width = sizeClient.Width - 2;
        rectMiddle.Height = sizeClient.Height;
        rectMiddle.X = 1;
        rectMiddle.Y = 0;
     }

     // Use colors defined by docking control that is using us
     pe.Graphics.DrawLine(DockingControl.LightPen, 
                          ptLight[0],
                          ptLight[1]);
     pe.Graphics.DrawLine(DockingControl.DarkPen,
                          ptDark[0],
                          ptDark[1]);
     pe.Graphics.FillRectangle(DockingControl.PlainBrush,
                               rectMiddle);

     // Ensure delegates are called
     base.OnPaint(pe);
  }
}

Dragging

Our next child control DockingHandle has three tasks to perform. It must first of all ensure that it is sized correctly to reflect the current orientation of the parent DockingControl. One of our dimensions will always be calculated determined for us as we are docked to one of the parent control edges. However, the other dimension should always be fixed to reflect the space needed for drawing and allowing the user to grab it. The routine SizeToOrientation performs this decision.

class DockingHandle : UserControl 
{
  // Class constants
  private const int _fixedLength = 12;
  private const int _hotLength = 20;
  private const int _offset = 3;
  private const int _inset = 3;

  // Instance variables
  private DockingControl _dockingControl = null;

  public DockingHandle(DockingControl dockingControl, 
                       DockStyle ds)
  {
     _dockingControl = dockingControl;
     this.Dock = DockingControl.HandleStyleFromControlStyle(ds);
     SizeToOrientation(ds);
  }  

  public void SizeToOrientation(DockStyle ds)
  {
     if ((ds == DockStyle.Top) || (ds == DockStyle.Bottom))
        this.ClientSize = new Size(_fixedLength, 0);
     else
        this.ClientSize = new Size(0, _fixedLength);
  }

The second task and the most interesting is performed inside OnMouseMove. Here we need to convert the mouse position from our own client position to the client position in the host form. By testing how near the cursor is to each edge of the form we can decide which edge should become the new docking position of the parent DockingControl. At the moment the code uses a constant value of _hotLength to decide if the mouse is close enough to an edge for the docking edge to be changed. Actually causing the docking to change is trivial, just change the Dock property on the DockingControl.

protected override void OnMouseMove(MouseEventArgs e)
{
   // Can only move the DockingControl is we have captured the
   // mouse otherwise the mouse is not currently pressed
   if (this.Capture)
   {
      // Must have reference to parent object
      if (null != _dockingControl)
      {
        this.Cursor = Cursors.Hand;

        // Convert from client point of DockingHandle to 
        // client of DockingControl
        Point screenPoint = PointToScreen(new Point(e.X, e.Y));
        Point parentPoint = 
            _dockingControl.HostForm.PointToClient(screenPoint);

        // Find the client rectangle of the form
        Size parentSize = _dockingControl.HostForm.ClientSize;

        // New docking position is defaulted to current style
        DockStyle ds = _dockingControl.Dock;

        // Find new docking position
        if (parentPoint.X < _hotLength)
        {
           ds = DockStyle.Left;
        }
        else if (parentPoint.Y < _hotLength)
        {
           ds = DockStyle.Top;
        }
        else if (parentPoint.X >= (parentSize.Width - _hotLength))
        {
           ds = DockStyle.Right;
        }
        else if (parentPoint.Y >= (parentSize.Height - _hotLength))
        {
           ds = DockStyle.Bottom;
        }

        // Update docking position of DockingControl we are part of
        if (_dockingControl.Dock != ds)
           _dockingControl.Dock = ds;
      }
   }
   else
      this.Cursor = Cursors.Default;

   // Ensure delegates are called
   base.OnMouseMove(e);
}

Lastly the control needs to draw the two lines that decorate the control area.

protected override void OnPaint(PaintEventArgs pe)
{
   Size sizeClient = this.ClientSize;
   Point[] ptLight = new Point[4];
   Point[] ptDark = new Point[4];

   // Depends on orientation
   if ((_dockingControl.Dock == DockStyle.Top) || 
       (_dockingControl.Dock == DockStyle.Bottom))
   {
      int iBottom = sizeClient.Height - _inset - 1;
      int iRight = _offset + 2;

      ptLight[3].X = ptLight[2].X = ptLight[0].X = _offset;
      ptLight[2].Y = ptLight[1].Y = ptLight[0].Y = _inset;
      ptLight[1].X = _offset + 1;
      ptLight[3].Y = iBottom;

      ptDark[2].X = ptDark[1].X = ptDark[0].X = iRight;
      ptDark[3].Y = ptDark[2].Y = ptDark[1].Y = iBottom;
      ptDark[0].Y = _inset;
      ptDark[3].X = iRight - 1;
   }
   else
   {
      int iBottom = _offset + 2;
      int iRight = sizeClient.Width - _inset - 1;

      ptLight[3].X = ptLight[2].X = ptLight[0].X = _inset;
      ptLight[1].Y = ptLight[2].Y = ptLight[0].Y = _offset;
      ptLight[1].X = iRight;
      ptLight[3].Y = _offset + 1;

      ptDark[2].X = ptDark[1].X = ptDark[0].X = iRight;
      ptDark[3].Y = ptDark[2].Y = ptDark[1].Y = iBottom;
      ptDark[0].Y = _offset;
      ptDark[3].X = _inset;
   }

   Pen lightPen = DockingControl.LightPen;
   Pen darkPen = DockingControl.DarkPen;

   pe.Graphics.DrawLine(lightPen, ptLight[0], ptLight[1]);
   pe.Graphics.DrawLine(lightPen, ptLight[2], ptLight[3]);
   pe.Graphics.DrawLine(darkPen, ptDark[0], ptDark[1]);
   pe.Graphics.DrawLine(darkPen, ptDark[2], ptDark[3]);

   // Shift coordinates to draw section grab bar
   if ((_dockingControl.Dock == DockStyle.Top) || 
      (_dockingControl.Dock == DockStyle.Bottom))
   {
      for(int i=0; i<4; i++)
      {
        ptLight[i].X += 4;
        ptDark[i].X += 4;
      }
   }
   else
   {
      for(int i=0; i<4; i++)
      {
        ptLight[i].Y += 4;
        ptDark[i].Y += 4;
      }
   }

   pe.Graphics.DrawLine(lightPen, ptLight[0], ptLight[1]);
   pe.Graphics.DrawLine(lightPen, ptLight[2], ptLight[3]);
   pe.Graphics.DrawLine(darkPen, ptDark[0], ptDark[1]);
   pe.Graphics.DrawLine(darkPen, ptDark[2], ptDark[3]);

   // Ensure delegates are called
   base.OnPaint(pe);
}

Shrink the user supplied control

When developing this code I noticed that placing the user supplied control for the DockingControl into the control would push that control right up against the edges of the resize bar and grab area. Although there is nothing wrong with this it didn't look very tidy, so instead I use this small helper class that places a border around the provided control. The DockingControl creates an instance of this BorderControl passing in the user supplied control. Then this BorderControl is used to fill the DockingControl rather than the user supplied one.

// Position the provided control inside a border to 
// give a portrait picture effect
class BorderControl : UserControl 
{
  // Instance variables
  private int _borderWidth = 3;
  private int _borderDoubleWidth = 6;
  private Control _userControl = null;

  public BorderControl(Control userControl)
  {
     _userControl = userControl;
     Controls.Add(_userControl);
  }  

  // Must reposition the embedded control whenever we change size
  protected override void OnResize(EventArgs e)
  {
     // Can be called before instance constructor
     if (null != _userControl)
     {
        Size sizeClient = this.Size;

        // Move the user control to enforce the border area we want
        _userControl.Location = new Point(_borderWidth, _borderWidth);

        _userControl.Size = new Size(sizeClient.Width - _borderDoubleWidth, 
                           sizeClient.Height - _borderDoubleWidth);
     }

     // Ensure delegates are called
     base.OnResize(e);
  }
}

Conclusion

This code was developed using a text editor and then calling the C# compiler on a command line. Therefore the look is modelled on the appearance of the VC6 environment (which I have) and not the newer look and feel of docking controls/windows from VC7 (which I don't have). I think it would be very easy to build on this code to allow floating controls and multiple docking controls in a docking bar. If you have any ideas or make any changes to the code then feel free to contact me as I would be very interested.

Downloads

Download demo project and source - 8 Kb


Comments

  • How to resize a textbox

    Posted by Legacy on 01/15/2004 12:00am

    Originally posted by: Jane

    I have read your article on "Dockable Control in C#". It's just great! Thank you for your contribution to me and other C# lovers.

    Now I am doing a project in C#. One of it is to make a textbox in a form resizeable. My idea is to define a MouseHover event. The purpose is to locate where the mouse is. If the mouse is located around the borders of the textbox. I would like change the shape of mouse to show that the resize is possible now. When user makes a left mouse down, I would like to remember the new location of mouse, and redraw the text with the new size.

    I use Cursor.Postion to get the location of the mouse. But it is from windows x-y coordination. But textbox.Location is not. Do you know how to communicate between the two coordination systems?

    Your help is very appreciated!

    Jane

    Reply
  • XP style docking version available

    Posted by Legacy on 01/12/2002 12:00am

    Originally posted by: Phil Wright

    I have written a version that allows the look and feel of the Visual Studio .NET IDE. Including docking/floating states, permissions and then ability to host both controls and forms. The source code, sample and tutorial are available for free at...

    http://www.crownwood.net

    Phil Wright
    phil.wright@crownwood.net

    Reply
  • Dockable Tool Windows

    Posted by Legacy on 11/02/2001 12:00am

    Originally posted by: sbayard

    That's great, but...I'm trying to figure out how to copy the functionality of the dockable tool windows in Visual Studio. These windows can exist either as a docked control or as a separate popup window. I've hit a few snags, and I think such an example would be a great help to a lot of people.
    Still, the point of this sample is true: the built in docking capabilities make life easier.

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

Top White Papers and Webcasts

  • Email is the most common communication vehicle used by organizations of all shapes and sizes. Among the billions of email messages sent every day are sensitive information, critical requests, and other essential business data. IT staff bear the burden of ensuring the confidentiality, integrity, and availability of the information contained within the communication. This white paper explores the email security landscape, an assessment of the threats organizations face,  and the building blocks of an effective …

  • Packaged application development teams frequently operate with limited testing environments due to time and labor constraints. By virtualizing the entire application stack, packaged application development teams can deliver business results faster, at higher quality, and with lower risk.

Most Popular Programming Stories

More for Developers

Latest Developer Headlines

RSS Feeds