如何正确获取WinForms按钮控件以绘制自定义文本

4
我正在尝试创建一个自定义的Winforms按钮控件,允许通过“rotation”属性旋转按钮文本。我已经基本实现了它,但是它非常笨拙,我想知道正确的方法是什么。
特别是现在文本重绘的行为很奇怪。如果控件移出屏幕,然后缓慢移回,文本要么变得混乱(例如只画了一半),要么完全消失,直到鼠标移到上面。显然我做错了什么,但是无法弄清楚原因。
我继承了按钮控件并重写了其OnPaint方法。
以下是代码:
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;
    }
}

所以我的主要问题是如何解决文字重绘问题?
此外,对于上面的代码,我还有一些其他问题/评论:
1. 首先,文本会显示两次,一次是在其默认位置,另一次是在旋转位置。我想这是因为当调用base.OnPaint方法时,文本首先被绘制。如果是这样,我该如何防止文本最初绘制? 我的解决方案是覆盖Text字符串,并在使用布尔值清除之前调用base.OnPaint,但我并不满意这种解决方案。
2. 我是否应该在结尾处使用e.dispose来处理PaintEventArgs?我猜我不太确定PaintEventArgs对象是如何处理的。
提前致谢!
附:这是我的第一个帖子/问题,所以如果我无意中忽略了一些礼节或规则,请您多多包涵。
2个回答

2
  1. VisibleClipBounds函数返回需要重新绘制的区域,例如如果半个按钮需要重新绘制(覆盖了一半按钮的顶部窗体被关闭),则VisibleClipBounds仅返回该区域。所以你不能用它来画居中文本。 SizeF sz = new SizeF(Width, Height); 应该可以解决重绘问题。

  2. Button不支持自绘,您的方法似乎很好。

  3. 通常情况下您不应该释放您没有创建的对象,并且可释放事件参数会由创建它们的逻辑(首先调用On...)处理,因此请不要担心释放PaintEventArgs。

欢迎来到Stack Overflow :)


我之前并不是很理解VisibleClipBounds的工作原理,但你解释后现在我明白了。非常感谢。 无论如何,感谢你提供的解决方案,它非常有效。同时也感谢你热情的欢迎。 - Scott Lemmon

1
首先,我通过用switch... case...替换if... else...并将每个情况下相同的两行移出来,对您的代码进行了一些重构。但这只是为了使它更易读:
    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;
    }

关于您的主要问题: 我通过创建一个RotateButton并将旋转类型设置为Right来进行了测试。我可以确认您所描述的行为。调试OnPaint很困难,因为每次在中断后恢复程序时,表单都会重新获得焦点,从而触发新的Paint事件。最终,我通过在方法末尾添加两行代码来追踪此行为的原因:
System.Diagnostics.Debug.WriteLine(sz.Width.ToString());
System.Diagnostics.Debug.WriteLine(sz.Height.ToString());

这段代码在Visual Studio的输出窗口中写入了宽度和高度的值。当控件移回屏幕时,可以看到sz.Width的值被设置为1。因此,您绘制的文本是在一个远远太小的矩形内,所以它不可见。这意味着您不能使用e.Graphics.VisibleClipBounds.Size,必须自己计算大小(如果您使用MeasureString,请注意将text作为参数传递,而不是像您的示例代码中一样传递Text)。

关于您的其他问题:

  1. 我认为你的解决方案是可以的。您可以考虑在调用base.OnPaint()之前将text设置为空字符串,并在之后恢复正确的值。
  2. 不,绝对不行。PaintEventArgs对象是在OnPaint方法之外某个地方创建的,释放(如果需要)应该在那里处理 - 只有对象的“创建者”才知道如何和何时适当地处理它的释放。(您不知道代码后来是否需要引发Paint事件)。

谢谢,我很感激你的工作和反馈。我以前从未使用过 switch... case...,但现在看起来非常有用。我将阅读更多相关资料并开始尝试使用它。另外,关于 MeasureString 的观点很好。我实际上忘记了它还在那里。 - Scott Lemmon
不用谢 - 我其实非常喜欢这个小谜题! - Treb

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接