Why must I paint the form background for an owner-draw control?

T

Tim Crews

Hello:

I have subclassed Button to create an owner-drawn non-rectangular
button.

I had assumed that the parent form's background color or image would
have already been painted in the area to be occupied by the button,
before the button's OnPaint handler was called. I could then draw over
that existing background with the custom shape I desired for my button.

This is not what is happening. The region to be occupied by the button
is blacked out. I am forced to write code that paints the parent form's
background color or image into the button's area. This performs
especially badly if the parent form has a BackgroundImage. For example,
on a form that has 40 buttons, my 3GHz machine takes a couple of seconds
to draw the form. If I change the buttons back to rectangular, or if I
change the parent form to only have a BackgroundColor instead of a
BackgroundImage, or if I remove the button subclass code that paints the
parent form's background color/image in the button area, the performance
goes back to normal (i.e. drawing the form in the blink of an eye.)

Is the entire form's background painted, and then the button areas
blacked out, and then the button OnPaint event handlers called? If so,
is there a way to simply skip the second step? It is a shame for part
of the form to be drawn, then erased, only to be drawn back again. I
tried overriding the button subclass background paint event, but this
had no effect.

The only other possibility is that the form is partially painted, i.e.
the regions corresponding to the child controls are not painted when the
form background is painted. This would surprise me, since this would
not seem to result in the best performance. But if this is really what
is happening, then I guess I have no choice but to do as I am currently
doing.

Thank you for any advice,

Tim Crews
GECO, Inc.
 
S

Stoitcho Goutsev \(100\) [C# MVP]

Hi Tim,

Here what happens. For optimization purposes noramlly windows (I'm talking
about native Windows' windows and becuase WindForms are build over them it
applies for WinForms as well) have styles WS_CLIPCHILDREN and
WS_CLIPSIBLINGS, which emans that all area covered by the child widnows or
sibling windows is clipped off the device context during WM_PAIN. In other
words even if the form wants to draw the background for you this area under
the button is clipped and nothing goes on the screen. That's why you get
that black rectangle.
However setting those styles for the parrent window is not enough because
invalidation of a child control won't trigger the repaint of the parent. In
order to do that the child window has to have WS_EX_TRANSPARENT.

Anyway, WindowsForms has nicer solution because it supports transparent
colors for the control's background.

Just add those lines in your botton constructor and you will get the
transparent background

SetStyle(ControlStyles.UserPaint, true);
SetStyle(ControlStyles.SupportsTransparentBackColor, true);
this.BackColor = Color.FromArgb(0,SystemColors.Control);

However it seems like the button doesn't use transparency for the border,
though.
 
T

Tim Crews

Stoitcho Goutsev said:
SetStyle(ControlStyles.UserPaint, true);
SetStyle(ControlStyles.SupportsTransparentBackColor, true);
this.BackColor = Color.FromArgb(0,SystemColors.Control);


Stoitcho:

Thank you for your response. Unfortunately, this still isn't working
for me. I still get a black background around the owner-drawn control.

You mentioned that at the native Windows level, this behavior interacts
with the form's WS_CLIPCHILDREN style. Do I need to do something at the
form level to accomplish this?

I notice that the documentation of the SupportsTransparentBackColor says
that this is a "simulated" transparency. I wonder what this implies.
If the Windows Forms framework still tries to make an intelligent
decision as to where to paint the form background around the child
control, this probably means I need to set the owner-drawn button's
region so that Windows Forms knows where to paint. Since I currently
don't modify the button's region, as far as Windows Forms is concerned,
my button still occupies its entire bounding rectangle, so Windows Forms
doesn't think it needs to do any painting of the form background.

Do you think this is my problem? For various reasons, it will be quite
difficult to pre-compute the button's region before the button's OnPaint
is called.

I should also note that my buttons do not use SystemColors.Control as
their background color. All of my buttons are different colors,
assigned at design time. I use the assigned BackColor as the face color
of the button. So my constructor looks like this:

public FancyButton() : base()
{
// class member variables
PenWidth = 1;
ButtonPressed = false;

// Control styles
SetStyle(ControlStyles.UserPaint, true);
SetStyle(ControlStyles.SupportsTransparentBackColor, true);
SetStyle(ControlStyles.AllPaintingInWmPaint, true);
this.BackColor = Color.FromArgb(0,this.BackColor);
}

You can see that I also added "AllPaintingInWmPaint", because of advice
I have received elsewhere. This makes no difference to my problem,
however.

My OnPaint handler does nothing except draw a rounded-corner rectangle,
with a drop shadow under it. To repeat myself just in case I
miscommunicated the first time: neither the button face nor the shadow
of the button face occupies the entire bounding rectangle that was
specified for the button at design time. So the area that is within the
bounding rectangle but outside the owner-drawn button face / button
shadow shape needs to have the form's background.

Here is how I am currently handling this problem in the button's OnPaint
handler:

Rectangle ButtonRectangle = new Rectangle(0,0,Width,Height);

if (Parent.BackgroundImage == null)
{
SolidBrush SolBrush = new SolidBrush (Parent.BackColor);
e.Graphics.FillRectangle (SolBrush,ButtonRectangle);
}
else
{
// Note: "Bounds" coordinates are relative to parent
// form, while ButtonRectangle coordinates are relative
// to the button.
TextureBrush TexBrush = new TextureBrush
(Parent.BackgroundImage, Bounds);
e.Graphics.FillRectangle (TexBrush,ButtonRectangle);
}

// Then draw the button face and shadow over this.

This is the code that is performing very badly if Parent.BackgroundImage
!= null.

Thank you for your time,

Tim Crews
GECO, Inc.
 
M

Mick Doherty

when overriding the button control you must also set ControlStyles.Opaque to
false.

\\\
SetStyle(ControlStyles.AllPaintingInWmPaint
| ControlStyles.DoubleBuffer
| ControlStyles.UserPaint
| ControlStyles.SupportsTransparentBackColor, true);

SetStyle(ControlStyles.Opaque, false);

this.BackColor = Color.Transparent;
///
 
T

Tim Crews

when overriding the button control you must also set ControlStyles.Opaque to
false.

\\\
SetStyle(ControlStyles.AllPaintingInWmPaint
| ControlStyles.DoubleBuffer
| ControlStyles.UserPaint
| ControlStyles.SupportsTransparentBackColor, true);

SetStyle(ControlStyles.Opaque, false);

this.BackColor = Color.Transparent;
///

I sincerely thank everyone for taking the time to look at this.

I added Opaque as you suggested, and _still_ get black backgrounds. I
also added the DoubleBuffer style as you suggested, although I had not
originally planned to double buffer the drawing. Either way, black
backgrounds around the buttons.

I'm going to go for broke and post all of the button subclass code.

To test this, create a form, give it a background image, create a button
on the form, then change the declaration and creation of the Button to
FancyButton. To see what the button should look like (i.e. without
black background), uncomment the code segment starting with "if
(Parent.BackgroundImage == null)". But the performance will be terrible
if you do that.

Thank you for your assistance,

Tim Crews
GECO, Inc.

####################################################

public class FancyButton : Button
{

private int PenWidth;
private int ShadowHeight;
private int ButtonThickness;
private bool ButtonPressed;

public FancyButton() : base()
{
PenWidth = 1;
ButtonPressed = false;

// All of the following added per suggestions of newsgroup
// respondents.
SetStyle(ControlStyles.UserPaint, true);
SetStyle(ControlStyles.SupportsTransparentBackColor, true);
SetStyle(ControlStyles.AllPaintingInWmPaint, true);
SetStyle(ControlStyles.Opaque, true);
this.BackColor = Color.FromArgb(0,this.BackColor);
}


public void SpecialRoundRect (Rectangle rect,
Color BaselineColor,
System.Windows.Forms.PaintEventArgs e,
bool Shade3D,
bool LoOutline,
bool HiOutline)
{

rect.Inflate(-1*PenWidth,-1*PenWidth);

int CurveSize = Math.Min(rect.Width, rect.Height) / 2;
int CurveHalfSize = CurveSize/2;
int CurveHalfLeft = rect.Left + CurveHalfSize;
int CurveHalfRight = rect.Right-CurveHalfSize;
int CurveHalfTop = rect.Top + CurveHalfSize;
int CurveHalfBottom = rect.Bottom-CurveHalfSize;

Rectangle TopLeft = new Rectangle
(rect.Left,rect.Top,CurveSize,CurveSize);
Rectangle TopRight = new Rectangle
(rect.Right-CurveSize,rect.Top,CurveSize,CurveSize);
Rectangle BottomLeft = new Rectangle
(rect.Left,rect.Bottom-CurveSize,CurveSize,CurveSize);
Rectangle BottomRight = new Rectangle
(rect.Right-CurveSize,
rect.Bottom-CurveSize,
CurveSize,CurveSize);

int MiddleX = (rect.Right + rect.Left) / 2;
int MiddleY = (rect.Top + rect.Bottom) / 2;

SolidBrush BaselineBrush, LoBrush, MedBrush, HiBrush;
Pen BaselinePen, LoPen, MedPen, HiPen;

BaselineBrush = new SolidBrush(BaselineColor);
BaselinePen = new Pen(BaselineBrush, PenWidth);

if (Shade3D)
{
// The low color should be a lot darker version of the
// specified BaselineColor
byte ShadowR, ShadowG, ShadowB;
ShadowR = (byte)((int)BaselineColor.R*2/3);
ShadowG = (byte)((int)BaselineColor.G*2/3);
ShadowB = (byte)((int)BaselineColor.B*2/3);
Color ShadowColor = Color.FromArgb
(ShadowR, ShadowG, ShadowB);
LoBrush = new SolidBrush(ShadowColor);
LoPen = new Pen(LoBrush, PenWidth);

// The medium color should be a somewhat darker version of
// the specified BaselineColor
byte MediumR, MediumG, MediumB;
MediumR = (byte)((int)BaselineColor.R*4/5);
MediumG = (byte)((int)BaselineColor.G*4/5);
MediumB = (byte)((int)BaselineColor.B*4/5);
Color MediumColor = Color.FromArgb
(MediumR, MediumG, MediumB);
MedBrush = new SolidBrush(MediumColor);
MedPen = new Pen(MedBrush, PenWidth);

// The high color should be a brighter version of the button
// BaselineColor
byte HighlightR, HighlightG, HighlightB;
HighlightR = (byte)(
(int)BaselineColor.R
+ (
(255-BaselineColor.R)
*1/3
)
);
HighlightG = (byte)(
(int)BaselineColor.G
+ (
(255-BaselineColor.G)
*1/3
)
);
HighlightB = (byte)(
(int)BaselineColor.B
+ (
(255-BaselineColor.B)
*1/3
)
);

Color HighlightColor = Color.FromArgb
(HighlightR, HighlightG, HighlightB);
HiBrush = new SolidBrush(HighlightColor);
HiPen = new Pen(HiBrush, PenWidth);

}
else
{
LoBrush = BaselineBrush;
LoPen = BaselinePen;
MedBrush = BaselineBrush;
MedPen = BaselinePen;
HiBrush = BaselineBrush;
HiPen = BaselinePen;
}

Pen OutlinePen;
if (LoOutline)
OutlinePen = new Pen(Color.Black,PenWidth);
else if (HiOutline)
OutlinePen = new Pen(Color.White,PenWidth);
else
OutlinePen = new Pen (Color.Black,PenWidth);
// unused, but prevents compiler warning

if (Shade3D)
{
GraphicsPath PathUpperLeft = new GraphicsPath();
PathUpperLeft.StartFigure();
PathUpperLeft.AddArc(TopLeft,180,90);
PathUpperLeft.AddLine
(CurveHalfLeft,rect.Top,MiddleX,MiddleY);
PathUpperLeft.CloseFigure();
e.Graphics.FillPath(HiBrush,PathUpperLeft);
e.Graphics.DrawPath(MedPen,PathUpperLeft);

GraphicsPath PathUpper = new GraphicsPath();
PathUpper.StartFigure();
PathUpper.AddLine
(CurveHalfLeft,rect.Top,CurveHalfRight,rect.Top);
PathUpper.AddLine(CurveHalfRight,rect.Top,MiddleX,MiddleY);
PathUpper.CloseFigure();
e.Graphics.FillPath(HiBrush,PathUpper);
e.Graphics.DrawPath(MedPen,PathUpper);

GraphicsPath PathUpperRight = new GraphicsPath();
PathUpperRight.StartFigure();
PathUpperRight.AddArc(TopRight,270,90);
PathUpperRight.AddLine
(rect.Right,CurveHalfTop,MiddleX,MiddleY);
PathUpperRight.CloseFigure();
e.Graphics.FillPath(MedBrush,PathUpperRight);
e.Graphics.DrawPath(MedPen,PathUpperRight);

GraphicsPath PathRight = new GraphicsPath();
PathRight.StartFigure();
PathRight.AddLine
(rect.Right,CurveHalfTop,rect.Right,CurveHalfBottom);
PathRight.AddLine
(rect.Right,CurveHalfBottom,MiddleX,MiddleY);
PathRight.CloseFigure();
e.Graphics.FillPath(LoBrush,PathRight);
e.Graphics.DrawPath(MedPen,PathRight);

GraphicsPath PathLowerRight = new GraphicsPath();
PathLowerRight.StartFigure();
PathLowerRight.AddArc(BottomRight,0,90);
PathLowerRight.AddLine
(CurveHalfRight,rect.Bottom,MiddleX,MiddleY);
PathLowerRight.CloseFigure();
e.Graphics.FillPath(LoBrush,PathLowerRight);
e.Graphics.DrawPath(MedPen,PathLowerRight);

GraphicsPath PathLower = new GraphicsPath();
PathLower.StartFigure();
PathLower.AddLine
(CurveHalfRight,rect.Bottom,CurveHalfLeft,rect.Bottom);
PathLower.AddLine
(CurveHalfLeft,rect.Bottom,MiddleX,MiddleY);
PathLower.CloseFigure();
e.Graphics.FillPath(LoBrush,PathLower);
e.Graphics.DrawPath(MedPen,PathLower);

GraphicsPath PathLowerLeft = new GraphicsPath();
PathLower.StartFigure();
PathLowerLeft.AddArc (BottomLeft,90,90);
PathLowerLeft.AddLine
(rect.Left,CurveHalfBottom,MiddleX,MiddleY);
PathLowerLeft.CloseFigure();
e.Graphics.FillPath(MedBrush,PathLowerLeft);
e.Graphics.DrawPath(MedPen,PathLowerLeft);

GraphicsPath PathLeft = new GraphicsPath();
PathLeft.StartFigure();
PathLeft.AddLine
(rect.Left,CurveHalfBottom,rect.Left,CurveHalfTop);
PathLeft.AddLine(rect.Left,CurveHalfTop,MiddleX,MiddleY);
PathLeft.CloseFigure();
e.Graphics.FillPath(HiBrush,PathLeft);
e.Graphics.DrawPath(MedPen,PathLeft);


}

GraphicsPath PathWhole = new GraphicsPath();
PathWhole.StartFigure();
PathWhole.AddArc(TopLeft,180,90);
PathWhole.AddArc(TopRight,270,90);
PathWhole.AddArc(BottomRight,0,90);
PathWhole.AddArc(BottomLeft,90,90);
PathWhole.CloseFigure();

if (!Shade3D)
{
e.Graphics.FillPath(BaselineBrush,PathWhole);
}

if (LoOutline || HiOutline)
{
e.Graphics.DrawPath(OutlinePen,PathWhole);
}

}

protected override void OnPaint (PaintEventArgs e)
{
if ((Width>50) && (Height>50))
{
ShadowHeight=10;
ButtonThickness=20;
}
else
{
ShadowHeight = 3;
ButtonThickness = 7;
}

Rectangle ButtonRectangle = new Rectangle(0,0,Width,Height);

// Start by filling in the background image of the parent form.
// This is necessary so that the parts of the bounding rectangle
// that are not actually occupied by any part of the button will
// look like they show the form's background.
// if (Parent.BackgroundImage == null)
// {
// SolidBrush SolBrush = new SolidBrush
// (Parent.BackColor);
// e.Graphics.FillRectangle (SolBrush,ButtonRectangle);
// }
// else
// {
// // Note: "Bounds" coordinates are relative to parent
// // form, while ButtonRectangle coordinates are
// // relative to the button.
// TextureBrush TexBrush = new TextureBrush
// (Parent.BackgroundImage, Bounds);
// e.Graphics.FillRectangle (TexBrush,ButtonRectangle);
// }

ButtonRectangle.Inflate
(-1*(ShadowHeight/2),-1*(ShadowHeight/2));

// Move the button if it is pressed
if (ButtonPressed)
{
ButtonRectangle.Offset(ShadowHeight/3,ShadowHeight/3);
}
else
{
ButtonRectangle.Offset
(-1*(ShadowHeight/2),-1*(ShadowHeight/2));
}

// Draw button shadow
Rectangle ShadowRectangle = e.ClipRectangle;
ShadowRectangle.Inflate
(-1*(ShadowHeight/2),-1*(ShadowHeight/2));
ShadowRectangle.Offset(ShadowHeight/2,ShadowHeight/2);
SpecialRoundRect
(ShadowRectangle,Color.Black,e,false,false,false);

// Draw button edges
SpecialRoundRect
(ButtonRectangle,this.BackColor,e,true,true,false);

// Draw button face
ButtonRectangle.Inflate
(-1*(ButtonThickness/2),-1*(ButtonThickness/2));
SpecialRoundRect
(ButtonRectangle,this.BackColor,e,false,false,true);

// Draw the button text in the specified font and color
if (this.Text.Length > 0)
{
StringFormat textFormat = new StringFormat();
textFormat.Alignment = StringAlignment.Center;
textFormat.LineAlignment = StringAlignment.Center;
e.Graphics.DrawString(this.Text, this.Font,
new SolidBrush(this.ForeColor),
ButtonRectangle, textFormat);
}
}

protected override void OnMouseDown
(System.Windows.Forms.MouseEventArgs e )
{
ButtonPressed = true;
this.Invalidate();
this.OnClick(e);
}

protected override void OnMouseUp
(System.Windows.Forms.MouseEventArgs e)
{
ButtonPressed = false;
this.Invalidate();
}

protected override void OnClick(System.EventArgs e)
{
base.OnClick(e);
}
}
 
M

Mick Doherty

The immediate problem I see here is that Opaque is true, it should be false.
 
M

Mick Doherty

By the way, if you want to see a minor bug in your button just follow these
instructions.

Depress button, and while depressed push and release space. Now release the
button and click anywhere outside the button.

I have some Button source on my site which you may like to use as a base for
your Button. You'll find the above bug fixed as well as a couple of others.
Just replace my Bevel style with your Fancy style.

http://dotnetrix.co.uk/buttons.html

--
Mick Doherty
http://dotnetrix.co.uk/nothing.html


"Mick Doherty"
 
T

Tommy Carlier

I think the problem might be that you set
ControlStyles.AllPaintingInWmPaint to true.
Normally, when a control has to be painted, it first paints the
background (OnPaintBackground), and then it paints the foreground
(OnPaint). Setting the ControlStyle AllPaintingInWmPaint means that
all the painting is done in OnPaint, and OnPaintBackground is ignored.
 
S

Stoitcho Goutsev \(100\) [C# MVP]

Hi Tim,

So here is how to fix the problem with the black background:
1. Take off those lines form the form constructor

SetStyle(ControlStyles.AllPaintingInWmPaint, true);
SetStyle(ControlStyles.Opaque, true);

Let only UserPaint and SupprtTransparentBackColor stay.

2. As long as (you mentioned yourself) this is simulated transparency it
means that the control class does some work to make this happen. In other
words in the OnPaint method you need to call the
base.OnPaint in order to let the control do it's work. Put that call as the
first line in the OnPaint method (you don't want the default method to
overdraw you fancy button, do you)

Here you go... the transparent backgroung. That was easy. However your
troubles doesn't stop here. The default OnPaint draws some stuff that you
don't want. Like (button 3d appearance, focus rectangle, Balck rectangle
around the default button). Among this only the focused rectangle I managed
to get rid of.
I did that by overriding ShowFocusCues. In my override I return *false*.
3D appearnace you can kind of remove by setting the button's FlatStyle.
Default-button black rectangle I removed by overriding NotifyDefault method
and calling the base method with *false* as a parameter. It has side effect
that the button cannot be default

Anyways, the result is far from perfect.

So my suggestion is to go with the Control as a base class. So, do what I
told you erlier (stpes 1 and 2) and change the base class to Control. It
works just fine. The problem here is that the FancyButton cannot be used
where Button type is expected.

If you still want to use the button as a base class then take a look in the
Contol.Region property. Setting the region of the control to match exactly
your button's outline shape will make all that crappy adornments to be
clipped off.
 
T

Tim Crews

I think the problem might be that you set
ControlStyles.AllPaintingInWmPaint to true.
Normally, when a control has to be painted, it first paints the
background (OnPaintBackground), and then it paints the foreground
(OnPaint). Setting the ControlStyle AllPaintingInWmPaint means that
all the painting is done in OnPaint, and OnPaintBackground is ignored.

Once I took care of the bug (pointed out elsewhere) that I was setting Opaque
to true instead of false, I got somewhat improved results. Now, instead of a
black background for my button, I get a background that is the color of the
face of the button. Still not what I want, though -- I want a background that
is the color (or image) of the parent form.

From that point, it seems that AllPaintingInWmPaint has no effect at all.
Either true or false, I still get a button background that is the color of the
face of the button.

Tim Crews
GECO, Inc.
 
T

Tim Crews

The immediate problem I see here is that Opaque is true, it should be false.


Doh! Yes, you are right. The new constructor reads as follows:

public FancyButton() : base()
{
PenWidth = 1;
ButtonPressed = false;
SetStyle(ControlStyles.UserPaint, true);
SetStyle(ControlStyles.SupportsTransparentBackColor, true);
//SetStyle(ControlStyles.AllPaintingInWmPaint, true);
SetStyle(ControlStyles.Opaque, false);
this.BackColor = Color.FromArgb(0,this.BackColor);
}


Now, I do not get a black background, but I still get a background that is the
color of the button face (i.e. this.BackColor) instead of the background that
is inherited from the form (either Parent.BackColor or Parent.BackgroundImage).

The results are the same with or without the AllPaintingInWmPaint style. In
face, I can also remove the SupportsTransparentBackColor style, and the
this.BackColor assignment, and see exactly the same results.

Thank you for catching my bug. Surely I am close to a solution now?

Tim Crews
GECO, Inc.
 
M

Mick Doherty

His problem stems from the fact that the button base Class has
ControlStyles.Opaque set to true by default so his setting it to true makes
no difference. This problem does not appear in a class based upon Control
because in a Control/UserControl ControlStyles.Opaque is false by default.
This is the only modification that is necessary to cure the black
background.

OnPaintBackground() is called to draw the controls parent in any non opaque
regions of the control. If the control is fully opaque then this method is
not called, that is why there is a black background.

I have created a button derived from button and have not set the region of
the control, doing so causes nasty jagged edges around curves. By not
setting the region you can set the Graphics.SmoothingMode to High and see
nice smooth edges around the button.

Without Doublebuffer, AllPaintingInWMPaint does not need to be set.

--
Mick Doherty
http://dotnetrix.co.uk/nothing.html


Stoitcho Goutsev (100) said:
Hi Tim,

So here is how to fix the problem with the black background:
1. Take off those lines form the form constructor

SetStyle(ControlStyles.AllPaintingInWmPaint, true);
SetStyle(ControlStyles.Opaque, true);

Let only UserPaint and SupprtTransparentBackColor stay.

2. As long as (you mentioned yourself) this is simulated transparency it
means that the control class does some work to make this happen. In other
words in the OnPaint method you need to call the
base.OnPaint in order to let the control do it's work. Put that call as
the first line in the OnPaint method (you don't want the default method to
overdraw you fancy button, do you)

Here you go... the transparent backgroung. That was easy. However your
troubles doesn't stop here. The default OnPaint draws some stuff that you
don't want. Like (button 3d appearance, focus rectangle, Balck rectangle
around the default button). Among this only the focused rectangle I
managed to get rid of.
I did that by overriding ShowFocusCues. In my override I return *false*.
3D appearnace you can kind of remove by setting the button's FlatStyle.
Default-button black rectangle I removed by overriding NotifyDefault
method and calling the base method with *false* as a parameter. It has
side effect that the button cannot be default

Anyways, the result is far from perfect.

So my suggestion is to go with the Control as a base class. So, do what I
told you erlier (stpes 1 and 2) and change the base class to Control. It
works just fine. The problem here is that the FancyButton cannot be used
where Button type is expected.

If you still want to use the button as a base class then take a look in
the Contol.Region property. Setting the region of the control to match
exactly your button's outline shape will make all that crappy adornments
to be clipped off.

--
HTH
Stoitcho Goutsev (100) [C# MVP]


Tim Crews said:
I sincerely thank everyone for taking the time to look at this.

I added Opaque as you suggested, and _still_ get black backgrounds. I
also added the DoubleBuffer style as you suggested, although I had not
originally planned to double buffer the drawing. Either way, black
backgrounds around the buttons.

I'm going to go for broke and post all of the button subclass code.

To test this, create a form, give it a background image, create a button
on the form, then change the declaration and creation of the Button to
FancyButton. To see what the button should look like (i.e. without
black background), uncomment the code segment starting with "if
(Parent.BackgroundImage == null)". But the performance will be terrible
if you do that.

Thank you for your assistance,

Tim Crews
GECO, Inc.

####################################################

public class FancyButton : Button
{

private int PenWidth;
private int ShadowHeight;
private int ButtonThickness;
private bool ButtonPressed;

public FancyButton() : base()
{
PenWidth = 1;
ButtonPressed = false;

// All of the following added per suggestions of newsgroup
// respondents.
SetStyle(ControlStyles.UserPaint, true);
SetStyle(ControlStyles.SupportsTransparentBackColor, true);
SetStyle(ControlStyles.AllPaintingInWmPaint, true);
SetStyle(ControlStyles.Opaque, true);
this.BackColor = Color.FromArgb(0,this.BackColor);
}


public void SpecialRoundRect (Rectangle rect,
Color BaselineColor,
System.Windows.Forms.PaintEventArgs e,
bool Shade3D,
bool LoOutline,
bool HiOutline)
{

rect.Inflate(-1*PenWidth,-1*PenWidth);

int CurveSize = Math.Min(rect.Width, rect.Height) / 2;
int CurveHalfSize = CurveSize/2;
int CurveHalfLeft = rect.Left + CurveHalfSize;
int CurveHalfRight = rect.Right-CurveHalfSize;
int CurveHalfTop = rect.Top + CurveHalfSize;
int CurveHalfBottom = rect.Bottom-CurveHalfSize;

Rectangle TopLeft = new Rectangle
(rect.Left,rect.Top,CurveSize,CurveSize);
Rectangle TopRight = new Rectangle
(rect.Right-CurveSize,rect.Top,CurveSize,CurveSize);
Rectangle BottomLeft = new Rectangle
(rect.Left,rect.Bottom-CurveSize,CurveSize,CurveSize);
Rectangle BottomRight = new Rectangle
(rect.Right-CurveSize,
rect.Bottom-CurveSize,
CurveSize,CurveSize);

int MiddleX = (rect.Right + rect.Left) / 2;
int MiddleY = (rect.Top + rect.Bottom) / 2;

SolidBrush BaselineBrush, LoBrush, MedBrush, HiBrush;
Pen BaselinePen, LoPen, MedPen, HiPen;

BaselineBrush = new SolidBrush(BaselineColor);
BaselinePen = new Pen(BaselineBrush, PenWidth);

if (Shade3D)
{
// The low color should be a lot darker version of the
// specified BaselineColor
byte ShadowR, ShadowG, ShadowB;
ShadowR = (byte)((int)BaselineColor.R*2/3);
ShadowG = (byte)((int)BaselineColor.G*2/3);
ShadowB = (byte)((int)BaselineColor.B*2/3);
Color ShadowColor = Color.FromArgb
(ShadowR, ShadowG, ShadowB);
LoBrush = new SolidBrush(ShadowColor);
LoPen = new Pen(LoBrush, PenWidth);

// The medium color should be a somewhat darker version of
// the specified BaselineColor
byte MediumR, MediumG, MediumB;
MediumR = (byte)((int)BaselineColor.R*4/5);
MediumG = (byte)((int)BaselineColor.G*4/5);
MediumB = (byte)((int)BaselineColor.B*4/5);
Color MediumColor = Color.FromArgb
(MediumR, MediumG, MediumB);
MedBrush = new SolidBrush(MediumColor);
MedPen = new Pen(MedBrush, PenWidth);

// The high color should be a brighter version of the button
// BaselineColor
byte HighlightR, HighlightG, HighlightB;
HighlightR = (byte)(
(int)BaselineColor.R
+ (
(255-BaselineColor.R)
*1/3
)
);
HighlightG = (byte)(
(int)BaselineColor.G
+ (
(255-BaselineColor.G)
*1/3
)
);
HighlightB = (byte)(
(int)BaselineColor.B
+ (
(255-BaselineColor.B)
*1/3
)
);

Color HighlightColor = Color.FromArgb
(HighlightR, HighlightG, HighlightB);
HiBrush = new SolidBrush(HighlightColor);
HiPen = new Pen(HiBrush, PenWidth);

}
else
{
LoBrush = BaselineBrush;
LoPen = BaselinePen;
MedBrush = BaselineBrush;
MedPen = BaselinePen;
HiBrush = BaselineBrush;
HiPen = BaselinePen;
}

Pen OutlinePen;
if (LoOutline)
OutlinePen = new Pen(Color.Black,PenWidth);
else if (HiOutline)
OutlinePen = new Pen(Color.White,PenWidth);
else
OutlinePen = new Pen (Color.Black,PenWidth);
// unused, but prevents compiler warning

if (Shade3D)
{
GraphicsPath PathUpperLeft = new GraphicsPath();
PathUpperLeft.StartFigure();
PathUpperLeft.AddArc(TopLeft,180,90);
PathUpperLeft.AddLine
(CurveHalfLeft,rect.Top,MiddleX,MiddleY);
PathUpperLeft.CloseFigure();
e.Graphics.FillPath(HiBrush,PathUpperLeft);
e.Graphics.DrawPath(MedPen,PathUpperLeft);

GraphicsPath PathUpper = new GraphicsPath();
PathUpper.StartFigure();
PathUpper.AddLine
(CurveHalfLeft,rect.Top,CurveHalfRight,rect.Top);
PathUpper.AddLine(CurveHalfRight,rect.Top,MiddleX,MiddleY);
PathUpper.CloseFigure();
e.Graphics.FillPath(HiBrush,PathUpper);
e.Graphics.DrawPath(MedPen,PathUpper);

GraphicsPath PathUpperRight = new GraphicsPath();
PathUpperRight.StartFigure();
PathUpperRight.AddArc(TopRight,270,90);
PathUpperRight.AddLine
(rect.Right,CurveHalfTop,MiddleX,MiddleY);
PathUpperRight.CloseFigure();
e.Graphics.FillPath(MedBrush,PathUpperRight);
e.Graphics.DrawPath(MedPen,PathUpperRight);

GraphicsPath PathRight = new GraphicsPath();
PathRight.StartFigure();
PathRight.AddLine
(rect.Right,CurveHalfTop,rect.Right,CurveHalfBottom);
PathRight.AddLine
(rect.Right,CurveHalfBottom,MiddleX,MiddleY);
PathRight.CloseFigure();
e.Graphics.FillPath(LoBrush,PathRight);
e.Graphics.DrawPath(MedPen,PathRight);

GraphicsPath PathLowerRight = new GraphicsPath();
PathLowerRight.StartFigure();
PathLowerRight.AddArc(BottomRight,0,90);
PathLowerRight.AddLine
(CurveHalfRight,rect.Bottom,MiddleX,MiddleY);
PathLowerRight.CloseFigure();
e.Graphics.FillPath(LoBrush,PathLowerRight);
e.Graphics.DrawPath(MedPen,PathLowerRight);

GraphicsPath PathLower = new GraphicsPath();
PathLower.StartFigure();
PathLower.AddLine
(CurveHalfRight,rect.Bottom,CurveHalfLeft,rect.Bottom);
PathLower.AddLine
(CurveHalfLeft,rect.Bottom,MiddleX,MiddleY);
PathLower.CloseFigure();
e.Graphics.FillPath(LoBrush,PathLower);
e.Graphics.DrawPath(MedPen,PathLower);

GraphicsPath PathLowerLeft = new GraphicsPath();
PathLower.StartFigure();
PathLowerLeft.AddArc (BottomLeft,90,90);
PathLowerLeft.AddLine
(rect.Left,CurveHalfBottom,MiddleX,MiddleY);
PathLowerLeft.CloseFigure();
e.Graphics.FillPath(MedBrush,PathLowerLeft);
e.Graphics.DrawPath(MedPen,PathLowerLeft);

GraphicsPath PathLeft = new GraphicsPath();
PathLeft.StartFigure();
PathLeft.AddLine
(rect.Left,CurveHalfBottom,rect.Left,CurveHalfTop);
PathLeft.AddLine(rect.Left,CurveHalfTop,MiddleX,MiddleY);
PathLeft.CloseFigure();
e.Graphics.FillPath(HiBrush,PathLeft);
e.Graphics.DrawPath(MedPen,PathLeft);


}

GraphicsPath PathWhole = new GraphicsPath();
PathWhole.StartFigure();
PathWhole.AddArc(TopLeft,180,90);
PathWhole.AddArc(TopRight,270,90);
PathWhole.AddArc(BottomRight,0,90);
PathWhole.AddArc(BottomLeft,90,90);
PathWhole.CloseFigure();

if (!Shade3D)
{
e.Graphics.FillPath(BaselineBrush,PathWhole);
}

if (LoOutline || HiOutline)
{
e.Graphics.DrawPath(OutlinePen,PathWhole);
}

}

protected override void OnPaint (PaintEventArgs e)
{
if ((Width>50) && (Height>50))
{
ShadowHeight=10;
ButtonThickness=20;
}
else
{
ShadowHeight = 3;
ButtonThickness = 7;
}

Rectangle ButtonRectangle = new Rectangle(0,0,Width,Height);

// Start by filling in the background image of the parent form.
// This is necessary so that the parts of the bounding rectangle
// that are not actually occupied by any part of the button will
// look like they show the form's background.
// if (Parent.BackgroundImage == null)
// {
// SolidBrush SolBrush = new SolidBrush
// (Parent.BackColor);
// e.Graphics.FillRectangle (SolBrush,ButtonRectangle);
// }
// else
// {
// // Note: "Bounds" coordinates are relative to parent
// // form, while ButtonRectangle coordinates are
// // relative to the button.
// TextureBrush TexBrush = new TextureBrush
// (Parent.BackgroundImage, Bounds);
// e.Graphics.FillRectangle (TexBrush,ButtonRectangle);
// }

ButtonRectangle.Inflate
(-1*(ShadowHeight/2),-1*(ShadowHeight/2));

// Move the button if it is pressed
if (ButtonPressed)
{
ButtonRectangle.Offset(ShadowHeight/3,ShadowHeight/3);
}
else
{
ButtonRectangle.Offset
(-1*(ShadowHeight/2),-1*(ShadowHeight/2));
}

// Draw button shadow
Rectangle ShadowRectangle = e.ClipRectangle;
ShadowRectangle.Inflate
(-1*(ShadowHeight/2),-1*(ShadowHeight/2));
ShadowRectangle.Offset(ShadowHeight/2,ShadowHeight/2);
SpecialRoundRect
(ShadowRectangle,Color.Black,e,false,false,false);

// Draw button edges
SpecialRoundRect
(ButtonRectangle,this.BackColor,e,true,true,false);

// Draw button face
ButtonRectangle.Inflate
(-1*(ButtonThickness/2),-1*(ButtonThickness/2));
SpecialRoundRect
(ButtonRectangle,this.BackColor,e,false,false,true);

// Draw the button text in the specified font and color
if (this.Text.Length > 0)
{
StringFormat textFormat = new StringFormat();
textFormat.Alignment = StringAlignment.Center;
textFormat.LineAlignment = StringAlignment.Center;
e.Graphics.DrawString(this.Text, this.Font,
new SolidBrush(this.ForeColor),
ButtonRectangle, textFormat);
}
}

protected override void OnMouseDown
(System.Windows.Forms.MouseEventArgs e )
{
ButtonPressed = true;
this.Invalidate();
this.OnClick(e);
}

protected override void OnMouseUp
(System.Windows.Forms.MouseEventArgs e)
{
ButtonPressed = false;
this.Invalidate();
}

protected override void OnClick(System.EventArgs e)
{
base.OnClick(e);
}
}
 
T

Tim Crews

By the way, if you want to see a minor bug in your button just follow these
instructions.

Depress button, and while depressed push and release space. Now release the
button and click anywhere outside the button.

I have some Button source on my site which you may like to use as a base for
your Button. You'll find the above bug fixed as well as a couple of others.
Just replace my Bevel style with your Fancy style.

http://dotnetrix.co.uk/buttons.html

Mike:

Your sample code is very helpful! Believe me, I spent a lot of time searching
for a good Button subclass implementation, but for some reason I did not run
across yours.

You do indeed account for a lot of cases that my class does not. Some of them
do not apply in my environment (a kiosk that has no keyboard, with no run-time
changes to any button properties); nevertheless, your design is well worth
incorporating.

But the "DrawParentBackground" function in your class remains just as costly,
if not more, than my own code segment. I think I will still be faced with the
same problem, that is, performance. If I have 40 buttons, it will still take a
LONG time to execute 40 copies of your DrawParentBackground. I will plug your
class in and see how it performs a little later today...

[Why does he have a form with 40 buttons, you ask? It's a virtual keyboard,
for a kiosk that has no keyboard, but unfortunately still has a few cases where
the user needs to enter some text.]

Since your expertise has lead you to a design that still has to do the one
thing I have been trying to avoid all along (explicitly filling in the parent
background), it appears that I am stuck with this performance problem. (At
least if I want to use Button as a parent class. Another respondent had an
alternate suggestion, which I will address in another post.) I will have to
find a different button style that is rectangular, or I will have to remove the
parent form background image and stick with a solid background color. Either
of these approaches will be difficult for the customer to accept.

Thank you again for your generosity with your time, including the time it took
you to write the sample code on your web site. It has been very helpful.

Tim Crews
GECO, Inc.
 
T

Tim Crews

His problem stems from the fact that the button base Class has
ControlStyles.Opaque set to true by default so his setting it to true makes
no difference. This problem does not appear in a class based upon Control
because in a Control/UserControl ControlStyles.Opaque is false by default.
This is the only modification that is necessary to cure the black
background.

Wouldn't it be easier just to change Opaque to false in my constructor, rather
than choosing a different base class that defaults it to false? Maybe I'm
missing what you intended here. (I think Stoitcho recommended the different
base class not only because of the Opaque property, but because I could then
suppress some of the button adornments like 3d edges and focus indicators.) If
I change the base class of my FancyButton to Control, of course I have lots of
forms that _do_ expect FancyButton to have various Button-specific properties
that are no longer available if I subclass it from Control.
OnPaintBackground() is called to draw the controls parent in any non opaque
regions of the control. If the control is fully opaque then this method is
not called, that is why there is a black background.

But if I change the Button style to non-opaque, my black background changes to
the background color of the Button, instead of the background of the parent
form.
I have created a button derived from button and have not set the region of
the control, doing so causes nasty jagged edges around curves. By not
setting the region you can set the Graphics.SmoothingMode to High and see
nice smooth edges around the button.

But I think not setting the region is the reason that I get the Button's
background color instead of the form's background color around my button.

So, as you say, the only other approach is to explicitly fill in the entire
background area with the parent form's color or image (as you do in
DrawParentBackground in your example code) before drawing the button. And this
gets me right back to my performance problem.

I was unaware of the issue with nasty jagged edges around curves. I was
willing to try setting the region, but if the result is going to look terrible,
I guess I will save myself the time I would have spent on this experiment.

Without Doublebuffer, AllPaintingInWMPaint does not need to be set.

Yes, I agree. I have removed it from my constructor.

Thank you again, everyone.

Tim Crews
GECO, Inc.
 
M

Mick Doherty

How are you setting the FaceColor?
Don't forget that in your constructor you added the line:
\\\
this.BackColor = Color.FromArgb(0,this.BackColor);
///

If you change this then the button Background will not be Transparent.

modify your constructor as follows and add the new BackColor Property

\\\
public FancyButton() : base()
{
PenWidth = 1;
ButtonPressed = false;

SetStyle(ControlStyles.UserPaint, true);
SetStyle(|ControlStyles.SupportsTransparentBackColor, true);
//extra style for IDE.
SetStyle(ControlStyles.ResizeRedraw, true);

SetStyle(ControlStyles.Opaque, false);

//this changed to base
base.BackColor = Color.Transparent;

}

// a new BackColor property for your ButtonFace
private Color faceColor = Control.DefaultBackColor;

[DefaultValue(typeof(Color),"Control")]
public new Color BackColor
{
get
{
return faceColor;
}
set
{
faceColor = value;
this.Invalidate();
}
}
///
 
M

Mick Doherty

I was just explaining why you get the Black Background in Button and not in
Control. There is no need to derive from Control, just set Opaque to false
in the constructor of your Button class.
You get the nasty button borders if you call the default OnPaint(). There is
no need to call it.

Inherit from Control if you don't need Button behaviour, otherwise Inherit
from Button and fix the problems as you encounter them.

My DrawParentBackground method was used mainly to show how to interrogate
the poperty of another control if that property exists. You don't need it.

What color should be used for the Background if you are using the BackColor
to change the buttonface?

See post above for solution.
 
T

Tim Crews

How are you setting the FaceColor?

Mick:

The suggestions you made in that post did indeed get the button backgrounds
looking the way they were supposed to look, without requiring any code in my
control to explicitly paint the form background around my button.

I was then faced with... crushing disappointment. (OK, perhaps it is not so
serious as that.) Now that the framework is doing the work of painting the
form background around my button, the performance is just as bad as when I was
doing it myself. My 40-button form still takes two seconds to display on my
3GHz machine. The processor that will be driving our kiosk is much slower than
3GHz. I have a notebook computer with similar performance to what we are
expecting to see on our kiosk. On the notebook computer, it takes _eight_
seconds to display the 40-button form.

Several of my co-workers are convinced that something is wrong, and that it
must be possible to draw these buttons faster. As an "existence proof", they
show me their windows desktops, with background images, and then they select a
couple dozen icons on their desktops, and move them around, demonstrating to me
that somehow Windows is able to lay those icons (including their text labels)
over a textured background without any performance penalty.

I don't know what to tell them, other than that we are up against a limitation
of Windows Forms, which does not support _true_ transparency, but only
simulates transparency through costly (in runtime) re-drawing of portions of
the parent image.

I evaluated many criteria when our project team was selecting a development
environment. If we had it to do over again, this performance issue might very
well have changed our decision. I don't look forward to reporting this at our
next project meeting.

Thank you again for your persistence with this issue.

Tim Crews
GECO, Inc.
 
F

Frank Hileman

Windows forms controls don't support transparency well. Since buttons are so
simple, you may wish to create a control that covers the entire form, and
paint both the background and a vector graphics "button" on top of that. It
will not be a true control, but will be easier to use.

We use this technique to create all kinds of "controls", not just buttons.

Regards,
Frank Hileman

check out VG.net: www.vgdotnet.com
Animated vector graphics system
Integrated Visual Studio .NET graphics editor
 
F

Frank Hileman

Here is an example of transparent buttons with VG.net, drawing in the way I
described. You can see they are very fast. You can create hundreds, no
problem. The Lite download has source for that demo.

http://weblogs.asp.net/frank_hileman/archive/2004/05/10/129387.aspx

Really you just need an optimized graphics setup. It is possible to do it
yourself without VG.net, but the VG.net run-time is free, and has a great
designer.

Regards,
Frank Hileman

check out VG.net: www.vgdotnet.com
Animated vector graphics system
Integrated Visual Studio .NET graphics editor
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Top