Практикум прикладного программирования на C# в среде VS.NET 2005

Интерфейс времени проектирования для компонента

Показывать лекцию целиком

Разработка компонента с развитым интерфейсом времени проектирования

Компоненты - это программные единицы, которые функционируют в двух режимах: проектирования (design-time) и выполнения (run-time). Готовый компонент представляется классом, который имеет три вида интерфейса:

  • Программный интерфейс - все сервисные методы, свойства и события класса, представляющего компонент, доступные клиентам этого класса
  • Интерфейс времени проектирования - средства, определяющие вид и поведение компонента под управлением оболочки во время проектирования приложения. Это дополнительные удобства быстрой настройки для пользователей компонента, которыми являются такие же программисты
  • Интерфейс времени выполнения - внешнее представление и поведение компонента, работающего в составе готового приложения
  • Интерфейс времени проектирования сказывается только на удобствах программистов, использующих компонент, и не влияет на поведение компонента во время выполнения. К тому же, разработка интерфейса времени проектирования требует дополнительных усилий, сравнимых, а иногда и превышающих затраты на разработку функциональности времени выполнения. Но если программист специализируется не на разработке готовых приложений, а на разработке готовых компонентов, то интерфейсу проектирования он должен уделить должное внимание.

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

    Создание проекта

    Мы хотим создать компонент GradientLabel как расширение библиотечной текстовой метки Label, в которой цвет фона текстового блока имел бы градиентную заливку. На этом примере мы познакомимся с основами техники создания пользовательского компонента, где внимание будет уделено не столько созданию самого компонента для его нормального функционирования во время выполнения, сколько созданию дополнительных средств, обеспечивающих его полноценное поведение как компонента во время использования в среде проектирования.

    Создадим решение и настроим оболочку для отладки компонента в режиме разработки.

  • Командой File/New/Project вызовите мастер создания решения и заполните его как показано на экранном снимке
  • Командой File/Add/New Project опять вызовите мастер создания проекта и заполните его так
  • После выполнения этих шагов мы получим каталог MySolution размещения решения, в котором будут созданы два подкаталога проектов с именами Test и MyControl.

  • В проводнике решений вызовите контекстное меню для узла MyControl проекта компонента и выполните команду Properties, чтобы вызвать мастер настройки свойств проекта
  • Откройте вкладку Debug, установите переключатель Start Action в значение Start external program и выберите в качестве внешней программы саму среду devenv.exe ( Development Environment - среда развития). Этот файл, скорее всего, будет иметь полное имя C:\Program Files\Microsoft Visual Studio 8\Common7\IDE\devenv.exe
  • В проводнике решений удалите автоматически добавленный при создании проекта MyControl файл UserControl1.cs
  • В проводнике решений вызовите контекстное меню для узла MyControl и командой Add/Component добавьте новый файл GradientLabel.cs
  • Переведите файл GradientLabel.cs в режим View Code и замените в классе GradientLabel базовый класс Component на производный от него компонент Label - ближайший по функциональности библиотечный предок нашего будущего пользовательского компонента
  • Добавьте в заголовок файла подключение дополнительных пространств имен
  • using System.Drawing;
  • using System.Drawing.Drawing2D;
  • using System.Windows.Forms;
  • После выполненных действий заготовка файла GradientLabel.cs должна стать такой

    using System;
    using System.ComponentModel;
    using System.Collections.Generic;
    using System.Diagnostics;
    using System.Text;
    using System.Drawing;
    using System.Drawing.Drawing2D;
    using System.Windows.Forms;
        
    /////////////////////////////////////////////////////////////////
    // Блок кода №1
    /////////////////////////////////////////////////////////////////
    namespace MyControl
    {
        public partial class GradientLabel : Label
        {
            public GradientLabel()
            {
                InitializeComponent();
            }
        
            public GradientLabel(IContainer container)
            {
                container.Add(this);
        
                InitializeComponent();
            }
        }
    }

    Нам нужно добавить к классу GradientLabel:

  • два свойства для управления границами цвета компонента;
  • одно событие с сигнатурой библиотечного делегата EventHandler ;
  • а также переопределить унаследованный от Control метод OnPaint() (переопределение метода начинают с ввода ключевого слова override, чтобы активизировать подсказчик кода IntelliSense ).
  • Для этого:

  • Поместите в самый конец файла GradientLabel.cs на свободное место следующий блок кода как самостоятельную единицу частичного (partial) класса GradientLabel
  • /////////////////////////////////////////////////////////////////
    // Блок кода №2 продолжения класса GradientLabel 
    /////////////////////////////////////////////////////////////////
    namespace MyControl
    {
        partial class GradientLabel
        {
            // Закрытые поля
            private Color startColor = Color.Yellow;
            private Color endColor = Color.Red;
        
            public Color StartColor // Общедоступное свойство 
            {
                get { return startColor; }
                set
                {
                    // Меняем значение
                    startColor = value;
        
                    // Инициируем событие
                    OnGradientChange(EventArgs.Empty);
                }
            }
        
            public Color EndColor // Общедоступное свойство 
            {
                get { return endColor; }
                set 
                { 
                    // Меняем значение
                    endColor = value;
        
                    // Инициируем событие
                    OnGradientChange(EventArgs.Empty);
                }
            }
        
            // Объявление события изменения свойств цвета градиента
            public event EventHandler GradientChange;
        
            // Функция диспетчеризации события GradientChange
            protected virtual void OnGradientChange(EventArgs args)
            {
                // Если есть зарегистрированные обработчики,
                // то инициируем событие и вызываем обработчики 
                if (GradientChange != null)
                    GradientChange(this, args);
            }
        
            // Переопределение метода OnPaint()
            protected override void OnPaint(PaintEventArgs e)
            {
                // Контекст графического устройства
                Graphics gr = e.Graphics;
        
                // Создаем кисть и заливаем фон текстового блока 
                float angle = 10.0F;
                Brush brush = new LinearGradientBrush(
                    this.ClientRectangle, startColor, endColor, angle);
                gr.FillRectangle(brush, this.ClientRectangle);
        
                // Сразу освобождаем кисть как ограниченный ресурс 
                brush.Dispose();
        
                // Вызываем после заливки, иначе закрашивается текст
                base.OnPaint(e);
            }
        }
    }
  • В проводнике решений вызовите контекстное меню для узла MyControl и выполните команду Rebuild (или Build ), чтобы откомпилировать компонент GradientLabel
  • Откройте в проекте Test файл Form1.cs в режиме View Designer и поместите на форму из панели инструментов Toolbox компонент GradientLabel, который там появится после компиляции
  • В результате компонент на форме должен выглядеть так

    Простейший интерфейс времени проектирования

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

  • Выделите на форме экземпляр компонента gradientLabel1 и в панели Properties установите режим представления свойств по категориям, щелкнув на соответствующей кнопке в верхней части панели
  • К поведению (во время проектирования) полученного на данном этапе компонента можно сделать следующие замечания:

  • При изменении значений свойств экземпляр компонента перерисовывается не сразу, а только при перерисовке самой формы
  • Добавленные нами свойства StartColor и EndColor отображаются в стандартной категории Misc, а не в желаемой нами категории
  • В нижней части панели отображается только название выделенного свойства и нет никаких разъяснений по его назначению и условиям применения
  • В рамках категории свойства отсортированы не по алфавиту
  • Начальные значения свойств компонента отображаются жирным шрифтом, что свидетельствует о том, что они не считаются значениями по умолчанию, а явно присваиваются оболочкой в функции InitializeComponent() класса Form1 родительской формы
  • Компонент в панели Toolbox не имеет своего индивидуального значка (пиктограммы)
  • Рядом последующих действий устраним эти замечания.

    Перерисовку компонента при изменении значений свойств можно обеспечить, если в функцию диспетчеризации OnGradientChange() события GradientChange, которая вызывается в аксессоре set редакции свойств, вставить вызов унаследованного метода разрушения окна компонента Invalidate().

  • Добавьте в метод диспетчеризации события компонента блока кода №2 следующую инструкцию
  • // Функция диспетчеризации события GradientChange
            protected virtual void OnGradientChange(EventArgs args)
            {
                // Перерисовываем компонент 
                this.Invalidate();
        
                // Если есть зарегистрированные обработчики,
                // то инициируем событие и вызываем обработчики 
                if (GradientChange != null)
                    GradientChange(this, args);
            }

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

    Атрибуты как средство дополнительной настройки компонента

    Атрибут, это дополнительный механизм уточнения поведения класса или его члена, относящийся к современным технологиям программирования. Атрибуты - это типы, порожденные абстрактным классом Attribute. Имена всех библиотечных атрибутов заканчиваются словом Attribute, но для простоты язык C# позволяет сокращать запись и указывать название атрибута без этого постфикса (postfix).

    Атрибут прикрепляется к объекту с помощью описания конструктора в квадратных скобках, но для атрибутов уровня сборки или модуля прикрепление выполняется с помощью ключевых слов Assembly и Module соответственно.

    К объекту могут прикрепляться несколько атрибутов в индивидуальных квадратных скобках или списком в общих квадратных скобках. Например, инструкция прикрепления нескольких атрибутов к классу вот такая

    // Всплывающая подсказка компонента в панели Toolbox
        [Description("Текстовая метка с градиентной заливкой фона")]
        // Подключение пиктограммы компонента для панели Toolbox 
        [ToolboxBitmap(typeof(GradientLabel))]
        // Подключение класса дизайнера к классу компонента       
        [Designer(typeof(GradientLabelDesigner))]
        
        partial class GradientLabel
        {
            .........................................
        }

    равносильна такой инструкции

    // Всплывающая подсказка компонента в панели Toolbox
        [Description("Текстовая метка с градиентной заливкой фона"),
        // Подключение пиктограммы компонента для панели Toolbox 
        ToolboxBitmap(typeof(GradientLabel)),
        // Подключение класса дизайнера к классу компонента       
        Designer(typeof(GradientLabelDesigner))]
        
        partial class GradientLabel
        {
            .........................................
        }

    Конструктор атрибута может принимать два вида параметров:

  • Позиционные параметры - параметры, указанные в конструкторе класса-атрибута и обязательные к упоминанию их первыми в строгом порядке следования
  • Именованные параметры - поля и свойства класса-атрибута, которые представляют собой пары 'имя=значение' и которые можно указывать по желанию (можно и не указывать) после позиционных параметров без соблюдения порядка следования. Именованные параметры внутри класса-атрибута должны быть объявлены доступными как для чтения, так и для записи. Параметры, доступные только для чтения, не могут использоваться как именованные.
  • Различают следующие атрибуты:

  • Атрибуты, используемые компилятором
  • Атрибуты, используемые средой проектирования
  • Атрибуты, используемые библиотекой классов
  • Пользовательские атрибуты
  • В таблице приведены некоторые атрибуты, используемые самой средой проектирования. Они нам понадобятся для настройки поведения компонента в режиме проектирования.

    Атрибуты настроек компонента в режиме проектирования
    Класс атрибута Конструкторы атрибута Примеры прикрепления Пояснения

    System.ComponentModel. DefaultPropertyAttribute

    public DefaultPropertyAttribute(string name)

    [DefaultProperty("Text")]

    Указывает свойство по умолчанию. Это свойство будет активно в панели Properties при первом редактировании экземпляра компонента. Обычно таким атрибутом помечают часто изменяемые свойства компонента, например, для Label, TextBox, Button - это свойство Text. Прикрепляется к классу

    System.ComponentModel. DefaultEventAttribute

    public DefaultEventAttribute(string name)

    [DefaultEvent("Click")]

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

    System.ComponentModel. DefaultValueAttribute

    public DefaultValueAttribute(bool value)
          public DefaultValueAttribute(byte value)
          public DefaultValueAttribute(char value)
          public DefaultValueAttribute(double value)      
          public DefaultValueAttribute(float value)
          public DefaultValueAttribute(int value)
          public DefaultValueAttribute(long value)
          public DefaultValueAttribute(object value)
          public DefaultValueAttribute(short value)
          public DefaultValueAttribute(string value)
          public DefaultValueAttribute(System.Type type, string value)

    [DefaultValue(10)]

    [DefaultValue(typeof(Color), "Red")]

    Указывает значение свойства по умолчанию. При наличии такого атрибута, прикрепленного к свойству, редактор свойств Properties отображает значение стилем Regular, а без атрибута - стилем Bold и оболочка генерирует код явного присвоения значения данному свойству в файле Designer.cs формы. Конструктор с одним параметром используется для свойств элементарных типов, а конструктор с двумя параметрами - для статических свойств класса. При наличии этого атрибута активизируется команда Reset контекстного меню редактора значения свойства, с помощью которой свойству можно вернуть значение по умолчанию. Прикрепляется к свойству

    System.ComponentModel. CategoryAttribute

    public CategoryAttribute()
        public CategoryAttribute(string category)

    [Category("Gradient")]

    Создает категорию в панели свойств и помещает в нее прикрепленное свойство. Прикрепляется к свойству

    System.ComponentModel. DescriptionAttribute

    public DescriptionAttribute()
        public DescriptionAttribute(string description)

    [Description("Цвет начала заливки")]

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

    System.ComponentModel. BrowsableAttribute

    public BrowsableAttribute(bool browsable)

    [Browsable(false)]

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

    System.ComponentModel. DisplayNameAttribute

    public DisplayNameAttribute()
        public DisplayNameAttribute(string displayName)

    [DisplayName("End Color")]

    [DisplayName("Цвет завершения заливки")]

     

    Задает представление, как имя свойства будет отображаться в панели свойств. Иногда может понадобиться разделить пробелами составное имя свойства или события при их представлении в панели Properties, или отобразить на русском языке, или по-турецки

    System.ComponentModel. PasswordPropertyTextAttribute

    public PasswordPropertyTextAttribute()
        public PasswordPropertyTextAttribute(bool password)

    [PasswordPropertyText(true)]

    Устанавливает режим отображения значения свойства в панели Properties звездочками

    System.ComponentModel. ReadOnlyAttribute

    public ReadOnlyAttribute(bool isReadOnly)

    [ReadOnly(true)]

    true - свойство нельзя редактировать в панели Properties (в режиме проектирования) и оно представляется серым цветом. Для статических свойств такой механизм установлен по умолчанию

    System.ComponentModel. MergablePropertyAttribute

    public MergablePropertyAttribute(bool allowMerge)

    [MergableProperty(false)]

    При групповом выделении разнотипных компонентов, имеющих одинаковое имя свойства, запрещает для помеченного этим атрибутом свойства изменение "за компанию". По умолчанию true - разрешено групповое изменение значения любого общедоступного свойства

    System.ComponentModel. ParenthesizePropertyNameAttribute

    public ParenthesizePropertyNameAttribute()
        public ParenthesizePropertyNameAttribute(bool needParenthesis)

    [ParenthesizePropertyName(true)]

    С параметром true указывает редактору свойств отображать имя свойства в круглых скобках

    System.ComponentModel. AttributeProviderAttribute

    public AttributeProviderAttribute(string typeName)
          public AttributeProviderAttribute(string typeName, string
        propertyName)
        public AttributeProviderAttribute(System.Type type)

    [AttributeProvider(typeof(Button))]

    Присоединяет к прикрепленному типу атрибуты из другого типа

    System.ComponentModel.ToolboxItemAttribute

    public ToolboxItemAttribute(bool defaultType)
        public ToolboxItemAttribute(string toolboxItemTypeName)
        public ToolboxItemAttribute(System.Type toolboxItemType)

    [ToolboxItem(true)]

    Значение true по умолчанию обязывает компонент появляться в панели Toolbox оболочки. Прикрепляется к классу

    System.Drawing.ToolboxBitmapAttribute

    public ToolboxBitmapAttribute(string imageFile)
        public ToolboxBitmapAttribute(System.Type t)
        public ToolboxBitmapAttribute(System.Type t, string name)

    [ToolboxBitmap(typeof(GradientLabel), "GradientLabel.bmp")]

    Загрузка пиктограммы компонента размером 16x16 px в формате BMP или ICO. Прикрепляется к классу

    System.ComponentModel.DesignTimeVisibleAttribute

    public DesignTimeVisibleAttribute()
        public DesignTimeVisibleAttribute(bool visible)

    [DesignTimeVisible(false)]

    Аргумент false указывает оболочке, что компонент не должен быть виден в режиме разработки

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

  • Дополните вторую часть класса компонента в файле GradientLabel.cs атрибутами, выделенными в следующем листинге
  • /////////////////////////////////////////////////////////////////
    // Блок кода №2 продолжения класса GradientLabel 
    /////////////////////////////////////////////////////////////////
    namespace MyControl
    {
        // Всплывающая подсказка компонента в панели Toolbox
        [Description("Текстовая метка с градиентной заливкой фона")]
        // Подключение пиктограммы компонента для панели Toolbox 
        [ToolboxBitmap(typeof(GradientLabel))]
        
        partial class GradientLabel
        {
            // Закрытые поля
            private Color startColor = Color.Yellow;
            private Color endColor = Color.Red;
        
            // Категория свойства в панели Properties
            [Category("Gradient")]
            // Пояснение для выделенного свойства 
            // в нижней части панели Properties
            [Description("Цвет начала заливки")]
            // Значение свойства по умолчанию 
            [DefaultValue(typeof(Color), "Yellow")] 
            // Представление имени свойства в панели Properties
            // [DisplayName("Начало заливки")]
        
            public Color StartColor // Общедоступное свойство 
            {
                get { return startColor; }
                set
                {
                    // Меняем значение
                    startColor = value;
        
                    // Инициируем событие
                    OnGradientChange(EventArgs.Empty);
                }
            }
        
            // Альтернативный синтаксис прикрепления атрибутов 
            [Category("Gradient"), Description("Цвет завершения заливки"), 
            DefaultValue(typeof(Color), "Red")]
            // [DisplayName("End Color")]
        
            public Color EndColor // Общедоступное свойство 
            {
                get { return endColor; }
                set 
                { 
                    // Меняем значение
                    endColor = value;
        
                    // Инициируем событие
                    OnGradientChange(EventArgs.Empty);
                }
            }
        
            // Пояснение для выделенного события 
            // в нижней части панели Properties 
            [Description("Событие изменения цвета\n"
                + "границ градиентной заливки")] 
        
            // Объявление события изменения свойств цвета градиента 
            public event EventHandler GradientChange; 
        
            // Функция диспетчеризации события GradientChange
            protected virtual void OnGradientChange(EventArgs args)
            {
                // Перерисовываем компонент 
                this.Invalidate();
        
                // Если есть зарегистрированные обработчики,
                // то инициируем событие и вызываем обработчики 
                if (GradientChange != null)
                    GradientChange(this, args);
            }
        
            // Переопределение метода OnPaint()
            protected override void OnPaint(PaintEventArgs e)
            {
                // Контекст графического устройства
                Graphics gr = e.Graphics;
        
                // Создаем кисть и заливаем фон текстового блока 
                float angle = 10.0F;
                Brush brush = new LinearGradientBrush(
                    this.ClientRectangle, startColor, endColor, angle);
                gr.FillRectangle(brush, this.ClientRectangle);
        
                // Сразу освобождаем кисть как ограниченный ресурс 
                brush.Dispose();
        
                // Вызываем после заливки, иначе закрашивается текст
                base.OnPaint(e);
            }
        }
    }

    Добавление к компоненту пиктограммы

    Мы добавили атрибут [ToolboxBitmap(typeof(GradientLabel))] уровня класса для присоединения пиктограммы компонента на этапе проектирования. Теперь нужно подготовить саму пиктограмму и настроить ее так, чтобы она размещалась в сборке компонента. Использованная перегрузка конструктора атрибута требует, чтобы файл рисунка имел формат BMP, имел имя, совпадающее с именем компонента, и значился для оболочки как внедряемый ( embedded ) ресурс.

  • Вызовите в проводнике решений контекстное меню для узла MyControl компонента и добавьте к проекту новый рисунок командой Add/New Item с именем компонента GradientLabel.bmp
  • Оболочка откроет окно графического редактора, в котором нужно будет нарисовать пиктограмму компонента размером 16x16 пикселов.

  • При выделенном шаблоне рисования установите в панели Properties размер рисунка 16x16
  • Нарисуйте пиктограмму компонента, например, такую
  • Сохраните рисунок, закройте окно графического редактора, в проводнике решений выделите файл рисунка и через панель свойств настройте его в соответствии со снимком как внедряемый ресурс с копированием при обновлении
  • В панели проводника решений вызовите контекстное меню для узла компонента MyControl и выполните команду Rebuild
  • Откройте в проекте Test на редактирование в режиме View Designer форму Form1.cs и убедитесь, что в дежурной вкладке MyControl Components появился компонент GradientLabel с пиктограммой по умолчанию
  • В панели Toolbox вызовите контекстное меню для компонента GradientLabel и командой Choose Items откройте окно добавления ссылок на динамические библиотеки с компонентами. Кнопкой Browse вызовите диалог открытия файла и подключите библиотеку MyControl.dll, расположенную в каталоге проекта C:\Tmp\MySolution\MyControl\bin\Debug, а библиотеку из каталога C:\Tmp\MySolution\MyControl\obj\Debug отключите
  • После выполнения указанных действий в панели Toolbox будет представлен наш компонент с собственной пиктограммой и всплывающей подсказкой, которые мы прикрепили к нему ранее соответствующими атрибутами

  • Перенесите компонент на форму Form1, выделите экземпляр компонента GradientLabel и убедитесь через панель Properties в наличие добавленных соответствующими атрибутами настроек
  • Добавление к компоненту класса дизайнера

    Дополнительные возможности по редактированию компонентов в режиме проектирования, в том числе смарт-теги, реализуются с помощью классов дизайнеров. Эти классы обеспечивают связь компонента на этапе разработки с редактором свойств, формой и другими панелями оболочки. Они находятся в сборке System.Design.dll и распределены по пространствам имен

    System.ComponentModel.Design System.Windows.Forms.Design

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

  • В панели Solution Explorer вызовите контекстное меню для узла References проекта MyControl, выполните команду Add Reference и выберите в списке сборку System.Design
  • Для того, чтобы четко разделить функциональность компонента на этапе проектирования и на этапе выполнения, будем размещать код этапа проектирования в отдельном файле и подключать его к компоненту одной инструкцией-атрибутом.

  • В панели проводника решений Solution Explorer вызовите контекстное меню для узла MyControl проекта компонента и командой Add/Class добавьте к проекту компонента новый файл с именем GradientLabelDesigner.cs
  • Модифицируйте заготовку файла GradientLabelDesigner.cs следующим образом
  • using System;
    using System.ComponentModel;
    using System.Collections.Generic;
    using System.Diagnostics;
    using System.Text;
    using System.Drawing;
    using System.Drawing.Drawing2D;
    using System.Windows.Forms;
        
    using System.ComponentModel.Design;
    using System.Windows.Forms.Design;
        
    namespace MyControl
    {
        // Класс-дизайнер компонента
        internal partial class GradientLabelDesigner : ControlDesigner
        {
        }
    }

    Созданный класс-дизайнер GradientLabelDesigner нужно привязать к классу GradientLabel компонента с помощью атрибута Designer.

  • Дополните в файле GradientLabel.cs блок кода №2 класса компонента GradientLabel следующим атрибутом привязки класса-дизайнера GradientLabelDesigner
  • /////////////////////////////////////////////////////////////////
    // Блок кода №2 продолжения класса GradientLabel 
    /////////////////////////////////////////////////////////////////
    namespace MyControl
    {
        // Всплывающая подсказка компонента в панели Toolbox
        [Description("Текстовая метка с градиентной заливкой фона")]
        // Подключение пиктограммы компонента для панели Toolbox 
        [ToolboxBitmap(typeof(GradientLabel))]
        // Подключение класса дизайнера к классу компонента       
        [Designer(typeof(GradientLabelDesigner))]
        
        partial class GradientLabel
        {
            // Закрытые поля
            private Color startColor = Color.Yellow;
            private Color endColor = Color.Red;
        
            ..............................................
        }
    }

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

    Добавление к компоненту смарт-тега

    Смарт-теги (smart tags - интеллектуальные дескрипторы) обеспечивают индивидуальные средства настройки компонентов, позволяющие вынести в специальный диалог наиболее важные свойства и методы. Смарт-теги встречаются во многих высокоразвитых библиотечных компонентах и раскрываются щелчком на треугольничке в правой верхней части представления компонента на форме в режиме проектирования. Например, для библиотечного компонента ComboBox это представление смарт-тега будет таким

    Смарт-теги создаются на базе библиотечного класса

    System.ComponentModel.Design.DesignerActionList

    Для создания смарт-тега компонента вначале нужно создать класс со списком свойств и методов, которые планируется вынести в смарт-тег для быстрого редактирования. Этот класс списка выносимых для редактирования элементов компонента должен быть создан как расширение библиотечного класса DesignerActionList. Он должен содержать ссылки на включаемые в смарт-тег свойства и методы самого компонента, полученные с применением теории отражения из экземпляра класса (объекта) компонента. Затем в привязываемом к компоненту классе-дизайнере, наследнике библиотечного класса ControlDesigner, нужно переопределить унаследованное виртуальное свойство ActionLists, чтобы включить список редактируемых элементов в смарт-тег.

    Создадим смарт-тег для нашего разрабатываемого компонента GradientLabel.

  • Добавьте в конец файла GradientLabelDesigner.cs следующий блок кода №3, определяющий список свойств и методов, помещаемых в смарт-тег
  • //*///////////////////////////////////////////////////////////////
    // Блок кода №3. Создание списка выносимых в смарт-тег элементов компонента
    //////////////////////////////////////////////////////////////////
    namespace MyControl
    {
        // Пусть класс доступен коду только внутри сборки компонента
        internal partial class GradientLabelActionList : DesignerActionList
        {
            // Объявили ссылку на объект компонента
            GradientLabel gradientLabel;
        
            // Конструктор
            public GradientLabelActionList(IComponent component)
                : base(component)
            {
                // Привели и сохранили ссылку на редактируемый компонент
                gradientLabel = (GradientLabel)component;
            }
        
            // Вспомогательная функция реализации отражения
            private PropertyDescriptor GetPropertyByName(String propName)
            {
                PropertyDescriptor prop =
                    TypeDescriptor.GetProperties(gradientLabel)[propName];
                if (prop == null)
                    throw new ArgumentException("Свойство не существует", propName);
                return prop;
            }
        
            // Элементы компонента, выносимые в диалог смарт-тега
            public Color StartColor
            {
                get { return this.gradientLabel.StartColor; }
                set 
                { 
                    this.GetPropertyByName("StartColor").SetValue(gradientLabel, value);
                }
            }
            public Color EndColor
            {
                get { return this.gradientLabel.EndColor; }
                set 
                { 
                    this.GetPropertyByName("EndColor").SetValue(gradientLabel, value);
                }
            }
            public void InvertColors()
            {
                Color tmp = gradientLabel.StartColor;
                this.StartColor = gradientLabel.EndColor;
                this.EndColor = tmp;
            }
        }
    }
    //***************************************************************/
  • Добавьте в конец файла GradientLabelDesigner.cs следующий блок кода №4, определяющий смарт-тег со списком выносимых в диалог свойств и методов
  • //*///////////////////////////////////////////////////////////////
    // Блок кода №4. Создание смарт-тега с заданным списком элементов
    //////////////////////////////////////////////////////////////////
    namespace MyControl
    {
        partial class GradientLabelDesigner
        {
            // Базовое поле
            DesignerActionListCollection actionLists;
        
            // Переопределение виртуального свойства ActionLists, унаследованного 
            // классом System.Windows.Forms.Design.ControlDesigner от
            // класса System.ComponentModel.Design.ComponentDesigner 
            public override DesignerActionListCollection ActionLists
            {
                get
                {
                    // Если еще не создавали смарт-тег actionLists
                    if (actionLists == null)
                    {
                        // Создали окно смарт-тега
                        actionLists = new DesignerActionListCollection();
                        // Создали список элементов окна
                        GradientLabelActionList gradientLabelActionList =
                            new GradientLabelActionList(this.Component);
                        // Добавили список элементов в коллекцию окна
                        actionLists.Add(gradientLabelActionList);
                    }
        
                    return actionLists;
                }
            }
        }
    }  
    //***************************************************************/
  • Откомпилируйте проект компонента и убедитесь, что у компонента появился смарт-тег с указанными элементами
  • Обратите внимание на то, что код интерфейсной части компонента работает в режиме проектирования и нам не нужно запускать приложение с компонентом на выполнение. Оболочка сама выполняет необходимые действия на этапе проектирования в соответствии с заложенной нами логикой поведения компонента на этом этапе.

    Усовершенствование смарт-тега компонента

    К созданному смарт-тегу можно сделать несколько замечаний

  • По обновлению окна диалога:
  • при выполнении метода InvertColors успешно меняются цвета компонента, но цветовые поля самого смарт-тега не обновляются
  • По оформлению окна диалога:
  • подписи элементов смарт-тега совпадают с именами свойств и методов, что не всегда может быть приемлемо для пользователя компонента. Было бы лучше написать их раздельно или вообще перевести на русский язык
  • порядком следования элементов в диалоговом окне смарт-тега формируется по алфавиту, а этим процессом желательно бы управлять
  • Устранение замечания по обновлению окна диалога

    Для обновления диалогового окна смарт-тега при изменении цветов компонента нужно в конструкторе класса GradientLabelActionList формирования списка элементов сохранить ссылку на сервис System.ComponentModel.Design.DesignerActionUIService. Затем в нашем методе InvertColors() или в аксессорах set{ } свойств StartColor и EndColor вызвать метод Refresh() этого сервиса.

  • Дополните класс GradientLabelActionList компонента следующим кодом
  • //*///////////////////////////////////////////////////////////////
    // Блок кода №3. Создание списка выносимых в смарт-тег элементов компонента
    //////////////////////////////////////////////////////////////////
    namespace MyControl
    {
        // Пусть класс доступен коду только внутри сборки компонента
        internal partial class GradientLabelActionList : DesignerActionList
        {
            // Объявили ссылку на объект компонента
            GradientLabel gradientLabel;
        
            // Объявили ссылку на сервис смарт-тега
            DesignerActionUIService designerActionUIService;
        
            // Конструктор
            public GradientLabelActionList(IComponent component)
                : base(component)
            {
                // Привели и сохранили ссылку на редактируемый компонент
                gradientLabel = (GradientLabel)component;
        
                // Привели и сохранили ссылку на сервис смарт-тега
                designerActionUIService = (DesignerActionUIService)
                    this.GetService(typeof(DesignerActionUIService));
            }
        
            // Вспомогательная функция реализации отражения
            private PropertyDescriptor GetPropertyByName(String propName)
            {
                PropertyDescriptor prop =
                    TypeDescriptor.GetProperties(gradientLabel)[propName];
                if (prop == null)
                    throw new ArgumentException("Свойство не существует", propName);
                return prop;
            }
        
            // Элементы компонента, выносимые в диалог смарт-тега
            public Color StartColor
            {
                get { return this.gradientLabel.StartColor; }
                set 
                { 
                    this.GetPropertyByName("StartColor").SetValue(gradientLabel, value);
        
                    // Обновляем диалоговое окно смарт-тега
                    designerActionUIService.Refresh(gradientLabel);
                }
            }
            public Color EndColor
            {
                get { return this.gradientLabel.EndColor; }
                set 
                { 
                    this.GetPropertyByName("EndColor").SetValue(gradientLabel, value);
        
                    // Обновляем диалоговое окно смарт-тега
                    designerActionUIService.Refresh(gradientLabel);
                }
            }
            public void InvertColors()
            {
                Color tmp = gradientLabel.StartColor;
                this.StartColor = gradientLabel.EndColor;
                this.EndColor = tmp;
            }
        }
    }
    //***************************************************************/

    Устранение замечания по оформлению окна диалога

    Для того, чтобы изменить порядок следования элементов смарт-тега и сделать окно диалога более информативным, нужно изменить коллекцию элементов окна, представленную классом System.ComponentModel.Design.DesignerActionItemCollection. Для этого в классе формирования списка элементов окна GradientLabelActionList нужно переопределить унаследованный библиотечный метод DesignerActionList.GetSortedActionItems() и в нем вручную формировать коллекцию окна смарт-тега с нужным порядком элементов.

    Каждый элемент коллекции окна смарт-тега представлен одним из классов:

  • System.ComponentModel.Design.DesignerActionHeaderItem - устанавливает заголовок группы элементов окна смарт-тега
  • System.ComponentModel.Design.DesignerActionTextItem - выводит надписи к элементам смарт-тега
  • System.ComponentModel.Design.DesignerActionPropertyItem - представляет свойство и в зависимости от типа свойства виден как текстовое поле, выпадающий список или флажок
  • System.ComponentModel.Design.DesignerActionMethodItem - представляет ссылку для вызова метода
  • Используем перечисленные классы представления элементов окна и сформируем коллекцию в переопределении виртуального метода GetSortedActionItems(). При этом создадим в смарт-теге три группы элементов:

  • Группу свойств
  • Группу методов
  • Группу для отображения дополнительной информации
  • Добавьте в файл GradientLabelDesigner.cs блок кода №5, переопределяющий виртуальный метод DesignerActionList.GetSortedActionItems() базового класса
  • //*///////////////////////////////////////////////////////////////
    // Блок кода №5. Ручное формирование списка элементов в классе GradientLabelActionList 
    //////////////////////////////////////////////////////////////////
    namespace MyControl
    {
        partial class GradientLabelActionList
        {
            // Переопределяем виртуальный метод базового класса DesignerActionList
            public override DesignerActionItemCollection GetSortedActionItems()
            {
                // Создаем объект коллекции элементов смарт-тега
                DesignerActionItemCollection items = new DesignerActionItemCollection();
        
                // Формируем категорию "Свойства" (Properties)
                // Заголовок категории
                items.Add(new DesignerActionHeaderItem("Свойства", "Properties"));
                // Элементы категории 
                items.Add(new DesignerActionPropertyItem("StartColor", "Начальный цвет", 
                    "Properties", "Цвет начала градиентной заливки"));
                items.Add(new DesignerActionPropertyItem("EndColor", "Конечный цвет",
                    "Properties", "Цвет завершения градиентной заливки"));
        
                // Формируем категорию "Методы" (Methods), если цвета не равны!
                if (StartColor != EndColor)
                {
                    // Заголовок категории 
                    items.Add(new DesignerActionHeaderItem("Методы", "Methods"));
                    // Ссылка вызова метода с одновременным созданием опции в контекстном меню
                    items.Add(new DesignerActionMethodItem(this, "InvertColors",
                        "Перевернуть цвета", "Methods",
                        "Поменять начальный и конечный цвета местами", true));
                }
        
                // Формируем категорию "Информация" (Info)
                // Заголовок категории
                items.Add(new DesignerActionHeaderItem("Информация", "Info"));
                // Текстовый элемент категории 
                string info = String.Format("Размер компонента {0}x{1}",
                    gradientLabel.Width, gradientLabel.Height);
                items.Add(new DesignerActionTextItem(info, "Info"));
        
                return items;
            }
        }
    }
    //***************************************************************/
  • Откомпилируйте проект компонента командой Build/Build Solution и убедитесь, что окно диалога компонента в режиме проектирования приобрело профессиональный вид
  • Управление контекстным меню этапа проектирования компонента

    Контекстное меню компонента появляется при щелчке на нем правой кнопкой мыши. Естественно, что пункты любого меню выполняют закрепленные за ними действия с помощью соответствующих методов. Без специальных действий над кодом компонента для него существует стандартное меню. Мы можем несколько модифицировать стандартное меню компонента режима разработки, если учтем следующее.

    При добавлении ссылки на метод в смарт-тег компонента имеются 6 перегрузок библиотечного метода System.ComponentModel.Design.DesignerActionMethodItem(), три из которых своим последним параметром управляют опцией с названием displayName в контекстном меню:

  • public DesignerActionMethodItem(System.ComponentModel.Design.DesignerActionList actionList, string memberName, string displayName, bool includeAsDesignerVerb)
  • public DesignerActionMethodItem(System.ComponentModel.Design.DesignerActionList actionList, string memberName, string displayName, string category, bool includeAsDesignerVerb)
  • public DesignerActionMethodItem(System.ComponentModel.Design.DesignerActionList actionList, string memberName, string displayName, string category, string description, bool includeAsDesignerVerb)
  • В блоке кода №5 класса GradientLabelActionList при переопределении виртуального метода GetSortedActionItems(), унаследованного от базового класса DesignerActionList, использована перегрузка с поднятым флагом includeAsDesignerVerb, что обеспечило включение опции метода InvertColors() с названием "Перевернуть цвета" в стандартное контекстное меню компонента.

    Для формирования своего контекстного меню необходимо в подключаемом к компоненту в качестве атрибута классе GradientLabelDesigner переопределить виртуальное свойство Verbs, унаследованное от класса ComponentDesigner. В свою очередь, к свойству Verbs нужно подключить экземпляр класса, содержащего коллекцию элементов контекстного меню, производного от библиотечного System.ComponentModel.Design.DesignerVerbCollection.

  • Добавьте в конец файла GradientLabelDesigner.cs блок кода №6, определяющий коллекцию элементов контекстного меню (в нашем случае это будет только один элемент, вызывающий выполнение метода VerbInvertColors() )
  • //*///////////////////////////////////////////////////////////////
    // Блок кода №6. Формирование коллекции элементов контекстного меню компонента
    //////////////////////////////////////////////////////////////////
    namespace MyControl
    {
        internal class GradientLabelVerbCollection : DesignerVerbCollection
        {
            // Объявили ссылку на объект компонента
            GradientLabel gradientLabel;
        
            // Конструктор
            public GradientLabelVerbCollection(IComponent component)
            {
                // Привели и сохранили ссылку на редактируемый компонент
                gradientLabel = (GradientLabel)component;
        
                // Создаем пункты меню (всего один пункт)
                this.Add(new DesignerVerb("Перевернуть цвета_1",
                    new EventHandler(VerbInvertColors)));
            }
        
            // Соблюдаем сигнатуру библиотечного делегата EventHandler 
            private void VerbInvertColors(object sender, EventArgs args)
            {
                // Извлекаем ссылки на свойства компонента 
                PropertyDescriptor start = GetPropertyByName("StartColor");
                PropertyDescriptor end = GetPropertyByName("EndColor");
        
                // Извлекаем значения свойств и меняем их местами
                Color tmp = (Color)start.GetValue(gradientLabel);
                start.SetValue(gradientLabel, end.GetValue(gradientLabel));
                end.SetValue(gradientLabel, tmp);
            }
        
            // Вспомогательная функция реализации отражения, дублирована
            private PropertyDescriptor GetPropertyByName(String propName)
            {
                PropertyDescriptor prop =
                    TypeDescriptor.GetProperties(gradientLabel)[propName];
                if (prop == null)
                    throw new ArgumentException("Свойство не существует", propName);
                return prop;
            }
        }
    }
    //***************************************************************/
  • В конец файла блок кода №7 переопределения наследуемого виртуального свойства Verbs для добавления опции " Перевернуть цвета_1 " в контекстное меню компонента
  • //*///////////////////////////////////////////////////////////////
    // Блок кода №7. Добавление коллекции контекстного меню к компоненту
    //////////////////////////////////////////////////////////////////
    namespace MyControl
    {
        partial class GradientLabelDesigner
        {
            DesignerVerbCollection verbs; // Базовое поле
        
            // Переопределение виртуального свойства Verbs, унаследованного 
            // классом System.Windows.Forms.Design.ControlDesigner от
            // класса System.ComponentModel.Design.ComponentDesigner 
            public override DesignerVerbCollection Verbs
            {
                get
                {
                    // Если еще не создавали контекстное меню компонента
                    if (verbs == null)
                    {
                        verbs=new GradientLabelVerbCollection(this.Component);
                    }
        
                    return verbs;
                }
            }
        }
    }  
    //***************************************************************/
  • Откомпилируйте проект и убедитесь, что контекстное меню компонента на этапе проектирования работает нормально и имеет вид
  • Мы рассмотрели два способа создания контекстного меню. На нашем примере можно убедиться, что оба способа обладают одинаковой функциональностью. Первый способ при добавлении элементов в смарт-тег с применением параметра includeAsDesignerVerb = true проще, но второй способ может пригодиться, когда формируется контекстное меню без предварительного создания смарт-тега. Хотя при формировании только контекстного меню все равно автоматически создается смарт-тег, содержащий опции контекстного меню. Чтобы убедиться в этом, выполните следующее:

  • Закомментируйте блок №4 файла GradientLabelDesigner.cs, отвечающий за создание смарт-тега. Для этого удалите один из двух слэшей, стоящих в заголовке блока кода перед символом звездочки
  • /*////////////////////////////////////////////////////////////////
    // Блок кода №4. Создание смарт-тега с заданным списком элементов
    //////////////////////////////////////////////////////////////////
    namespace MyControl
    {
        partial class GradientLabelDesigner
        {
            // Базовое поле
            DesignerActionListCollection actionLists;
        
            // Переопределение виртуального свойства ActionLists, унаследованного 
            // классом System.Windows.Forms.Design.ControlDesigner от
            // класса System.ComponentModel.Design.ComponentDesigner 
            public override DesignerActionListCollection ActionLists
            {
                get
                {
                    // Если еще не создавали смарт-тег actionLists
                    if (actionLists == null)
                    {
                        // Создали окно смарт-тега
                        actionLists = new DesignerActionListCollection();
                        // Создали список элементов окна
                        GradientLabelActionList gradientLabelActionList =
                            new GradientLabelActionList(this.Component);
                        // Добавили список элементов в коллекцию окна
                        actionLists.Add(gradientLabelActionList);
                    }
        
                    return actionLists;
                }
            }
        }
    }
    //***************************************************************/
  • Откомпилируйте все решение и убедитесь, что смарт-тег с опциями, добавляемыми в контекстное меню, создается автоматически
  • Раскомментируйте блок кода №4 построения смарт-тега для продолжения исследования техники построения интерфейса компонента для режима проектирования. Для этого добавьте еще один слэш перед звездочкой в начале заголовка блока кода
  • //*////////////////////////////////////////////////////////////////
    // Блок кода №4. Создание смарт-тега с заданным списком элементов
    //////////////////////////////////////////////////////////////////
    namespace MyControl
    {
        partial class GradientLabelDesigner
        {
            ..................................................
        }
    }
    //***************************************************************/

    Исключение свойств и событий из панели Properties

    При конструировании компонентов на основе наследования других классов некоторые свойства и события в расширяемом классе возможно потребуется скрыть. Так например, в нашем примере градиентной заливки фона унаследованное свойство BackColor становится излишним. Мы не можем его скрыть с помощью атрибута Browsable(false), поскольку оно в текущем слое расширения не объявляется, а наследуется из библиотечного класса System.Windows.Forms.Control через класс Label. Здесь нужно применить специальную технику, основанную на расширении класса дизайнера System.Windows.Forms.Design.ControlDesigner.

    В классе-расширении для сокрытия свойств и событий нужно переопределить методы

    protected virtual void PreFilterProperties(System.Collections.IDictionary properties)
        protected virtual void PreFilterEvents(System.Collections.IDictionary events)

    Этим методам передаются коллекции properties и events всех свойств и методов, отображаемых дизайнером в панели Properties.

    Для отключения показа свойства или метода в панели Properties достаточно внутри соответствующего метода-фильтра выполнить одну из инструкций

    properties.Remove("BackColor");
        events.Remove("DoubleClick");
  • Добавьте в конец файла GradientLabelDesigner.cs следующий блок кода №8, скрывающий свойство BackColor и событие DoubleClick из панели Properties оболочки
  • //*///////////////////////////////////////////////////////////////
    // Блок кода №8. Сокрытие свойств и событий из панели Properties
    //////////////////////////////////////////////////////////////////
    namespace MyControl
    {
        partial class GradientLabelDesigner
        {
            protected override void PreFilterProperties(System.Collections.IDictionary properties)
            {
                base.PreFilterProperties(properties);
        
                // Скрываем в панели Properties свойство BackColor
                properties.Remove("BackColor");
            }
        
            protected override void PreFilterEvents(System.Collections.IDictionary events)
            {
                base.PreFilterEvents(events);
        
                // Скрываем в панели Properties событие DoubleClick
                events.Remove("DoubleClick");
            }
        }
    }
    //***************************************************************/
  • Откомпилируйте решение и убедитесь в работоспособности кода, который скрывает как свойство BackColor, так и событие DoubleClick компонента GradientLabel
  • Свойства режима проектирования

    Некоторые свойства нужны и существуют только на этапе проектирования. Например, свойства категории Design в панели свойств: GenerateMember, Locked, Modifiers - реально отсутствуют у экземпляра нашего компонента, но в то же время на этапе разработки оболочка их создает и поддерживает.

    Для того, чтобы добавить свойство для режима разработки, нужно добавить его в коллекцию properties в перегрузке метода PreFilterProperties() класса ControlDesigner. Очень важно проследить, чтобы добавляемое свойство режима разработки объявлялось с атрибутом

    [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]

    Этот атрибут не дает такому свойству класса компонента попасть в режим выполнения.

  • Дополните блок кода №8 в файле GradientLabelDesigner.cs следующими строками
  • //*///////////////////////////////////////////////////////////////
    // Блок кода №8. Сокрытие свойств и событий из панели Properties
    // Добавление свойств режима разработки в панель Properties
    //////////////////////////////////////////////////////////////////
    namespace MyControl
    {
        partial class GradientLabelDesigner
        {
            // Создание свойства в классе компонента
            int designProp; // Базовое поле
            [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
            [Category("Design")]
            [Description("Свойство режима проектирования")]
            public int DesignProp
            {
                get { return designProp; }
                set { designProp = value; }
            }
        
            protected override void PreFilterProperties(System.Collections.IDictionary properties)
            {
                base.PreFilterProperties(properties);
        
                // Скрываем в панели Properties свойство BackColor
                properties.Remove("BackColor");
        
                ////////////////////////////////////////////////////////////////
                // Добавление свойства этапа проектирования в панель Properties:
                ////////////////////////////////////////////////////////////////
                // Извлекаем существующее свойство из класса компонента
                PropertyDescriptor oldPropDescr = TypeDescriptor.GetProperties(this)["DesignProp"];
                // Объявляем массив для атрибутов нужной размерности
                Attribute[] attributes = new Attribute[oldPropDescr.Attributes.Count];
                // Копируем атрибуты свойства в массив
                oldPropDescr.Attributes.CopyTo(attributes, 0);
                // Создаем дополнительное свойство для панели Properties
                PropertyDescriptor propDescr = 
                    TypeDescriptor.CreateProperty(this.GetType(), oldPropDescr, attributes);
                // Добавляем дополнительное свойство в панель Properties
                properties["DesignProp"] = propDescr;
            }
        
            protected override void PreFilterEvents(System.Collections.IDictionary events)
            {
                base.PreFilterEvents(events);
        
                // Скрываем в панели Properties событие DoubleClick
                events.Remove("DoubleClick");
            }
        }
    }
    //***************************************************************/
  • Откомпилируйте компонент и убедитесь, что свойство DesignProp класса GradientLabelDesigner появилось в категории Design панели Properties, хотя в самом классе компонента GradientLabel такого свойства нет
  • Вывод дополнительной надписи на компоненте

    Иногда может возникнуть необходимость пометить компонент в режиме проектирования надписью, которая в режиме выполнения не будет видна. Здесь нужно перегрузить метод OnPaintAdornments() класса дизайнера, присоединенного к компоненту, который работает только в режиме проектирования.

  • Добавьте в конец файла GradientLabelDesigner.cs блок кода №9 дизайнера, переопределяющий метод вывода дополнительной надписи на компоненте на этапе проектирования
  • //*///////////////////////////////////////////////////////////////
    // Блок кода №9. Отрисовка компонента в режиме проектирования 
    //////////////////////////////////////////////////////////////////
    namespace MyControl
    {
        partial class GradientLabelDesigner
        {
            protected override void OnPaintAdornments(PaintEventArgs pe)
            {
                base.OnPaintAdornments(pe);
        
                pe.Graphics.Clear(Color.White);// Стираем старое
        
                // Рисуем дополнительную информацию в поле компонента
                String text = "Мой компонент";
                pe.Graphics.DrawString(
                    text,
                    this.Control.Font,
                    new SolidBrush(Color.Black),
                    0.0F,
                    0.0F
                );
            }
        }
    }
    //***************************************************************/
  • Откомпилируйте решение и убедитесь, что дополнительная надпись на компоненте работает только на этапе проектирования
  • Режим времени проектирования Режим времени выполнения

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

    Класс System.Windows.Forms.Design.ControlDesigner содержит ряд виртуальных методов, которые автоматически вызываются оболочкой при попадании мыши на компонент в режиме проектирования. В данном случае нам понадобятся только два метода, которые мы можем переопределить в классе-наследнике GradientLabelDesigner, а именно

  • protected virtual void OnMouseEnter() - вызывается автоматически при попадании курсора мыши на компонент
  • protected virtual void OnMouseLeave() - вызывается автоматически при уходе курсора мыши с компонента
  • Нам удобно ввести булев флаг mouseOver, который будет подниматься в методе OnMouseEnter() и опускаться в методе OnMouseLeave(). В этих же методах будет вызываться метод разрушения окна компонента, что будет инициировать выполнение метода перерисовки компонента OnPaintAdornments(). А в самом методе перерисовки код очистки поля компонента и вывода дополнительной надписи мы поставим на условие в зависимости от состояния флага mouseOver.

  • С учетом сказанного модифицируйте блок №9 файла GradientLabelDesigner.cs следующим образом
  • //*///////////////////////////////////////////////////////////////
    // Блок кода №9. Отрисовка компонента в режиме проектирования 
    //////////////////////////////////////////////////////////////////
    namespace MyControl
    {
        partial class GradientLabelDesigner
        {
            bool mouseOver = false;
        
            protected override void OnPaintAdornments(PaintEventArgs pe)
            {
                base.OnPaintAdornments(pe);
        
                if (mouseOver)
                {
                    pe.Graphics.Clear(Color.White);// Стираем старое
        
                    // Рисуем дополнительную информацию в поле компонента
                    String text = "Мой компонент";
                    pe.Graphics.DrawString(
                        text,
                        this.Control.Font,
                        new SolidBrush(Color.Black),
                        0.0F,
                        0.0F
                    );
                }
            }
        
            // Мышь попала на компонент
            protected override void OnMouseEnter()
            {
                base.OnMouseEnter();
        
                mouseOver = true;
                this.Control.Invalidate();
            }
        
            // Мышь сошла с компонента
            protected override void OnMouseLeave()
            {
                base.OnMouseLeave();
        
                mouseOver = false;
                this.Control.Invalidate();
            }
        }
    }
    //***************************************************************/

    Управление действием по умолчанию графического редактора

    Действие по умолчанию - это действие графического редактора (дизайнера) оболочки, выполняемое по двойному щелчку мыши на компоненте. Стандартным реагированием является создание обработчика события по умолчанию. Чтобы компоненту назначить событие по умолчанию, нужно прикрепить к классу этого компонента атрибут DefaultEvent с именем события в качестве аргумента.

  • Откройте файл Form1.cs в режиме View Designer и выполните двойной щелчок на помещенном на форму экземпляре компонента gradientLabel1. Удостоверьтесь, что графический редактор автоматически создал обработчик события Click объекта компонента gradientLabel1 с заготовкой
  • private void gradientLabel1_Click(object sender, EventArgs e)
            {
        
            }
  • Откатите действие графического редактора, выполнив команду Undo (комбинация клавиш Ctrl+Z )
  • В файле GradientLabel.cs добавьте в блок кода №2 перед объявлением класса GradientLabel атрибут, назначающий компоненту событие по умолчанию
  • /////////////////////////////////////////////////////////////////
    // Блок кода №2 продолжения класса GradientLabel 
    /////////////////////////////////////////////////////////////////
    namespace MyControl
    {
        // Всплывающая подсказка компонента в панели Toolbox
        [Description("Текстовая метка с градиентной заливкой фона")]
        // Подключение пиктограммы компонента для панели Toolbox 
        [ToolboxBitmap(typeof(GradientLabel))]
        // Подключение класса дизайнера к классу компонента       
        [Designer(typeof(GradientLabelDesigner))]
        // Событие по умолчанию
        [DefaultEvent("GradientChange")]
        
        partial class GradientLabel
        {
            // Закрытые поля
            private Color startColor = Color.Yellow;
            private Color endColor = Color.Red;
        
            // Категория свойства в панели Properties
            [Category("Gradient")]
            ...............................................................
        }
    }
  • Откомпилируйте решение с введенными изменениями в коде компонента
  • Вновь откройте файл Form1.cs в режиме View Designer и выполните двойной щелчок на помещенном на форму экземпляре компонента gradientLabel1. Удостоверьтесь, что графический редактор автоматически создал теперь уже обработчик события GradientChange объекта компонента gradientLabel1 с заготовкой
  • private void gradientLabel1_GradientChange(object sender, EventArgs e)
            {
        
            }

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

    public virtual void DoDefaultAction()

    в котором не вызывать базовый метод base.DoDefaultAction() цепочки наследования.

  • Добавьте в конец файла GradientLabelDesigner.cs блок кода №10, перекрывающий виртуальный метод DoDefaultAction()
  • //*///////////////////////////////////////////////////////////////
    // Блок кода №10. Перекрытие события по умолчанию
    //////////////////////////////////////////////////////////////////
    namespace MyControl
    {
        partial class GradientLabelDesigner
        {
            public override void DoDefaultAction()
            {
                //base.DoDefaultAction();
                MessageBox.Show("Событие по умолчанию компонента перекрыто");
            }
        }
    }
    //***************************************************************/
  • Откомпилируйте компонент и выполните двойной щелчок мыши на экземпляре компонента, помещенного на форму Form1
  • Дизайнер запустит только диалоговое окно, но сам обработчик события по умолчанию создавать не будет.

    Прямая обработка очереди сообщений Windows

    У базового класса System.Windows.Forms.Design.ControlDesigner имеется виртуальный метод

    protected virtual void WndProc(ref System.Windows.Forms.Message m)

    который в качестве параметра принимает ссылку на структуру System.Windows.Forms.Message, связанную с очередью сообщений Windows.

    Метод WndProc() вызывается автоматически всякий раз, когда в цикл сообщений операционной системы попадает новое сообщение от внутреннего или внешнего источника. Испытаем работу этой техники на примере обработки левого щелчка мыши на компоненте. Будем отлавливать сам щелчок левой кнопки мыши, определять координаты курсора относительно формы, приводить эти координаты относительно компонента и проверять, выходят ли они за пределы компонента.

  • Добавьте в конец файла GradientLabelDesigner.cs блок кода №11, перекрывающий виртуальный метод WndProc() для прямой обработки сообщений Windows
  • //*///////////////////////////////////////////////////////////////
    // Блок кода №11. Перекрытие метода WndProc()
    // для прямой обработки сообщений Windows 
    //////////////////////////////////////////////////////////////////
    namespace MyControl
    {
        partial class GradientLabelDesigner
        {
            const int WM_LBUTTONCLICK = 0x201;
        
            protected override void WndProc(ref Message m)
            {
                if (m.Msg == WM_LBUTTONCLICK)
                {
                    // Нажата левая кнопка мыши, приводим координаты курсора к компоненту
                    Point point = this.Control.PointToClient(Cursor.Position);
                    if (point.X > 0  point.X < this.Control.Width
                         point.Y > 0  point.Y < this.Control.Height)
                    {
                        MessageBox.Show(String.Format("Попали в компонент\n"
                        + "с координатами {0}x{1}", point.X, point.Y));
                    }
                }
        
                base.WndProc(ref m);
            }
        }
    }
    //***************************************************************/
  • Откомпилируйте компонент GradientLabel и выполните щелчок левой кнопки мыши на его экземпляре, помещенном на форму Form1
  • Диалоговое окно сообщений свидетельствует о реакции дизайнера на щелчки мыши способом прямой обработки событий Windows.

    Управление слоями и маркерами компонентов

     

    Вернуться к учебному плану