创建一个多页 Windows 窗体控件(带设计时支持)






4.59/5 (69投票s)
一个分步指南,
引言
您是否曾经面临在同一个 Windows 窗体上显示多个页面的任务?我们大多数人肯定会回答“是”,并且大多数人已经通过使用老旧的 Tab 控件来解决这个问题。虽然选项卡无疑是处理此类情况的可靠方法,但有时也需要一种“不那么通用”的方法。如果我们想让窗体更具视觉吸引力,并使用图标或图形按钮来翻页怎么办?如果我们根本不想显示选项卡怎么办?许多熟悉的应用程序都具有这种图形界面(请参见图例);然而,.NET Framework 并没有提供内置工具来完成此操作,至少在撰写本文时是这样。

出于我的目的,我不得不从头开始构建一个控件,这得益于框架的可扩展性。项目完成后,我认为这段经历值得与开发者社区分享,所以现在开始:一个自定义的多页控件。
请注意,特定翻页机制(例如图标或列表框项)的实现超出了本文的范围。相反,它侧重于一个自定义 Windows Forms 控件,该控件可以托管多个子控件页面,以及在 Windows Forms 项目中使用该控件的编程模型。为简单起见,我使用标准的 Windows Forms 控件 - 按钮和组合框项 - 来激活页面。
在我们深入研究代码之前,有几点关于推荐的培训水平,以更好地理解主题。预计具备 C# 和面向对象编程的知识,以及一些 Windows Forms 编程经验。了解 .NET Framework 的一些高级功能(如反射)将会有所帮助,但并非 100% 必需。
您还会注意到,我在代码中不使用匈牙利命名法;然而,阅读代码应该不是问题,因为这种命名法实际上非常简单且易于理解。前缀分配如下:
a
:局部变量the
:方法的正式参数,以及传递给异常处理catch
块的参数my
:类的实例字段our
:类的静态字段
属性和方法使用驼峰命名法(每个单词首字母大写)
DoActionOne();
PropertyTwo = 1;
第一步:创建控件
我们的 MultiPaneControl
是一个继承自 System.Windows.Forms.Control
的 .NET 类。单个页面是 MultiPanePage
类的实例。由于 MultiPanePage
类是用来容纳其他控件的,因此将其派生自 System.Windows.Forms.Panel
是很有意义的。
public class MultiPaneControl : System.Windows.Forms.Control
{
// implementation goes here
}
public class MultiPanePage : System.Windows.Forms.Panel
{
// implementation goes here
}
向我们的控件添加页面非常简单。
Controls.Add( new MultiPanePage() );
现在,要设置当前页面,我们将添加一个专用属性并在其 set
块中切换可见性。
protected MultiPanePage mySelectedPage;
public MultiPanePage SelectedPage
{
get
{ return mySelectedPage; }
set
{
if (mySelectedPage != null)
mySelectedPage.Visible = false;
mySelectedPage = value;
if (mySelectedPage != null)
mySelectedPage.Visible = true;
}
}
由于 MultiPaneControl
的 Controls
集合的所有成员(即所有页面)都应共享诸如定位和像素尺寸等基本特征,因此最好从父控件内部设置这些属性。我们的控件还必须支持背景透明。并且我们希望确保除了 MultiPanePage
之外,任何其他类实例都不能添加到 MultiPaneControl
中,因此我们将用条件语句来保护添加操作。
public MultiPaneControl()
{
ControlAdded += new ControlEventHandler(Handler_ControlAdded);
SetStyle(ControlStyles.SupportsTransparentBackColor, true);
BackColor = Color.Transparent;
}
private void Handler_ControlAdded(object theSender, ControlEventArgs theArgs)
{
if (theArgs.Control is MultiPanePage)
{
MultiPanePage aPg = (MultiPanePage) theArgs.Control;
// prevent the page from being moved and/or sized independently
aPg.Location = new Point(0, 0);
aPg.Dock = DockStyle.Fill;
if (Controls.Count == 1)
mySelectedPage = aPg; // automatically set the current page
else
theArgs.Control.Visible = false;
}
else
{
// block anything other than MultiPanePage
Controls.Remove(theArgs.Control);
}
}
我们最后要做的一件事是重写 DefaultSize
属性。
protected static readonly Size ourDefaultSize = new Size(200, 100);
protected override Size DefaultSize
{
get { return ourDefaultSize; }
}
此时,我们的控件已准备好进行编译并在代码中进行测试。
编译 **Step1** 示例项目会得到一个在运行时运行良好的控件;然而,在设计环境中对其进行处理会暴露出一系列严重的缺陷。
- 除了修改其源代码之外,无法在窗体上创建
MultiPaneControl
的实例。 - 在不修改源代码的情况下,也无法添加和删除页面。
- 页面仍然可以单独移动和调整大小:尽管我们最初设置了
Dock
属性以防止独立拖动/调整大小,但可以通过“属性”窗口轻松更改它。 - 要以图形方式编辑页面的唯一方法是设置控件的
SelectedPage
属性,使该页面可见。 - 通过“属性”窗口编辑
SelectedPage
属性时,Visual Studio 会显示MultiPanePage
类型的所有对象,包括属于其他MultiPaneControl
实例的对象。实际上,只有控件本身的子项才应该是可访问的。
这类不足在缺乏设计时支持的自定义控件中很常见,因此我们的下一步将正是如此:使我们的控件能够与 Visual Studio 的 RAD 工具顺畅地协同工作。
第二步:添加设计时支持:基础知识
工具箱项
在这一步中,我们将为我们的 MultiPaneControl
开发一个工具箱项。由于工具箱项不是一个独立的实体,而是 MultiPaneControl
的扩展,它们将一起编译到一个独立的程序集中。这将允许我们在将来遇到的任何 Windows Forms 项目中使用我们的控件。为了在工具箱中唯一地标识我们的控件,我们还将为其设计一个自定义图标。
当工具箱项在工作时,它会响应拖放或“绘制”事件来创建我们控件的实例。默认情况下,新控件将包含一页。要将工具箱项与控件关联,我们需要将 ToolboxItem
属性应用于控件的类。
[ToolboxItem(typeof(MultiPaneControlToolboxItem))]
public class MultiPaneControl : Control
{
...
}
上面的代码指示 Visual Studio 创建 MultiPaneControlToolboxItem
的实例,然后使用它来实例化我们的 MultiPaneControl
。前者类必须继承自 System.Drawing.Design.ToolboxItem
。它还必须声明为 Serializable
,因为 Visual Studio 会将其序列化到其内部数据结构中。由于 System.Drawing.Design.ToolboxItem
实现了 ISerializable
接口,因此我们还需要定义一个特殊的序列化构造函数,正如 Microsoft 文档中所述。
[PermissionSet(SecurityAction.Demand, Name = "FullTrust")]
[Serializable]
public class MultiPaneControlToolboxItem : ToolboxItem
{
public MultiPaneControlToolboxItem() : base(typeof(MultiPaneControl))
{
}
// Serialization constructor, required for deserialization
public MultiPaneControlToolboxItem(SerializationInfo theInfo,
StreamingContext theContext)
{
Deserialize(theInfo, theContext);
}
protected override IComponent[] CreateComponentsCore(IDesignerHost theHost)
{
// Control
MultiPaneControl aCtl =
(MultiPaneControl)theHost.CreateComponent(typeof(MultiPaneControl));
// Control's page
MultiPanePage aNewPage1 =
(MultiPanePage)theHost.CreateComponent(typeof(MultiPanePage));
aCtl.Controls.Add(aNewPage1);
return new IComponent[] { aCtl };
}
}
正如我们所见,CreateComponentsCore
方法负责创建我们的控件并为其提供页面。Visual Studio 会自动为我们处理其余部分,将适当的代码添加到窗体中。
让我们花点时间暂时放下编码,戴上艺术家的帽子,因为现在是时候为我们的控件设计图标了!这个图标将显示在工具箱(所有版本的 Visual Studio)和文档大纲窗口(VS 2005 及更高版本)中。
- 在您喜欢的图形编辑器中,绘制一个 16x16 像素的图像。
- 将其保存为位图图像,并将文件名命名为[您的控件的类名].bmp。在我们的例子中,
MultiPaneControl
和MultiPanePage
控件的图像将分别命名为 MultiPaneControl.bmp 和 MultiPanePage.bmp。 - 使用 **项目 -> 添加现有项...** 菜单,将图像放在项目的根目录中,即您的*.cs 文件所在的文件夹。
- 在每个位图的“属性”窗口中,将 **生成操作** 属性设置为*嵌入的资源*。
- 确保您的控件类位于项目默认命名空间中,如下所示:

为了防止独立创建 MultiPanePage
实例,我们将以下属性应用于该类:
[ToolboxItem(false)] // do not place in the Toolbox
public class MultiPanePage : Panel
{
...
}
UI 类型编辑器
接下来,我们将为 SelectedPage
属性实现一个编辑器。正如我之前提到的,当我们在“属性”窗口中编辑此属性时,Visual Studio 仅根据项类型来过滤项列表。假设我们的窗体包含两个 MultiPaneControl
实例:控件 A 和 控件 B,每个实例都有自己的页面集合。将属于控件 A 的页面设置为控件 B 的选定页面是不可取的且容易出错,此外,这根本没有意义。可用对象的列表应限制为正在编辑的控件所属的页面。
下图显示了一个包含两个 MultiPaneControl
的窗体,上部控件包含三个页面(myCtlPanePage1
、myCtlPanePage2
、myCtlPanePage3
),下部控件包含两个页面(myCtlPanePage4
和 myCtlPanePage5
)。下部控件当前被选中。UI 类型编辑器反映了页面属于两个不同控件的事实,并且仅显示适当的对象。
为实现此目的,我们需要创建一个直接或间接继承自 System.ComponentModel.Design.UITypeEditor
的类。我们的编辑器将继承自 System.ComponentModel.Design.ObjectSelectorEditor
,这对于我们试图解决的问题非常理想。该类会在响应箭头单击时显示一个项下拉列表。我们派生的类将只负责填充列表。
internal class MultiPaneControlSelectedPageEditor : ObjectSelectorEditor
{
protected override void FillTreeWithData(Selector theSel,
ITypeDescriptorContext theCtx, IServiceProvider theProvider)
{
base.FillTreeWithData(theSel, theCtx, theProvider); //clear the selection
MultiPaneControl aCtl = (MultiPaneControl) theCtx.Instance;
foreach (MultiPanePage aIt in aCtl.Controls)
{
SelectorNode aNd = new SelectorNode(aIt.Name, aIt);
theSel.Nodes.Add(aNd);
if (aIt == aCtl.SelectedPage)
theSel.SelectedNode = aNd;
}
}
}
有了这个类之后,我们只需要将 Editor
属性应用于该属性。
[
Editor(
typeof(MultiPaneControlSelectedPageEditor), //the class we've just created!
typeof(System.Drawing.Design.UITypeEditor) )
]
public MultiPanePage SelectedPage
{
...
}
我们差不多可以编译 **Step2** 示例项目了。为了完成这项工作,我们将隐藏 MultiPanePage
的两个不用于直接编辑的属性,即 Dock
和 Location
。
[EditorBrowsable(EditorBrowsableState.Never)]
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
public override DockStyle Dock
{
get { return base.Dock; }
set { base.Dock = value; }
}
[EditorBrowsable(EditorBrowsableState.Never)]
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
public new Point Location
{
get { return base.Location; }
set { base.Location = value; }
}
请注意,我们**不**应将相同的方法应用于 Size
属性,因为 Visual Studio 会使用它来布局子控件。
第三步:添加设计时支持。控件设计器。
到目前为止,我们只涵盖了 前面概述的 五个设计时问题中的三个。结果是:
- 现在可以从 Visual Studio 的工具箱中以图形方式创建
MultiPaneControl
实例。 - 页面不再能独立于其容器控件进行拖动。
SelectedPage
属性有一个防错的 UI 类型编辑器。
解决剩余问题需要为 MultiPaneControl
和 MultiPanePage
控件创建控件设计器。在本节中,我们将处理两个组件:控件和设计器。重要的是不要混淆设计器的方法和属性与控件的方法和属性。
为 MultiPaneControl
提供设计时支持的步骤是:
- 创建一个实现
IDesigner
接口的MultiPaneControlDesigner
,如 Microsoft 文档中所述。 - 将我们的控件与设计器关联,并使 Visual Studio 了解此关联。这是通过将
Designer
属性应用于MultiPaneControl
类来实现的。
我们可以从 .NET Framework 提供的实现 IDesigner
接口的几个类中派生我们的设计器类。这些是:
ControlDesigner
- 用于所有没有自定义设计器的控件的默认设计器类ParentControlDesigner
- 继承ControlDesigner
的全部功能,并支持将控件拖放到被编辑控件的表面上ScrollableControlDesigner
- 继承ParentControlDesigner
的全部功能,并支持在设计时处理滚动事件
由于 MultiPaneControl
类的唯一目的是托管其他控件,因此 ParentControlDesigner
似乎是我们设计器的最佳选择。
MultiPaneControlDesigner
开发控件设计器的关键在于其与开发环境的交互。这种交互依赖于所谓的*设计器服务*,即通过调用 GetService
方法获得的对象。每个对象都实现一个服务于其自身特定目的的接口。例如,ISelectionService
用于以编程方式选择/取消选择窗体上的组件。
必须*获取*对该接口的引用才能访问其功能。换句话说,我们不能保证我们的开发环境(或其版本)会支持某个接口。例如,为 Visual Studio 2008 开发的控件设计器可能会导致旧版本 VS 崩溃,如果后者不支持必需的接口。为了确定是否存在此类支持,我们需要显式检查 GetService
方法返回的值。
void MyFunc(ComponentDesigner theDesigner)
{
Type aType = typeof(ISelectionService);
ISelectionService aSrv = (ISelectionService) theDesigner.GetService(aType);
if (aSrv != null) //REQUIRED!!!
{
// do actions with the service
}
}
如果您有 COM 编程经验,您会发现这里在获取引用的方式以及如何检查接口是否实际受支持方面有很强的相似性。然而,与 COM 不同的是,在 .NET 环境中,我们不需要释放引用。
因此,我们将开始建立 IDE 与我们的控件设计器之间的联系。为此,我们将重写 Initialize
方法,并订阅设计器服务提供的几个事件。我们还将在此方法中初始化*设计器动词*。
设计器动词可以被视为单个操作或命令。每个设计器都有其特定于其构建任务的动词。Visual Studio 在控件的上下文菜单中显示动词。2005 及更高版本还在通过单击控件边界区域左上角的小三角形激活的弹出窗口中显示动词。

当用户调用动词时,会引发一个事件,系统会自动调用其处理程序。在处理程序中,设计器执行动词特定的操作,可能会修改控件的属性。现在,让我们看看 Initialize
方法。
public override void Initialize(IComponent theComponent)
{
base.Initialize(theComponent); // IMPORTANT! This must be the very first line
// ISelectionService events
ISelectionService aSrv_Sel =
(ISelectionService)GetService(typeof(ISelectionService));
if (aSrv_Sel != null)
aSrv_Sel.SelectionChanged += new EventHandler(Handler_SelectionChanged);
// IComponentChangeService events
IComponentChangeService aSrv_CH =
(IComponentChangeService)GetService(typeof(IComponentChangeService));
if (aSrv_CH != null)
{
aSrv_CH.ComponentRemoving += new ComponentEventHandler(Handler_ComponentRemoving);
aSrv_CH.ComponentChanged +=
new ComponentChangedEventHandler(Handler_ComponentChanged);
}
// Prepare the verbs
myAddVerb = new DesignerVerb("Add page", new EventHandler(Handler_AddPage));
myRemoveVerb = new DesignerVerb("Remove page", new EventHandler(Handler_RemovePage));
mySwitchVerb =
new DesignerVerb("Switch pages...", new EventHandler(Handler_SwitchPage));
myVerbs = new DesignerVerbCollection();
myVerbs.AddRange(new DesignerVerb[] { myAddVerb, myRemoveVerb, mySwitchVerb });
}
我们在这里订阅了三个事件,因此我们必须在 Dispose
方法重写中执行相反的操作。
protected override void Dispose(bool theDisposing)
{
if (theDisposing)
{
// ISelectionService events
ISelectionService aSrv_Sel =
(ISelectionService)GetService(typeof(ISelectionService));
if (aSrv_Sel != null)
aSrv_Sel.SelectionChanged -= new EventHandler(Handler_SelectionChanged);
// IComponentChangeService events
IComponentChangeService aSrv_CH =
(IComponentChangeService)GetService(typeof(IComponentChangeService));
if (aSrv_CH != null)
{
aSrv_CH.ComponentRemoving -=
new ComponentEventHandler(Handler_ComponentRemoving);
aSrv_CH.ComponentChanged -=
new ComponentChangedEventHandler(Handler_ComponentChanged);
}
}
base.Dispose(theDisposing);
}
让我们看一下事件处理程序。由于一次只能显示一个页面,因此选择位于隐藏页面上的子控件必须使该页面可见。SelectionChanged
事件的处理程序将用于监视控件选择并切换可见性。
ComponentRemoving
的处理程序是必需的,如果需要,可以更新 SelectedPage
属性。这样,该属性将始终与 Controls
集合同步。
最后,ComponentChanged
的处理程序对于启用/禁用特定动词很有用。例如,如果控件只有一个页面,我们将不允许删除页面。
我们的设计器将重写一些方法。例如,我们想阻止任何非 MultiPanePage
实例被添加为子控件(这与我们必须为控件本身实现的相同概念相同)。
public override bool CanParent(Control theControl)
{
if (theControl is MultiPanePage)
return !Control.Contains(theControl);
else
return false;
}
此外,还将重写几个与拖放相关的方法。因为我们的控件只能托管页面,而不能托管其他任何内容,所以拖放操作必须由控件的页面来处理。例如,拖放 CheckBox
控件必须将其放置在其中一个页面上,而不是容器上。这可以通过*重定向*拖放操作到页面设计器来实现,我们将在稍后介绍。让我们看一下 OnDragDrop
重写。
protected override void OnDragDrop(DragEventArgs theDragEvents)
{
MultiPanePageDesigner aDsgn_Sel = GetSelectedPageDesigner();
if (aDsgn_Sel != null)
aDsgn_Sel.InternalOnDragDrop(theDragEvents);
}
MultiPanePageDesigner
的内部方法 – InternalOnDragDrop
– 将为我们完成工作。对于 OnDragEnter
、OnDragLeave
、OnDragOver
和 OnGiveFeedback
方法也是如此。
在 GetSelectedPageDesigner
方法中,我们将使用一个设置页面可见性的机制,该机制与我们在控件的 SelectedPage
属性中定义的机制类似。然而,在 MultiPaneControlDesigner
中,此机制仅适用于设计时,允许我们在页面之间切换,但保留控件的 SelectedPage
属性不变。
为了提供同步这两个属性的手段,我们将在 MultiPaneControl
中引入另外两个事件,并稍微修改其 SelectedPage
set 块的主体。
// MultiPaneControl class
public event EventHandler SelectedPageChanging;
public event EventHandler SelectedPageChanged;
public MultiPanePage SelectedPage
{
get { return mySelectedPage; }
set
{
if (mySelectedPage == value)
return;
// fire the event before switching
if (SelectedPageChanging != null)
SelectedPageChanging(this, EventArgs.Empty);
if (mySelectedPage != null)
mySelectedPage.Visible = false;
mySelectedPage = value;
if (mySelectedPage != null)
mySelectedPage.Visible = true;
// fire the event after switching
if (SelectedPageChanged != null)
SelectedPageChanged(this, EventArgs.Empty);
}
}
与设计器服务一样,我们可以在 Initialize
方法中为我们新的 MultiPaneControl
事件指定处理程序。同样,需要在 Dispose
方法覆盖中添加相应的清理代码。
// ...Initialize
DesignedControl.SelectedPageChanged += new EventHandler(Handler_SelectedPageChanged);
// ...Dispose
DesignedControl.SelectedPageChanged -= new EventHandler(Handler_SelectedPageChanged);
事件的处理是微不足道的。
private void Handler_SelectedPageChanged(object theSender, EventArgs theArgs)
{
mySelectedPage = DesignedControl.SelectedPage;
}
mySelectedPage
是一个 MultiPaneControl
的字段,用于保存设计时可见的页面。最后,GetSelectedPageDesigner
方法的实现如下:
private MultiPanePageDesigner GetSelectedPageDesigner()
{
MultiPanePage aSelPage = mySelectedPage; // not DesignedControl.SelectedPage
if (aSelPage == null)
return null;
MultiPanePageDesigner aDesigner = null;
IDesignerHost aSrv = (IDesignerHost)GetService(typeof(IDesignerHost));
if (aSrv != null)
aDesigner = (MultiPanePageDesigner)aSrv.GetDesigner(aSelPage);
return aDesigner;
}
处理页面
这也许是整个项目中 L 最有趣和最有益的部分,因为我们终于可以看到我们的控件和设计器在工作了。
我们的 Handler_AddPage
需要执行以下操作:
- 创建页面。
- 将其添加到控件集合中。
- 使页面可见以便进一步编辑。
- 将更改反映到窗体的源代码中。
private void Handler_AddPage(object theSender, EventArgs theArgs)
{
IDesignerHost aHost = (IDesignerHost)GetService(typeof(IDesignerHost));
if (aHost == null)
return;
MultiPaneControl aCtl = DesignedControl;
MultiPanePage aNewPage = (MultiPanePage)aHost.CreateComponent(typeof(MultiPanePage));
MemberDescriptor aMem_Controls = TypeDescriptor.GetProperties(aCtl)["Controls"];
RaiseComponentChanging(aMem_Controls);
aCtl.Controls.Add(aNewPage);
DesignerSelectedPage = aNewPage;
RaiseComponentChanged(aMem_Controls, null, null);
}
为了使源代码反映更改,我们使用 aHost.CreateComponent(typeof(MultiPanePage))
而不是 new MultiPanePage()
。两个通知方法 RaiseComponentChanging
和 RaiseComponentChanged
会为代码序列化调用。
理论上,上面的列表应该可以正常工作。但是……如果出现问题,导致异常发生在执行过程中?考虑一下:
RaiseComponentChanging(aMem_Controls);
aCtl.Controls.Add(aNewPage);
throw new Exception("Test exception"); // throw a test exception so that
// further code is not executed
DesignerSelectedPage = aNewPage;
RaiseComponentChanged(aMem_Controls, null, null);
想到的第一个解决方案是将易抛出异常的代码包装到 try
块中。然而,在我们的示例中,在抛出异常之前执行的代码已经改变了我们控件的状态,而在 catch
块中反转这一点将非常困难。
为了解决这个问题,我们将启动一个*事务*,该事务将控制我们代码的执行。事务将在成功执行后提交,否则回滚。 Microsoft 文档指出事务用于撤销/重做支持,但说实话,我无法在无事务环境中重现缺乏此类支持。
我强烈建议为将在事务中执行的方法采用统一的设计策略。这样,一个包装器方法就可以用来启动和提交所有事务,以及调用一个委托来执行实际处理。我们的 **Step3** 示例包含一个名为 DesignerTransactionUtility
的小型类,它正好用于此目的。
public abstract class DesignerTransactionUtility
{
public static object DoInTransaction(
IDesignerHost theHost,
string theTransactionName,
TransactionAwareParammedMethod theMethod,
object theParam)
{
DesignerTransaction aTran = null;
object aRetVal = null;
try
{
aTran = theHost.CreateTransaction(theTransactionName);
aRetVal = theMethod(theHost, theParam); // perform actual execution
}
catch (CheckoutException theEx) // transaction initiation failed
{
if (theEx != CheckoutException.Canceled)
throw theEx;
}
catch
{
if (aTran != null)
{
aTran.Cancel();
aTran = null; // the transaction won't commit in the 'finally' block
}
throw;
}
finally
{
if (aTran != null)
aTran.Commit();
}
return aRetVal;
}
}
TransactionAwareParammedMethod
是一个*委托*,它封装了一个在其执行发生在事务内的操作。
public delegate object TransactionAwareParammedMethod(IDesignerHost theHost,
object theParam);
然后,我们稍微修改事件处理程序。
private void Handler_AddPage(object theSender, EventArgs theArgs)
{
IDesignerHost aHost = (IDesignerHost)GetService(typeof(IDesignerHost));
if (aHost == null)
return;
DesignerTransactionUtility.DoInTransaction
(
aHost,
"MultiPaneControlAddPage",
new TransactionAwareParammedMethod(Transaction_AddPage),
null
);
}
private object Transaction_AddPage(IDesignerHost theHost, object theParam)
{
MultiPaneControl aCtl = DesignedControl;
MultiPanePage aNewPage = (MultiPanePage)theHost.CreateComponent(typeof(MultiPanePage));
MemberDescriptor aMem_Controls = TypeDescriptor.GetProperties(aCtl)["Controls"];
RaiseComponentChanging(aMem_Controls);
aCtl.Controls.Add(aNewPage);
DesignerSelectedPage = aNewPage;
RaiseComponentChanged(aMem_Controls, null, null);
return null;
}
我们将采用相同的策略来处理工具箱项,并相应地修改其代码。
页面删除也在事务中执行。
private void Handler_RemovePage(object theSender, EventArgs theEvent)
{
// validation goes here, skipped
IDesignerHost aHost = (IDesignerHost)GetService(typeof(IDesignerHost));
if (aHost == null)
return;
DesignerTransactionUtility.DoInTransaction
(
aHost,
"MultiPaneControlRemovePage",
new TransactionAwareParammedMethod(Transaction_RemovePage),
null
);
}
在 Transaction_RemovePage
中,我们销毁了当前正在设计的页面。在页面被销毁之前,会触发 ComponentRemoving
事件,并调用其处理程序 Handler_ComponentRemoving
。
private void Handler_ComponentRemoving(object theSender, ComponentEventArgs theArgs)
{
// validation goes here, skipped
IDesignerHost aHost = (IDesignerHost)GetService(typeof(IDesignerHost));
DesignerTransactionUtility.DoInTransaction
(
aHost,
"MultiPaneControlRemoveComponent",
new TransactionAwareParammedMethod(Transaction_UpdateSelectedPage)
);
}
private object Transaction_UpdateSelectedPage(IDesignerHost theHost, object theParam)
{
MultiPaneControl aCtl = DesignedControl;
MultiPanePage aPgTemp = mySelectedPage;
int aCurIndex = aCtl.Controls.IndexOf(mySelectedPage);
if (mySelectedPage == aCtl.SelectedPage)
//we also need to update the SelectedPage property
{
MemberDescriptor aMember_SelectedPage =
TypeDescriptor.GetProperties(aCtl)["SelectedPage"];
RaiseComponentChanging(aMember_SelectedPage);
if (aCtl.Panes.Count > 1)
{
// begin update current page
if (aCurIndex == aCtl.Panes.Count - 1) // NOTE: after SelectedPage has
aCtl.SelectedPage = aCtl.Panes[aCurIndex - 1]; // been updated, mySelectedPage
else // has also changed
aCtl.SelectedPage = aCtl.Panes[aCurIndex + 1];
// end update current page
}
else
aCtl.SelectedPage = null;
RaiseComponentChanged(aMember_SelectedPage, null, null);
}
else
{
if (aCtl.Panes.Count > 1)
{
if (aCurIndex == aCtl.Panes.Count - 1)
DesignerSelectedPage = aCtl.Panes[aCurIndex - 1];
else
DesignerSelectedPage = aCtl.Panes[aCurIndex + 1];
}
else
DesignerSelectedPage = null;
}
return null;
}
对于页面切换,我们将创建一个显示选择对话框的处理程序。实现很简单,对话框看起来像这样:

MultiPanePageDesigner
我们的 MultiPanePageDesigner
继承自 ScrollableControlDesigner
。它处理拖放和绘制操作,同时将鼠标选择和设计器动词重定向到 MultiPaneControlDesigner
。
该类有三个用于重定向鼠标选择的字段。
private int myOrigX = -1; // store the original position of the
private int myOrigY = -1; // mouse cursor
private bool myMouseMovement = false; // true if mouse movement occurred
OnMouseDragMove
方法确定是否发生了鼠标移动。OnMouseDragEnd
则完成其余工作,要么选择父 MultiPaneControl
,要么调用其基类方法。如果用户实际上没有意图进行选择,则会发生后者。
protected override void OnMouseDragBegin(int theX, int theY)
{
myOrigX = theX;
myOrigY = theY;
// no call to base.OnMouseDragBegin
}
protected override void OnMouseDragMove(int theX, int theY)
{
if ( theX > myOrigX + 3 || theX < myOrigX - 3 ||
theY > myOrigY + 3 || theY < myOrigY - 3 )
{
myMouseMovement = true;
base.OnMouseDragBegin(myOrigX, myOrigY);
base.OnMouseDragMove(theX, theY);
}
}
protected override void OnMouseDragEnd(bool theCancel)
{
bool aProcess = !myMouseMovement && Control.Parent != null;
if (aProcess)
{
ISelectionService aSrv = (ISelectionService)GetService(typeof(ISelectionService));
if (aSrv != null)
aSrv.SetSelectedComponents(new Control[] { Control.Parent });
else
aProcess = false;
}
if (!aProcess)
base.OnMouseDragEnd(theCancel);
myMouseMovement = false;
}
Verbs
属性也被重写。它返回的 DesignerVerbCollection
从基类设计器和父 MultiPaneControl
的设计器填充。
public override DesignerVerbCollection Verbs
{
get
{
// 1. Obtain verbs from the base class
DesignerVerbCollection aRet = new DesignerVerbCollection();
foreach ( DesignerVerb aIt in base.Verbs)
aRet.Add(aIt);
// 2. Obtain verbs from the parent control's designer
MultiPaneControlDesigner aDs = GetParentControlDesigner();
if (aDs != null)
foreach (DesignerVerb aIt in aDs.Verbs)
aRet.Add(aIt);
return aRet;
}
}
从 MultiPaneControlDesigner
传递过来的拖放操作也在 MultiPanePaneDesigner
类中进行处理。请注意,我们无需做任何特殊处理,因为 ScrollableControlDesigner
会自动为我们处理所有内容。
internal void InternalOnDragDrop(DragEventArgs theArgs)
{ OnDragDrop(theArgs); }
internal void InternalOnDragEnter(DragEventArgs theArgs)
{ OnDragEnter(theArgs); }
internal void InternalOnDragLeave(EventArgs theArgs)
{ OnDragLeave(theArgs); }
internal void InternalOnGiveFeedback(GiveFeedbackEventArgs theArgs)
{ OnGiveFeedback(theArgs); }
internal void InternalOnDragOver(DragEventArgs theArgs)
{ OnDragOver(theArgs); }
为了在窗体上呈现页面边框,我们将重写 OnPaintAdornments
方法。我们的实现将绘制一个虚线矩形,使我们的页面看起来类似于 Tab 控件的页面。
protected override void OnPaintAdornments(PaintEventArgs theArgs)
{
DrawBorder(theArgs.Graphics);
base.OnPaintAdornments(theArgs);
}
protected void DrawBorder(Graphics theG)
{
MultiPanePage aCtl = DesignedControl;
if (aCtl == null)
return;
else if (!aCtl.Visible)
return;
Rectangle aRct = aCtl.ClientRectangle;
aRct.Width--; // decrement width and height so that the bottom
aRct.Height--; // and right lines become visible
theG.DrawRectangle(BorderPen, aRct);
}
——我们完成了!最终版本在 **Step3** 解决方案中。
一些最后的说明
- 所有示例代码均在 Visual Studio .NET 2003、Visual Studio 2005 和 Visual Studio 2008 Beta 2 下进行了测试。
- 提供的项目文件是 2003 版。使用项目导入向导将项目在更高版本的 Visual Studio 中打开。
- 在设计模式下打开示例窗体之前,请运行**重新生成解决方案**。
提供的源代码可以自由分发,并可全部或部分包含在任何第三方软件产品中,前提是我的作者身份在版权部分得到适当承认并明确声明。
非常感谢您的评论、建议和错误报告。欢迎任何改进的想法。
历史
- 2007-11-25. 初版发布
- 2007-11-30. 进行了一些拼写修正
- 2007-12-08. 由于 CodeProject 的页面服务存在 bug,所有文章文件(图形和 ZIP 存档)已移至镜像站点。
- 2009-03-31. 添加了对背景透明的支持。