Как нарисовать пользовательские границы на элементах управления .Net WinForms

Я пытался нарисовать пользовательские границы для существующих элементов управления .Net WinForms. Я попытался это сделать, создав класс, в элементе управления которого я хочу изменить цвет границы, а затем попробовал несколько вещей во время рисования. Я пробовал следующее:

1. Лови WM_NCPAINT. Это в некоторой степени работает. Проблема с приведенным ниже кодом заключается в том, что при изменении размера элемента управления граница будет обрезана справа и снизу. Фигово.

protected override void WndProc(ref Message m)
{
  if (m.Msg == NativeMethods.WM_NCPAINT) {
    WmNcPaint(ref m);
    return;
  }
  base.WndProc(ref m);
}

private void WmNcPaint(ref Message m)
{
  if (BorderStyle == BorderStyle.None) {
    return;
  }

  IntPtr hDC = NativeMethods.GetWindowDC(m.HWnd);
  if (hDC != IntPtr.Zero) {
    using (Graphics g = Graphics.FromHdc(hDC)) {
      ControlPaint.DrawBorder(g, new Rectangle(0, 0, this.Width, this.Height), _BorderColor, ButtonBorderStyle.Solid);
    }
    m.Result = (IntPtr)1;
    NativeMethods.ReleaseDC(m.HWnd, hDC);
  }
}

2. Отменить void OnPaint. Это работает для некоторых элементов управления, но не для всех. Это также требует, чтобы вы установили BorderStyle в BorderStyle.None, и вам нужно вручную очистить фон от краски, иначе вы получите это при изменении размера.

protected override void OnPaint(PaintEventArgs e)
{
  base.OnPaint(e);
  ControlPaint.DrawBorder(e.Graphics, new Rectangle(0, 0, this.Width, this.Height), _BorderColor, ButtonBorderStyle.Solid);
}

3. Переопределение void OnResize и void OnPaint (как в способе 2). Таким образом, он хорошо отображается при изменении размера, но не тогда, когда на панели AutoScroll включен, и в этом случае он будет выглядеть следующим образом при прокрутке вниз. Если я попытаюсь использовать WM_NCPAINT для закрашивания границы, Refresh() не даст никакого эффекта.

protected override void OnResize(EventArgs eventargs)
{
  base.OnResize(eventargs);
  Refresh();
}

Предложения более чем приветствуются. Я хотел бы знать, какой лучший способ сделать это для нескольких типов элементов управления (мне придется сделать это для нескольких элементов управления WinForms по умолчанию).


person Codecat    schedule 29.09.2014    source источник
comment
Я уверен, что вы слышали это раньше, но я честно предлагаю просто использовать WPF вместо WinFroms. Кроме того, у вас мои пожелания удачи и мои +1 за хорошо написанный вопрос.   -  person BradleyDotNET    schedule 29.09.2014
comment
Спасибо! И да, я слышал это раньше, много раз :) Мне все еще нужно найти время, чтобы изучить WPF, но этот проект слишком глубоко погружен в WinForms, чтобы преобразовать его в WPF. Возможно в будущем.   -  person Codecat    schedule 29.09.2014
comment
защищенное переопределение void OnResize (EventArgs eventargs) {base.OnResize (eventargs); Обновить (); }   -  person houssam    schedule 30.09.2014
comment
Хусам, это, к сожалению, не работает. (По крайней мере, для Панели.)   -  person Codecat    schedule 30.09.2014
comment
На самом деле, немного повозившись, я заставил его работать. Позвольте мне провести еще несколько тестов. Изменить: Нет, к сожалению, это нарушает рисование автопрокрутки на панелях из-за рисования границы в OnPaint (как видно из метода 2 в моем вопросе) прокрутки вниз: 4o4.nl/20140929R67rl.png Я обновлю свой вопрос.   -  person Codecat    schedule 30.09.2014
comment
Просто подумайте: где бы вы ни отключили все границы, сделайте это и разместите элементы управления на панели, для которой вы можете нарисовать границу. Не знаю, как отключить границы для каждого элемента управления, хотя вкладка, например, выиграла ' не позволяю тебе делать это ..   -  person TaW    schedule 30.09.2014
comment
@TaW Даже если бы я мог это сделать, мне все равно нужно было бы рисовать настраиваемую границу на панелях, что, как уже отмечалось, не работает с AutoScroll. (Если вы не намекаете, я должен поместить панель внутрь панели ..: P)   -  person Codecat    schedule 30.09.2014
comment
Хм, не уверен, что могу понять. Я хотел добавить Panel только для рисования ее границ, а не для обеспечения функций AutoScroll. Итак, да, там, где это было необходимо, я действительно имел в виду поместить Panel внутри Panel. Такой тип стекирования на самом деле не так уж и плох ... Конечно, это не WPF, где стекинг является нормальным и достигает гораздо более экстремальных уровней, но там, где это необходимо, это может помочь решить проблему с границами, по крайней мере, в некоторых случаях. Учитывая разрозненность элементов управления Winforms, я сомневаюсь, что вы сможете найти универсальное решение.   -  person TaW    schedule 30.09.2014
comment
Хм, я боюсь, что вы правы, и это может быть единственное решение, что очень жаль. Оставим вопрос открытым еще немного, может, у кого-то есть идея получше.   -  person Codecat    schedule 30.09.2014
comment
Если единственная проблема с вариантом №1 - обрезка, почему бы просто не вычесть 1 из ширины и высоты? Это обычное дело, кстати, с рисованием бордюров.   -  person DonBoitnott    schedule 30.09.2014
comment
К сожалению, это не работает. Я знаю, что для рисования бордюров часто требуются Width-1 и Height-1 для границ, но для ControlPaint.DrawBorder это не нужно. Обратите внимание, что он отлично отрисовывается, если вы не измените размер элемента управления (с помощью привязки или чего-то подобного).   -  person Codecat    schedule 30.09.2014


Ответы (2)


РЕДАКТИРОВАТЬ: Итак, я понял, что было причиной моих первоначальных проблем. После очень долгих попыток, экспериментов и изучения исходного кода .Net framework, вот окончательный способ сделать это (учитывая, что у вас есть элемент управления, который наследуется от элемента управления, на котором вы хотите нарисовать настраиваемую границу):

[DllImport("user32.dll")]
public static extern bool RedrawWindow(IntPtr hWnd, IntPtr lprcUpdate, IntPtr hrgnUpdate, RedrawWindowFlags flags);

[Flags()]
public enum RedrawWindowFlags : uint
{
  Invalidate = 0X1,
  InternalPaint = 0X2,
  Erase = 0X4,
  Validate = 0X8,
  NoInternalPaint = 0X10,
  NoErase = 0X20,
  NoChildren = 0X40,
  AllChildren = 0X80,
  UpdateNow = 0X100,
  EraseNow = 0X200,
  Frame = 0X400,
  NoFrame = 0X800
}

// Make sure that WS_BORDER is a style, otherwise borders aren't painted at all
protected override CreateParams CreateParams
{
  get
  {
    if (DesignMode) {
      return base.CreateParams;
    }
    CreateParams cp = base.CreateParams;
    cp.ExStyle &= (~0x00000200); // WS_EX_CLIENTEDGE
    cp.Style |= 0x00800000; // WS_BORDER
    return cp;
  }
}

// During OnResize, call RedrawWindow with Frame|UpdateNow|Invalidate so that the frame is always redrawn accordingly
protected override void OnResize(EventArgs e)
{
  base.OnResize(e);
  if (DesignMode) {
    RecreateHandle();
  }
  RedrawWindow(this.Handle, IntPtr.Zero, IntPtr.Zero, RedrawWindowFlags.Frame | RedrawWindowFlags.UpdateNow | RedrawWindowFlags.Invalidate);
}

// Catch WM_NCPAINT for painting
protected override void WndProc(ref Message m)
{
  if (m.Msg == NativeMethods.WM_NCPAINT) {
    WmNcPaint(ref m);
    return;
  }
  base.WndProc(ref m);
}

// Paint the custom frame here
private void WmNcPaint(ref Message m)
{
  if (BorderStyle == BorderStyle.None) {
    return;
  }

  IntPtr hDC = NativeMethods.GetWindowDC(m.HWnd);
  using (Graphics g = Graphics.FromHdc(hDC)) {
    g.DrawRectangle(new Pen(_BorderColor), new Rectangle(0, 0, this.Width - 1, this.Height - 1));
  }
  NativeMethods.ReleaseDC(m.HWnd, hDC);
}

Итак, вкратце, оставьте OnPaint как есть, убедитесь, что WS_BORDER установлен, затем поймайте WM_NCPAINT и нарисуйте границу через hDC, и убедитесь, что RedrawWindow вызывается в OnResize.

Это можно было бы даже расширить, чтобы нарисовать настраиваемую полосу прокрутки, потому что это часть рамки окна, которую вы можете рисовать во время WM_NCPAINT.

Я удалил из этого свой старый ответ.

РЕДАКТИРОВАТЬ 2: Для ComboBox вам нужно поймать WM_PAINT в WndProc(), потому что по какой-то причине источник .Net для рисования ComboBox не использует OnPaint(), а WM_PAINT. Так что-то вроде этого:

protected override void WndProc(ref Message m)
{
  base.WndProc(ref m);

  if (m.Msg == NativeMethods.WM_PAINT) {
    OnWmPaint();
  }
}

private void OnWmPaint()
{
  using (Graphics g = CreateGraphics()) {
    if (!_HasBorders) {
      g.DrawRectangle(new Pen(BackColor), new Rectangle(0, 0, this.Width - 1, this.Height - 1));
      return;
    }
    if (!Enabled) {
      g.DrawRectangle(new Pen(_BorderColorDisabled), new Rectangle(0, 0, this.Width - 1, this.Height - 1));
      return;
    }
    if (ContainsFocus) {
      g.DrawRectangle(new Pen(_BorderColorActive), new Rectangle(0, 0, this.Width - 1, this.Height - 1));
      return;
    }
    g.DrawRectangle(new Pen(_BorderColor), new Rectangle(0, 0, this.Width - 1, this.Height - 1));
  }
}
person Codecat    schedule 02.10.2014

На самом деле вы можете использовать элементы управления взаимодействием WPF для создания любой границы, которую захотите.

  1. Создать форму
  2. Поместите элемент управления ElementHost (из WPF Interoperability) в форму
  3. Создайте пользовательский элемент управления WPF (или используйте существующую панель) с настраиваемой рамкой
  4. Поместите элемент управления WindowsFormsHost в пользовательский элемент управления WPF (этот элемент управления будет использоваться позже для размещения вашего элемента управления)
  5. Установите свойство ElementHost Child с помощью пользовательского элемента управления WPF из предыдущего шага.

    Я согласен с тем, что мое решение содержит много вложенных элементов управления, но, с моей точки зрения, оно значительно снижает количество проблем, связанных с OnPaint   вложенные элементы управления WPF + WinForm

person tarasn    schedule 01.10.2014
comment
Это выглядит слишком ресурсоемким, к тому же, как вы уже сказали, слишком сложным. - person Codecat; 01.10.2014
comment
Ресурсоемкий? Это зависит от того, сколько элементов управления вы хотите нарисовать настраиваемой рамкой. - person tarasn; 01.10.2014
comment
Каждый элемент управления, имеющий границу. Ваш ответ - возможное решение, но не идеальное. - person Codecat; 01.10.2014