问题
I'm trying to create a custom winforms button control that will allow rotation of the button text through a rotation property. I've mostly got it working, but it's very kludgy and I would like to know the proper way to do this.
Particularly right now the text redrawing is behaving strangely. If the control is moved off screen, and then moved slowly back on the text either becomes very messed up (such as only half drawn), or disappears entirely, until moused over. Obviously I'm doing something wrong, but can't figure out what.
I'm inheriting from the button control and overriding its OnPaint method.
Here is the code:
public class RotateButton : Button
{
private string text;
private bool painting = false;
public enum RotationType { None, Right, Flip, Left}
[DefaultValue(RotationType.None), Category("Appearance"), Description("Rotates Button Text")]
public RotationType Rotation { get; set; }
public override string Text
{
get
{
if (!painting)
return text;
else
return "";
}
set
{
text = value;
}
}
protected override void OnPaint(PaintEventArgs e)
{
painting = true;
base.OnPaint(e);
StringFormat format = new StringFormat();
Int32 lNum = (Int32)Math.Log((Double)this.TextAlign, 2);
format.LineAlignment = (StringAlignment)(lNum / 4);
format.Alignment = (StringAlignment)(lNum % 4);
int padding = 2;
SizeF txt = e.Graphics.MeasureString(Text, this.Font);
SizeF sz = e.Graphics.VisibleClipBounds.Size;
if (Rotation == RotationType.Right)
{
//90 degrees
e.Graphics.TranslateTransform(sz.Width, 0);
e.Graphics.RotateTransform(90);
e.Graphics.DrawString(text, this.Font, Brushes.Black, new RectangleF(padding, padding, sz.Height - padding, sz.Width - padding), format);
e.Graphics.ResetTransform();
}
else if (Rotation == RotationType.Flip)
{
//180 degrees
e.Graphics.TranslateTransform(sz.Width, sz.Height);
e.Graphics.RotateTransform(180);
e.Graphics.DrawString(text, this.Font, Brushes.Black, new RectangleF(padding, padding, sz.Width - padding, sz.Height - padding), format);
e.Graphics.ResetTransform();
}
else if (Rotation == RotationType.Left)
{
//270 degrees
e.Graphics.TranslateTransform(0, sz.Height);
e.Graphics.RotateTransform(270);
e.Graphics.DrawString(text, this.Font, Brushes.Black, new RectangleF(padding, padding, sz.Height - padding, sz.Width - padding), format);
e.Graphics.ResetTransform();
}
else
{
//0 = 360 degrees
e.Graphics.TranslateTransform(0, 0);
e.Graphics.RotateTransform(0);
e.Graphics.DrawString(text, this.Font, Brushes.Black, new RectangleF(padding, padding, sz.Width - padding, sz.Height - padding), format);
e.Graphics.ResetTransform();
}
painting = false;
}
}
So my main question is how might I fix the text redrawing problem?
In addition I have a few other questions/comments on the above code:
At first the text was showing up twice, once in it's default location, and once in the rotated location. I assume this is because the text is being drawn first when the
base.OnPaint
method is called. If this is the case, how do I keep the text from drawing initially?My solution is to to override the Text string and clear it before calling
base.OnPaint
using a boolean, which is not a solution I'm particularly happy with.Should I be disposing of the PaintEventArgs at the end with
e.dispose
? I guess I'm not sure how the PaintEventArgs object is being dealt with.
Thanks in advance!
Ps. This is my first post/question, so I apologize in advance if I inadvertently ignored some etiquette or rules.
回答1:
VisibleClipBounds returns the area that needs to be repainted, for example if half the button needs to be repainted (a top form covering half the button is closed) VisibleClipBounds returns only that area. So you can't use that for painting centered text. SizeF sz = new SizeF(Width, Height); Should take care of the repainting problem.
Button doesn't support owner drawing, your way seems fine.
As a rule you shouldn't dispose objects you haven't created, and disposable event arguments are disposed by the logic that created them (and called the On... in the first place) so don't worry about disposing PaintEventArgs.
Welcome to Stack Overflow :)
回答2:
First, I refactored your code a bit by replacing the if... else...
with a switch... case...
, and by moving the two lines that were identical in every case out of it. But that is just to make it more readable:
protected override void OnPaint(PaintEventArgs e)
{
painting = true;
base.OnPaint(e);
StringFormat format = new StringFormat();
Int32 lNum = (Int32)Math.Log((Double)this.TextAlign, 2);
format.LineAlignment = (StringAlignment)(lNum / 4);
format.Alignment = (StringAlignment)(lNum % 4);
int padding = 2;
SizeF txt = e.Graphics.MeasureString(Text, this.Font);
SizeF sz = e.Graphics.VisibleClipBounds.Size;
switch (Rotation)
{
case RotationType.Right: //90 degrees
{
e.Graphics.TranslateTransform(sz.Width, 0);
e.Graphics.RotateTransform(90);
break;
}
case RotationType.Flip: //180 degrees
{
e.Graphics.TranslateTransform(sz.Width, sz.Height);
e.Graphics.RotateTransform(180);
break;
}
case RotationType.Left: //270 degrees
{
e.Graphics.TranslateTransform(0, sz.Height);
e.Graphics.RotateTransform(270);
break;
}
default: //0 = 360 degrees
{
e.Graphics.TranslateTransform(0, 0);
e.Graphics.RotateTransform(0);
break;
}
}
e.Graphics.DrawString(text, this.Font, Brushes.Black, new RectangleF(padding, padding, sz.Height - padding, sz.Width - padding), format);
e.Graphics.ResetTransform();
painting = false;
}
Regarding your main question:
I tested this by creating a RotateButton
and setting the rotation type to Right
. I can confirm the behaviour you describe. Debugging OnPaint
is difficult, because every time you resume the program after a break, the form regains the focus, which triggers a new Paint
event. I finally tracked down the cause of this behaviour by adding two lines to the end of the method:
System.Diagnostics.Debug.WriteLine(sz.Width.ToString());
System.Diagnostics.Debug.WriteLine(sz.Height.ToString());
This writes the values of width and height to the output window in Visual Studio. There I could see that when the control is moved back on screen, sz.Width
is set to a value of 1. So your text is drawn on the control, but in a rectangle that is far too small, therefore it's not visible. Which means that you can't use e.Graphics.VisibleClipBounds.Size
, you have to compute the sizes yourself (if you use MeasureString
, be careful to pass text
as a parameter, not Text
, as in your example code).
Regarding your additional questions:
- I think your solution is ok. You may consider to just set
text
to an empty string before callingbase.OnPaint()
, and restoring the correct value afterwards instead. - No, definitely not. The
PaintEventArgs
object is created somewhere outside theOnPaint
method, disposing (if necessary) should be handled there - only the 'creator' of an object knows how and when to dispose of it properly. (You don't know if it will be needed by the code that raised thePaint
event afterwards).
来源:https://stackoverflow.com/questions/12244392/how-to-correctly-get-a-winforms-button-control-to-draw-custom-text