65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.53/5 (9投票s)

2008年10月24日

MIT

5分钟阅读

viewsIcon

61370

downloadIcon

613

本文将引导您开始构建自己的多控件组件,而无需使用UserControl类。

引言

在完成了一些商业项目后,我萌生了在窗体上绘制重复控件的想法。我发现制作包含30多个控件的多个窗体是一项相当繁琐的任务。尤其是在制作包含大量文本字段的窗体时更是如此。您需要创建几个TextBox,并且每个TextBox都必须附带一个Label。更重要的是,每个LabelTextBox的距离必须相同,这样您的窗体才会显得整洁。在最糟糕的情况下,您必须为每个标签指定一个不错的名称,因为总会存在一种情况,即某个标签在您键入文本框中的信息时更改其文本,而您不想最终得到像“labelXXX”这样的名称。

在VC#.NET中,我们有一个构建多控件组件的机制,它被称为UserControl类。这基本上是一个容器,您可以在其中放置工具箱中的其他控件。然而,这种机制存在缺点。如果您创建标签和文本框并希望对所有文本框进行对齐,您将遇到大麻烦,您不能再使用引导了。

Problem with UserControl

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

Problem with 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部分时,“重新加载”组件的位置,这样每次更改属性时,标签都会重新定位以适应。当然,您需要重写一些事件,如EnableVisible,以使其看起来更豪华,但这基本上是入门的基础。现在,我们将转向下一个类,它是一个带有按钮的LabeledText

带按钮的LabeledTextBox:继承自我们刚创建的组件。

这稍微复杂一些,因为我们需要创建一个响应按钮点击的事件。我们无法直接做到这一点,因为设计器只看到我们的TextBox,对它来说,LabelButton并不存在。所以,我们必须做一点技巧...

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. 创建一个具有与事件相同参数的类。
  2. 将此类的事件处理程序添加到您正在从中转移的控件。
  3. 为要转移到的组件创建委托和事件。
  4. 在第1步创建的类中触发事件。

如果现在构建这个,我们将在事件部分看到这个:

This is what you will see in the design time

无论您在该函数中实现什么,当您按下按钮时都会触发。这是完整的类。

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);
    }
}

如何使用它

  1. 将zip文件解压到某个目录。
  2. 创建一个新的Windows Forms项目并保存。
  3. 按照这些图片中的步骤操作...
  4. 尽情享用!

关注点

我在日常工作中经常使用这类组件。带标签的ComboBoxDateTimePicker,各种各样的东西。对我来说……这确实节省了很多时间。我很想听听你们(男士或女士)对此的看法。非常欢迎反馈。

谢谢

感谢Ralf Jansen - 他提供了添加控件的正确事件。

历史

  • 24.X.2008 - v1.0
  • 27.X.2008 - v1.1 - 修复了事件在每次位置方法触发时都会添加的bug,现在事件是在创建按钮之后添加的。
© . All rights reserved.