Introduction
For a long time, I needed a control that can act just like a tab control but without displaying any tabs. This control can be very useful because you can use the powerful forms designer to add many different pages in the same form. Otherwise you are forced to add a different user control for every page, which can be a real pain in the ass.
Unfortunately I didn't find one. In the past I even used a TabControl and pasted an ugly panel above the tabs so they won't be seen. Ugly Ugly Ugly ..
In my final project, I needed one again and I've decided to solve this problem once and for all.
Background
Trying to be as lazy as I can, I've searched the Internet for something that I can copy paste with a minimal number of changes to my new control.
After a few minutes, I found what I was looking for - an article on this very site http://www.codeproject.com/KB/miscctrl/yatabcontrol.aspx written by curtis schlak. This great article provided the basis for my control.
Code Description
The code is divided into several classes:
- The
MultiPanel
control - a very simple class that derives from the Panel
class and holds the collection of pages and the currently selected page instance.
- The
MultiPanelPagesCollection
class - derives from the ControlCollection
class in order to force all contained controls to be of type MultiPanelPage
control.
- The
MultiPanelPage
control - provides the implementation for all multi panel pages. Derives from ContainerControl
in order to act as a container for all controls that we drag into it.
- Two designer classes:
MultiPanelDesigner
needed for designing the MultiPanel
control, and MultiPanelPageDesigner
needed for designing the multipanelpage control (and also drawing the page's Text
property for readability).
As you can see - the MultiPanel
class is very simple. Basically it is a panel that replaces the selected page control based on the value stored in _selectedPage
variable:
[ToolboxBitmap(typeof(MultiPanel), "multipanel")]
[Designer(typeof(Liron.Windows.Forms.Design.MultiPanelDesigner))]
public class MultiPanel : Panel
{
public MultiPanelPage SelectedPage
{
get { return _selectedPage; }
set
{
_selectedPage = value;
if (_selectedPage != null)
{
foreach (Control child in Controls)
{
if (object.ReferenceEquals(child, _selectedPage))
child.Visible = true;
else
child.Visible = false;
}
}
}
}
protected override void OnPaint(PaintEventArgs e)
{
base.OnPaint(e);
Graphics g = e.Graphics;
using (SolidBrush br = new SolidBrush(BackColor))
g.FillRectangle(br, ClientRectangle);
}
protected override ControlCollection CreateControlsInstance()
{
return new MultiPanelPagesCollection(this);
}
private MultiPanelPage _selectedPage;
}
Note that it declares the type of its designer in the class level Designer attribute.
Also note that we've replaced the class used for holding controls by overriding the CreateControlsInstance()
method.
The MultiPanelPage
class is very simple as well. Basically - I've overridden the controls collection class with my own class in order to prevent MultiPanelPage
instance from being inserted into the multi panel pages and I've declared the design class for the page class:
[Designer(typeof(Liron.Windows.Forms.Design.MultiPanelPageDesigner))]
public class MultiPanelPage : ContainerControl
{
public MultiPanelPage()
{
base.Dock = DockStyle.Fill;
}
public override DockStyle Dock
{
get
{
return base.Dock;
}
set
{
base.Dock = DockStyle.Fill;
}
}
public override string Text
{
get
{
return base.Text;
}
set
{
base.Text = value;
}
}
protected override
System.Windows.Forms.Control.ControlCollection CreateControlsInstance()
{
return new MultiPanelPage.ControlCollection(this);
}
#region Classes
public new class ControlCollection : Control.ControlCollection
{
public ControlCollection(Control owner)
: base(owner)
{
if (owner == null)
throw new ArgumentNullException("owner",
"Tried to create a MultiPanelPage.ControlCollection
with a null owner.");
MultiPanelPage c = owner as MultiPanelPage;
if (c == null)
throw new ArgumentException("Tried to create a
MultiPanelPage.ControlCollection with a
non-MultiPanelPage owner.", "owner");
}
public override void Add(Control value)
{
if (value == null)
throw new ArgumentNullException("value",
"Tried to add a null value to the
MultiPanelPage.ControlCollection.");
MultiPanelPage p = value as MultiPanelPage;
if (p != null)
throw new ArgumentException("Tried to add a
MultiPanelPage control to the
MultiPanelPage.ControlCollection.", "value");
base.Add(value);
}
}
#endregion
}
The MultiPanelDesigner
class is responsible for allowing the user to add and remove panel pages. This is done by defining support for two verbs:
public override DesignerVerbCollection Verbs
{
get
{
if (_verbs == null)
{
_verbs = new DesignerVerbCollection();
_verbs.Add(new DesignerVerb("Add Page", new EventHandler(AddPage)));
_verbs.Add(new DesignerVerb("Remove Page", new EventHandler(RemovePage)));
}
return _verbs;
}
}
And defining them as follows:
private void AddPage(object sender, EventArgs ea)
{
IDesignerHost dh = (IDesignerHost)GetService(typeof(IDesignerHost));
if (dh != null)
{
DesignerTransaction dt = dh.CreateTransaction("Added new page");
MultiPanelPage before = _mpanel.SelectedPage;
string name = GetNewPageName();
MultiPanelPage ytp = dh.CreateComponent(typeof(MultiPanelPage),
name) as MultiPanelPage;
ytp.Text = name;
_mpanel.Controls.Add(ytp);
_mpanel.SelectedPage = ytp;
RaiseComponentChanging(TypeDescriptor.GetProperties(Control)
["SelectedPage"]);
RaiseComponentChanged(TypeDescriptor.GetProperties(Control)
["SelectedPage"], before, ytp);
dt.Commit();
}
}
private void RemovePage(object sender, EventArgs ea)
{
IDesignerHost dh = (IDesignerHost)GetService(typeof(IDesignerHost));
if (dh != null)
{
DesignerTransaction dt = dh.CreateTransaction("Removed page");
MultiPanelPage page = _mpanel.SelectedPage;
if (page != null)
{
MultiPanelPage ytp = _mpanel.SelectedPage;
_mpanel.Controls.Remove(ytp);
dh.DestroyComponent(ytp);
if (_mpanel.Controls.Count > 0)
_mpanel.SelectedPage = (MultiPanelPage)_mpanel.Controls[0];
else
_mpanel.SelectedPage = null;
RaiseComponentChanging(TypeDescriptor.GetProperties
(Control)["SelectedPage"]);
RaiseComponentChanged(TypeDescriptor.GetProperties
(Control)["SelectedPage"], ytp, _mpanel.SelectedPage);
}
dt.Commit();
}
}
private string GetNewPageName()
{
int i = 1;
Hashtable h = new Hashtable(_mpanel.Controls.Count);
foreach (Control c in _mpanel.Controls)
{
h[c.Name] = null;
}
while (h.ContainsKey("Page_" + i))
{
i++;
}
return "Page_" + i;
}
Note that I'm using the GetNewPageName()
method in order to create a new name for the page control, and that the designer interacts with the underlying multipanel class in order to add the new page and select it.
Finally - there is the MultiPanelPageDesigner
class that is responsible for managing design time interaction with the MultiPanelPage
control. This class is responsible for drawing the Text
property of the underlying page control (OnPaintAdornments
method) and handle changes in the Text
property of the page control (done by shadowing the Text
property of the underlying page control).
public class MultiPanelPageDesigner : ScrollableControlDesigner
{
public MultiPanelPageDesigner()
{
}
public string Text
{
get
{
return _page.Text;
}
set
{
string ot = _page.Text;
_page.Text = value;
IComponentChangeService iccs =
GetService(typeof(IComponentChangeService))
as IComponentChangeService;
if (iccs != null)
{
MultiPanel ytc = _page.Parent as MultiPanel;
if (ytc != null)
ytc.Refresh();
}
}
}
protected override void OnPaintAdornments(PaintEventArgs pea)
{
base.OnPaintAdornments(pea);
using (Pen p = new Pen(SystemColors.ControlDark, 1))
{
p.DashStyle = System.Drawing.Drawing2D.DashStyle.Dash;
pea.Graphics.DrawRectangle(p, 0, 0, _page.Width - 1, _page.Height - 1);
}
using (Brush b = new SolidBrush(Color.FromArgb(100, Color.Black)))
{
float fh = _font.GetHeight(pea.Graphics);
RectangleF tleft = new RectangleF(0, 0, _page.Width / 2, fh);
RectangleF bleft = new RectangleF(0, _page.Height - fh, _
page.Width / 2, fh);
RectangleF tright = new RectangleF(_page.Width / 2, 0,
_page.Width / 2, fh);
RectangleF bright = new RectangleF(_page.Width / 2,
_page.Height - fh, _page.Width / 2, fh);
pea.Graphics.DrawString(_page.Text, _font, b, tleft);
pea.Graphics.DrawString(_page.Text, _font, b, bleft);
pea.Graphics.DrawString(_page.Text, _font, b, tright, _rightfmt);
pea.Graphics.DrawString(_page.Text, _font, b, bright, _rightfmt);
}
}
public override void Initialize(IComponent component)
{
_page = component as MultiPanelPage;
if (_page == null)
DisplayError(new Exception("You attempted to use a
MultiPanelPageDesigner with a class that does not
inherit from MultiPanelPage."));
base.Initialize(component);
}
protected override void PreFilterProperties(IDictionary properties)
{
base.PreFilterProperties(properties);
properties["Text"] = TypeDescriptor.CreateProperty
(typeof(MultiPanelPageDesigner), (PropertyDescriptor)properties
["Text"], new Attribute[0]);
}
private MultiPanelPage _page;
private Font _font = new Font("Courier New", 8F, FontStyle.Bold);
private StringFormat _rightfmt = new StringFormat
(StringFormatFlags.NoWrap | StringFormatFlags.DirectionRightToLeft);
}
Using the Code
If you are like me - this is the section that you want to read first (actually - this is the only section you'll want to read...).
Using this control is very simple. You simply drag it into the form, use the AddPage verb to add new pages or the RemovePage verb to remove an existing page.
Once a page is added - you'll notice that the page's Text
property appears on all the four sides of the page. This text appears only in design time and is very useful for knowing which page is currently selected in the multipanel control.
The way I like to work is to open the document-outline view (like is shown in the screenshot above) and jump between the various pages using the mouse. You can open the document-outline view from the View/Other Windows/Document Outline menu.
Now you can drag various controls into the various pages and switch them in design time using the document outline view.
OK - this solves the design time problem. When you want to use the control in runtime - you'll use the multipanel control's SelectedPage
in order to change the currently displayed page. The test application contains a very short code to demonstrate this.
That's it! I hope it will save you some time and make your life a bit easier.
History
- v0.1 17-June-2009 - Initial version