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

AquaButton: A sample custom button control with a Mac OS X look

, 11 Sep 2002
Rate this:
Please Sign up or sign in to vote.
Sample custom button control to help you write your own custom controls

Sample Image - AquaButton.png

Introduction

You can learn a lot about .NET Windows Forms programming by building a custom control. There are several books on the topic, but you'll soon find yourself reaching for Google to answer questions about Forms, GDI+, and Visual Studio you don't even know how to ask. When you find answers, they will be frustratingly incomplete.

What better way to learn?

That's how it went for me when I wrote Aqua Button. Since this was a learning project and I wasn't bound by practicality, I set out to build a button that looks and feels like push buttons in Apple Mac OS X. Apple's user interface is called Aqua®, and it's alive with transparent, colorful controls. Aqua buttons and Windows buttons have some things in common, but they also have several rather large differences:

  • Aqua buttons pulse when they are the default button
  • Aqua buttons are not in the tab order
  • Aqua buttons generally do not have keyboard equivalents
  • Aqua buttons do not press down when clicked -- they change color instead

So, it's safe to say that AquaButton won't satisfy Windows interface guidelines. But it may help you make that leap from using Windows Forms controls to designing and building your own custom controls.

Drawing the 3D button

AquaButton has a 3D look with text shadows, button shadows, and highlights. While it may be possible to recreate this look with GDI+ in OnPaint, I took the easier path and created the button bitmaps in Photoshop. I used PixelJerk's Photoshop action to create my initial source bitmap, then removed the background layer and merged the remaining layers to make the button partially transparent. I sliced that bitmap into three segments: a left end cap (left.png), a right end cap (right.png), and a single-pixel column from the middle (fill.png). Each time AquaButton paints itself, it uses DrawImage to quickly draw the two end caps, and FillRectangle to fill in the body. This means that you can set the width of AquaButton, but not the height.

If you need taller or thinner buttons, replace the source bitmaps with your own, then set the ButtonHeight class constant to the height of your bitmap. If your bitmaps have a shadow, set the ButtonShadowOffset class constant so that it specifies the distance from the bottom of the button to the bottom of the image. AquaButton uses this last constant to center the label on the button.

Aqua buttons are aqua-colored when they are the default button (specified with the Form.AcceptButton property). Non-default buttons draw in grayscale. I didn't need to manage separate source bitmaps just to draw the button in grayscale -- it's easy enough to draw the button in grayscale using GDI+ ImageAttributes. AquaButton declares ImageAttribute and ColorMatrix variables for each state:

   protected ImageAttributes iaDefault, iaNormal;

   protected ColorMatrix cmDefault, cmNormal;

I setup the image attributes and color matrices in InitializeGraphics. I use the cmDefault color matrix to make the button lighter (you'll see why in a minute, when I explain how I use gamma correction to simulate the pulse effect):

   // lighten the default image by reducing opacity
   cmDefault = new ColorMatrix();
   cmDefault.Matrix33 = 0.5f;
   iaDefault = new ImageAttributes();
   iaDefault.SetColorMatrix( cmDefault, ColorMatrixFlag.Default,
                             ColorAdjustType.Bitmap );

and I use the cmNormal color matrix to desaturate and lighten the image:

   // desaturate the normal image
   cmNormal = new ColorMatrix();
   cmNormal.Matrix00 = 1/3f;
   cmNormal.Matrix01 = 1/3f;
   cmNormal.Matrix02 = 1/3f;
   cmNormal.Matrix10 = 1/3f;
   cmNormal.Matrix11 = 1/3f;
   cmNormal.Matrix12 = 1/3f;
   cmNormal.Matrix20 = 1/3f;
   cmNormal.Matrix21 = 1/3f;
   cmNormal.Matrix22 = 1/3f;

   // lighten the normal image by reducing opacity
   cmNormal.Matrix33 = 0.5f;

   iaNormal = new ImageAttributes();
   iaNormal.SetColorMatrix( cmNormal, ColorMatrixFlag.Default, 
     ColorAdjustType.Bitmap );

Now all I have to do is draw the three button bitmaps (left.png, right.png, and fill.png) using iaDefault or iaNormal, which is a parameter to DrawButtonState:

   protected virtual void DrawButtonState (Graphics g, ImageAttributes ia)
   {
      TextureBrush tb;

      // Draw the left end cap
      g.DrawImage( imgLeft, rcLeft, 0, 0, imgLeft.Width, imgLeft.Height, 
                  GraphicsUnit.Pixel, ia );

      // Draw the right end cap
      g.DrawImage( imgRight, rcRight, 0, 0, imgRight.Width, imgRight.Height, 
                  GraphicsUnit.Pixel, ia );

      // Draw the middle
      tb = new TextureBrush( imgFill, new Rectangle( 0, 0, 
                                   imgFill.Width, imgFill.Height ), ia );
      tb.WrapMode = WrapMode.Tile;  

      g.FillRectangle ( tb, imgLeft.Width, 0, 
                        this.Width - (imgLeft.Width + imgRight.Width), 
                        imgFill.Height);

      tb.Dispose( );
   }

That's all there is to drawing AquaButton in it's basic states. With just a little more code, we can extend this to make AquaButton pulse.

Making the button pulse

Aqua buttons pulse with a glow that seems to originate inside the button. I considered using a GIF-like animation with a sequence of bitmaps showing the button in several intermediate states of illumination, controlled by a timer. While this would allow me to create realistic lighting in Photoshop, I would need many intermediate bitmaps to create a fluid animation.

I decided instead to use Gamma Correction, a simpler technique that sacrifices some lighting quality. Earlier I showed you how I lightened up the default and normal button images using a ColorMatrix. I did this so that I can use gamma correction to draw lighter (1.8 gamma) and darker (0.7 gamma) versions of the image using gamma correction. Change PulseGammaMin and PulseGammaMax if these look too light or dark.

This is how it works. AquaButton starts a timer to invalidate itself every 70 milliseconds (PulseInterval). On each timer tick, AquaButton uses gamma correction to draw itself progressively lighter or darker, with almost seamless transitions. My first attempt looked more like blinking than pulsing -- the button bounced almost immediately from light to dark. So I added logic to slow the lighting change as it approaches min or max gamma. If you're not happy with the way it looks, tune the PulseGammaShift, PulseGammaReductionThreshold, and PulseGammaShiftReduction constants. Here is the gamma shift logic from TimeOnTick:

if ((gamma - Button.PulseGammaMin < Button.PulseGammaReductionThreshold ) || 
    (Button.PulseGammaMax - gamma < Button.PulseGammaReductionThreshold ))
    gamma += gammaShift * Button.PulseGammaShiftReduction;
else
    gamma += gammaShift;

if ( gamma <= Button.PulseGammaMin || gamma >= Button.PulseGammaMax )
    gammaShift = -gammaShift;

Now all we have to do is update the ImageAttributes with the new gamma value and repaint the button. In Aqua, only the default button pulses, so I just need to update iaDefault:

iaDefault.SetGamma( gamma, ColorAdjustType.Bitmap );

Invalidate( );
Update( );

Supporting Visual Design

AquaButton exposes several properties to support the Visual Studio designer:

  • Pulse - determines whether an AquaButton pulses when it is the default button.
  • SizeToLabel - determines whether AquaButton automatically sets it's width based on it's label. Set this to true, then set the button label. The AquaButton will automatically size itself at design time.

AquaButton also shadows several properties from System.Windows.Forms.Control:

  • Size - AquaButtons have a fixed height, so it doesn't make sense to allow you to set Size (which includes Height) in the Visual Studio property grid. For lack of a better solution, I hid this property from the Visual Studio property grid using a custom designer (see below).
  • Height - AquaButtons don't reveal their Size property, so you need another way to see Height. I shadowed Control.Height and made it browsable and read only in the property grid.
  • Width - As with Height, I shadowed this property and made it browsable in the property grid. You decide whether to set width explicitly, or use SizeToLabel to automatically size the button.

I also wrote a custom designer, Wildgrape.Aqua.Controls.ButtonDesigner, to filter out properties that don't make sense for AquaButton: AllowDrop, BackColor, BackgroundImage, ContextMenu, FlatStyle, ForeColor, Image, ImageAlign, ImageIndex, ImageList, Size, and TextAlign. I did this to simplify visual design, but I did not bother to shadow them to prevent callers from setting them in code.

Extending AquaButton

I've already mentioned a few ways to customize AquaButton. If you're looking for a learning project, here are a few ideas.

AquaButton looks like an Aqua button, but behaves differently when it comes to selection. You could extend AquaButton to implement these missing behaviors to make AquaButton more faithful to the Aqua look and feel:

  • Aqua buttons are not in the tab order, but AquaButton leaves that decision to you.
  • Aqua buttons do not receive focus, so the default button is always the default button, and pulses even when another control has focus. AquaButton inherits .NET button selection behavior, which means you can make another button the default button simply by tabbing or mousing to it.

Or you could go the other way and make AquaButton behave more like .NET Windows Forms buttons:

  • Add focus hints
  • Make the selected button pulse (even if it isn't the default button)
  • Allow users to set the button's height (one reader has suggested a solution -- see the feedback for this article)

I'm interested to see how you extend AquaButton. I would be happy to post your enhancements here and give you credit.

References

  1. CodeProject, www.codeproject.com
  2. GotDotNet, www.gotdotnet.com
  3. Microsoft .NET Windows Forms news groups, microsoft.public.dotnet.windowsforms and microsoft.public.dotnet.windowsforms.controls
  4. Microsoft Developer Network, msdn.microsoft.com
  5. Apple Computer, Aqua, www.apple.com/macosx/technologies/aqua.html
  6. Apple Computer, Aqua Human Interface Guidelines, developer.apple.com/techpubs/macosx/Essentials/AquaHIGuidelines/
  7. Charles Petzold, Programming Microsoft Windows with C#, Microsoft Press, 2002
  8. Richard L. Weeks, .NET Windows Forms Custom Controls, SAMS Publishing, 2002
  9. Ted Faison, Component-Based Development with Visual C#, M&T Books, 2002
  10. Andrew Troelsen, C# and the .NET Platform, Apress, 2001

Credits

AquaButton is an independent creation and has not been authorized, sponsored, or otherwise approved by Apple Computer, Inc. Aqua is a trademark of Apple Computer, Inc.

Revisions

  • September 12, 2002 - Readers pointed out that the button wasn't forwarding Click events to the form. The problem was that I was doing too much in the mouse tracking code, and not giving the base class a chance to process events. After experimenting with Button events, I rewrote the mouse tracking logic and made it much simpler.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here

About the Author

No Namegiven
Web Developer
United States United States
No Biography provided

Comments and Discussions

 
QuestionThe type 'Wildgrape.Aqua.Controls.Button' has no property named 'Size' [modified] PinmemberDEGT19-Mar-12 11:13 
GeneralNatura Cleanse Pinmemberrandapetitt2-Sep-10 3:02 
GeneralFantastic control, here are some enhancements to the code PinmemberRob241224-Dec-09 2:26 
1. To overcome the two fires on mouse click
========================================
 
protected bool mouseClicked = false;
 
public bool Clicked
{
get { return mouseClicked; }
}
 
protected override void OnMouseClick(MouseEventArgs e)
{
base.OnMouseClick(e);
mouseClicked = true;
}
 
modify:
protected override void OnMouseUp(MouseEventArgs e)
{
base.OnMouseUp(e);
if (mousePressed)
{
mousePressed = false;
mouseClicked = false; // <- add this line
 
StartPulsing();
 
Invalidate();
Update();
}
}
 
2. Multiple colors
===============
 
load the blue png's into photoshop and overlay it with a new color, ocapacy 42% worked fine with me, save and load into the .resx file
 
add the following code (BTW, I made some changes to the namespace and resource file)
 
protected bool green = false;
 
public bool Green
{
get { return green; }
set
{
green = value;
if (green)
{
LoadImages_Green();
pulse = false;
StopPulsing();
Invalidate();
Update();
}
else
{
pulse = false;
StopPulsing();
LoadImages();
Invalidate();
Update();
}
}
}
 
protected virtual void LoadImages_Green()
{
imgLeft = CustomControls.Resource.left_green;
imgRight = CustomControls.Resource.right_green;
imgFill = CustomControls.Resource.fill_green;
}
Questionvista compatibility Pinmembershash4evr11-May-07 21:09 
AnswerRe: vista compatibility PinmemberAlpha Nerd2-May-08 8:19 
NewsRefer MAC-UI suite for better improvement PinmemberHenry Jane7-Jun-06 16:40 
GeneralRe: Refer MAC-UI suite for better improvement PinmemberCheckov17-Sep-09 23:48 
GeneralCan't set the widthe when SizeToLabel is false PinmemberDave Midgley26-Feb-06 4:15 
GeneralRe: Can't set the widthe when SizeToLabel is false PinmemberReallyTallPaul25-May-09 12:18 
Questionhow do I implement this in my application Pinmemberguyrobertbastien29-Nov-05 15:36 
QuestionWhat a great control. Added some functions and became the best i have ever found Pinmembermayprog15-Nov-05 9:54 
AnswerRe: What a great control. Added some functions and became the best i have ever found Pinmembermayprog15-Nov-05 10:47 
AnswerRe: What a great control. Added some functions and became the best i have ever found Pinmemberrsmseymou16-Jan-06 6:15 
AnswerRe: What a great control. Added some functions and became the best i have ever found PinmemberBruno R. Figueiredo1-Feb-06 0:34 
AnswerRe: What a great control. Added some functions and became the best i have ever found Pinmemberbabamara5-Jul-08 5:19 
GeneralClick event is called twice PinmemberKisilevich Slava9-Oct-05 3:49 
QuestionWhy it hasn't a mousemove behave? Pinmemberryowu20-Sep-05 16:49 
GeneralThis buttom is really great PinmemberDiogo Alves1-Aug-05 5:03 
Generalstrange behaviour when created on top of a transparent panel Pinsussluozhan116-Mar-05 19:16 
GeneralDisable color Pinmembergabis12-Feb-05 20:45 
GeneralSizable Pinmemberhunterb10-Feb-05 16:37 
GeneralRe: Sizable Pinmembertimtam5428-Jul-06 15:14 
GeneralAqua buttoms with tab's forms Pinmembernew_eng_0725-Jan-05 4:02 
Generalwow it looks great ! Pinmember^^o000o^^22-Dec-04 5:46 
GeneralDemo just crashes PinmemberJohnnyfartpants10-Dec-04 20:06 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Mobile
Web02 | 2.8.140721.1 | Last Updated 12 Sep 2002
Article Copyright 2002 by No Namegiven
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid