如何构建多控件组件,同时继承自现有控件(简介和 TextBox 示例)






4.53/5 (9投票s)
本文将引导您开始构建自己的多控件组件,而无需使用UserControl类。
引言
在完成了一些商业项目后,我萌生了在窗体上绘制重复控件的想法。我发现制作包含30多个控件的多个窗体是一项相当繁琐的任务。尤其是在制作包含大量文本字段的窗体时更是如此。您需要创建几个TextBox
,并且每个TextBox
都必须附带一个Label
。更重要的是,每个Label
与TextBox
的距离必须相同,这样您的窗体才会显得整洁。在最糟糕的情况下,您必须为每个标签指定一个不错的名称,因为总会存在一种情况,即某个标签在您键入文本框中的信息时更改其文本,而您不想最终得到像“labelXXX”这样的名称。
在VC#.NET中,我们有一个构建多控件组件的机制,它被称为UserControl
类。这基本上是一个容器,您可以在其中放置工具箱中的其他控件。然而,这种机制存在缺点。如果您创建标签和文本框并希望对所有文本框进行对齐,您将遇到大麻烦,您不能再使用引导了。

使用我的技术时,尽管我有一个多控件组件,但我仍然可以使用引导。

当然,总会有个问题,那就是布局……但既然我不是布局的忠实粉丝,我可以忍受。
背景
如果您曾经用Delphi写过东西,您一定注意到VCL(Visual Component Library)有一个名为LabeledEdit
的控件。在本文中,我们将尝试在C# .NET中实现相同的功能。然而,我们不使用设计器并从UserControl
继承,而是仅使用代码并直接从TextBox
继承。
解决方案
当您想在组件中绘制另一个控件时,有三个方法需要重写和自定义
void OnParentChanged(EventArgs e)
void OnLocationChanged(EventArgs e)
void Dispose(bool disposing)
您可以阅读MSDN或其他文档网站上的相关信息,但您需要知道的是,在OnParentChanged(EventArgs e)
中,您将放置控件的创建和绘制代码。例如:
protected override void OnParentChanged(EventArgs e)
{
// this one is mandatory, unless you enjoy exceptions :-)
if (this.Parent != null)
{
_control = new SomeControl(); // creation of our control
this.Parent.Controls.Add(_control); // adding to container
setCoordsAndOtherStuff(); // compute Top Left Width Height
}
base.OnParentChanged(e); // call method from base-class
}
第二个,OnLocationChanged(EventArgs e)
,很明显。当位置改变时,您希望重新定位您的控件。
protected override void OnLocationChanged(EventArgs e)
{
setCoordsAndOtherStuff(); // compute Top Left Width Height
base.OnLocationChanged(e); // call method from base-class
}
第三个并不是真正的强制要求,因为您的组件在没有它的情况下也能工作,但是一旦您从设计器中删除它,其他组件将不会被删除。问题是,您无法选择它们,因为从设计器的角度来看,它们并不存在:-)。Dispose
代码在*.designer.cs文件中,但我总是喜欢将其移到我的主*.cs文件中。
protected override void Dispose(bool disposing)
{
if (_control != null)
_control.Dispose(); // make sure it's not null and dispose it
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}
就是这样。接下来,我们将创建一个真正的组件,一个带标签的文本框。:)
右侧带有标签的文本框 - LabeledTextBox
这是组件的完整源代码。
public partial class LabeledTextBox : TextBox
{
public LabeledTextBox()
{
InitializeComponent();
}
// our label
protected Label _label = null;
// caption of our label
protected string _LabelText = "";
// space between editbox and label
protected int _offset = 5;
public int offset
{
get { return _offset; }
set
{
_offset = value;
setControlsPosition(); // re-position
}
}
public string LabelText
{
get { return _LabelText; }
set
{
_LabelText = value;
setControlsPosition(); // re-position
}
}
// notice that I make this method virtual so I can enhance this to
// position more controls as I will be extending this class
protected virtual void setControlsPosition()
{
if (_label != null)
{
// setting text
_label.Text = _LabelText;
// autosize is important cause it saves us a lot of code
_label.AutoSize = true;
// little bit to the right
_label.Left = this.Left - _label.Width - _offset;
// and little bit below top
_label.Top = this.Top + 3;
}
}
protected override void OnParentChanged(EventArgs e)
{
if (this.Parent != null)
{
// create label
_label = new Label();
// add to form
this.Parent.Controls.Add(_label);
setControlsPosition();
}
base.OnParentChanged(e);
}
protected override void OnLocationChanged(EventArgs e)
{
setControlsPosition();
base.OnLocationChanged(e);
}
protected override void Dispose(bool disposing)
{
if (_label != null)
_label.Dispose();
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}
}
我认为最重要的事情是记住,在执行set
部分时,“重新加载”组件的位置,这样每次更改属性时,标签都会重新定位以适应。当然,您需要重写一些事件,如Enable
、Visible
,以使其看起来更豪华,但这基本上是入门的基础。现在,我们将转向下一个类,它是一个带有按钮的LabeledText
。
带按钮的LabeledTextBox:继承自我们刚创建的组件。
这稍微复杂一些,因为我们需要创建一个响应按钮点击的事件。我们无法直接做到这一点,因为设计器只看到我们的TextBox
,对它来说,Label
和Button
并不存在。所以,我们必须做一点技巧...
C#的一个优点是我们可以通过使用+=
运算符向组件添加事件。当然,我们必须创建一个执行所需工作的类。
// *STEP 1* create this method
protected virtual void OnButtonClick(object sender, EventArgs e)
{
// just to test the event
MessageBox.Show("You have just clicked you button");
}
// and in overrided "position" function
...
// *STEP 2* link method and event
// 27.X.2008 put this below adding component to Parent (OnParentChanged)
// so it will execute only once
_button.Click += new EventHandler(OnButtonClick);
所以现在,我们有了一个在按钮被点击时会触发的类,但由于我们想将这个事件转移到我们的组件中,我们必须创建一个delegate
和一个event
。
// arguments that will be used for our event *STEP 3*
public delegate void ButtonClickDelegate(object sender, EventArgs e);
// name of our event, look for it in events section of properties *STEP 3*
public event ButtonClickDelegate LabeledButtonClick;
然后,我们修改的OnButtonClick
protected virtual void OnButtonClick(object sender, EventArgs e)
{
// just to test the event
MessageBox.Show("You have just clicked you button");
//event with delegate arguments, this method will be
//implemented when you double click on event
LabeledButtonClick(sender, e); // LabeledButtonClick *STEP 4*
}
所以请记住,如果您尝试将事件从一个控件转移到另一个控件
- 创建一个具有与事件相同参数的类。
- 将此类的事件处理程序添加到您正在从中转移的控件。
- 为要转移到的组件创建委托和事件。
- 在第1步创建的类中触发事件。
如果现在构建这个,我们将在事件部分看到这个:

无论您在该函数中实现什么,当您按下按钮时都会触发。这是完整的类。
public partial class LabButtonEdit : LabeledTextBox
{
public delegate void ButtonClickDelegate(object sender, EventArgs e);
public event ButtonClickDelegate LabeledButtonClick;
public LabButtonEdit()
{
InitializeComponent();
}
protected Button _button;
protected bool _drawButton = false;
protected string _buttonText = "";
public bool drawButton
{
get { return _drawButton; }
set
{
_drawButton = value;
setControlsPosition();// re-position both controls
}
}
public string buttonText
{
get { return _buttonText; }
set
{
_buttonText = value;
setControlsPosition(); // // re-position
}
}
protected override void OnParentChanged(EventArgs e)
{
if (this.Parent != null)
{
_button = new Button();
this.Parent.Controls.Add(_button);
// add new event and make it fire my method
_button.Click += new EventHandler(OnButtonClick);
setControlsPosition(); // overridden method
}
base.OnParentChanged(e);
}
protected override void OnLocationChanged(EventArgs e)
{
setControlsPosition();
base.OnLocationChanged(e);
}
protected override void setControlsPosition()
{
base.setControlsPosition(); // positioning Label
if (_button != null)
{
_button.Text = _buttonText;
_button.Left = this.Left + this.Width + _offset;
_button.Top = this.Top;
_button.Height = this.Height;
// this is used to measure string width in pixels
Graphics g = this.CreateGraphics();
// button width is calculated so it can fit Text
_button.Width = (int)(g.MeasureString
(_buttonText, _button.Font)).Width + 15; nicely
g.Dispose(); // we don't need graphics anymore
if (!_drawButton)
{
_button.Visible = false;
}
else
{
_button.Visible = true;
}
// 27.X.2008 EVENT ADDITION CODE MOVED TO OnParentChanged()
}
}
protected virtual void OnButtonClick(object sender, EventArgs e)
{
// just to test the event
MessageBox.Show("You have just clicked you button");
LabeledButtonClick(sender, e);
}
protected override void Dispose(bool disposing)
{
if (_button != null)
_button.Dispose();
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}
}
如何使用它
- 将zip文件解压到某个目录。
- 创建一个新的Windows Forms项目并保存。
- 按照这些图片中的步骤操作...
- 尽情享用!
关注点
我在日常工作中经常使用这类组件。带标签的ComboBox
、DateTimePicker
,各种各样的东西。对我来说……这确实节省了很多时间。我很想听听你们(男士或女士)对此的看法。非常欢迎反馈。
谢谢
感谢Ralf Jansen - 他提供了添加控件的正确事件。
历史
- 24.X.2008 - v1.0
- 27.X.2008 - v1.1 - 修复了事件在每次位置方法触发时都会添加的bug,现在事件是在创建按钮之后添加的。