Introduction
ImageButton
is a simple .NET class for a WinForm version of the web hover button, supporting an "idle" image, hover, and "down" image. It can be really useful when designing custom UIs in .NET.
Background
A general knowledge of overriding, inheriting, hiding, images, and attributes in .NET would be useful if you will be modifying the source. You'll need to know how to work with an image editor to create your buttons, or use a button generator off the web.
Using the Code
With Text in the Image
Before you start working with the code, you'll need to create a set of images - normal, hover, and down - all of which are optional, but all together combined make the best effect.
You'll create something like this:
Now add ImageButton.dll from bin/Release as a reference to your project, and drag the ImageButton
control to your form. You will have a PictureBox
-like control on your form.
Change the NormalImage
, HoverImage
, and DownImage
properties in the Property window to match the created set of images.
Without Text in the Image
You can create an image set without text in it so you will be able to set the text using the VS designer. This means you won't need to create a separate image-set for every button.
Note: There is no word wrap in the text feature, you'll have to add line-breaks (vbCrLf in VB or \n in C#) to the text yourself.
First, create an image set without the button text inside; something like this:
Then, like before, add the ImageButton
control and set the image properties, but this time, set the Text
property to the text you want to show, and the Font
property to the font you want to use.
How It Works
To create the control, I created my ImageButton
class, overriding the PictureBox
control and implementing IButtonControl
. Implementing IButtonControl
will allow the ImageButton
to be used, as any other button on the form, as a default button or a cancel button.
Mouse Methods
The concept is simple - we create an image and display it on screen. If the user hovers over the image, we swap the image to the hover image, and if the user holds his mouse down, we change the image to the depressed ("down") image.
So, for this purpose exists the following method overrides:
#region HoverImage
private Image m_HoverImage;
[Category("Appearance")]
[Description("Image to show when the button is hovered over.")]
public Image HoverImage
{
get { return m_HoverImage; }
set { m_HoverImage = value; if (hover) Image = value; }
}
#endregion
#region DownImage
private Image m_DownImage;
[Category("Appearance")]
[Description("Image to show when the button is depressed.")]
public Image DownImage
{
get { return m_DownImage; }
set { m_DownImage = value; if (down) Image = value; }
}
#endregion
#region NormalImage
private Image m_NormalImage;
[Category("Appearance")]
[Description("Image to show when the button is not in any other state.")]
public Image NormalImage
{
get { return m_NormalImage; }
set { m_NormalImage = value; if (!(hover || down)) Image = value; }
}
#endregion
private bool hover = false;
private bool down = false;
protected override void OnMouseMove(MouseEventArgs e)
{
hover = true;
if (down)
{
if ((m_DownImage != null) && (Image != m_DownImage))
Image = m_DownImage;
}
else
if (m_HoverImage != null)
Image = m_HoverImage;
else
Image = m_NormalImage;
base.OnMouseMove(e);
}
protected override void OnMouseLeave(EventArgs e)
{
hover = false;
Image = m_NormalImage;
base.OnMouseLeave(e);
}
protected override void OnMouseDown(MouseEventArgs e)
{
base.Focus();
down = true;
if (m_DownImage != null)
Image = m_DownImage;
base.OnMouseDown(e);
}
protected override void OnMouseUp(MouseEventArgs e)
{
down = false;
if (hover)
{
if (m_HoverImage != null)
Image = m_HoverImage;
}
else
Image = m_NormalImage;
base.OnMouseUp(e);
}
Let's go over them:
OnMouseMove
protected override void OnMouseMove(MouseEventArgs e)
{
hover = true;
if (down)
{
if ((m_DownImage != null) && (Image != m_DownImage))
Image = m_DownImage;
}
else
if (m_HoverImage != null)
Image = m_HoverImage;
else
Image = m_NormalImage;
base.OnMouseMove(e);
}
OnMouseMove
is called when the button is moving on the form. We avoided using OnMouseHover
, because of the delay before the method is called.
We set the hover boolean to true
for other methods to know when the mouse is hovering over the control. Then we check if the mouse button is down or not. If it is, we set the button image to the DownImage
if the down image is not null
and if the image is not already the down image.
If the mouse isn't being pressed, we check if a hover image exists - if it does, we set the image to the hover image, otherwise, we set it to the normal image.
On the last line, we call the Picturebox
's version of OnMouseMove
.
OnMouseLeave
protected override void OnMouseLeave(EventArgs e)
{
hover = false;
Image = m_NormalImage;
base.OnMouseLeave(e);
}
This needs a little explanation. :) If the mouse has left the bounds of the control, we set a switch about if the mouse is hovering over the button or not to false
, and set the image to the "normal" image. We let the PictureBox
take the job from there.
OnMouseDown
protected override void OnMouseDown(MouseEventArgs e)
{
base.Focus();
down = true;
if (m_DownImage != null)
Image = m_DownImage;
base.OnMouseDown(e);
}
If the mouse has pressed down on the control, we shift focus to the image button (this is not default behavior, so we must implement it) and set the down boolean to true
. If the down image has been set by the designer, we change the image to the down image. Then we call the picturebox
's own copy of OnMouseDown
.
OnMouseUp
protected override void OnMouseUp(MouseEventArgs e)
{
down = false;
if (hover)
{
if (m_HoverImage != null)
Image = m_HoverImage;
}
else
Image = m_NormalImage;
base.OnMouseUp(e);
}
The mouse button is no longer being pressed, so we set down
to false
. If we are hovering over the control and have let go, then we set the image to the hover image if it isn't nothing, otherwise, we set it to the "normal image". After that, we tell the PictureBox
to keep going and handle OnMouseUp
.
Null Values
As you may have noticed, we are checking if the hover and down images are null
or not before we change the image, but when dealing with NormalImage
, we don't. This is to prevent the hover/down images from sticking when the user isn't hovering or clicking on the control if the user didn't specify a normal image. In the demo application, this is demonstrated in Example F.
Text
The PictureBox
control has Text
and Font
properties as required by the Control
class, but they are not implemented and are hidden in the Property window. We can change this so that text is rendered:
[Browsable(true)]
[DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)]
[Category("Appearance")]
[Description("The text associated with the control.")]
public override string Text
{
get
{
return base.Text;
}
set
{
base.Text = value;
}
}
[Browsable(true)]
[DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)]
[Category("Appearance")]
[Description("The font used to display text in the control.")]
public override Font Font
{
get
{
return base.Font;
}
set
{
base.Font = value;
}
}
protected override void OnPaint(PaintEventArgs pe)
{
base.OnPaint(pe);
if ((!string.IsNullOrEmpty(Text)) && (pe != null) && (base.Font != null))
{
SolidBrush drawBrush = new SolidBrush(base.ForeColor);
SizeF drawStringSize = pe.Graphics.MeasureString(base.Text, base.Font);
PointF drawPoint;
if (base.Image != null)
drawPoint = new PointF(base.Image.Width / 2 - drawStringSize.Width / 2,
base.Image.Height / 2 - drawStringSize.Height / 2);
else
drawPoint = new PointF(base.Width / 2 - drawStringSize.Width / 2,
base.Height / 2 - drawStringSize.Height / 2);
pe.Graphics.DrawString(base.Text, base.Font, drawBrush, drawPoint);
}
}
protected override void OnTextChanged(EventArgs e)
{
Refresh();
base.OnTextChanged(e);
}
Overrides
The PictureBox
hides the Text
and Font
properties from the Property window. To get around this, we create "dummy" overrides for the Text
and Font
properties which simply set the base class properties, but assign them the Browsable
and DesignerSerializationVisibility
attributes which will tell the Designer to "notice" these properties.
Painting
Since the PictureBox
control from which we are inheriting does not render the text, we must add the code to paint the text on the button, in OnPaint
. First we call the PictureBox
base class method for OnPaint
, which handles painting of the image and everything else. After that, we paint the text on top of the painted image in the middle, by measuring the size of the Text
when in the selected Font
, and comparing it to the size of the ImageButton
, to find where to begin painting the text.
OnTextChanged
When the text of the control is changed, we must repaint the control. Therefore, we override the OnTextChanged
method, and in it add the call to the Refresh
method (which is inherited from PictureBox
), which repaints the button.
Hiding Properties
There are some properties which are not as useful for the ImageButton
as the PictureBox
, and we want to hide them from the Property window. To do this, we do the opposite of what we did with the Text
and Font
properties, using the code below:
[Browsable(false)]
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
public new Image Image { get { return base.Image; } set { base.Image = value; } }
[Browsable(false)]
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
public new ImageLayout BackgroundImageLayout {
get { return base.BackgroundImageLayout; }
set { base.BackgroundImageLayout = value; } }
[Browsable(false)]
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
public new Image BackgroundImage {
get { return base.BackgroundImage; }
set { base.BackgroundImage = value; } }
[Browsable(false)]
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
public new String ImageLocation {
get { return base.ImageLocation; }
set { base.ImageLocation = value; } }
[Browsable(false)]
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
public new Image ErrorImage {
get { return base.ErrorImage; }
set { base.ErrorImage = value; } }
[Browsable(false)]
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
public new Image InitialImage {
get { return base.InitialImage; }
set { base.InitialImage = value; } }
[Browsable(false)]
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
public new bool WaitOnLoad {
get { return base.WaitOnLoad; }
set { base.WaitOnLoad = value; } }
Description Changes
The SizeMode
and BorderStyle
properties mention PictureBox
instead of ImageButton
. To solve this, we simply use attributes and "dummy" properties to change the description of the properties with this code:
[Description("Controls how the ImageButton will handle
image placement and control sizing.")]
public new PictureBoxSizeMode SizeMode {
get { return base.SizeMode; }
set { base.SizeMode = value; } }
[Description("Controls what type of border the ImageButton should have.")]
public new BorderStyle BorderStyle {
get { return base.BorderStyle; }
set { base.BorderStyle = value; } }
IButtonControl
We must also implement IButtonControl
. This is simple to do, all we must do is implement the methods as such:
private bool isDefault = false;
private bool isDefault = false;
private DialogResult m_DialogResult;
public DialogResult DialogResult
{
get
{
return m_DialogResult;
}
set
{
m_DialogResult = value;
}
}
public void NotifyDefault(bool value)
{
isDefault = value;
}
public void PerformClick()
{
base.OnClick(EventArgs.Empty);
}
Keyboard Methods
We must implement keyboard events so that the user can use the spacebar and the enter key to "click" the button, just like any other button on any other Windows form.
private const int WM_KEYDOWN = 0x0100;
private const int WM_KEYUP = 0x0101;
private bool holdingSpace = false;
public override bool PreProcessMessage(ref Message msg)
{
if (msg.Msg == WM_KEYUP)
{
if (holdingSpace)
{
if ((int)msg.WParam == (int)Keys.Space)
{
OnMouseUp(null);
PerformClick();
}
else if ((int)msg.WParam == (int)Keys.Escape
|| (int)msg.WParam == (int)Keys.Tab)
{
holdingSpace = false;
OnMouseUp(null);
}
}
return true;
}
else if (msg.Msg == WM_KEYDOWN)
{
if ((int)msg.WParam == (int)Keys.Space)
{
holdingSpace = true;
OnMouseDown(null);
}
else if ((int)msg.WParam == (int)Keys.Enter)
{
PerformClick();
}
return true;
}
else
return base.PreProcessMessage(ref msg);
}
protected override void OnLostFocus(EventArgs e)
{
holdingSpace = false;
OnMouseUp(null);
base.OnLostFocus(e);
}
Simply put, we trap the message sent to the control if it is a key up or key down event. If it is, we check what key it is. If it's the enter key, we simply invoke a click event. If it's the spacebar, we hold the button down while it's being pressed until:
- the user lets go of the spacebar, in which case we perform a click, or
- the user presses Escape, Tab, or the control loses focus, in which case we do not invoke the click event
If it is not the spacebar and it is not the enter key, we let the PictureBox
base class method handle the message.
Demo Application
A demo application is included in the ZIP. Here's a screenshot:
Enjoy!