Introduction
I recently was involved in a project for the TabletPC where we had keep in mind that the user of the application would be using a pen to navigate the forms instead of the mouse. In addition, our application was going to be on the tablet in the hands of users with little to no Microsoft Windows exposure. We needed a controlled, idiot-proof, environment that was also friendly with a pen.
We decided to create our own 'skin' for the application, similar to many media and MP3 players available. Therefore, we needed buttons that could handle the four states of user interaction:
- Normal (Inactive)
- Mouse Over
- Pressed (Active)
- Disabled
Control Architecture
The controls are implemented as derived classes from one another and one base class, which extend the .NET Framework's System.Windows.Forms.Button
class. All controls are compatible with the windows forms designer in Visual Studio.
Images
Naturally, for a graphical button to work you must provide the graphics for it to use. There is a property for the graphic to be used for each of the states indicated above, as a System.Drawing.Image
object. All the GraphicControls
classes require at least the NormalImage
to be assigned, or else an exception will be thrown. If one of the states does not have an image assigned, the controls default to displaying the normal state image. I chose not to implement these classes using image indexing, because it was easier for our Adobe PhotoShop graphics developer to manage the numerous images using seperate images. These can be easily assigned using the forms designer or directly in the code.
Click Regions
Each control allows for the button to respond to mouse events from the entire control surface, or an optional ClickRegion
can be assigned using in your code to restrict the area of the control surface. When a mouse event occurs, the mouse position is checked against the figure created by the click region (in button window coordinates) to make sure it falls within the region before it is processed. This allows for buttons which are not the standard rectangle shape to respond as the user would expect, with no ghost actions outside of the obvious button graphic. There are several methods available to define the click region, but we found that the version accepting a Point[]
array was best because we could use the image map (intended for a web control) point coordinates generated by Adobe PhotoShop.
Button State Echo
Our design provided for multiple visual cues (buttons) to trigger an eaction or indicate a state change among those listed above. To allow these buttons to manage this relationship, all of them process and inherit the ability to echo a change in its own state to other interested buttons and to process the events triggered by other buttons it has interest in. This is done through the standard .NET event-handler model. Static methods are provided to manage the buttons interested in one another, EnableEchoButtonPair
and DisableEchoButtonPair
. This allows a change in state due to a mouse over event for one button to also mimic in those buttons subscribed to the GraphicButtonStateEcho
event. This makes the multiple visual cues react consistently regardless of which button is being manipulated by the end user. Note that the disable state is managed by the standard control property Enabled
, and is also processed as an echo only through a different event.
GraphicControls Button Classes
GraphicButtonBase
This is the abstract base class defining the fundamental methods and properties needed to manage a graphic-based button in this context. I defined this class in the event I needed to derive other graphic-based buttons which could not or should not inherit from the other controls, but I haven't found a need to yet.
GraphicButton
This is the graphic-based button which effectively mimics a standard Windows button. All that is needed is the relevant images assigned and it works the same. The property ButtonState
is available at run-time to query the current state of the button.
GraphicToggleButton
This control is derived from the GraphicButton
and alters the behavior so the button toggles itself on or off, like a light switch or toolbar setting. The toggle button requires one addition image, the PressedImage
, which is used to indicate the On or Active state. It also has an additional property Active
, to indicate whether or not the toggle button is Active or Inactive. It disables the Click
event, since it really doesn't make sense for this button and adds another event called Toggled
, which can be handled to be notified when the event toggles between the active and inactive states.
GraphicMultiButtonPanelButton
I have been arguing with myself on the name for this control, and finally gave up and settled on this one. This control mimics a selector panel, where only one button on the panel can be active at the same point in time. This control is derived from the GraphicToggleButton
, with an added twist that it automatically becomes inactive when one of the other buttons in the panel becomes active, it automatically changes itself to inactive. This control also only triggers the Toggled
event when it becomes active, so handlers do not get unnecessarily notified of the toggle buttons becoming inactive when only the new state of the selector panel is required. I used it to manage a task progression/selection bar in our application. I chose not to implement a logical or control container, so placement of the buttons can be anywhere within the application.
Demo and Sample Source
Looking at the snapshot of the demo application included you can see that the various feature available from these controls are shown. Below I have cut out a minimal piece of code to show what needs to be added to get the additional functionality above the standard Windows button. I also include the code generated by the forms designer for the control.
using System;
using System.Drawing;
using System.Collections;
using System.ComponentModel;
using System.Windows.Forms;
using System.Data;
namespace GraphicControlDemo
{
public class GraphicButtonsWithEcho : System.Windows.Forms.Form
{
.
.
.
private GraphicControls.GraphicToggleButton graphicToggleButton1;
private GraphicControls.GraphicToggleButton graphicToggleButton2;
private GraphicControls.GraphicMultiButtonPanelButton
graphicMultiButtonPanelButton1;
private GraphicControls.GraphicMultiButtonPanelButton
graphicMultiButtonPanelButton2;
.
.
.
public GraphicButtonsWithEcho()
{
//
// Required for Windows Form Designer support
//
InitializeComponent();
graphicMultiButtonPanelButton1.SetClickRegion(
new Point[] { new Point( 1, 2 ), new Point( 49, 1 ),
new Point( 77, 17 ), new Point( 89, 51 ),
new Point( 1, 52 ) } );
graphicMultiButtonPanelButton2.SetClickRegion( new Point[]
{ new Point( 1, 52 ), new Point( 12, 19 ),
new Point( 38, 4 ), new Point( 89, 1 ),
new Point( 88, 52 ) } );
graphicMultiButtonPanelButton1.AddPanelButton(
graphicMultiButtonPanelButton2 );
graphicMultiButtonPanelButton2.AddPanelButton(
graphicMultiButtonPanelButton1 );
//initialize one button unless the initial state should be
// undetermined
graphicMultiButtonPanelButton1.Active = true;
// alternate assignment method
// GraphicControls.GraphicMultiButtonPanelButton[] HandSelectors =
new GraphicControls.GraphicMultiButtonPanelButton[ 2 ];
// HandSelectors[0] = graphicMultiButtonPanelButton1;
// HandSelectors[1] = graphicMultiButtonPanelButton2;
// graphicMultiButtonPanelButton1.AddPanelButtons( HandSelectors );
// graphicMultiButtonPanelButton2.AddPanelButtons( HandSelectors );
}
.
.
.
#region Windows Form Designer generated code
/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
private void InitializeComponent()
{
.
.
.
this.graphicToggleButton1 =
new GraphicControls.GraphicToggleButton();
this.graphicToggleButton2 =
new GraphicControls.GraphicToggleButton();
this.graphicMultiButtonPanelButton1 =
new GraphicControls.GraphicMultiButtonPanelButton();
this.graphicMultiButtonPanelButton2 =
new GraphicControls.GraphicMultiButtonPanelButton();
.
.
.
//
// graphicToggleButton1
//
this.graphicToggleButton1.DisabledImage =
((System.Drawing.Bitmap)(resources.GetObject(
"graphicToggleButton1.DisabledImage")));
this.graphicToggleButton1.Location =
new System.Drawing.Point(320, 80);
this.graphicToggleButton1.Name = "graphicToggleButton1";
this.graphicToggleButton1.NormalImage =
((System.Drawing.Bitmap)(resources.GetObject(
"graphicToggleButton1.NormalImage")));
this.graphicToggleButton1.OverImage =
((System.Drawing.Bitmap)(resources.GetObject(
"graphicToggleButton1.OverImage")));
this.graphicToggleButton1.PressedImage =
((System.Drawing.Bitmap)(resources.GetObject(
"graphicToggleButton1.PressedImage")));
this.graphicToggleButton1.Size = new System.Drawing.Size(112, 23);
this.graphicToggleButton1.TabIndex = 10;
this.graphicToggleButton1.Toggled +=
new System.EventHandler(this.graphicToggleButton1_Toggled);
//
// graphicToggleButton2
//
this.graphicToggleButton2.DisabledImage =
((System.Drawing.Bitmap)(resources.GetObject(
"graphicToggleButton2.DisabledImage")));
this.graphicToggleButton2.Location =
new System.Drawing.Point(320, 48);
this.graphicToggleButton2.Name = "graphicToggleButton2";
this.graphicToggleButton2.NormalImage =
((System.Drawing.Bitmap)(resources.GetObject(
"graphicToggleButton2.NormalImage")));
this.graphicToggleButton2.OverImage =
((System.Drawing.Bitmap)(resources.GetObject(
"graphicToggleButton2.OverImage")));
this.graphicToggleButton2.PressedImage =
((System.Drawing.Bitmap)(resources.GetObject(
"graphicToggleButton2.PressedImage")));
this.graphicToggleButton2.Size = new System.Drawing.Size(112, 23);
this.graphicToggleButton2.TabIndex = 9;
this.graphicToggleButton2.Toggled +=
new System.EventHandler(this.graphicToggleButton2_Toggled);
//
// graphicMultiButtonPanelButton1
//
this.graphicMultiButtonPanelButton1.DisabledImage =
((System.Drawing.Bitmap)(resources.GetObject(
"graphicMultiButtonPanelButton1.DisabledImage")));
this.graphicMultiButtonPanelButton1.Location =
new System.Drawing.Point(0, 88);
this.graphicMultiButtonPanelButton1.Name =
"graphicMultiButtonPanelButton1";
this.graphicMultiButtonPanelButton1.NormalImage =
((System.Drawing.Bitmap)(resources.GetObject(
"graphicMultiButtonPanelButton1.NormalImage")));
this.graphicMultiButtonPanelButton1.OverImage =
((System.Drawing.Bitmap)(resources.GetObject(
"graphicMultiButtonPanelButton1.OverImage")));
this.graphicMultiButtonPanelButton1.PressedImage =
((System.Drawing.Bitmap)(resources.GetObject(
"graphicMultiButtonPanelButton1.PressedImage")));
this.graphicMultiButtonPanelButton1.Size =
new System.Drawing.Size(92, 55);
this.graphicMultiButtonPanelButton1.TabIndex = 1;
//
// graphicMultiButtonPanelButton2
//
this.graphicMultiButtonPanelButton2.DisabledImage =
((System.Drawing.Bitmap)(resources.GetObject(
"graphicMultiButtonPanelButton2.DisabledImage")));
this.graphicMultiButtonPanelButton2.Location =
new System.Drawing.Point(352, 88);
this.graphicMultiButtonPanelButton2.Name =
"graphicMultiButtonPanelButton2";
this.graphicMultiButtonPanelButton2.NormalImage =
((System.Drawing.Bitmap)(resources.GetObject(
"graphicMultiButtonPanelButton2.NormalImage")));
this.graphicMultiButtonPanelButton2.OverImage =
((System.Drawing.Bitmap)(resources.GetObject(
"graphicMultiButtonPanelButton2.OverImage")));
this.graphicMultiButtonPanelButton2.PressedImage =
((System.Drawing.Bitmap)(resources.GetObject(
"graphicMultiButtonPanelButton2.PressedImage")));
this.graphicMultiButtonPanelButton2.Size =
new System.Drawing.Size(92, 55);
this.graphicMultiButtonPanelButton2.TabIndex = 2;
//
// GraphicButtonsWithEcho
//
this.AutoScaleBaseSize = new System.Drawing.Size(5, 13);
this.ClientSize = new System.Drawing.Size(480, 374);
this.Controls.AddRange(new System.Windows.Forms.Control[] {
this.panel1,
this.button4,
this.button5,
this.label3,
this.button6,
this.graphicToggleButton1,
this.graphicToggleButton2,
this.button3,
this.button2,
this.label2,
this.label1,
this.graphicButton3,
this.button1,
this.graphicButton2,
this.graphicButton1});
this.Name = "GraphicButtonsWithEcho";
this.Text = "Graphic Buttons with Echo";
this.panel1.ResumeLayout(false);
this.ResumeLayout(false);
}
#endregion
.
.
.
private void button1_Click(object sender, System.EventArgs e)
{
// enable the echo of the button state
GraphicControls.GraphicButton.EnableEchoButtonPair(g
raphicButton1,graphicButton2);
button1.Enabled = false;
button2.Enabled = true;
button3.Enabled = true;
}
.
.
.
private void graphicToggleButton2_Toggled(object sender,
System.EventArgs e)
{
MessageBox.Show( this,
"GraphicToggleButton2 Toggled to " +
(graphicToggleButton2.Active ? "Active!" : "Inactive!"));
}
private void graphicToggleButton1_Toggled(object sender,
System.EventArgs e)
{
MessageBox.Show( this,
"GraphicToggleButton1 Toggled to " +
(graphicToggleButton1.Active ? "Active!" : "Inactive!"));
}
}
}
Using the Controls in Your Own Application with the Forms Designer
It is easy to use these controls with the forms designer. You just have to add them to the Windows Forms component tab of the Toolbox.
- First, open the Toolbox in the Windows Forms Designer. Then, from the context menu in the toolbox (right mouse button click), select the 'Customize Toolbox...' menu item.
- Select the tab named '.NET Framework Components' and press the 'Browse...' button.
- Locate and select the GraphicControls.dll.
- Back in the component list on the tab, locate the three components added with this DLL and check the checkbox next to them, then press the 'OK' button.
The controls will now be in the list for you to select from. I know they do not have a customized snazzy toolbox icon, but I'm not the graphics guy. Perhaps I will add them at a later time.
Open Issues
The only behavior I note with these controls is when the end user moves the mouse out of the control, sometimes the cursor momentarily appears to jump a little to the right. I seem to remember this problem from a long time ago, but I can't figure it out. I hope to have an answer soon from Microsoft.
References
- The idea for creating support for buttons without rectangular surfaces came from another CodeProject article by Ryan LaNeve, C# Windows Forms ImageMap Control.
- Programming Microsoft Windows with C#, by Charles Petzold, Microsoft Press 2002.