How To Build Multi-control Components when Inheriting from an Existing Control (Intro and TextBox Example)
Posted on 2010-10-26 20:28 gczhao 阅读(244) 评论(0) 编辑 收藏 举报本文转自http://www.codeproject.com/KB/edit/afpmultictrls.aspx
Introduction
The idea of drawing duplicate-controls on a form came to me after I did a few commercial projects. I found that making several forms with 30+ controls can be a pretty tiresome task. That is especially true when you are making a form with lots of text fields. You have to make several TextBox
es, and each of them must be accompanied by a Label
. More than that, every Label
must have the same distance to the TextBox
so your forms would look neat. In the worst case scenario, you have to assign each label with some nice name, because there can always be a situation where some label changes its text when you type some information on the text box and you don't want to end up with names like 'labelXXX'.
In VC#.NET, we have a mechanism to build multi-control components, and it's called the UserControl
class. This is basically a container where you can put other controls from the toolbox. However, this mechanism has disadvantages. If you make labels and textboxes and you want to align all the textboxes, you will have big problems, you cannot use leads anymore:
When using my technique, I can still use leads, despite the fact that I have a multi-control component:
Of course, there is a catch, and that is layouts... but since I'm not a big fan of layouts, I can live with that.
Background
If you ever wrote something in Delphi, you must have noticed that the VCL (Visual Component Library) has a control which is called LabeledEdit
. In this article, we will try to make the same thing in C# .NET. However, instead of using the designer and inheriting from UserControl
, we will use only code and inherit directly from TextBox
.
The Solution
When you want to draw another control in your component, there are three methods you have to override and customize:
void OnParentChanged(EventArgs e)
void OnLocationChanged(EventArgs e)
void Dispose(bool disposing)
You can read in MSDN or other documentation sites about these, but what you need to know is that on OnParentChanged(EventArgs e)
, you want to place the creation and drawing code of your control. For example:
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
}
The second one, OnLocationChanged(EventArgs e)
, is pretty obvious. When the location changes, you want to re-position your controls:
protected override void OnLocationChanged(EventArgs e)
{
setCoordsAndOtherStuff(); // compute Top Left Width Height
base.OnLocationChanged(e); // call method from base-class
}
The third one is not really mandatory because your component will work without this, but as soon as you delete it from the designer, the other components will not be deleted. The problem is, you cannot select them because from the designer point of view, they do not exist :-). The Dispose
code is in the *.designer.cs file, but I always like to move it to my main *.cs file.
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);
}
And that's it. Next, we're going to do a real component, a labeled textbox. :)
TextBox with Label on Right Side - LabeledTextBox
Here is the complete source of the component:
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);
}
}
I think the most important thing to remember is when you do the set
section, "reload" the component's position, so every time you change the property, the label will re-position to fit nicely. Of course, you have to override some events like Enable
, Visible
to make it look more deluxe, but this is pretty much basic to get you started. Now, we are going to move to the next class, which is a LabeledText
with a button.
LabeledTextBox with Button: Inherited from the Component We Just Made
This is a little more complicated because we have to make an event that will respond to the button click. We cannot do it directly because the designer only sees our TextBox
, for it, the Label
and Button
do not exist. So, we have to do a little trick...
One of the nice things in C# is the fact that we can add events to a component, simply by using the operator +=
. Of course, we have to make some method which will do work that must be done.
// *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);
So now, we have a method that will fire when a button is clicked, but since we want to transfer this event to our component, we have to make a delegate
and an 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;
And, our modified 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*
}
So remember, if you try to transfer events from one control to another:
- Make a method with the same parameters as the event.
- Add an event handler with this method to the control you are transferring from.
- Make a delegate and an event for the component you are transferring to.
- Fire an event in the method created in step 1.
If we build this now, we will see this in the event section:
And, whatever you implement in that function, it will be fired as you press the button. Here is the complete class:
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);
}
}
How To Use It
- Unpack the zip to some directory.
- Make a new Windows Forms project and save it.
- Follow the steps in these pictures...
- Enjoy!
Points of Interest
I am using these kinds of components in my every day work. Labeled ComboBox
es, DateTimePicker
s, all kinds of stuff. As for me... it really saves a lot of time. I'd love to hear what you guys (or girls) think about this. Feedback is always appreciated.