Продолжается подписка на наши издания! Вы не забыли подписаться?

Создание контролов в .NET.Основные классы иерархии WinForms контролов.

Авторы: Голованов Михаил
Евгений Веселов
Введение
Базовый класс контролов System.Windows.Forms.Control.
Дескриптор окна контрола
Обработка сообщений Windows.
Свойства, методы и события, управляющие размерами контрола
Отрисовка контрола
Контрол как контейнер
Ambient- и Default-свойства
Основные классы иерархии контролов в .NET
Заключение

Введение

В предыдущей статье Владислава Чистякова рассматривалось создание компонентов в .NET (статья Владислава Чистякова опубликована в журнале RSDN Magazine № 3 за 2002 год). Продолжая дело, начатое им, рассмотрим наиболее важные аспекты создания контролов.

Для начала определимся с терминологией. В .NET контрол – это компонент, представляющий часть пользовательского интерфейса (GUI). Аналогом англоязычного термина control в русском языке является «элемент управления», однако такой перевод длиннее, и труднее произносится, поэтому мы будем использовать, может и не слишком удачную, кальку контрол.

Контролы делятся на два подкласса: WinForms-контролы и ASP.NET-контролы. Первые соответственно предназначены для использования в обычных оконных приложениях, а вторые – в Web-приложениях. В данной статье мы рассмотрим WinForms-контролы, поэтому термин "контрол" далее будет использоваться в контексте WinForms.

Можно выделить три основные группы разрабатываемых контролов:

Базовый класс контролов System.Windows.Forms.Control.

Базовым классом для контролов является System.Windows.Forms.Control – прямой потомок System.ComponentModel.Component. System.Windows.Forms.Control содержит скелет контрола, на который можно наращивать «мясо». Рассмотрим класс System.Windows.Forms.Control более подробно.

Дескриптор окна контрола

Поскольку контрол представляет собой обособленную часть пользовательского интерфейса, он имеет окно и связанный с окном дескриптор.

Получить значение дескриптора окна можно, используя свойство Handle, объявленное как:

public IntPtr Handle {get;}

Поскольку созданное окно потребляет ресурсы системы, то можно создавать окно не в конструкторе контрола, а позже, когда оно действительно понадобится. Создать дескриптор окна контрола можно, вызвав метод CreateHandle, объявленный как:

protected virtual void CreateHandle();

Можно переопределить данный метод в собственном контроле, при этом нужно не забывать вызвать метод CreateHandle. Как правило, не приходится вызывать CreateHandle непосредственно, так как он вызывается методом CreateControl. Дескриптор также будет создан автоматически и при обращении к свойству Handle.

Метод DestroyHandle освобождает дескриптор окна. Декларация метода:

protected virtual void DestroyHandle();

Некоторые оконные опции можно задать только до создания окна. Поэтому иногда бывает необходимо пересоздать окно. Для этого предназначен метод:

protected void RecreateHandle();

Проверить, создан ли дескриптор окна контрола, можно с помощью свойства:

public bool IsHandleCreated {get;}

Свойство:

public bool RecreatingHandle {get;}

указывает на то, что в данный момент ведется пересоздание окна, и нежелательны какие-либо обращения к дескриптору окна.

Свойство

protected virtual CreateParams CreateParams {get;}

позволяет получить и настроить свойства создаваемого контрола. Установленные параметры используются при создании окна контрола. Наиболее часто при создании собственных контролов приходится иметь дело с полем ClassName возвращаемого класса CreateParams. Данное свойство задает класс Windows-контрола, оберткой которого является .NET-контрол. Свойства Style и ExStyle задают стиль создаваемого Windows-контрола и должны быть хорошо известны программистам, разрабатывавшим контролы под Win32.

С дескриптором контрола также связаны два события:

public event EventHandler HandleCreated;

и

public event EventHandler HandleDestroyed;

оповещающие о создании и уничтожении дескриптора окна. Как нетрудно догадаться методы диспетчеризации этих событий называются OnHandleCreated и OnHandleDestroyed и объявлены как:

protected virtual void OnHandleCreated(EventArgs e);
protected virtual void OnHandleDestroyed(EventArgs e);

их можно переопределить в наследнике и обрабатывать данные сообщения без подключения обработчиков событий.

В классе System.Windows.Forms.Control определены два статических метода для поиска контрола по его handle. Первый:

public static Control FromHandle(IntPtr handle);

ищет контрол, имеющий переданный в параметре handle дескриптор окна. Однако для контролов, имеющих несколько дескрипторов, он работает некорректно. Если вам нужно найти такие контролы, то следует использовать второй метод поиска:

public static Control FromChildHandle(IntPtr handle);

В качестве небольшого примера работы с дескриптором окна создадим текстовую метку, отображающую свой дескриптор.

Для этого переопределим ее свойство Text:

public override String Text 
{
  get { return "My handle is " + this.Handle.ToString(); }
}

На рисунке 1 приведен скриншот компонента на этапе разработки.


Рисунок 1.

Обработка сообщений Windows.

Как известно, в Windows взаимодействие с приложениями основано на пересылке сообщений. Приложение получает от OC сообщения и обрабатывает их, используя оконную процедуру. Класс Control содержит несколько методов работы с сообщениями.

Для демонстрации работы контрола с сообщениями Windows введем дополнительное свойство:

private TextBox loggerTextBox;
public  TextBox LoggerTextBox 
{
  get { return loggerTextBox; }
  set { loggerTextBox = value;}
}

Это свойство позволяет задать строку ввода для отображения строк тестовых сообщений, которые будет выводить контрол. Для удобства вывода строк сообщений определим метод LogMessage:

private void LogMessage(string message)
{
  if (loggerTextBox != null &&  DesignMode == false)
    loggerTextBox.Text += message + System.Environment.NewLine;
}

Оконная процедура контрола объявлена как:

protected virtual void WndProc(ref Message m);

Как видно из декларации, контрол может переопределить оконную процедуру. В нашем примере мы переопределили оконную процедуру таким образом, что она выводит тестовые сообщения и вызывает оконную функцию своего предка (класса Control).

protected override void WndProc(ref Message m)
{
  // Вывод тестового сообщения о начале работы оконной процедуры.
  LogMessage("WndProc: start");
  // Вызов оконной процедуры предка
  base.WndProc(ref m);
  // Вывод тестового сообщения о завершении работы оконной процедуры.
  LogMessage("WndProc: finish");
}

Еще одним методом, относящимся к обработке сообщений, является метод:

protected virtual void DefWndProc(ref Message m);

Он позволяет перекрыть оконную процедуру по умолчанию и вызывается из метода WndProc. При этом мы получаем доступ к обработке сообщений Win32 окна контрола.

Переопределим метод DefWndProc нашего контрола следующим образом:

protected override void DefWndProc(ref Message m)
{
  LogMessage("DefWndProc start");
  base.DefWndProc(ref m);
  LogMessage("DefWndProc: finish");
}

Для реакции контрола на сообщения Windows можно воспользоваться событием OnNotifyMessage. Диспетчер события декларирован следующим образом:

protected virtual void OnNotifyMessage(Message m);

Генерация события происходит в методе WndProc, если в стиле контрола установлен флаг EnableNotifyMessage. Данный флаг устанавливается в конструкторе:

public HandledLabel()
{
  this.SetStyle(ControlStyles.EnableNotifyMessage, true);
  InitializeComponent();
}

Диспетчер события мы перекроем следующим образом:

protected override void OnNotifyMessage(Message m)
{
  const String MessageTemplate = 
    "OnNotifyMessage: Message ID = {0}, LParam = {1}, WParam = {2}";
  LogMessage(“OnNotifyMessage:start”);
  LogMessage(String.Format(MessageTemplate, m.Msg, m.LParam, m.WParam));
  base.OnNotifyMessage(m);
LogMessage(“OnNotifyMessage:finish”);
}

Таким образом, мы перекрыли методы нашего контрола так, что можем визуально отслеживать порядок вызова методов обработки сообщений. Ниже приведен скриншот главного окна демонстрационного приложения:


Рисунок 2.

Как видно из скриншота, при получении сообщения вызывается метод WndProc. Метод проверяет значение флага EnableNotifyMessage, и если последний установлен, вызывает OnNotifyMessage. Далее вызывается метод DefWndProc, после чего обработка сообщения завершается.

К сожалению, класс Control не имеет метода отправки сообщения, хотя как показывает исследование кода класса рефлектором, активно использует native метод Win32 SendMessage. В бета-версиях .NET Framework метод SendMessage был доступен и прикладным программистам, однако его применение дает возможность практически прямого доступа к Win32, что может привести к серьезным дырам в безопасности и надежности приложений .NET. В окончательной версии это было устранено.

Свойства, методы и события, управляющие размерами контрола

Класс System.Windows.Forms.Control определяет несколько свойств, методов и событий для управления размерами контрола. В таблицах 1, 2, 3, 4 приведено описание этих свойств, методов и событий.

Отрисовка контрола

Наиболее общим случаем, когда требуется самостоятельно заниматься отрисовкой контрола, является случай разработки компонента «с нуля». Для отрисовки контрола необходимо переопределить метод OnPaint.

Для управления аспектами отрисовки контрола предназначен метод

protected void SetStyle(
   ControlStyles flag,
   bool value
);

Параметр ControlStyles flag должен содержать набор устанавливаемых битовых флагов, задающих параметры отрисовки контрола.

Параметр value указывает на необходимость установить заданные в ControlStyles параметры отрисовки.

В таблице 5 приведено описание флагов ControlStyles.

Свойство Описание
public int Top {get; set;} Координата верхней границы контрола.
public int Left {get; set;} Координата левой границы контрола.
public int Bottom {get;} Координаты нижней границы контрола.
public int Right {get;} Координаты правой границы контрола.
public int Height {get; set;} Высота контрола.
public int Width {get; set;} Ширина контрола.
public Rectangle ClientRectangle {get;} Клиентская область контрола, т.е. область в которой контрол может отрисовывать свое содержимое. В клиентскую область не входят полосы прокрутки, меню, заголовки (title bars).
public Size ClientSize {get; set;} Размер клиентской области.
public Point Location {get; set;} Координаты левого верхнего угла контрола.
public Region Region {get; set;} Регион контрола. Задает форму окна контрола.
public Size Size {get; set;} Размер контрола.
[C#]protected virtual Size DefaultSize {get;} Размер контрола по умолчанию.

Таблица 2. Методы управления размерами контрола.

Метод Описание
public Point PointToClient( Point p); Метод преобразует координаты точки из экранных координат в систему координат контрола.
public Point PointToScreen( Point p); Метод преобразует координаты точки из системы координат контрола в экранные координаты.
public Rectangle RectangleToClient( Rectangle r); Метод преобразует координаты прямоугольника из экранной в систему координат контрола.
public Rectangle RectangleToScreen( Rectangle r); Метод преобразует координаты прямоугольника из системы координат контрола в систему координат экрана.
public void Scale(float ratio);public void Scale(float dx, float dy); Метод масштабирования контрола (растяжение/сжатие).
public void SetBounds( int x, int y, int width, int height, BoundsSpecified specified);public void SetBounds( int x, int y, int width, int height); Метод установки границ контрола. Параметр specified определяет, какие параметры границ устанавливать.
protected virtual void ScaleCore( float dx, float dy); Выполняет работу по масштабированию контрола.
protected virtual void SetBoundsCore( int x, int y, int width, int height, BoundsSpecified specified); Выполняет работу по изменению границ контрола.
protected virtual void SetClientSizeCore(int x, int y); Устанавливает размер клиентской области контрола.

Таблица 3. События, относящиеся к управлению размерами контрола.

Событие Описание
public event EventHandler Move; Событие, возникающее при перемещении контрола.
public event EventHandler Resize; Событие изменения размеров контрола.
public event EventHandler SizeChanged; Событие, возникающее после измения свойства Size.

Таблица 4. Основные свойства позиционирования контрола на форме и управления фокусом ввода.

Свойство Описание
public virtual AnchorStyles Anchor {get; set;} Углы формы, к которым заякорен контрол.
public virtual DockStyle Dock {get; set;} Настройки выравнивания контрола на форме.
public bool CanFocus {get;} Определяет, может ли контрол принимать фокус ввода.
public bool CanSelect {get;} Определяет, может ли контрол быть выбранным. Свойство возвращает true, если у контрола установлен стиль ControlStyles.Selectable. Такие контролы как Panel, GroupBox, PictureBox, ProgressBar, Splitter, Label, LinkLabel не могут быть выбраны.

Таблица 5. Флаги ControlStyles.

Наименование флага Описание Значение
AllPaintingInWmPaint Если данный флаг установлен, контрол игнорирует сообщение WM_ERASEBKGND для уменьшения «шума» (flicker). Данный флаг должен быть установлен в комбинации с флагом UserPaint. 8192
CacheText Кеширование текста контрола. При установке данного флага текст контола кэшируются. Кеширование увеличивает скорость отрисовки, но может порождать проблемы синхронизации. По умолчанию флаг сброшен. 16384
ContainerControl Данный флаг указывает на то, что контрол является контейнером. 1
DoubleBuffer Установка данного флага буферизирует отрисовку контрола, позволяя снизить мерцания и «шумы» отрисовки. 65536
EnableNotifyMessage Установка данного флага приводит к вызову OnNotifyMessage из WndProc при получении контролом сообщений. По умолчанию сброшен. 32768
FixedHeight, FixedWidth Фиксированная высота/ширина контрола. При auto-scale высота/ширина не меняются. 64, 32
Opaque Прозрачность контрола. При установке флага подложка контрола не прорисовывается. 4
ResizeRedraw Отрисовка контрола при изменении его размеров. 16
Selectable Контрол может получать фокус ввода. 512
StandardClick Контрол использует стандартное поведение при щелчке мышью. 256
StandardDoubleClick Контрол использует стандартное поведение при двойном щелчке мышью. 4096
SupportsTransparentBackColor Цвет фона контрола берется в качестве «прозрачного» цвета. Таким образом эмулируется прозрачность. Установка данного флага должна сопровождаться установкой флага . 2048
UserMouse Контрол самостоятельно обрабатывает события мыши, операционной системе не нужно обрабатывать события мыши данного контрола. 1024
UserPaint Контрол отрисовывает себя сам полностью сам. 2

При создании контролов наиболее часто используются флаги AllPaintingInWmPaint, CacheText, DoubleBuffer, Opaque, ResizeRedraw, SupportsTransparentBackColor, UserPaint.

В качестве иллюстрации отрисовки контрола мы создадим контрол, отображающий картинку из ресурсов сборки. Поместим в ресурсы сборки картинку. Для этого правой кнопкой мыши вызовем контекстное меню сборки и выберем из него пункт Add/Add Existing Item… С помощью появившегося диалога выберем любую картинку. В данном примере была выбрана картинка с именем MS.jpg. Далее выберем пункт Properties из контекстного меню этой картинки. В появившемся окне Properties установим свойство «Build Action» в значение «Embedded Resource». Теперь выбранная нами картинка будет включена в сборку в виде внедренного ресурса. В качестве базового класса для нашего контрола мы выбрали класс System.Windows.Forms.Control.

Для отображения картинки переопределим метод OnPaint следующим образом:

protected override void OnPaint(PaintEventArgs pe)
{
1  base.OnPaint(pe);
2  System.Reflection.Assembly assembly = GetType().Module.Assembly;
3  Bitmap image = new Bitmap(assembly.GetManifestResourceStream(
     "DrawingControl.MS.JPG");
4  Brush brush = new TextureBrush(image);
5  pe.Graphics.DrawImage(image, pe.ClipRectangle);
}

Для удобства изложения строки пронумерованы. Метод OnPaint получает в качестве параметра объект PaintEventArgs. Он содержит два свойства:

В строке 1 вызывается метод OnPaint базового класса. Это позволит избавиться от необходимости выполнять базовую отрисовку. В строке 2 мы получаем текущую сборку, а в строке 3 извлекаем из сборки, включенный в нее ресурс-картинку. Строки 2 и 3 не имеют непосредственного отношения к отрисовке контрола, вся полезная работа происходит в строках 4 и 5. В строек 4 создается кисть, предназначенная для отрисовки картинки, а в строке 5, с помощью «полотна» рисования контрола, картинка выводится на экран. Как видите, ничего сложного в собственной отрисовке контрола нет. Полотно рисования Graphics позволяет использовать для отрисовки все возможности GDI+, что делает задачу написания отрисовки контрола простой и необременительной задачей.

Как вы, наверно, уже заметили, метод OnPaint объявлен защищенным (protected), то есть вызвать его можно только из класса контрола и его потомков. С одной стороны, это правильно, так как прикладным программистам, которые будут использовать ваш контрол, явно вызывать метод отрисовки ни к чему. Однако при этом возникает вопрос: как осуществлять собственную отрисовку дочерних контролов в том случае, когда контрол представляет собой совокупность нескольких стандартных контролов (composite control). Мы не можем переопределить методы OnPaint составляющих контрола. В данном случае применяется второй способ отрисовки – добавление собственного обработчика в список обработчиков события Paint. Для каждого контрола, входящего в составной контрол, можно добавить обработчик Paint и в нем производить дорисовку. Из-за тривиальности такой задачи мы не будем приводить пример ее реализации.

Контрол как контейнер

Контрол может выступать в качестве контейнера для других контролов и компонентов. Ярким примером контрола-контейнера является панель (Panel). Панель управляет контролами, размещенными на ней. Базовые механизмы поддержки контейнеров компонентов реализованы в классе Contol.

В классе Control объявлена коллекция Controls предназначенная для хранения и управления своими дочерними контролами.

public Control.ControlCollection Controls {get;}

Класс Control.ControlCollection представляет собой типизированную коллекцию. Перед добавлением нескольких контролов в контейнер рекомендуется вызывать метод SuspendLayout, а сразу после добавления – метод ResumeLayout. Вызов метода SuspendLayout останавливает работу менеджера выравнивания (layout logic), отвечающего за обработку свойств Size, Location, Anchor, или Dock дочерних контролов, а метод ResumeLayout возобновляет работу менеджера. Пример работы с коллекцией и менеджером слоев можно увидеть, открыв код метода InializeComponent любой формы, содержащей контролы.

private void InitializeComponent()
{
  this.components = new System.ComponentModel.Container();
  this.handledLabel1 = new HandledLabel.HandledLabel();
  this.textBox1 = new System.Windows.Forms.TextBox();
  this.button1 = new System.Windows.Forms.Button();
  this.timer1 = new System.Windows.Forms.Timer(this.components);

  this.SuspendLayout();
  // 
  // handledLabel1
  // Установка свойств 
  ...
  
  // 
  // textBox1
  // Установка свойств 
  ...
  
  // 
  // Form1
  // 
  this.AutoScaleBaseSize = new System.Drawing.Size(5, 13);
  this.ClientSize = new System.Drawing.Size(520, 350);
  this.Controls.AddRange(new System.Windows.Forms.Control[] 
  {
    this.button1,
    this.userControl12,
    this.drawingControl1,
    this.textBox1,
    this.handledLabel1});
    this.Name = "Form1";
    this.Text = "Form1";
    this.ResumeLayout(false);
  }

В коллекции Control.ControlCollection могут находиться только элементы-наследники System.Windows.Forms.Control. Сразу возникает вопрос – каким образом хранятся и обрабатываются невизуальные компоненты наподобие Timer? Ответ на этот вопрос можно увидеть все в том же коде формы. В классе формы объявлено поле:

private System.ComponentModel.IContainer components;

Все невизуальные компоненты добавляются в контейнер components. Пример:

this.timer1 = new System.Windows.Forms.Timer(this.components);

Далее в методе Dispose, происходит освобождение ресурсов занятых невизуальными контролами.

protected override void Dispose(bool disposing)
{
  if( disposing )
  {
    if (components != null) 
    {
      components.Dispose();
    }
  }
  base.Dispose( disposing );
}

Кроме хранения контролов, необходимо так же управлять фокусом ввода и другими визуальными аспектами работы контейнера. Данная функциональность реализована в классе System.Windows.Forms.ContainerControl.

Ambient- и Default-свойства

Одной из малодокументированных областей, часто вызывающей вопросы при разработке контролов, является вопрос установки значений свойства по умолчанию.

Предположим, что у контрола есть свойство ExtraColor. Значение по умолчанию данного свойства необходимо установить равным значению BackColor формы, на которой расположен контрол.

public class AmbientControl : System.Windows.Forms.Control
{
  public AmbientControl()
  {
  }

  private Color extraColor;

public Color ExtraColor 
  {
    get 
    {
      if (extraColor == Color.Empty && Parent != null) 
      {
        return Parent.BackColor;
      }
      return extraColor;
    }
    set 
    {
      extraColor = value;
    }
  }
}

Попытаемся задать значение по умолчанию, использовав атрибут DefaultValue. Объявим свойство следующим образом:

private Color extraColor;
[DefaultValue(typeof(Color), "Empty")]
public Color ExtraColor
{
  get 
  {
    if (extraColor == Color.Empty && Parent != null) 
      return Parent.BackColor;
    return extraColor;
  }
  set 
  {
    extraColor = value; 
  }
}

Изменив значение свойства, мы увидим, что пункт контекстного меню Reset доступен, и позволяет нам восстановить значение свойства по умолчанию. Но это замечательное решение будет работать, только если значение по умолчанию известно заранее. При этом (при размещении контрола на форме) в код инициализации контрола будет добавлена строка:

this.AmbientControl.ExtraColor = System.Drawing.SystemColors.Control;

То есть мы явно задаем значение свойства ExtraColor. Однако если мы внимательно посмотрим на код инициализации формы, то нигде не обнаружим задания ее свойства BackColor. Аналогичного эффекта хотелось бы добиться и для свойства ExtraColor нашего контрола. Проблема заключается в том, что значение по умолчанию свойства BackColor формы не известно заранее, и явное задание ExtraColor при инициализации нашего контрола не может решить поставленной нами задачи.

Для случая, когда значение свойства контрола по умолчанию не известно заранее, а должно браться из внешнего по отношению к контролу источника, предназначен атрибут AmbientValue. Объявим свойство с использованием этого атрибута:

private Color extraColor;
[AmbientValue(typeof(Color), "Empty")]
public Color ExtraColor
...

Кроме этого необходимо реализовать метод:

private bool ShouldSerializeExtraColor() 
{
  return extraColor != Color.Empty;
}

Этот метод сообщает среде разработки о необходимости явной сериализации значения свойства. Метод возвращает true только в том случае, если значения свойства задано пользователем явно.

Поместим контрол на форму. При этом значение свойства ExtraColor будет браться из свойства BackColor формы, и, в отличие от случая использования атрибута DefaultValue, явного задания значения свойства в методе InitializeComponent формы не произойдет. Изменим значение свойства ExtraColor и убедимся, что в методе InitializeComponent формы добавилась строка, явно инициализирующая значение ExtraColor. Затем в контекстном меню свойства выберем пункт Reset, вернув значение свойства в состояние по умолчанию. Строка явной инициализации значения свойства ExtraColor исчезнет, а его значение снова будет браться из значения BackColor формы, что и требовалось реализовать.

Основные классы иерархии контролов в .NET

Классы-наследники System.Windows.Forms.Control представляют собой либо базовые классы для создания контролов (System.Windows.Forms.ButtonBase – базовый класс для создания кнопок, System.Windows.Forms.TextBoxBase – базовый класс для создания строк ввода текста), либо готовые контролы:

Большинство из вышеперечисленных контролов представляют собой .NET обертки над Win32 контролами.

В качестве небольшой иллюстрации возможностей наследования от классов-наследников Control мы разработали компонент текстовой метки с тенью. Ниже приведен код компонента.

  public class dfLabel : System.Windows.Forms.Label
  {
    /// <summary>
    /// Required designer variable.
    /// </summary>
    private System.ComponentModel.Container components = null;
    private SolidBrush textShadowBrush;

    public dfLabel(System.ComponentModel.IContainer container)
    {
      /// <summary>
      /// Required for Windows.Forms Class Composition Designer support
      /// </summary>
      container.Add(this);
      InitializeComponent();

      //
      // TODO: Add any constructor code after InitializeComponent call
      //
        }

    public dfLabel()
    {
      /// <summary>
      /// Required for Windows.Forms Class Composition Designer support
      /// </summary>
      InitializeComponent();

      //
      // TODO: Add any constructor code after InitializeComponent call
      //
    }

    #region Component Designer generated code
    /// <summary>
    /// Required method for Designer support - do not modify
    /// the contents of this method with the code editor.
    /// </summary>
    private void InitializeComponent()
    {
      components = new System.ComponentModel.Container();
    }
    #endregion

    private int shadowOffset =  1;
    public int ShadowOffset
    {
      get {return shadowOffset; }
      set 
      {
        shadowOffset = value;
        Refresh();
      }
    }
    
    private StringAlignment ShadowAlign
    {
      get 
      {
        switch(this.TextAlign)
        {
          case ContentAlignment.BottomLeft: 
          {
            return StringAlignment.Near;
          } 
          case ContentAlignment.MiddleLeft: 
            {goto case ContentAlignment.BottomLeft; }
          case ContentAlignment.TopLeft: 
            {goto case ContentAlignment.BottomLeft; }
          case ContentAlignment.BottomCenter:
          { 
            return StringAlignment.Center;
          }
          case ContentAlignment.MiddleCenter: 
          { 
            goto case ContentAlignment.BottomCenter; 
          } 
          case ContentAlignment.TopCenter: 
          {
            goto case ContentAlignment.BottomCenter; 
          }
          case ContentAlignment.BottomRight:
          {
            return StringAlignment.Far;
          }
          case ContentAlignment.MiddleRight:
          {
            goto case ContentAlignment.BottomRight;
          }
          case ContentAlignment.TopRight:
          {
            goto case ContentAlignment.BottomRight;
          }
          default: return StringAlignment.Near; 
        }
      }
    }

    private int ShadowY()
    {
      switch(this.TextAlign)
      {
        case ContentAlignment.TopCenter: return shadowOffset;
        case ContentAlignment.TopLeft:
          goto case ContentAlignment.TopCenter;
        case ContentAlignment.TopRight:
          goto case ContentAlignment.TopCenter;
        case ContentAlignment.BottomCenter: 
          return this.ClientRectangle.Height 
            - this.FontHeight + shadowOffset - 1;
        case ContentAlignment.BottomLeft:
          goto case ContentAlignment.BottomCenter;
        case ContentAlignment.BottomRight:
          goto case ContentAlignment.BottomCenter;
        case ContentAlignment.MiddleCenter: 
          return ((this.ClientRectangle.Height - this.FontHeight)/2)
            + shadowOffset;
        case ContentAlignment.MiddleLeft:
          goto case ContentAlignment.MiddleCenter;
        case ContentAlignment.MiddleRight:
          goto case ContentAlignment.MiddleCenter;
        default: goto case ContentAlignment.TopCenter;
      }
    }
    
    protected override void OnPaint(PaintEventArgs e)
    {
      base.OnPaint(e);
      
      this.textShadowBrush = 
        new SolidBrush(Color.FromArgb(70, this.ForeColor));
      
      Graphics g = e.Graphics;
      
      StringFormat formatter = new StringFormat();
      formatter.Alignment = ShadowAlign;
      
      Rectangle shadowrect = this.ClientRectangle;
      shadowrect.X += ShadowOffset;
      shadowrect.Y = ShadowY();
      
      g.DrawString(this.Text, this.Font, 
        textShadowBrush, shadowrect, formatter);
    }

  }

В методе отрисовки OnPaint, помимо стандартной отрисовки контрола, вызываемой через base.OnPaint, мы дорисовываем тень, смещая ее вниз и вправо. Получение цвета тени осуществляется вызовом Color.FromArgb(70, this.ForeColor). Наличие остального кода связано с различными режимами выравнивания текстовой метки по горизонтали и вертикали. Таким образом, пример демонстрирует возможность модификации вида стандартных контролов.

Гораздо более интересен с точки зрения разработчиков компонентов класс System.Windows.Forms.ScrollableControl.

Данный класс является базовым для всех контролов, поддерживающих автоматическую прокрутку содержимого (auto-scroll). Для поддержки автоматического отображения полос прокрутки введено свойство AutoScroll. Свойство AutoScrollMinSize задает минимальные размеры контрола, при которых отображаются полосы прокрутки. Свойства VScroll и HScroll позволяют управлять видимостью вертикальной и горизонтальной полос прокрутки соответственно.

Класс ContainerControl является непосредственным потомком ScrollableControl и реализует поддержку контейнера контролов. Кроме того, класс управляет передачей фокуса ввода дочерними контролам. Класс реализует интерфейс IContainerControl, содержащий одно свойство ActiveControl и один метод ActivateControl().

Свойство ActiveControl позволяет получать и изменять дочерний контрол, имеющий фокус ввода. Метод:

bool ActivateControl(Control active);

предназначен для переключения фокуса ввода на указанный в параметре дочерний контрол. Класс ContainerControl имеет четырех наследников:

Наиболее часто при разработке компонентов используются классы System.Windows.Forms.Form и System.Windows.Forms.UserControl. Класс System.Windows. Forms.Form является базовым классом для создания форм. Класс System.Windows.Forms.UserControl является базовым классом для создания композитных контролов.

Композитный контрол представляет собой повторно используемый контейнер, содержащий набор контролов с заранее установленными значениями свойств. В качестве примера создания композитного контрола разработаем контрол-строку ввода с текстовой меткой. Для этого необходимо в среде VS.NET создать новый проект, либо в существующем проекте, используя пункт контекстного меню Add/Add UserControl, добавить заготовку композитного контрола. В появившемся диалоге создания контрола зададим имя контрола – CompositeControl. При этом в проект будет добавлен файл CompositeControl.cs, содержащий код нашего контрола. Помимо текстового представления доступен и визуальный редактор контрола. Разместим с его помощью на контроле строку ввода textBox1 и текстовую метку label1.


Рисунок 3.

Даже в таком виде контрол уже может использоваться. Однако нам необходимо обеспечить возможность доступа к свойствам контролов, расположенных внутри композитного контрола. В частности, нужно обеспечить возможность задания значений свойств Text метки и строки ввода.

Для решения поставленной задачи существует несколько путей. Первый вариант – расширение области видимости контролов контейнера. По умолчанию в композитном контроле вложенные контролы имеют область видимости private и, соответственно, недоступны снаружи.

public class CompositeControl : System.Windows.Forms.UserControl
{
  private System.Windows.Forms.Label label1;
  private System.Windows.Forms.TextBox textBox1;
…

Изменив область видимости на public, мы делаем возможным доступ к контролам. Описанный способ имеет один серьезный недостаток – так как label1 и textBox1 объявлены как обычные переменные, а не свойства, то о визуальном редактировании их свойств придется забыть, и вводить код установки нужных значений свойств вложенных контролов вручную.

Чтобы обеспечить возможность визуального редактирования, необходимо объявить данные контролы свойствами композитного контрола, доступными только для чтения.

public class CompositeControl : System.Windows.Forms.UserControl
{

  private System.Windows.Forms.Label label1;
  private System.Windows.Forms.TextBox textBox1;

[DesignerSerializationVisibility(DesignerSerializationVisibility.Content)]
  public System.Windows.Forms.Label label
{
  get { return label1; }
}

[DesignerSerializationVisibility(DesignerSerializationVisibility.Content)]
public System.Windows.Forms.TextBox textBox
{
  get { return textBox1; }
}

Обратите внимание, что у свойств имеется атрибут DesignerSerializationVisibility со значением, равным DesignerSerializationVisibility.Content. Если не задавать этого атрибута, установленные у textbox и label свойства не будут сериализоваться формой.

Заключение

В данной статье была кратко рассмотрена иерархия классов WinForms-контролов. :( Мы постарались кратко осветить наиболее важные с точки зрения разработчиков контролов классы иерархии. При разработке собственных компонентов трудно переоценить понимание иерархии классов используемой объектной библиотеки, поэтому если после прочтения данной статьи в голове читателя отложилось четкое понимание иерархии контролов WinForms, то мы считаем, что цель написания данной статьи достигнута. Мы преднамеренно не останавливались подробно на побочных аспектах рассмотренных классов.

В дальнейших статьях планируется рассмотреть иерархию классов WebForms, поведение контролов на этапе проектирования, а также затронуть другие актуальные вопросы создания элементов управления в .NET.


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

Copyright © 1994-2016 ООО "К-Пресс"