Click here to Skip to main content
Click here to Skip to main content

Rolling your own Custom-Drawn Button Control

, 24 Feb 2015 CPOL
Rate this:
Please Sign up or sign in to vote.
A completely Custom-Drawn ConfirmationButton Control for Windows Forms using C# and GDI+.

This article will be revised in the next few days. I'll be adding a few improvements which I'll list below. I have yet to include these improvements in the downloadable source, but that will be done this afternoon (AEST).

Here's a breakdown of what's to come:

  1.  In this article, support for TabStop will be added, and code will be added to indicate to the user that the control has GotFocus.

As stated near the end of the article, there are some things missing from this control. I will address these issues and ensure the source code remains up to date. Within the next few days I will talk about implementing IButtonControl and IDialogResult interfaces. By then, I'll have both VB and C# versions of this code and will discuss them both in great lengths as we move on to the next part in this series.

Introduction

I like re-inventing the wheel - that's right. Even if the finished wheel's tires are flat. I've always felt that sub-classing or even inheriting from other controls was more time-consuming and difficult (or maybe I just didn't want to do it because I wasn't in complete control). As I was updating iOS on my iPad, I was presented with some legal text that I had to "Agree" to before I could proceed with installation. I tapped Agree, and then it popped up another dialog asking me to confirm my previous action! This is very frustrating, especially now. You just can't do anything without either signing up for something or signing your life away [probably to the devil].

So I thought, what if I created a ConfirmationButton. It's kind of like a CheckBox and a Button all in one! You can Tick it (to say, Yes, I confirm - or - Yes, I agree), and then click the Button itself (to say, I confirm that this is what I want). Hopefully some many most all of you can see the benefit this could have for not only the developer, but also the user. It saves the developer time. Think about it... All you need is one Button, and one method (although there are other methods, which could be interesting so I will bring those up later). And for the user - well, I think it's a far-less frustrating alternative to a Button and a dialog which contains yet another Button - and more text to read.

I'd have to say that this was one of the more challenging projects I've worked on in recent years. It's the first time I've ever worked with GDI - and the first time I've created a custom control.

So, now you know what this is about. A distraction-free, less-frustrating way to confirm an action. This is a ConfirmationButton. Did I mention that it can also double-up as a plain old Button? Oh, and it's super-duper customizable, too!

Do you agree?

Background

The basic idea behind writing this ConfirmationButton is to allow for a distraction-free, less-frustrating way to confirm actions, sign up for things, submit forms and to allow more customization. The current Windows Forms Button does not offer much in the way of customization.

Using the code

You're probably bored to death by now, so let's take a quick look at the usage. This is how you use this button:

private void button_Confirmed(object sender, JTS.Controls.Button.ButtonConfirmedEventArgs arguments)
{
    Console.WriteLine("Confirmation acknowledged. Thank you.");
}

And if you're simply using this button as a regular button, then it behaves just like any other button:

private void button_Clicked(object sender, JTS.Controls.Button.ButtonClickedEventArgs arguments)

{

    Console.WriteLine("Stop clicking me!");

    button.Enabled = false; // Ha-ha... Now you can't click me anymore!

}

Accidental Invocation

That's right. And it may even seem like a lame excuse, and warrant somebody to say, "Well maybe you should be more careful!" Yeah. But that's not good enough. Accidents happen. Now let's try to prevent them. The standard Windows Forms Button control is an easy target. This is by no means a problem with the Windows Forms Button. It's just doing what it's designed to do; run some fancy code when it's clicked.

But what if a Button has focus, and you accidentally nudge the space-bar with your arm while you're resting your arms on the desk? Did you just agree to install SuperDuperAdDownloadingService when all you really wanted was to install that media player you just downloaded? Or did you fail to read and/or understand that you were about to install something you were unwilling to install, before clicking that Button?

A ConfirmationButton solves this problem. You cannot submit anything by accidentally clicking or tapping the Button, or the enter Button or the space-bar. This is because it's a ConfirmationButton. You click it once to say, "Yeah, whatever, let me in!" - and then you click it again to acknowledge that you know what you are doing, and are fine with it.

Getting Started

There are many things to take into consideration when developing a custom-drawn Button control - or any control for that matter. Some of which, are:

  • What do we want to achieve by writing this control?
  • How will we write this control?
  • What are the Pro's and Con's of writing and using this control?

It's those three questions that helped me decide whether or not I should even bother learning how to write my own Control from scratch, rather than just sub-classing or inheriting from another control. I won't go over the answers for those questions - I'll leave that up to you.

Now let's get started. Fire up Visual Studio and create a new Class Library project with C# as the target language and name it Controls - for simplicity's sake. Next, open Solution Explorer and find the Class file that Visual Studio has generated for us. In my case, it has a default name of Class1.cs. Delete it. Now right-click the Project Name in Solution Explorer and select Add Item > New Item > Custom Control - and name that Custom Control Button.cs You'll notice that when you open that file, our namespace is Controls, and our class is now Button - so when we reference this Button in future projects, we'll be doing something like:

Controls.Button button = new Controls.Button();

Or...

using Controls;

Let's get started on our base class:

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace Controls
{
  public partial class Button : Control
  {
    public Button()
    {
      InitializeComponent();
    }

    protected override void OnPaint(PaintEventArgs pe)
    {
      base.OnPaint(pe);
    }
  }
}

We'll start off by hooking up our event data. We do this so we know when the user interacts with our Button, and allows us to respond to these interactions accordingly. You should place the following code directly inside the base class:

public event EventHandler<ButtonConfirmedEventArgs> Confirmed;
public event EventHandler<ButtonClickedEventArgs> Clicked;
public event EventHandler<ButtonCheckedEventArgs> Checked;

public class ButtonConfirmedEventArgs : EventArgs
{
    public ButtonConfirmedEventArgs(MouseEventArgs e) { }
}

public class ButtonClickedEventArgs : EventArgs
{
    public ButtonClickedEventArgs(MouseEventArgs e) { }
}

public class ButtonCheckedEventArgs : EventArgs
{
    public ButtonCheckedEventArgs(Boolean buttonChecked, MouseEventArgs e) { }
}

Alright, now the fun stuff begins. We need some stuff so we can do some internal checks. I like to capitalize secret internal things:

/// <summary> 
/// This serves as a kind of backup of the original background color.
/// </summary>
protected internal Color INTERNAL_ORIGINAL_BACKGROUNDCOLOR { get; set; }
/// <summary>
/// Indicates whether the user has clicked inside the CheckBox.
/// </summary>
protected internal Boolean INTERNAL_BUTTON_CHECKFOR_CLICKEDINSIDECHECKBOX { get; set; }
/// <summary>
/// Indicates whether the button should flash.
/// </summary>
protected internal Boolean INTERNAL_BUTTON_SHOULDFLASH { get; set; }
/// <summary>
/// Indicates whether the user's action has been confirmed, and /// is "Set in stone".
/// </summary>
protected internal Boolean INTERNAL_BUTTON_CONFIRMATION_SETINSTONE { get; set; }
/// <summary>
/// The number of times the button should flash.
/// </summary>
protected internal Int32 INTERNAL_FLASHINGBUTTONTIMER_TIMESTOFLASH { get; set; }
/// <summary>
/// Gets or sets the number of times the Timer's Tick event has fired.
/// </summary>
protected internal Int32 INTERNAL_BUTTONFLASHTIMER_TICKCOUNT { get; set; }
/// <summary>
/// Gets or sets the time, in milliseconds between each Tick event.
/// </summary>
protected internal Int32 INTERNAL_BUTTONFLASHTIMER_INTERVAL { get; set; }

Now here are some important variables that we need to set up:

/// <summary>
/// Indicates whether the mouse is being held down on the control.
/// </summary>
private bool mouseDown;

/// <summary>
/// Indicates whether the mouse has entered the control.
/// </summary>
private bool mouseEntered;

/// <summary>
/// Indicates whether there is a need to check for Mouse Actions.
/// </summary>
private bool checkForMouseActions;

 

And don't forget the Flash variables for the Timer:

protected internal DateTime
    startFlashingTime,
    endFlashingTime;
protected internal DateTime[]
    changeFlashingTimes;

 

Properties Window

Now we're going to add some properties so that the user can customize the Button control through the Properties Window in Visual Studio:

[EditorBrowsable(EditorBrowsableState.Always), 
    Category("Behavior"), 
    Description(
    "Determines whether this button requires confirmation form the user."
    )
]
public Boolean ConfirmationRequired { get; set; }

[EditorBrowsable(EditorBrowsableState.Always), Category("Behavior"), Description("Determines whether the user has confirmed or agreed.")]
protected internal Boolean ButtonConfirmed { get; set; }

[EditorBrowsable(EditorBrowsableState.Always), Category("Appearance"), Description("The highlight color of the component, which is displayed when the user moves the cursor within the bounds of the control.")]
public Color HighlightColor { get; set; }

[EditorBrowsable(EditorBrowsableState.Always), Category("Appearance"), Description("The active color of the component when it is being clicked.")]
public Color ActiveColor { get; set; }

[EditorBrowsable(EditorBrowsableState.Always), Category("Appearance"), Description("The border color of the component.")]
public Color BorderColor { get; set; }

[EditorBrowsable(EditorBrowsableState.Always), Category("Appearance"), Description("The thickness of the component's borders.")]
public Int32 BorderThickness { get; set; }

[EditorBrowsable(EditorBrowsableState.Always), Category("Appearance"), Description("The background color of the component's checkbox when it is flashing.")]
public Color FlashColor { get; set; }

[EditorBrowsable(EditorBrowsableState.Always), Category("Appearance"), Description("The background color of the component when its state has been set to Confirmed.")]
public Color ConfirmedBackgroundColor { get; set; }

[EditorBrowsable(EditorBrowsableState.Always), Category("Appearance"), Description("The border color of the component when its state has been set to Confirmed.")]
public Color ConfirmedBorderColor { get; set; }

 

Now we need to update our Constructor:

public Button()
{
    InitializeComponent();

    // Reduce or prevent flickering.
    this.DoubleBuffered = true;

    INTERNAL_ORIGINAL_BACKGROUNDCOLOR = this.BackColor;

    INTERNAL_FLASHINGBUTTONTIMER_TIMESTOFLASH = 5;
    INTERNAL_BUTTONFLASHTIMER_INTERVAL = 200;
}

 

When the user resizes the parent window at runtime, and if the Button has its Anchor properties set, so that it resizes with the form, we'll need to ensure that the control repaints itself, otherwise it'll look horrible. So we need to override the OnSizeChanged method:

protected override void OnSizeChanged(EventArgs e)
{
    this.Refresh();
}

 

Now we'll handle the Mouse Events. These should be pretty self-explanatory:

private void Button_MouseEnter(object sender, EventArgs e)
{
    checkForMouseActions = true;
    mouseEntered = true;

    INTERNAL_ORIGINAL_BACKGROUNDCOLOR = this.BackColor;
    this.BackColor = HighlightColor;

    this.Refresh();
}

private void Button_MouseLeave(object sender, EventArgs e)
{
    checkForMouseActions = true;
    mouseEntered = false;

    if (!INTERNAL_BUTTON_CONFIRMATION_SETINSTONE)
        this.BackColor = INTERNAL_ORIGINAL_BACKGROUNDCOLOR;

    this.Refresh();
}

private void Button_Enter(object sender, EventArgs e)
{
    this.Refresh();
}

private void Button_Leave(object sender, EventArgs e)
{
    this.Refresh();
}

 

MouseDown Event

Essentially, this Button has two parts. Each part behaves as if it is its own control. There's the CheckBox on the left and the actual Button on the right. We need to know where the user's mouse is located when they are holding down the mouse button (i.e. Is the mouse within the bounds of the CheckBox?):

    private void Button_MouseDown(object sender, MouseEventArgs e)
    {
        if (e.Location.X >= 0 && e.Location.X <= 27 && e.Location.Y >= 0 && e.Location.Y <= this.Height)
        {
            INTERNAL_BUTTON_CHECKFOR_CLICKEDINSIDECHECKBOX = true;
        }
        else
        {
            INTERNAL_BUTTON_CHECKFOR_CLICKEDINSIDECHECKBOX = false;
            this.BackColor = ActiveColor;
        }

        checkForMouseActions = true;
        mouseDown = true;

        this.Focus();

        this.Refresh();
    }

 

MouseUp Event

The MouseUp Event is used to inform the code in the OnPaint method that the user has probably interacted with the Button. We also need to ensure that we set mouseDown to false at this point.

We must also check to see if the user has placed a tick in the CheckBox and clicked the Button. If the user has indeed Checked and Confirmed, then we need to convey that by changing the visuals of the Button. We can do this in a number of ways, howver, for this control, we'll do that by changing the BackgroundColor and BorderColor - and then we'll disable the control. Wait, what? you may ask... Why would you disable the control? Well, it's a ConfirmationButton. The ConfirmationButton is to be used where the user is about to agree to something pretty important, likely to be Terms of Use or a Privacy Policy.

In the real world, you can't just go back on your word after you've accepted the terms and been given access to a product or service - so why should this be any different? But still, the scenario and functionality is up to each individual developer as to what happens after this Button has been Confirmed, so you are by no means restricted to this view-point:

    private void Button_MouseUp(object sender, MouseEventArgs e)
    {
        checkForMouseActions = true;
        mouseDown = false;

        if (ConfirmationRequired && ButtonConfirmed)
        {
            if (e.Location.X >= 28 && e.Location.X <= this.Width && e.Location.Y >= 0 && e.Location.Y <= this.Height)
            {
                this.BackColor = ConfirmedBackgroundColor;
                this.Enabled = false;
            }
        }
        else
        {
            this.BackColor = HighlightColor;
            if (!this.Enabled) this.Enabled = true;
        }

        this.Refresh();
    }

 

MouseClick Event

Again, more checks:

    private void Button_MouseClick(object sender, MouseEventArgs e)
    {
        if (e.Location.X >= 0 && e.Location.X <= 27 && e.Location.Y >= 0 && e.Location.Y <= this.Height)
        {
            if (ButtonConfirmed)
            {
                ButtonConfirmed = false;
            }
            else
            {
                ButtonConfirmed = true;
            }

            this.Refresh();
        }
        else
        {
            if (ConfirmationRequired)
            {
                if (ButtonConfirmed)
                {
                    INTERNAL_BUTTON_CONFIRMATION_SETINSTONE = true;
                    this.Enabled = false;
                    this.BackColor = ConfirmedBackgroundColor;

                    Confirmed(this, new ButtonConfirmedEventArgs(e));

                    this.Refresh();
                }
            }

            this.Focus();

            if (Clicked != null)
            {
                Clicked(this, new ButtonClickedEventArgs(e));
            }
        }
    }

 

OnPaint Event

protected override void OnPaint(PaintEventArgs pe)
{
    int buttonWidth = this.Size.Width,
        buttonHeight = this.Size.Height;

    if (ConfirmationRequired)
    {
        if (ButtonConfirmed)
        {
            // Since confirmation is required, draw a checkbox. And since the user
            // has already ButtonConfirmed, draw the check mark, too.
            pe.Graphics.DrawLine(new Pen(BorderColor, BorderThickness), 27, 0, 27, buttonHeight);

            pe.Graphics.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.HighQuality;

            pe.Graphics.DrawLine(new Pen(ForeColor, BorderThickness),
                7, 15, 11, 18);

            pe.Graphics.DrawLine(new Pen(ForeColor, BorderThickness),
                11, 18, 18, 10);

            TextFormatFlags flags = TextFormatFlags.HorizontalCenter | TextFormatFlags.VerticalCenter | TextFormatFlags.EndEllipsis;
            TextRenderer.DrawText(pe.Graphics, this.Text, this.Font, new Rectangle(27, 0, this.Width - 27, this.Height), this.ForeColor, flags);

            if (checkForMouseActions)
            {
                checkForMouseActions = false;

                if (mouseDown)
                {
                    // It's also entered.
                    if (INTERNAL_BUTTON_CHECKFOR_CLICKEDINSIDECHECKBOX)
                    {
                        Brush brush = new SolidBrush(ActiveColor);
                        pe.Graphics.FillRectangle(brush, 1, 1, 26, this.Height - 1);

                        pe.Graphics.DrawLine(new Pen(ForeColor, BorderThickness),
                            7, 15, 11, 18);

                        pe.Graphics.DrawLine(new Pen(ForeColor, BorderThickness),
                            11, 18, 18, 10);

                        brush.Dispose();
                    }
                }
            }
        }
        else
        {
            // Draw a checkbox but do not draw a check mark because the user has yet to confirm.
            pe.Graphics.DrawLine(new Pen(BorderColor, BorderThickness), 27, 0, 27, buttonHeight);

            TextFormatFlags flags = TextFormatFlags.HorizontalCenter | TextFormatFlags.VerticalCenter | TextFormatFlags.EndEllipsis;
            TextRenderer.DrawText(pe.Graphics, this.Text, this.Font, new Rectangle(27, 0, this.Width - 27, this.Height), this.ForeColor, flags);

            if (checkForMouseActions)
            {
                checkForMouseActions = false;

                if (mouseDown)
                {
                    // It's also entered.
                    if (INTERNAL_BUTTON_CHECKFOR_CLICKEDINSIDECHECKBOX)
                    {
                        Brush brush = new SolidBrush(ActiveColor);

                        if (INTERNAL_BUTTON_SHOULDFLASH)
                        {
                            Brush flashBrush = new SolidBrush(FlashColor);
                            pe.Graphics.FillRectangle(brush, 1, 1, 26, this.Height - 1);

                            //flashBrush.Dispose();
                        }
                        else
                            pe.Graphics.FillRectangle(brush, 1, 1, 26, this.Height - 1);

                        brush.Dispose();
                    }
                    else
                    {
                        if (INTERNAL_BUTTON_CHECKFOR_CLICKEDINSIDECHECKBOX)
                        {
                            Brush brush = new SolidBrush(ActiveColor);

                            if (INTERNAL_BUTTON_SHOULDFLASH)
                            {
                                Brush flashBrush = new SolidBrush(FlashColor);
                                pe.Graphics.FillRectangle(brush, 1, 1, 26, this.Height - 1);

                                //flashBrush.Dispose();
                            }

                            brush.Dispose();
                        }
                    }
                }
            }
        }
    }
    else
    {
        // This is a Standard button. Do not draw a checkbox.

        TextFormatFlags flags = TextFormatFlags.HorizontalCenter | TextFormatFlags.VerticalCenter | TextFormatFlags.EndEllipsis;
        TextRenderer.DrawText(pe.Graphics, this.Text, this.Font, new Rectangle(0, 0, this.Width, this.Height), this.ForeColor, flags);

        if (checkForMouseActions)
        {
            checkForMouseActions = false;
        }
    }

    if (this.Enabled)
        pe.Graphics.DrawRectangle(new Pen(BorderColor, BorderThickness), 0, 0, buttonWidth - 1, buttonHeight - 1);
    else if (!this.Enabled && ConfirmationRequired && ButtonConfirmed)
    {
        pe.Graphics.DrawLine(new Pen(ConfirmedBorderColor, BorderThickness), 27, 0, 27, buttonHeight);

        pe.Graphics.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.HighQuality;

        pe.Graphics.DrawLine(new Pen(ForeColor, BorderThickness),
            7, 15, 11, 18);

        pe.Graphics.DrawLine(new Pen(ForeColor, BorderThickness),
            11, 18, 18, 10);

        pe.Graphics.DrawRectangle(new Pen(ConfirmedBorderColor, BorderThickness), 0, 0, buttonWidth - 1, buttonHeight - 1);
    }
}

 

Flash Timer

private void flashTimer_Tick(object sender, EventArgs e)
{
    if (changeFlashingTimes.Contains(DateTime.Now))
    {
        this.Refresh();
    }
}

 

Finally, we're done! You now have a working custom-drawn ConfirmationButton Control, which can double-up as a standard Button and can be customized.

 

The Result

 

The Three Important States

Default State:     Checked State:   Confirmed State:

              

 

Customization

The Properties Window now contains all of the properties you'll need in order to style this button. There are many other ways you could customize this button, and I'll be updating this article at some point with more features, however, I'll leave the rest up to you, for now.

 

Points of Interest

While I have been using this for a long time now, there is still much more room for improvement. At one point, I introduced a nice fade-in/fade-out hover effect for the Button, but that is now obsolete and soon to be replaced with something way better.

Any and all feedback is welcome and if you have any questions or have found a bug, I'll do my best to help. I also plan to regularly update and revise and improve this article and its code and samples, so watch out for future updates.

 

Next Up

Part 2 in this series, Rolling your own Custom-Drawn DropDownList Control, will guide you through writing your very own custom drawn DropDownList for Windows Forms. We'll build on the knowledge and code from this article and use it to build some really cool stuff, like being able to insert ImageItems and ControlItems into the DropDownList.

Here's a sneak preview of what's to come once this article has been revised:

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

Evil Iceblock
Software Developer
Australia Australia
No Biography provided

Comments and Discussions

 
-- There are no messages in this forum --
| Advertise | Privacy | Terms of Use | Mobile
Web02 | 2.8.150414.1 | Last Updated 24 Feb 2015
Article Copyright 2015 by Evil Iceblock
Everything else Copyright © CodeProject, 1999-2015
Layout: fixed | fluid