Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Collapsible Split Container

0.00/5 (No votes)
20 Sep 2014 1  
A collapsible split container control for Windows Forms

Introduction

The Collapsible SplitContainer control enhances the Windows Forms SplitContainer by providing a region on the movable bar that collapses the associated panel when one of two buttons is clicked. You customize the button region by selecting a button style and, when the selected style is Image or PushButton, by importing a user-supplied image either in the Visual Studio designer or during runtime.

When a left-oriented image is loaded into the control, the other three collapse directions (right, up, and down) are automatically generated. In addition, the splitter width automatically adjusts to fit the size of the image.

Multiple visual styles are available for the splitter buttons, ranging from basic image display to functional pushbuttons with images. Buttons can be located at the left/top, center, or right/bottom of the splitter bar. The ScrollBar option of the button style property uses the Windows Forms ScrollBarRenderer class to generate scrollbar buttons on the splitter.

A few bits of custom code add functionality or correct quirks inherent in the native Windows Forms SplitContainer control, such as controlling flickering, correcting the inability of the native control to make bitmap format background images transparent, and adding the ability to hide the focus rectangle.

Using the Control

The class name is CollapsibleSplitContainer. The control is installable as a Toolbox item that can be dropped onto your Windows Form. Alternatively, the control can be created programmatically by instantiating the CollapsibleSplitContainer in your code.

Properties

Settable properties are grouped in the Collapsible section of the control's property window in the Visual Studio designer when the control is installed as a Toolbox item.

  • SplitterButtonBitmap - The bitmap used on the splitter pushbuttons. It is set via the LoadImage method. Bmp, gif, ico, and png formats are supported (see method description below).
  • SplitterButtonStyle - Visual style applied to the splitter control. PushButton and ScrollBar styles support hot highlights on mouse hover. The picture below shows a few examples of button images and styles.
    • None - Standard splitter control
    • Image - User supplied image is shown on splitter
    • PushButton - Functional buttons using image as background
    • ScrollBar - ScrollBar-style arrow buttons

  • SplitterButtonPosition - Where the collapse buttons are located on the splitter
    • TopLeft, Center, BottomRight
  • SplitterCollapseDistance - How completely the affected panel collapses
    • Collapsed - Only a single pane is visible
    • MinSize - Affected pane collapses to splitter minimum size property
  • SplitterFocusHide - Whether the focus rectangle is shown or hidden. Base class processing still activates the halftone hatch pattern that is shown when the splitter is dragged. There is no way to disable the drag crosshatch without a major rewrite of the base class.

LoadImage Method

Displays a file open dialog so user can select the image used for the buttons. Supported image types include bitmap, png, gif, and icon. The initial image must be left oriented and will be used to create arrows for the 3 other directions. The splitter width and button size are set automatically to accommodate the loaded image's size.

Best visual appeal for pushbutton style is obtained when the image is between 12 and 32 pixels wide and high and there are 3 pixels of clearance on all sides of the arrow to accommodate the button borders. See the Resources folder of the demo project for sample button images.

Drawing Routines

There are two primary drawing functions that display the buttons on the splitter bar. In addition, a support routine that handles drawing a resized focus rectangle around the buttons is provided.

DrawSplitterBackground

Fills the splitter background with the background color and, if available, a background image.

// Fill the splitter background with the background color and image
private void DrawSplitterBackground(Graphics g)
{
	Color backcolor = this.BackColor;
	if (backcolor == Color.Transparent)
	{
		// Find the base color that underlies transparency
		Control parent = this.Parent;
		while (parent.BackColor == Color.Transparent)
		{
			parent = parent.Parent;
		}
		backcolor = parent.BackColor;
	}

	// Paint the background with the underlying background color
	using (SolidBrush brush = new SolidBrush(backcolor))
	{
		g.FillRectangle(brush, this.SplitterRectangle);
	}

	// Draw the background image if present
	if (this.BackgroundImage != null)
	{
		// Use a texture brush to replicate base class tiling
		using (TextureBrush brush = new TextureBrush(this.BackgroundImage))
		{
			g.FillRectangle(brush, this.SplitterRectangle);
		}
	}
}

DrawSplitterButtons

Renders the splitter buttons based on system capability and button style. Uses DrawImage, ButtonRenderer, or ScrollbarRenderer to draw the buttons depending upon SplitterButtonStyle. ScrollBar button states are mapped to Push Button button states to reduce complexity.

// Render the splitter buttons based on system capability and button style
private void DrawSplitterButtons(Graphics g)
{
	if (splitterButtonStyle == ButtonStyle.Image)
	{
		if (!panel1Minimized)
		{
			if (splitterVertical) { g.DrawImage(splitterButtonBitmap, rectLeftDown); }
			else { g.DrawImage(bitmapUp, rectRightUp); }
		}

		if (!panel2Minimized)
		{
			if (splitterVertical) { g.DrawImage(bitmapRight, rectRightUp); }
			else { g.DrawImage(bitmapDown, rectLeftDown); }
		}
	}
	else if (splitterButtonStyle == ButtonStyle.PushButton)
	{
		// Map ScrollBarArrowButtonStates to PushButtonStates
		PushButtonState pbs1 = (PushButtonState)((int)button1State & 3);
		PushButtonState pbs2 = (PushButtonState)((int)button2State & 3);

		if (!panel1Minimized)
		{
			if (splitterVertical) { ButtonRenderer.DrawButton(g, rectLeftDown, pbs1); }
			else { ButtonRenderer.DrawButton(g, rectRightUp, pbs1); }

			if (splitterButtonBitmap != null)
			{
				if (splitterVertical) { g.DrawImage(splitterButtonBitmap, rectLeftDown); }
				else { g.DrawImage(bitmapUp, rectRightUp); }
			}
		}

		if (!panel2Minimized)
		{
			if (splitterVertical) { ButtonRenderer.DrawButton(g, rectRightUp, pbs2); }
			else { ButtonRenderer.DrawButton(g, rectLeftDown, pbs2); }

			if (splitterButtonBitmap != null)
			{
				if (splitterVertical) { g.DrawImage(bitmapRight, rectRightUp); }
				else { g.DrawImage(bitmapDown, rectLeftDown); }
			}
		}
	}
	else if (ScrollBarRenderer.IsSupported && splitterButtonStyle == ButtonStyle.ScrollBar)
	{
		if (!panel1Minimized)
		{
			if (splitterVertical) { ScrollBarRenderer.DrawArrowButton(g, rectLeftDown, button1State); }
			else{  ScrollBarRenderer.DrawArrowButton(g, rectRightUp, button1State); }
		}

		if (!panel2Minimized)
		{
			if (splitterVertical) { ScrollBarRenderer.DrawArrowButton(g, rectRightUp, button2State); }
			else { ScrollBarRenderer.DrawArrowButton(g, rectLeftDown, button2State); }
		}
	}
}

DrawSplitterFocus

Redimensions and draws the focus rectangle with space to accommodate the splitter buttons. Note the use of the DrawFocusRectangle function from ControlPaint class. ControlPaint contains a plethora of useful drawing routines for rendering controls.

// Draw the modified focus rectangle if focus is not hidden
private void DrawSplitterFocus(Graphics g)
{
	if (splitterButtonStyle == ButtonStyle.None) return;

	if (this.Focused && !splitterFocusHide)
	{
		Rectangle focus = new Rectangle(this.SplitterRectangle.Location, this.SplitterRectangle.Size);

		// Draw the focus rectangle to the left/top of the buttons
		if (splitterVertical) { focus.Height = rectLeftDown.Top; }
		else { focus.Width = rectLeftDown.Left; }
		focus.Inflate(-1, -1);
		ControlPaint.DrawFocusRectangle(g, focus, this.ForeColor, this.BackColor);

		// Draw the focus rectangle to the right/bottom of the buttons
		if (splitterVertical)
		{
			focus.Location = new Point(rectRightUp.Left, rectRightUp.Bottom);
			focus.Size = new Size(rectRightUp.Width, this.SplitterRectangle.Bottom - rectRightUp.Bottom);
		}
		else
		{
			focus.Location = new Point(rectRightUp.Right + 1, rectRightUp.Top);
			focus.Size = new Size(this.SplitterRectangle.Right - rectRightUp.Right - 1, 
                         rectRightUp.Height);
		}
		focus.Inflate(-1, -1);
		ControlPaint.DrawFocusRectangle(g, focus, this.ForeColor, this.BackColor);
	}
}

Overridden Event Handlers

A substantial portion of all the code in CollapsibleSplitContainer deals with housekeeping measures, such as keeping track of button orientation and button hover/activation. Several base class event handlers are overridden to add button functionality to the control.

OnLayout

Forces redraw of the splitter after a property change that affects the control's layout.

OnPaint

Draws the splitter and, if enabled, the buttons.

OnKeyUp, OnMouseMove, etc.

Handle keyboard and mouse actions. Overridden to add tests for button style, orientation, and the like.

OnBackgroundImageChanged

Adds image transparency for background bitmaps. The base class supports it for PNG and GIF but not bitmap.

protected override void OnBackgroundImageChanged(EventArgs e)
{
	base.OnBackgroundImageChanged(e);

	// Add image transparency for bitmap background images. Base class
	// supports it for PNG and GIF but not bitmap format
	if (this.BackgroundImage != null)
	{
		((Bitmap)this.BackgroundImage).MakeTransparent();
		this.Refresh();
	}
}

Points of Interest

The SplitContainer has a few quirks and bugs. This adaptation of the control corrects some of them. For example, the following code helps reduce the native control's flicker and resize problems:

public CollapsibleSplitContainer()
{
	// Bug fix for SplitContainer problems with flickering and resizing
	ControlStyles cs = ControlStyles.ResizeRedraw | ControlStyles.AllPaintingInWmPaint | 
                       ControlStyles.UserPaint | ControlStyles.OptimizedDoubleBuffer;
	this.SetStyle(cs, true);
	object[] objArgs = new object[] { cs, true };
	MethodInfo objMethodInfo = typeof(Control).GetMethod
                               ("SetStyle", BindingFlags.NonPublic | BindingFlags.Instance);
	objMethodInfo.Invoke(this.Panel1, objArgs);
	objMethodInfo.Invoke(this.Panel2, objArgs);
}

Known Issue: When switching from vertical to horizontal orientation and back, the position of the splitter bar may differ from what you expect because the width and height dimensions are used in the calculation and affect where the splitter bar is positioned.

History

  • 9/19/2014: V1.0. Initial release

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here