Click here to Skip to main content
Email Password   helpLost your password?

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:

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:

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

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:

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

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

You must Sign In to use this message board.
 
 
Per page   
 FirstPrevNext
GeneralFantastic control, here are some enhancements to the code
Rob2412
3:26 24 Dec '09  
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
shash4evr
22:09 11 May '07  
its look grate work
hwevr , i juz need to knw whehtr dis button component is vista compatible ?Smile

shawa

AnswerRe: vista compatibility
Alpha Nerd
9:19 2 May '08  
Yes.

People using Windows XP are still living in 2001.

NewsRefer MAC-UI suite for better improvement
Henry Jane
17:40 7 Jun '06  
I think refer to this MAC-UI suite, we can findout additional improvement ideas for this button. For example: Any Color for the button and text, button Styles, Different button colors and text colors for 4 states,better alignment and allocation for text and image...
MAC-UI suite contains also 30 controls for building MAC-UI style application on .NET. (http://www.econtechvn.com/en/macuisuite_detail.htm)
GeneralRe: Refer MAC-UI suite for better improvement
Checkov
0:48 18 Sep '09  
it also costs $285.00!!!!!! Frown :(
GeneralCan't set the widthe when SizeToLabel is false
Dave Midgley
5:15 26 Feb '06  
If I set SizeToLabel to false and specify the Width (125 in this case) in my design view the width of the control is not set when I run the program. Instead the width looks as if it defaults to ButtonDefaultWidth = 80.
This seems to be because when I alter the width in the design view no corresponding statement is put into the "Windows Form Designer generated code" section of my code. I would expect "this.AddAgentAquaButton.Width = 125;" to appear in my code. If I put this line in manually it works fine, but something is suppressing the generation of this code in the designer.
On further investigation this seems tp be caused because the Size property is suppressed in ButtonDesigner. If the line Properties.Remove( "Size" ); is commented out then the Size, and the Width, can be set.
I'd be interested to know if anyone else has had this problem, and what they did about it.

Dave
GeneralRe: Can't set the widthe when SizeToLabel is false
ReallyTallPaul
13:18 25 May '09  
I know that this thread is dead old and people have probably figured it out - but nobody's answered so ...

In the button.cs class create an int member variable called say ControlWidth, remove the width property code and replace with:

public int ControlWidth{
get {return controlWidth;}
set { controlWidth = value; base.Width = value;}
}

Recompile the project, reconnect to the new dll in the consumer project - It should work fine.
Questionhow do I implement this in my application
guyrobertbastien
16:36 29 Nov '05  
I am new to C#. I used the Wildgrape.Aqua.Controls.dll file but I don't know how to modify it so the click event doesn't fire twice. I really would like to know how to have that done. By The way the button Rocks

Let's do it better.
GeneralWhat a great control. Added some functions and became the best i have ever found
mayprog
10:54 15 Nov '05  
I spen quite a lot of time to make your contol having a transparent background.
Finaly i figured out the solution:
just make the following changes:
Just chnage OnPaint() with this:

protected override void OnPaint( PaintEventArgs e )
{
base.InvokePaintBackground(this, e);
Draw( e.Graphics );
}

Poke tongue


Mayprog
GeneralRe: What a great control. Added some functions and became the best i have ever found
mayprog
11:47 15 Nov '05  
Sleepy
Sorry, forgot to say to make the actuall button that that you oevrload to have a Transparent color
GeneralRe: What a great control. Added some functions and became the best i have ever found
rsmseymou
7:15 16 Jan '06  
Thank you, Mayprog! That was my major problem with the button. I was having to place it on a panel to get the transparency to work. Now I can use it like a normal button!Big Grin
QuestionRe: What a great control. Added some functions and became the best i have ever found
Bruno R. Figueiredo
1:34 1 Feb '06  
How can I make the Button transparent to a background image?
With the original code and with your change, the button set the transparency to the background color of the form but not to the background image... Any ideia?Confused

Bruno
GeneralRe: What a great control. Added some functions and became the best i have ever found
babamara
6:19 5 Jul '08  
Pleace excuse me,
I am new in c# but I think that:
g.Clear(Parent.BackColor);
function will copy the background color from upper left corner of the form. If the form is transparent in this corner the background color of the button will be different from the form one. So I use this code instead:
ButtonRenderer.DrawParentBackground(e.Graphics, ClientRectangle, this);
It works for me. No matter if the control is placed on colored part of the form or on a transparent part.
GeneralClick event is called twice
Kisilevich Slava
4:49 9 Oct '05  
Hi,

Click event is called two times when one clicks the button.

GeneralWhy it hasn't a mousemove behave?
ryowu
17:49 20 Sep '05  
I hope that when I move the cousor to the front of button, it can change color or do some other changes. Now, the effect is nothing.
Thanks and look forward your reply.
GeneralThis buttom is really great
Diogo Alves
6:03 1 Aug '05  
perhaps I found some good things that should be improved to make it perfect
-the duplicated events
-the transparency...(yes , because If you want to put the botton on a dark background it becames really ungly ( and unreadable );

Got nothing more to say, I will rated as 4.90 Stars

Keep Up the good working
Generalstrange behaviour when created on top of a transparent panel
luozhan1
20:16 16 Mar '05  
Hi, there is no doubt that the button looks great. However, if the button is created on top of a panel, and the panel's backcolor is set to transparent, the button's color looks really bad. Anyone knows how to solve this problem?

Thanks,
Leo
GeneralDisable color
gabis
21:45 12 Feb '05  
Very nice button.
The only thing I am missing is the disabled color which
will give the user the correct look of the button.
I would also add a pushed state (beside the color paint suggested below).
GeneralSizable
hunterb
17:37 10 Feb '05  
This button is pretty sweet, here is some of the work needed to make it sizable. seems to work decent.

protected virtual void DrawButtonState (Graphics g, ImageAttributes ia)
{
rcLeft.Height = this.Size.Height;
g.DrawImage( imgLeft, rcLeft, 0, 0,
imgLeft.Width, imgLeft.Height, GraphicsUnit.Pixel,ia);
rcRight.Height = this.Size.Height;
g.DrawImage( imgRight, rcRight, 0, 0,
imgRight.Width, imgRight.Height, GraphicsUnit.Pixel,ia);
int place = imgLeft.Width;
while(place < this.Size.Width - imgRight.Width)
{
g.DrawImage( imgFill,new Rectangle(place,0,imgFill.Width,
this.Size.Height),0,0,imgFill.Width, imgFill.Height, GraphicsUnit.Pixel,ia);
place = place + imgFill.Width;
}

}
GeneralRe: Sizable
timtam54
16:14 28 Jul '06  
Your fix seems to work for small buttons. However if the height is set to above about 40 the rounded edges do not make a smooth join onto the top and bottom edges. ie they interset at different angles.





timhams
GeneralAqua buttoms with tab's forms
new_eng_07
5:02 25 Jan '05  
first thanx for ur buttoms its really cool
second the problem is when there is a tap control inthe form it doesnt work and give me null refrence exciption

could u help me ??? thx alot

Smile
Generalwow it looks great !
^^o000o^^
6:46 22 Dec '04  
I am yet to try it out.


Cheers Blush
GeneralDemo just crashes
Johnnyfartpants
21:06 10 Dec '04  
I noticed that the demo sample came with a DLL called "Wildgrape.Aqua.Controls.dll" which I extracted into C:\Windows\System32. However, when I try to run the demo application under Windows XP, it just crashes, giving the message:- "Application has generated an exception that could not be handled. Process id=0xbd0 [3024] Thread id-0xbe4 [3044]". This happens every time, even after re-booting.

Do I maybe need some other DLL's that only ship with .Net? I'm not a .NET user. I've only got VC++ 6 installed at the moment.
GeneralRe: Demo just crashes
Johnnyfartpants
21:34 10 Dec '04  
Okay - I just figured it out.... The demo only works if the DLL is in the same folder as the running app. Presumably, this might make more sense once I start looking at the source code??
GeneralRe: Demo just crashes
Anonymous
4:23 14 Jul '05  
You can just register it in GAC insteadSuspicious


Last Updated 12 Sep 2002 | Advertise | Privacy | Terms of Use | Copyright © CodeProject, 1999-2010