【问题标题】:How to correctly get a WinForms Button control to draw custom text如何正确获取 WinForms Button 控件以绘制自定义文本
【发布时间】:2012-08-27 23:40:07
【问题描述】:

我正在尝试创建一个自定义 winforms 按钮控件,该控件将允许通过旋转属性旋转按钮文本。我大部分时间都在工作,但它非常笨拙,我想知道正确的方法。

尤其是现在文本重绘的行为很奇怪。如果控件被移出屏幕,然后缓慢地移回文本上,则文本要么变得非常混乱(例如只绘制了一半),要么完全消失,直到鼠标悬停。显然我做错了什么,但不知道是什么。

我从按钮控件继承并覆盖其 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;
    }
}

所以我的主要问题是如何解决文本重绘问题?

此外,我对上述代码还有一些其他问题/cmets:

  1. 起初文本显示了两次,一次在默认位置,一次在旋转位置。我认为这是因为在调用 base.OnPaint 方法时首先绘制文本。如果是这种情况,我该如何防止最初绘制文本?

    我的解决方案是在使用布尔值调用 base.OnPaint 之前覆盖文本字符串并清除它,这不是我特别满意的解决方案。

  2. 我应该用e.dispose 处理最后的 PaintEventArgs 吗?我想我不确定 PaintEventArgs 对象是如何被处理的。

提前致谢!

附言。这是我的第一个帖子/问题,所以如果我无意中忽略了一些礼仪或规则,我提前道歉。

【问题讨论】:

    标签: c# winforms onpaint


    【解决方案1】:
    1. VisibleClipBounds 返回需要重新绘制的区域,例如,如果需要重新绘制一半按钮(覆盖一半按钮的顶部窗体已关闭)VisibleClipBounds 仅返回该区域。所以你不能用它来绘制居中的文本。 SizeF sz = new SizeF(宽度, 高度); 应该注意重绘问题。

    2. 按钮不支持所有者绘制,你的方式看起来不错。

    3. 作为一项规则,您不应该处置您尚未创建的对象,并且一次性事件参数由创建它们的逻辑处置(并首先调用 On...)所以不要担心处理 PaintEventArgs。

    欢迎来到 Stack Overflow :)

    【讨论】:

    • 我很清楚 VisibleClipBounds 是如何工作的,但现在你解释了它是有道理的。无论如何,谢谢你。该解决方案非常有效。也感谢您的热烈欢迎。
    【解决方案2】:

    首先,我通过将if... else... 替换为switch... case... 来稍微重构您的代码,并将在每种情况下都相同的两行移出。但这只是为了让它更具可读性:

        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,这也是一个很好的观点。我实际上忘记了它还在里面。
    • 不客气 - 我真的很喜欢这个小谜题!
    猜你喜欢
    • 2012-04-03
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2010-12-15
    • 2015-05-06
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多