Four-Way Splitter Control

Hi, for some time now I wanted a control that would allow me to have four panels separated with splitters which could be resized horizontally, vertically or all at the same time and that didn’t redraw the panels when resizing. I searched for a solution but didn’t find one that fitted my needs either because it would redraw the panels or it wouldn’t draw a shadow splitter like the default behaviour of the windows’ splitters, thus, I decided to make one.

THE IMPLEMENTATION

Since the normal splitter control from the .Net framework always draws the shadow splitter when you try to move it, I had to make a splitter that wouldn’t do this since I didn’t want it to happen in certain occasions. So this SimpleSplitter derives from the System.Windows.Forms.Splitter and overrides some methods that would draw the shadow splitter: OnMouseDown, OnMouseUp, OnSplitterMoved and OnSplitterMoving but I wanted to still be able to catch these events so what I did was to call the base event but changing the number of clicks because in the base class (the Splitter) will only draw the shadow splitter if the number of clicks in the splitter is one (Thanks to Hans Passant):

Simple Splitter code:

protected override void OnMouseDown(MouseEventArgs e)
{
    MouseEventArgs ebase = new MouseEventArgs(e.Button, 2, e.X, e.Y, e.Delta);
    base.OnMouseDown(ebase);
}
protected override void OnMouseUp(MouseEventArgs e)
{
    MouseEventArgs ebase = new MouseEventArgs(e.Button, 2, e.X, e.Y, e.Delta);
    base.OnMouseUp(ebase);
}
protected override void OnSplitterMoved(SplitterEventArgs e)
{
}
protected override void OnSplitterMoving(SplitterEventArgs e)
{
}

Other possible solution was was to create new events to catch these: MouseDown2 and MouseUp2:

public delegate void MouseEventHandler(object sender, MouseEventArgs e);
public event MouseEventHandler MouseDown2;
public event MouseEventHandler MouseUp2;
protected override void OnMouseDown(MouseEventArgs e)
{
    if (MouseDown2 != null)
        MouseDown2(this, e);
}
protected override void OnMouseUp(MouseEventArgs e)
{
    if (MouseUp2 != null)
        MouseUp2(this, e);
}

Next, I’ll describe the algorithm that defines this control works.

  • Event handlers for MouseDown/MouseUp/MouseMove/MouseEnter/MouseLeave events for every splitter.
  • The MouseDown handlers start by checking the mouse button (only the left button is accepted) and check if the cursor is in the middle (function MouseInCenter).
private bool MouseInCenter(object sender)
{
  Point p = this.PointToClient(MousePosition);
  // are we in the intersection?
  if ((Math.Abs(splitterHorLeft.SplitPosition - p.Y) < m_centerDelta &&
    Math.Abs(splitterVertical.SplitPosition - p.X) < m_centerDelta))
  {
    // yup, let's change the cursor to SizeAll.
    Cursor = Cursors.SizeAll;
    return true;
  }
  else
  {
    // Let's see to which cursor we have to change depending on the splitter we're on.
    Splitter splitter = sender as Splitter;
    if (splitter != null)
    {
      if (splitter.Dock == DockStyle.Left || splitter.Dock == DockStyle.Right)
      {
        Cursor = Cursors.VSplit;
      }
      else if (splitter.Dock == DockStyle.Top || splitter.Dock == DockStyle.Bottom)
      {
        Cursor = Cursors.HSplit;
      }
    }
    return false;
  }
}

This function is easy to understand (as any other function in this code), converts the mouse coordinates to the control’s local coordinates and checks if we’re not apart from the center more than a delta distance. If this is not the case, checks if we’re on top of any splitter and if so, change the cursor to the right shape.

  • Save the contents of the panels so we can restore them when we move the splitter(s) (function SaveGraphicContents).
private void SaveGraphicContents()
{
  // save the graphics contents before we move the splitter
  for (int i = 0; i < m_numPanels; i++)
  {
    // save top panels
    using (Graphics graphics = topPanels[i].CreateGraphics())
    {
      topImgs[i] = new Bitmap(topPanels[i].ClientSize.Width, topPanels[i].ClientSize.Height, graphics);
      Graphics g = Graphics.FromImage(topImgs[i]);
      IntPtr hdc = graphics.GetHdc();
      IntPtr memdc = g.GetHdc();
      BitBlt(memdc, 0, 0, topPanels[i].ClientSize.Width, topPanels[i].ClientSize.Height, hdc, 0, 0,
        RasterOperations.SRCCOPY);
      graphics.ReleaseHdc(hdc);
      g.ReleaseHdc(memdc);
    }
    // save bottom panels
    using (Graphics graphics = bottomPanels[i].CreateGraphics())
    {
      bottomImgs[i] = new Bitmap(bottomPanels[i].ClientSize.Width, bottomPanels[i].ClientSize.Height,
        graphics);
      Graphics g = Graphics.FromImage(bottomImgs[i]);
      IntPtr hdc = graphics.GetHdc();
      IntPtr memdc = g.GetHdc();
      BitBlt(memdc, 0, 0, bottomPanels[i].ClientSize.Width, bottomPanels[i].ClientSize.Height, hdc, 0, 0,
        RasterOperations.SRCCOPY);
      graphics.ReleaseHdc(hdc);
      g.ReleaseHdc(memdc);
    }
  }
}

Here a bitmap is created for each panel and the panel’s surface is copied to the bitmap through the function BitBlt. Notice that this function is not available in .Net so we have to access it through the unmanaged dll gdi32.dll using DllImport.

  • Still in the MouseDown handlers, we hide the child controls of the panels. This is done because if we didn’t hide the controls, the shadow splitters would get drawn under the controls and we wouldn’t see the shadow splitters (Thanks to David Júlio):
private void ShowHidePanelContents(bool show)
{
  for (int i = 0; i < MAX_NUM_PANELS; i++)
  {
    foreach(Control c in m_topPanels[i].Controls)
    {
      c.Visible = show;
    }
    foreach (Control c in m_bottomPanels[i].Controls)
    {
      c.Visible = show;
    }
  }
}

This function goes through every panel in the splitter control and hide all the controls inside each panel. It’s also used to show the controls once we’re done with the drawing.

  • The MouseMove handler is the one that draws the splitters in the right place when we’re moving them. First the old panel’s images are drawn and then auxiliary functions draw a shadow splitter (it’s just a rectangle with a hatchbrush at 50%) in the right place.
  • Finally we have the MouseUp handler that finishes the movement by setting the splitters and their new position, shows the child controls again and refreshes/redraws all panels and, consequently, their child controls.
  • The MouseEnter and MouseLeave handlers only change the shape of the cursor if necessary.

THE DESIGNER

Since we want to be able to add controls to each panel, we have to make those panels available for the designer. First we create a new control designer and then we open the access to the panels by calling the EnableDesignMode function on each panel.

public class FourWaySplitterDesigner : ControlDesigner
{
  public override void Initialize(IComponent comp)
  {
    base.Initialize(comp);
    FourWaySplitter usercontrol = (FourWaySplitter)comp;
    EnableDesignMode(usercontrol.TopLeftPanel, "TopLeftPanel");
    EnableDesignMode(usercontrol.TopRightPanel, "TopRightPanel");
    EnableDesignMode(usercontrol.BottomLeftPanel, "BottomLeftPanel");
    EnableDesignMode(usercontrol.BottomRightPanel, "BottomRightPanel");
  }
}

And that’s it! Pretty simple uh?! 🙂
Keep in mind that with time, I’ll add more functionalities to this control, most of them will be improve its behaviour in the VS designer so, keep checking this page for updates.

You’re free to use this code as long as you don’t blame me for any changes/bugs that were introduced and don’t demand anything from me. Just download it here if you accept these conditions.

Any bugs/questions/comments/suggestions are always welcome. Enjoy! 😉

Changelog:

v0.1.2

  • Changed the way how we handle the OnMouseDown and OnMouseUp events. (Thanks to Hans Passant.)
  • Now shadow splitters are drawn even if the panels in the control contain any controls inside them. (Thanks to David Júlio.)
  • Fixed bug in the designer that didn’t add other controls to the splitter panels.

References:

http://msdn.microsoft.com – MSDN Home Page

Tagged , , , , , . Bookmark the permalink.

Leave a Reply

Your email address will not be published. Required fields are marked *