C# 中的多面板控件






4.87/5 (36投票s)
此控件的功能类似于选项卡控件,但没有选项卡...

引言
很长一段时间以来,我需要一个控件,它能像选项卡控件一样工作,但又不显示任何选项卡。这个控件非常有用,因为你可以使用强大的窗体设计器在同一个窗体中添加许多不同的页面。否则,你将被迫为每个页面添加一个不同的用户控件,这可能会非常令人头疼。
不幸的是,我没有找到一个。过去我甚至使用了一个 TabControl,并在选项卡上方放置了一个难看的面板,这样它们就不会被看到。 难看 难看 难看 ..
在我的最终项目中,我再次需要一个,并决定一劳永逸地解决这个问题。
背景
为了尽可能地偷懒,我搜索了互联网,希望能找到一些可以复制粘贴并只需最少修改就能用到我的新控件里的东西。
几分钟后,我找到了我想要的东西——一篇在本网站上的文章 https://codeproject.org.cn/KB/miscctrl/yatabcontrol.aspx ,作者是 curtis schlak。 这篇精彩的文章为我的控件奠定了基础。
代码描述
代码分为几个类
MultiPanel
控件 - 一个非常简单的类,它继承自Panel
类,并保存页面集合和当前选定的页面实例。MultiPanelPagesCollection
类 - 继承自ControlCollection
类,以强制所有包含的控件必须是MultiPanelPage
控件类型。MultiPanelPage
控件 - 为所有多面板页面提供实现。继承自ContainerControl
,以便作为我们拖放到其中的所有控件的容器。- 两个设计器类:
MultiPanelDesigner
用于设计MultiPanel
控件,以及MultiPanelPageDesigner
用于设计 multipanelpage 控件(还用于绘制页面的Text
属性以便于阅读)。
正如你所见——MultiPanel
类非常简单。它基本上是一个面板,根据 _selectedPage
变量中存储的值替换选定的控件。
[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;
}
请注意,它在类级别的 Designer 属性中声明了其设计器的类型。
另外请注意,我们通过重写 CreateControlsInstance()
方法来替换用于保存控件的类。
MultiPanelPage
类也非常简单。基本上——我用自己的类重写了控件集合类,以防止 MultiPanelPage
实例被插入到多面板页面中,并且我为页面类声明了设计类:
[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
}
MultiPanelDesigner
类负责允许用户添加和删除面板页面。这是通过定义对两个动词的支持来完成的
/// <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;
}
}
并定义它们如下:
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();
}
}
/// <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;
}
请注意,我使用 GetNewPageName()
方法为页面控件创建新名称,并且设计器与底层的 multipanel 类进行交互,以添加新页面并选择它。
最后——是 MultiPanelPageDesigner
类,它负责管理设计时与 MultiPanelPage
控件的交互。此类负责绘制底层页面控件的 Text
属性(OnPaintAdornments
方法),并处理页面控件 Text
属性的变化(通过覆盖底层页面控件的 Text
属性来实现)。
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
如果你和我一样——这是你想先读的部分(实际上——这是你唯一想读的部分...)。
使用这个控件非常简单。你只需将其拖放到窗体上,使用 AddPage 动词添加新页面,或使用 RemovePage 动词删除现有页面。
添加页面后——你会注意到页面的 Text
属性出现在页面的所有四条边上。此文本仅在设计时出现,对于知道当前在 multipanel 控件中选择哪个页面非常有用。
我喜欢的工作方式是打开文档大纲视图(如上图所示),然后用鼠标在各个页面之间跳转。你可以从“视图/其他窗口/文档大纲”菜单打开文档大纲视图。
现在,你可以将各种控件拖放到各个页面中,并在设计时使用文档大纲视图进行切换。
好的——这解决了设计时的问题。当你想要在运行时使用该控件时——你将使用 multipanel 控件的 SelectedPage
来更改当前显示的页面。测试应用程序包含非常简短的代码来演示这一点。
就这样!希望它能为你节省一些时间,让你的生活更轻松一些。
历史
- v0.1 2009 年 6 月 17 日 - 初始版本