Click here to Skip to main content
15,867,141 members
Articles / Desktop Programming / Windows Forms

A Multipanel Control in C#

Rate me:
Please Sign up or sign in to vote.
4.87/5 (37 votes)
17 Jun 2009Public Domain4 min read 111.1K   3.3K   88   51
This control acts like a tab control but without the tabs...
Image 1

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:

C#
 [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;
                    } // foreach
                }
            }
        }

        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: 

C#
[Designer(typeof(Liron.Windows.Forms.Design.MultiPanelPageDesigner))]
    public class MultiPanelPage : ContainerControl
    {
        public MultiPanelPage()
        {
            base.Dock = DockStyle.Fill;
        }

        /// <summary>
        /// Overridden from <see cref="Panel"/>.
        /// </summary>
        /// <remarks>
        /// Since the <see cref="MultiPanelPage"/> exists only
        /// in the context of a <see cref="MultiPanelControl"/>,
        /// it makes sense to always have it fill the
        /// <see cref="MultiPanelControl"/>. Hence, this property
        /// will always return <see cref="DockStyle.Fill"/>
        /// regardless of how it is set.
        /// </remarks>
        public override DockStyle Dock
        {
            get
            {
                return base.Dock;
            }
            set
            {
                base.Dock = DockStyle.Fill;
            }
        }

        /// <summary>
        /// Only here so that it shows up in the property panel.
        /// </summary>
        public override string Text
        {
            get
            {
                return base.Text;
            }
            set
            {
                base.Text = value;
            }
        }

        /// <summary>
        /// Overridden from <see cref="Control"/>.
        /// </summary>
        /// <returns>
        /// A <see cref="MultiPanelPage.ControlCollection"/>.
        /// </returns>
        protected override 
	System.Windows.Forms.Control.ControlCollection CreateControlsInstance()
        {
            return new MultiPanelPage.ControlCollection(this);
        }

        #region Classes
        public new class ControlCollection : Control.ControlCollection
        {
            /// <summary>
            /// </summary>
            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");
            }

            /// <summary>
            /// </summary>
            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:

C#
/// <summary>
/// Overridden. Inherited from <see cref="ControlDesigner"/>.
/// </summary>
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: 

C#
 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();
            }
        } 
C#
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();
        }
    }

    /// <summary>
    /// Gets a new page name for the a page.
    /// </summary>
    /// <returns></returns>
    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).

C#
public class MultiPanelPageDesigner : ScrollableControlDesigner
    {
        public MultiPanelPageDesigner()
        {
        }

        /// <summary>
        /// Shadows the <see cref="MultiPanelPage.Text"/> property.
        /// </summary>
        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();
                }
            }
        }

        /// <summary>
        /// Overridden. Inherited from
        /// <see cref="ControlDesigner.OnPaintAdornments(PaintEventArgs)"/>.
        /// </summary>
        /// <param name="pea">
        /// Some <see cref="PaintEventArgs"/>.
        /// </param>
        protected override void OnPaintAdornments(PaintEventArgs pea)
        {
            base.OnPaintAdornments(pea);

            // My thanks to bschurter (Bruce), CodeProject member #1255339 for this!
            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);
            }
        }

        /// <summary>
        /// Overridden. Inherited from 
        /// <see cref="ControlDesigner.Initialize( IComponent )"/>.
        /// </summary>
        /// <param name="component">
        /// The <see cref="IComponent"/> hosted by the designer.
        /// </param>
        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);
        }

        /// <summary>
        /// Overridden. Inherited from 
        /// <see cref="ControlDesigner.PreFilterProperties(IDictionary)"/>.
        /// </summary>
        /// <param name="properties"></param>
        protected override void PreFilterProperties(IDictionary properties)
        {
            base.PreFilterProperties(properties);
            properties["Text"] = TypeDescriptor.CreateProperty
		(typeof(MultiPanelPageDesigner), (PropertyDescriptor)properties
				["Text"], new Attribute[0]);
        }        

        /// <summary>
        /// </summary>
        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 

License

This article, along with any associated source code and files, is licensed under A Public Domain dedication


Written By
Software Developer
Israel Israel
My name is Liron Levi and I'm developing software for fun & profit for 15 years already.

Comments and Discussions

 
GeneralRe: MultiPanel Control Pin
liron.levi14-Dec-10 20:16
professionalliron.levi14-Dec-10 20:16 
GeneralRe: MultiPanel Control Pin
Denville14-Dec-10 22:45
Denville14-Dec-10 22:45 
GeneralThank you Pin
Meir Shaull16-Nov-10 5:15
Meir Shaull16-Nov-10 5:15 
GeneralRe: Thank you Pin
liron.levi16-Nov-10 21:49
professionalliron.levi16-Nov-10 21:49 
GeneralAppreciated! learned a lot Pin
Yankee Imperialist Dog!7-Nov-10 5:42
Yankee Imperialist Dog!7-Nov-10 5:42 
GeneralRe: Appreciated! learned a lot Pin
liron.levi7-Nov-10 20:16
professionalliron.levi7-Nov-10 20:16 
GeneralSuggestion - adding panels at start Pin
Gregory Gadow23-Mar-10 7:30
Gregory Gadow23-Mar-10 7:30 
GeneralGood one, founded an alternative Pin
scosta_FST11-Mar-10 3:52
scosta_FST11-Mar-10 3:52 
This control is very useful and well written.
I've found an alternative that I want to share:
http://social.msdn.microsoft.com/forums/en-US/winforms/thread/c290832f-3b84-4200-aa4a-7a5dc4b8b5bb/[^]

public  class MultiPanel : TabControl
{
 protected override void WndProc(ref Message m)
 {
  if (m.Msg == 0×1328 && !DesignMode)
  {
   m.Result = (IntPtr) 1;
  }
  else
  {
   base.WndProc(ref m);
  }
 }
}

GeneralProblem with second instance of Multipanel Control Pin
volody25-Jan-10 7:09
volody25-Jan-10 7:09 
GeneralRe: Problem with second instance of Multipanel Control - Solution Pin
Gregory Gadow23-Mar-10 7:22
Gregory Gadow23-Mar-10 7:22 
GeneralSelectedPage annoyance [edit] Pin
vtchris-peterson3-Dec-09 5:19
vtchris-peterson3-Dec-09 5:19 
GeneralGreat work Pin
vtchris-peterson2-Dec-09 5:35
vtchris-peterson2-Dec-09 5:35 
GeneralRe: Great work Pin
liron.levi2-Dec-09 8:42
professionalliron.levi2-Dec-09 8:42 
GeneralPanel inside a page of another panel problem Pin
adrian99899-Oct-09 3:11
adrian99899-Oct-09 3:11 
GeneralNice! Pin
hadi_saloko9-Sep-09 17:37
hadi_saloko9-Sep-09 17:37 
GeneralRe: Nice! Pin
liron.levi9-Sep-09 20:37
professionalliron.levi9-Sep-09 20:37 
GeneralAdd dynamically pages Pin
Edward Ceballos4-Aug-09 15:41
Edward Ceballos4-Aug-09 15:41 
GeneralRe: Add dynamically pages Pin
liron.levi4-Aug-09 20:39
professionalliron.levi4-Aug-09 20:39 
GeneralNice article Pin
jtdavies25-Jun-09 8:03
jtdavies25-Jun-09 8:03 
GeneralNice... and a few suggestions Pin
I'm Chris24-Jun-09 21:46
professionalI'm Chris24-Jun-09 21:46 
GeneralRe: Nice... and a few suggestions Pin
liron.levi24-Jun-09 21:53
professionalliron.levi24-Jun-09 21:53 
GeneralOutstanding Pin
Stumproot22-Jun-09 23:20
Stumproot22-Jun-09 23:20 
GeneralRe: Outstanding Pin
liron.levi23-Jun-09 2:21
professionalliron.levi23-Jun-09 2:21 
GeneralAn excellent control Pin
Juan Carlos San Roman17-Jun-09 10:50
Juan Carlos San Roman17-Jun-09 10:50 
GeneralRe: An excellent control Pin
liron.levi18-Jun-09 8:59
professionalliron.levi18-Jun-09 8:59 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.