Как правильно получить элемент управления WinForms Button для рисования пользовательского текста

Я пытаюсь создать собственный элемент управления кнопки 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;
    }
}

Итак, мой главный вопрос: как я могу решить проблему перерисовки текста?

Кроме того, у меня есть несколько других вопросов/комментариев к приведенному выше коду:

  1. Сначала текст появлялся дважды, один раз в месте по умолчанию и один раз в повернутом месте. Я предполагаю, что это потому, что текст рисуется первым, когда вызывается метод base.OnPaint. Если это так, как мне сделать так, чтобы текст изначально не рисовался?

    Мое решение состоит в том, чтобы переопределить текстовую строку и очистить ее перед вызовом base.OnPaint с использованием логического значения, что не является решением, которым я особенно доволен.

  2. Должен ли я удалять PaintEventArgs в конце с помощью e.dispose? Думаю, я не уверен, как обрабатывается объект PaintEventArgs.

Заранее спасибо!

Пс. Это мой первый пост/вопрос, поэтому заранее извиняюсь, если случайно проигнорировал какой-то этикет или правила.


person Scott Lemmon    schedule 03.09.2012    source источник


Ответы (2)


  1. VisibleClipBounds возвращает область, которую необходимо перекрасить, например, если нужно перекрасить половину кнопки (верхняя форма, покрывающая половину кнопки, закрыта), VisibleClipBounds возвращает только эту область. Таким образом, вы не можете использовать это для рисования текста по центру. SizeF sz = новый SizeF (ширина, высота); Стоит решить проблему перекраски.

  2. Кнопка не поддерживает отрисовку владельца, ваш способ выглядит нормально.

  3. Как правило, вы не должны удалять объекты, которые вы не создали, а одноразовые аргументы событий удаляются логикой, которая их создала (и вызвала On... в первую очередь), поэтому не беспокойтесь об удалении PaintEventArgs.

Добро пожаловать в Stack Overflow :)

person RollingCog    schedule 03.09.2012
comment
Я вполне понимаю, как работает VisibleClipBounds, но теперь, когда вы объяснили, это имеет смысл. В любом случае, спасибо. Это решение прекрасно сработало. Также спасибо за теплый прием. - person Scott Lemmon; 04.09.2012

Во-первых, я немного реорганизовал ваш код, заменив 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. Я думаю, что ваше решение в порядке. Вы можете просто установить text в пустую строку перед вызовом base.OnPaint() и вместо этого восстановить правильное значение.
  2. Нет, определенно нет. Объект PaintEventArgs создается где-то за пределами метода OnPaint, удаление (при необходимости) должно выполняться там - только "создатель" объекта знает, как и когда правильно избавиться от него. (Вы не знаете, понадобится ли это коду, который впоследствии вызвал событие Paint).
person Treb    schedule 03.09.2012
comment
Спасибо, я ценю работу и отзыв. Я никогда раньше не использовал switch... case..., но теперь, когда я увидел его, он выглядит довольно полезным. Я собираюсь прочитать больше об этом и начать пытаться использовать его. Кроме того, это хорошее замечание по поводу MeasureString. Я даже забыл, что он все еще там. - person Scott Lemmon; 04.09.2012
comment
Не за что - мне очень понравилась эта маленькая головоломка! - person Treb; 04.09.2012