Разработка приложений на C# в среде Visual Studio 2005

Пользовательские элементы управления

Разбить на страницы
Показывать лекцию целиком
Файлы к лекции Вы можете скачать здесь.

Элементы управления являются посредниками между приложением и пользователем. Они составляют так называемый графический интерфейс пользователя (GUI - Graphics User Interface ). Их задача состоит в отображении информации для пользователя, обеспечении удобного ввода и генерации событий, отрабатывающих предпочтения пользователя.

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

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

Упражнение 1. Построение элемента управления BeepButton

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

Создание заготовки библиотеки компонентов

  • Создайте новый пустой проект командой File/New/Project и заполните окно мастера так
  • В панели Solution Explorer выделите корневой узел проекта UserControls и через контекстное меню выполните команду Properties
  • Откройте вкладку Application свойств оболочки и настройте поля Default namespace и Output type как показано на рисунке
  • В панели Solution Explorer вызовите для корневого узла проекта UserControls контекстное меню и выполните команду Add/Component
  • Добавьте к проекту новый файл компонента с именем BeepButton.cs
  • В рабочей области конструктора форм щелкните на ссылке click here to switch to code view или исполните команду контекстного меню View Code, чтобы перейти в режим редактирования кода
  • В панели Solution Explorer для узла References, где уже должна находиться ссылка на библиотечную сборку System, выполните команду Add Reference
  • Удерживая клавишу Ctrl через окно Add Reference выделите и добавьте к проекту ссылки на библиотечные сборки System.Drawing и System.Windows.Forms
  • Убедитесь, что теперь узел References проекта стал выглядеть так
  • Перейдите в режим редактирования файла BeepButton.cs и откорректируйте его так
  • using System;
    using System.ComponentModel;
    using System.Drawing;
    using System.Windows.Forms;
      
    using System.Media; // Для звука 
        
    namespace MyCompany.UserControls
    {
        public partial class BeepButton : Button 
        {
            public BeepButton()
            {
                InitializeComponent();
            }
        
            public BeepButton(IContainer container)
            {
                container.Add(this);
        
                InitializeComponent();
            }
        }
    }

    Класс Button является наследником класса Component, поэтому мы спокойно применяем его в качестве базового, продолжая цепочку наследования.

    Переопределение наследуемого от Control виртуального метода диспетчеризации события Click

    Большинство библиотечных классов, в которых определены события, имеют соответствующие виртуальные методы диспетчеризации с приставкой On. Эти методы можно переопределять в классах-наследниках, дополняя нужным кодом. Воспользуемся этой возможностью в нашем классе расширения кнопки, упаковав переопределение метода в отдельную часть класса BeepButton.

  • Дополните конец файла BeepButton.cs следующим кодом
  • namespace MyCompany.UserControls
    {
        partial class BeepButton
        {
            // Переопределяем виртуальный метод диспетчеризации
            // класса Control (задействуется наследуемый метод
            // диспетчеризации класса кнопки)
            protected override void OnClick(EventArgs e)
            {
                // Проиграть системный звук 
                SystemSounds.Exclamation.Play();// Сочный через внешние динамики
                //SystemSounds.Beep.Play();     // Простой через внешние динамики
                //System.Console.Beep();        // Простой через внутренний динамик
        
                // Подняться до базового класса Control,
                // чтобы сгенерировать его событие Click и 
                // запустить наш будущий обработчик ButtonOnClick
                base.OnClick(e);
            }
        }
    }

    В переопределенном методе диспетчеризации события Click кнопки мы предусмотрели вызов базового метода диспетчеризации непосредственно кнопки Button, чтобы дать возможность клиентам нашего класса BeepButton подписать свои обработчики на это событие. Событие Click, в свою очередь, унаследовано классом Button от класса Control и генерируется его методом OnClick примерно так

    protected virtual void OnClick(EventArgs args)
            {
                //...
                // Если хоть один обработчик зарегистрирован
                if (Click != null)
                    Click(this, args);
                //...
            }

    Испытание кнопки BeepButton мы проведем позднее, разработав для этого тестовую исполнимую Windows -сборку.

    Упражнение 2. Проектирование диалогового окна сообщений MyDialogBox

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

    Создание диалогового окна будет состоять из трех шагов:

  • Создание самой формы диалогового окна (класс MyDialogForm )
  • Размещение на форме интерфейсных элементов пользователя
  • Создание класса MyDialogBox.cs, управляющего диалоговым окном
  • В панели Solution Explorer выделите узел проекта UserControls и командой Add/Windows Form добавьте к компоненту файл формы с именем MyDialogForm.cs
  • Удалите из узла References лишние добавленные мастером ссылки на библиотечные сборки System.Data и System.Xml
  • Удалите соответствующие инструкции using подключения пространств имен в начале файла MyDialogForm.cs
  • Настройте форму MyDialogForm в соответствии с таблицей
  • Таблица свойств формы MyDialogForm
    Свойство Значение Пояснения
    StartPosition CenterScreen Открывать в центре экрана
    FormBorderStyle FixedDialog Запретить пользователю менять размер
    MaximizeBox false Отключить системную кнопку развертывания
    MinimizeBox false Отключить системную кнопку свертывания
    ShowInTaskbar false Запретить появление в панели задач
    AutoSize true Подстраиваться под содержимое
    AutoSizeMode GrowAndShrink Подстраиваться под содержимое (расширяться и сжиматься)
    Text Пусто Заголовок окна
  • Из свитка Containers панели инструментов Toolbox поместите на форму компоновочную панель FlowLayoutPanel и настройте ее в соответствии с таблицей
  • Таблица свойств компоновочной панели FlowLayoutPanel
    Свойство Значение Пояснения
    Name flow Имя компоновочной панели
    AutoSize true Подстраиваться под содержимое
    FlowDirection TopDown Размещать сверху вниз
    WrapContents false Не переносить дочерние элементы в следующий столбец
    Padding.All 8 Внутренний отступ дочерних элементов по периметру окна
    Dock Fill Распахнуть на всю клиентскую область родителя (формы)

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

  • Поместите внутрь экземпляра компоновочной панели flow из свитка Common Controls текстовую метку Label и настройте ее свойства в соответствии с таблицей
  • Таблица свойств текстовой метки Label
    Свойство Значение Пояснения
    Name lblMessage Имя текстовой метки
    AutoSize true Подстраиваться под содержимое
    Anchor None Расположить посредине
    Margin.All 8 Отступ по наружному периметру текстовой метки
    Modifiers Public Публичный доступ для клиентов формы (для класса MyDialogBox )

    Для того, чтобы объект lblMessage был доступен из метода класса MyDialogBox, при его создании мы сделали его общедоступным.

  • Поместите внутрь компоновочной панели flow из свитка Common Controls кнопку Button и настройте ее свойства в соответствии с таблицей
  • Таблица свойств кнопки Button
    Свойство Значение Пояснения
    Name btnShowDialog Имя кнопки
    AutoSize true Подстраиваться под содержимое
    Anchor None Расположить посредине
    Margin.All 8 Отступ по наружному периметру кнопки
    Text OK Надпись
    DialogResult OK Назначить статус "кнопки OK" в родительской форме

    Свойству дочерних кнопок формы DialogResult можно присвоить любое значение из перечисления System.Windows.Forms.DialogResult ( Abort, Cancel, Ignore, No, None, OK, Retry, Yes ). Оно и будет возвращено функцией ShowDialog() формы, если пользователь выберет именно эту кнопку.

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

  • В панели Solution Explorer выделите узел проекта UserControls и, выполнив команду меню Project/Add Class, добавьте к проекту новый класс с именем MyDialogBox
  • Заполните файл MyDialogBox.cs следующим кодом
  • using System;
    using System.Windows.Forms;
        
    namespace MyCompany.UserControls
    {
        // Наше диалоговое окно сообщения
        public class MyDialogBox
        {
        public static DialogResult Show(string strMessage, string strCaption)
        {
          // Создаем диалоговое окно
          MyDialogForm frm = new MyDialogForm();
          // Заголовок окна
          frm.Text = strCaption;
          // Содержимое текстовой метки
          frm.lblMessage.Text = strMessage;
        
          // Отобразить диалоговое окно в модальном режиме,
          // а после его закрытия вернуть выбор пользователя 
          // вызывающему коду
          return frm.ShowDialog();// Выполняем диалоговое окно
                                        // и возвращаем статус нажатой кнопки
          }
        }
    }

    Класс MyDialogBox нужно объявить с модификатором public, чтобы он был виден в клиентских сборках. Объявление метода Show() в классе MyDialogBox как статического позволяет его вызывать без создания экземпляра этого класса в клиенте. Через аргументы метода Show() можно задавать заголовок диалогового окна и содержимое текстовой метки lblMessage, как это принято в библиотечном классе System.Windows.Forms.MessageBox.

    Теперь осталось собрать все вместе и испытать на тестовом приложении, но прежде нужно откомпилировать проект UserControls и убедиться, что нет синтаксических ошибок.

  • Выполните команду меню оболочки Build/Build UserControls, чтобы откомпилировать проект нашей библиотеки пользовательских элементов управления
  • Тестирование пользовательских элементов BeepButton и MyDialogBox

  • Добавьте к решению MySolution новый проект с именем Test типа Windows Application. Для этого выполните команду меню оболочки File/Add/New Project и заполните окно мастера примерно так
  • В панели Solution Explorer выделите узел проекта Test и назначьте его стартовым командой Project/Set as StartUp Project меню оболочки или аналогичной командой контекстного меню
  • Выделите форму Form1 и настройте ее свойства как показано в таблице
  • Таблица свойств формы тестового приложения
    Свойство Значение Пояснения
    Text Кнопка со звуком Заголовок тестового окна
    StartPosition CenterScreen Открывать в центре экрана
  • Поместите из панели Toolbox на верхнюю часть формы экземпляр textBox1 текстового поля TextBox
  • Поместите из панели Toolbox на середину формы разработанную нами кнопку BeepButton и настройте ее в соответствии с таблицей
  • Таблица свойств кнопки BeepButton
    Свойство Значение Пояснения
    AutoSize true Подстраиваться под содержимое
    Text Звукнуть Надпись
    Location 100; 100 Позиционировали
  • Для выделенного экземпляра кнопки BeepButton переведите панель Properties оболочки в режим Events и в поле события Click задайте имя обработчика ButtonOnClick
  • Заполните обработчик кодом вызова нашего диалогового окна MyDialogBox так
  • private void ButtonOnClick(object sender, EventArgs e)
    {
    string msg;
        
    if (textBox1.Text.Trim().Length == 0)
      msg = "Щелкнули на кнопке";
    else
      msg = textBox1.Text.Trim();
        
    // Запускаем наше диалоговое окно статическим методом
    MyDialogBox.Show(msg, this.Text);
    }
  • Добавьте в начало файла Form1.cs строку кода подключения пространства имен нашей библиотеки элементов
  • using MyCompany.UserControls;

    для того, чтобы иметь возможность написать сокращенное имя класса MyDialogBox

  • Выполните приложение и получите примерно такой результат
  • В производном от кнопки классе BeepButton мы только переопределили виртуальный метод OnClick(), в который встроили вызов звукового сигнала. В вызывающем коде тестового приложения, использующем экземпляр класса расширения кнопки, мы подписались на событие Click, чтобы щелчок на кнопке запускал самодельное диалоговое окно.

    Но мы этот метод перехватили в нашем расширении, и если не предусмотреть передачу вызова base.OnClick(e) дальше по цепочке наследования в класс Control, то событие Click инициировано не будет и наш зарегистрированный обработчик ButtonOnClick() никогда не сработает. А значит и наше диалоговое окно вызываться не будет (попробуйте закоментировать вызов base.OnClick(e) в файле BeepButton.cs ).

    Таким образом, в переопределенном методе OnClick(), который срабатывает автоматически при щелчке на кнопке, вызывается звуковой сигнал. А при вызове этого метода в базовом классе генерируется событие Click, которое заставляет выполниться все зарегистрированные для него обработчики. В нашем случае это один обработчик, косвенно запускающий самодельное диалоговое окно. Метод OnClick() для кнопок вызывается и в тех случаях, когда кнопка имеет текущий фокус ввода и нажимается клавиша Enter или Space (пробел).

    Если в переопределенном методе OnClick() класса BeepButton поменять местами строки вызова звука и события Click, то звук появится только после закрытия диалогового окна (попробуйте).

    Обратите внимание, что при формировании интерфейса диалогового окна мы не расставляли по форме кнопку и текстовую метку. Но за счет применения библиотечного компонента FlowLayoutPanel эти элементы на этапе выполнения позиционируются автоматически.

    Упражнение 3. Проектирование музыкальной кнопки SoundButton

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

    Добавление компонента кнопки SoundButton в проект библиотеки

  • Закройте все окна редактора оболочки, выполнив команду Window/Close All Documents, чтобы они не мешали при выполнении нового упражнения
  • В панели Solution Explorer к узлу UserControls добавьте новый компонент с именем SoundButton
  • Откорректируйте заготовку файла SoundButton.cs следующим образом
  • using System;
    using System.ComponentModel;
    using System.Drawing;
    using System.Windows.Forms;
        
    using System.Media; // Для проигрывателя wav-файлов SoundPlayer
                        // и системного звука SystemSounds
        
    namespace MyCompany.UserControls
    {
        public partial class SoundButton : Button
        {
            public SoundButton()
            {
                InitializeComponent();
            }
        
            public SoundButton(IContainer container)
            {
                container.Add(this);
        
                InitializeComponent();
            }
        }
    }

    Добавление свойств загрузки звука

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

  • Добавьте в конец класса SoundButton файла SoundButton.cs следующий код, определяющий свойства загрузки звука
  • // Создаем объект-проигрыватель как 
            // поле класса для управления звуком
            SoundPlayer soundPlayer = new SoundPlayer();
        
            // Свойство подключения звука к проигрывателю из файла
            public string WaveFile
            {
                get
                {
                    // Что загружено для проигрывания
                    return soundPlayer.SoundLocation;
                }
                set
                {
                    // Подключить файл с диска или URL
                    soundPlayer.SoundLocation = value;
                }
            }
        
            // Свойство подключения звука к проигрывателю из потока
            public System.IO.Stream WaveStream
            {
                get
                {
                    // Вернуть ссылку на загруженный в поток файл
                    return soundPlayer.Stream;
                }
                set
                {
                    // Подключить файл к потоку
                    soundPlayer.Stream = value;
                    // Загрузить асинхронно (в фоновом режиме)
                    soundPlayer.LoadAsync();
                }
            }

    В классе расширения кнопки мы создали объект класса SoundPlayer как поле, чтобы он был виден в членах этого класса - в свойствах и переопределенном методе. Свойства WaveFile и WaveStream настраивают объект проигрывания звука и загружают в него указанный в вызывающем коде звук для выполнения в асинхронном (фоновом) режиме в отдельном потоке. Для синхронного проигрывания звука нужно использовать метод Load(), который будет держать приложение, пока звук полностью не проиграется. Свойство WaveFile используется для проигрывания звука из отдельного файла, хранящегося на диске или в Web по URL -адресу.

    Переопределение стандартного метода диспетчеризации щелчка на кнопке

    Как и в случае с кнопкой BeepButton, перехватим событие щелчка на кнопке SoundButton в переопределенном виртуальном методе диспетчеризации библиотечного класса Control и запустим в нем проигрывание адресуемого в свойствах файла

  • Добавьте в конец класса SoundButton файла SoundButton.cs следующий код, переопределяющий виртуальный метод диспетчеризации OnClick() класса Control и запускающий проигрывание звука
  • // Переопределяем виртуальный метод класса Control,
            // чтобы начать проигрывание по щелчку на кнопке
            protected override void OnClick(EventArgs e)
            {
                // Если имя экземпляра кнопки - "Web", 
                // то вместо адресации к удаленному звуку
                // в Internet проигрываем стандартный звуковой
                // файл локального компьютера, встроенный
                // в класс SystemSounds библиотеки .NET Framework 
                if (this.Name.ToUpper() == "Web".ToUpper())
                {
                    SystemSounds.Asterisk.Play();
                    return;
                }
        
                // Загрузить асинхронно - в фоновом режиме.
                // Поток для кнопки Stream открывается 
                // один раз в конструкторе клиента
                if (this.Name.ToUpper() != "Stream".ToUpper())
                    soundPlayer.LoadAsync();
        
                // Если загрузка закончена, то начинаем проигрывание
                if (soundPlayer.IsLoadCompleted)
                    soundPlayer.Play();
                else
                    MessageBox.Show("Проигрыватель пока не готов!");
        
                // Подняться до класса Control,
                // чтобы сгенерировать событие Click.
                // Но в этом упражнении не нужно,
                // поскольку не планируем создавать обработчики
                // (хотя можно и оставить незакомментированной)
                // base.OnClick(e);
            }
  • Перекомпилируйте библиотеку пользовательских элементов вызовом команды Rebuild через контекстное меню узла UserControls панели Solution Explorer
  • Все! Музыкальная кнопка готова. Теперь осталось упаковать ее экземпляры в блок, жестко подключающий проигрываемые файлы, а также поместить его в тестирующее приложение.

    Создание компонента блока музыкальных кнопок SoundButtonBox

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

  • Выделите в панели Solution Explorer узел UserControls и добавьте к проекту новый файл с именем SoundButtonBox.cs командой контекстного меню Add/User Control
  • В режиме редактирования SoundButtonBox.cs[Design] перейдите в панель инструментов Toolbox и двойным щелчком на компоненте FlowLayoutPanel свитка Containers создайте экземпляр компоновочной панели
  • Настройте компоновочную панель согласно таблице свойств
  • Таблица свойств экземпляра компонента FlowLayoutPanel
    Свойство Значение Пояснения
    Name flow Имя экземпляра компоновочной панели
    AutoSize true Подстраиваться под содержимое
    FlowDirection TopDown Размещать сверху вниз
    WrapContents false Не переносить дочерние элементы в следующий столбец
    Dock Fill Распахнуть на всю клиентскую область родителя (формы)
    Location 0; 0 Позиционировать в клиентской области родителя
  • В режиме редактирования SoundButtonBox.cs[Design] выделите экземпляр flow, перейдите в панель инструментов Toolbox и двойным щелчком на компоненте SoundButton добавьте в коллекцию объекта flow четыре экземпляра музыкальной кнопки
  • В силу настроек компоновочной панели, которая является родителем кнопок, они расположатся в столбец друг за другом в порядке их создания.

  • В раскрывающемся списке верхней части панели Properties выберите элемент SoundButtonBox, после чего он будет выделен в визуальном редакторе оболочки
  • Настройте класс SoundButtonBox согласно таблице свойств
  • Таблица свойств класса SoundButtonBox
    Свойство Значение Пояснения
    AutoSize true Подстраиваться под содержимое
    AutoSizeMode GrowAndShrink Подстраиваться под содержимое (расширяться и сжиматься)

    После этих действий вид компонента SoundButtonBox в визуальном конструкторе должен стать таким

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

    Настройка части свойств объектов музыкальных кнопок через панель свойств оболочки

  • Откройте на редактирование файл SoundButtonBox.cs в режиме [Design] и, выделяя последовательно экземпляры кнопки, настройте их свойства в соответствии с таблицами свойств, приведенными ниже
  • Таблица свойств первой музыкальной кнопки
    Свойство Значение Пояснения
    Name soundButton1 Имя экземпляра кнопки (сгенерировано автоматически оболочкой)
    AutoSize true Подстраиваться под надпись
    Anchor None Выравнивать по центру родителя
    Text Файл tada.wav в системном каталоге Надпись на кнопке
    Margin.All 8 Наружный отступ по контуру кнопки
    WaveFile Пусто Будет определен в коде конструктора класса для загрузки звука, расположенного в системном каталоге

    Таблица свойств второй музыкальной кнопки
    Свойство Значение Пояснения
    Name soundButton2 Имя экземпляра кнопки (сгенерировано автоматически оболочкой)
    AutoSize true Подстраиваться под надпись
    Anchor None Выравнивать по центру родителя
    Text Файл SOUND827.WAV в каталоге исполняемой сборки Надпись на кнопке
    Margin.All 8 Наружный отступ по контуру кнопки
    WaveFile Пусто Будет определен в коде конструктора класса для загрузки звука, расположенного в файле

    Таблица свойств третьей музыкальной кнопки
    Свойство Значение Пояснения
    Name Stream Имя экземпляра кнопки. Помечаем, чтобы распознать в переопределенном методе деспетчеризации OnClick() и не открывать уже открытый в конструкторе поток
    AutoSize true Подстраиваться под надпись
    Anchor None Выравнивать по центру родителя
    Text Файл Trumpet1.wav в ресурсе исполняемой сборки Надпись на кнопке
    Margin.All 8 Наружный отступ по контуру кнопки
    WaveStream Пусто Будет определен в коде конструктора класса для загрузки звука, расположенного в файле ресурсов

    Таблица свойств четвертой музыкальной кнопки
    Свойство Значение Пояснения
    Name Web Имя экземпляра кнопки. Помечаем, чтобы распознать в переопределенном методе деспетчеризации OnClick() и не обращаться в Web (считаем, что Internet пока что не подключен)
    AutoSize true Подстраиваться под надпись
    Anchor None Выравнивать по центру родителя
    Text Файл azlinsnd.wav, загружаемый из Web по URL (отключено) Надпись на кнопке
    Margin.All 8 Наружный отступ по контуру кнопки
    WaveFile http://www.oaklandzoo.org/atoz/azlinsnd.wav Подключаем для загрузки из Web, но в переопределенном методе деспетчеризации OnClick() распознаем по имени нажатой кнопки и заменяем другим звуком, встроенным в класс SystemSounds библиотеки .NET Framework

    Интерфейсная часть блока кнопок после их настройки должна выглядеть так

    Настройка части свойств объектов музыкальных кнопок в конструкторе класса SoundButtonBox

  • Добавьте в конец конструктора класса SoundButtonBox одноименного файла C# код, чтобы класс стал таким
  • using System;
    using System.Collections.Generic;
    using System.ComponentModel;
    using System.Drawing;
    using System.Data;
    using System.Text;
    using System.Windows.Forms;
        
    namespace MyCompany.UserControls
    {
        public partial class SoundButtonBox : UserControl
        {
        public SoundButtonBox()
        {
        InitializeComponent();
        
        // Подключить первую кнопку к звуковому файлу
        soundButton1.WaveFile = System.IO.Path.Combine(     // Склеить два пути
        Environment.GetEnvironmentVariable("windir"),   // Каталог системы
          @"Media\tada.wav");
        
        // Подключить вторую кнопку к звуковому файлу
        soundButton2.WaveFile = "SOUND827.WAV";
        
        // Подключить третью кнопку к звуковому файлу из файла ресурсов сборки
        Stream.WaveStream = this.GetType().Assembly.GetManifestResourceStream(
          "MyCompany.UserControls.Trumpet1.wav");
        }
        }
    }

    Ниже приведен код, формируемый оболочкой при настройке компонента SoundButtonBox в визуальном режиме

    namespace MyCompany.UserControls
    {
    partial class SoundButtonBox
      {
      /// <summary> 
      /// Required designer variable.
      /// </summary>
      private System.ComponentModel.IContainer components = null;
        
      /// <summary> 
      /// Clean up any resources being used.
      /// </summary>
      /// <param name="disposing">true 
        if managed resources should be disposed; 
        otherwise, false.</param>
      protected override void Dispose(bool disposing)
      {
      if (disposing  (components != null))
        {
        components.Dispose();
        }
      base.Dispose(disposing);
      }
        
      #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()
        {
        this.components = new System.ComponentModel.Container();
        this.flow = new System.Windows.Forms.FlowLayoutPanel();
        this.soundButton1 = new MyCompany.UserControls.SoundButton
        (this.components);
        this.soundButton2 = new MyCompany.UserControls.SoundButton
        (this.components);
        this.Stream = new MyCompany.UserControls.SoundButton
        (this.components);
        this.Web = new MyCompany.UserControls.SoundButton
        (this.components);
        this.flow.SuspendLayout();
        this.SuspendLayout();
        // 
        // flow
        // 
        this.flow.AutoSize = true;
        this.flow.Controls.Add(this.soundButton1);
        this.flow.Controls.Add(this.soundButton2);
        this.flow.Controls.Add(this.Stream);
        this.flow.Controls.Add(this.Web);
        this.flow.Dock = System.Windows.Forms.DockStyle.Fill;
        this.flow.FlowDirection = 
        System.Windows.Forms.FlowDirection.TopDown;
        this.flow.Location = new System.Drawing.Point(0, 0);
        this.flow.Name = "flow";
        this.flow.Size = new System.Drawing.Size(440, 172);
        this.flow.TabIndex = 0;
        this.flow.WrapContents = false;
        // 
        // soundButton1
        // 
        this.soundButton1.Anchor = System.Windows.Forms.AnchorStyles.None;
        this.soundButton1.AutoSize = true;
        this.soundButton1.Location = new System.Drawing.Point(88, 8);
        this.soundButton1.Margin = new System.Windows.Forms.Padding(8);
        this.soundButton1.Name = "soundButton1";
        this.soundButton1.Size = new System.Drawing.Size(264, 27);
        this.soundButton1.TabIndex = 0;
        this.soundButton1.Text = "Файл tada.wav в системном каталоге";
        this.soundButton1.UseVisualStyleBackColor = true;
        this.soundButton1.WaveFile = "";
        this.soundButton1.WaveStream = null;
        // 
        // soundButton2
        // 
        this.soundButton2.Anchor = System.Windows.Forms.AnchorStyles.None;
        this.soundButton2.AutoSize = true;
        this.soundButton2.Location = new System.Drawing.Point(28, 51);
        this.soundButton2.Margin = new System.Windows.Forms.Padding(8);
        this.soundButton2.Name = "soundButton2";
        this.soundButton2.Size = new System.Drawing.Size(384, 27);
        this.soundButton2.TabIndex = 1;
        this.soundButton2.Text = "Файл SOUND827.WAV 
        в каталоге исполняемой сборки";
        this.soundButton2.UseVisualStyleBackColor = true;
        this.soundButton2.WaveStream = null;
        // 
        // Stream
        // 
        this.Stream.Anchor = System.Windows.Forms.AnchorStyles.None;
        this.Stream.AutoSize = true;
        this.Stream.Location = new System.Drawing.Point(41, 94);
        this.Stream.Margin = new System.Windows.Forms.Padding(8);
        this.Stream.Name = "Stream";
        this.Stream.Size = new System.Drawing.Size(358, 27);
        this.Stream.TabIndex = 2;
        this.Stream.Text = "Файл Trumpet1.wav 
        в ресурсе исполняемой сборки";
        this.Stream.UseVisualStyleBackColor = true;
        this.Stream.WaveFile = "";
        this.Stream.WaveStream = null;
        // 
        // Web
        // 
        this.Web.Anchor = System.Windows.Forms.AnchorStyles.None;
        this.Web.AutoSize = true;
        this.Web.Location = new System.Drawing.Point(8, 137);
        this.Web.Margin = new System.Windows.Forms.Padding(8);
        this.Web.Name = "Web";
        this.Web.Size = new System.Drawing.Size(424, 27);
        this.Web.TabIndex = 3;
        this.Web.Text = "Файл azlinsnd.wav, 
        загружаемый из Web по URL (отключено)";
        this.Web.UseVisualStyleBackColor = true;
        this.Web.WaveFile = 
      "http://www.oaklandzoo.org/atoz/azlinsnd.wav";
        this.Web.WaveStream = null;
        // 
        // SoundButtonBox
        // 
        this.AutoScaleDimensions = new System.Drawing.SizeF(8F, 16F);
        this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
        this.AutoSize = true;
        this.AutoSizeMode = 
        System.Windows.Forms.AutoSizeMode.GrowAndShrink;
        this.Controls.Add(this.flow);
        this.Name = "SoundButtonBox";
        this.Size = new System.Drawing.Size(440, 172);
        this.flow.ResumeLayout(false);
        this.flow.PerformLayout();
        this.ResumeLayout(false);
        this.PerformLayout();
        
        }
        
        #endregion
        
        private System.Windows.Forms.FlowLayoutPanel flow;
        private SoundButton soundButton1;
        private SoundButton soundButton2;
        private SoundButton Stream;
        private SoundButton Web;
        }
    }

    Подготовка файлов звука к проигрыванию компонентом SoundButtonBox

    Присоединим адресуемые звуки к проекту нашей библиотеки компонентов UserControls.

    Первый звук: Первый звук tada.wav хранится в системном каталоге Windows и путь к нему мы формируем с использованием библиотечного класса Environment.

    Второй звук: Второй звук SOUND827.WAV (или любой другой) код компонента ищет в каталоге размещения самой сборки. Для того, чтобы он там оказался, выполните следующее:

  • В панели Solution Explorer выделите узел проекта UserControls и командой оболочки Project/Add Existing Item загрузите нужный файл в каталог проекта. Файл будет скопирован в каталог размещения проекта (звук SOUND827.WAV находится в прилагаемом к данной работе каталоге Source )
  • Выполненным действием мы скопировали файл звука пока-что в каталог размещения проекта. А он должен находиться в каталоге размещения самой сборки кода компонента. Нам нужно настроить оболочку для работы с этим файлом так, чтобы при компиляции кода компонента она автоматически копировала звук в каталог размещения сборки самого компонента.

  • В панели Solution Explorer вызовите контекстное меню для скопированного файла звука SOUND827.WAV и выполнить команду Properties. Установите опцию Copy to Output Directory в значение Copy if newer (копировать при обновлении файла звука) или Copy always (копировать всегда). Теперь оболочка сама при компиляции проекта разместит файл в одном со сборкой компонента каталоге
  • Третий звук: Свойство WaveStream используется для проигрывания звука, присоединенного к исполняемому файлу как внедренный ресурс. Файл Trumpet1.wav внедряемого как ресурс звука нужно добавить к проекту, а затем настроить его свойства так, чтобы компилятор поместил его в сборку компонента.

  • В панели Solution Explorer выделите узел проекта UserControls и командой Add/Add Existing Item контекстного меню загрузите файл Trumpet1.wav в каталог проекта из прилагаемого к работе каталога Source
  • Через команду Properties контекстного меню, вызванного в панели Solution Explorer для файла Trumpet1.wav, установите в таблице свойств Properties поле Build Action в значение Embedded Resource (назначить внедренным ресурсом)
  • Теперь звук Trumpet1.wav на этапе компиляции будет внедрен в исполнимую сборку и готовое приложение может далее выполняться отдельно от первоисточника звука. Так удобно поступать с файлами небольших размеров. Для загрузки звука из ресурса нужно указывать полный путь к ресурсу, включая пространство имен проекта.

    Четвертый звук: Специальных подготовительных действий не требует, поскольку извлекается из библиотеки .NET Framework.

  • Откомпилируйте проект с компонентами, выполнив команду Rebuild контекстного меню узла UserControls в панели Solution Explorer
  • Запустите проводник Windows Explorer и убедитесь, что размер сборки UserControls существенно увеличился за счет внедренного звука Trumpet1.wav.
  • Тестирование компонента SoundButtonBox

  • Добавьте к решению командой File/Add/New Project новый проект типа оконного приложения ( Windows Application ) с именем SoundButtonsTest и назначьте его стартовым командой Project/Set as StartUp Project меню оболочки (или аналогичной командой контекстного меню)
  • Поместите на форму из панели Toolbox экземпляр компонента SoundButtonBox и выровняйте его по размеру формы
  • Подключите динамики, запустите приложение и испытайте его работу
  • Отметьте, что разные исполнимые сборки используют одну и ту же библиотеку компонентов UserControls.dll при условии, что эта библиотека находится в том же каталоге, что и исполнимая сборка. При добавлении к нашему решению MySolution, содержащему проект библиотеки компонентов UserControls, нового проекта SoundButtonsTest исполнимого приложения оболочка автоматически копирует эту библиотеку в каталог, где будет находиться исполнимая сборка. При любом изменении библиотеки компонентов при ее компиляции оболочка обновляет файл библиотеки во всех каталогах, где находятся файлы исполнимых сборок.

    Упражнение 4. Рисование круглой кнопки

    Вид стандартного элемента управления можно изменить, переопределив его виртуальные методы OnPaint() и OnResize(). Предпочтимые размеры элемента управления устанавливаются в переопределении наследуемого метода GetPreferredSize(). Для прорисовки элементов управления можно использовать класс System.Windows.Forms.ControlPaint. Системная палитра, перья и кисти находятся в трех классах пространства имен SystemDrawing:

  • SystemColors
  • SystemPens
  • SystemBrushes
  • Пространство имен System.Windows.Forms также содержит цвета в классах

  • ProfessionalColors - цвета в статических свойствах класса
  • ProfessionalColorTable - цвета в виртуальных экземплярных свойствах класса
  • В этом упражнении мы создадим кнопку непрямоугольной формы, которая будет отрисовываться по пользовательскому алгоритму. На этом примере будет показано, как создавать пользовательские элементы управления непрямоугольной формы.

    Создание заготовки компонента CircleButton

  • В панели Solution Explorer выделите узел UserControls и добавьте к проекту библиотеки новый компонент командой Project/Add Component с именем CircleButton
  • В заголовке класса CircleButton файла CircleButton.cs замените базовый класс Component на Button
  • Добавьте в начало файла CircleButton.cs инструкции подключения пространств имен
    using System.Drawing;
    using System.Windows.Forms;
  • Добавьте в класс CircleButton закрытый метод Init() и поместите его вызовы в перегруженные конструкторы компонента
  • using System;
    using System.ComponentModel;
    using System.Collections.Generic;
    using System.Diagnostics;
        
    using System.Drawing;
    using System.Windows.Forms;
        
    namespace MyCompany.UserControls
    {
        public partial class CircleButton : Button
        {
            public CircleButton()
            {
                InitializeComponent();
        
                Init();
            }
        
            public CircleButton(IContainer container)
            {
                container.Add(this);
        
                InitializeComponent();
        
                Init();
            }
        
            // Начальные настройки для выполнения в конструкторе
            private void Init()
            {
                // Извещаем операционную систему, что будем сами
                // заботиться об отрисовке компонента
                this.SetStyle(ControlStyles.UserPaint, true);
        
                // Игнорировать системное сообщение WM_ERASEBKGND 
                // очистки окна, чтобы уменьшить нежелательное мерцание 
                this.SetStyle(ControlStyles.AllPaintingInWmPaint, true);
        
                // Наружный отступ по контуру кнопки
                this.Margin = new Padding(this.Font.Height);
        
                // Подстраиваться под размер надписи
                this.AutoSize = true;
            }
        }
    }

    Установка флага ControlStyles.UserPaint извещает операционную систему, что отрисовка окна компонента будет выполняться в переопределении его метода OnPaint().

    Вычисление размеров кнопки

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

  • Добавьте в класс CircleButton код переопределения метода GetPreferredSize()
  • // Вычисляем размер описывающего квадрата кнопки
       // в зависимости от размера надписи на ней
       public override Size GetPreferredSize(Size proposedSize)
       {
       // Получаем ссылку на контекст графического устройства
       Graphics gr = CreateGraphics();
                    
       // Измеряем размер описывающего текст прямоугольника
       SizeF box = gr.MeasureString(this.Text, this.Font);
                    
       // Вычисляем диагональ текстового блока надписи
       int diagonal = (int)Math.Sqrt(Math.Pow(box.Width, 2) 
                    + Math.Pow(box.Height, 2));
                    
       // Возвращаем объект с размерами описывающего квадрата кнопки
       return new Size(diagonal, diagonal);
    }

    Метод GetPreferredSize() вызывается диспетчером размещения для определения предпочтительных размеров компонента, когда значение его свойства AutoSize=true (мы его установили в методе Init() ).

    Задание круглой формы для кнопки

    using System;
    using System.Drawing;
    using System.Windows.Forms;
        
    using System.Drawing.Drawing2D;
        
    namespace Test
    {
        // Класс круглой кнопки
        class CircleButton : Button
        {
        public CircleButton()
        {
        // Сами отрисуем вместо операционной системы
        this.SetStyle(ControlStyles.UserPaint, true);
        // Отключить от системы автоматическую отрисовку
        this.SetStyle(ControlStyles.AllPaintingInWmPaint, true);
        // Наружный отступ по контуру кнопки
        this.Margin = new Padding(this.Font.Height);
        }
        
        // Возвращается предпочтимый размер кнопки в зависимости от текста
        public override Size GetPreferredSize(Size proposedSize)
        {
        // Создаем временный контекст устройства
        Graphics gr = CreateGraphics();
        // Вычисляем размер текстового блока надписи
        SizeF sizeF = gr.MeasureString(this.Text, this.Font);
        // Освобождаем контекст
        gr.Dispose();
        // Теорема Пифагора
        int radius = (int)Math.Sqrt(
          Math.Pow(sizeF.Width / 2, 2)
          + Math.Pow(sizeF.Height / 2, 2));
        
        // Возвращаем диаметр
        return new Size(2 * radius, 2 * radius);
        }
        
        // Вычисление контура кнопки при изменении ее размеров
        protected override void OnResize(EventArgs args)
        {
        // Рисуем кнопку круглой, используя класс Region
        GraphicsPath path = new GraphicsPath();// Объект для региона
        path.AddEllipse(this.ClientRectangle);// Преобразовать в элипс
        this.Region = new Region(path);
        
        // Вызываем предка для инициации OnPaint()
        base.OnResize(args);
        }
        
        // Отрисовка круглой кнопки
        protected override void OnPaint(PaintEventArgs args)
        {
        Graphics gr = args.Graphics;// Адресовались к контексту
        // Качество рисования - сглаживать контуры
        gr.SmoothingMode = SmoothingMode.AntiAlias;
        Rectangle rect = this.ClientRectangle;
        
        // Флаг попадания курсора внутрь кнопки при нажатии
        bool bPressed = this.Capture 
          ((Control.MouseButtons  MouseButtons.Left) != 0) 
          this.ClientRectangle.
          Contains(this.PointToClient(Control.MousePosition));
        
        // Отрисовка внутренней части кнопки
        // При нажатии она становится темнее
        GraphicsPath path = new GraphicsPath();
        path.AddEllipse(rect);
        PathGradientBrush gradient = new PathGradientBrush(path);
        int k = bPressed ? 2 : 1;// Для нажатой увеличим вес градиента
        gradient.CenterPoint = new PointF(
          k * (rect.Left + rect.Right) / 3,
          k * (rect.Top + rect.Bottom) / 3);
        gradient.CenterColor = bPressed ? Color.Blue : Color.White;
        gradient.SurroundColors = new Color[] { Color.SkyBlue };
        gr.FillRectangle(gradient, rect);
        
        // Отрисовка контура кнопки, утолщенного для активных кнопок
        Brush brush = new LinearGradientBrush(
          rect,
          Color.FromArgb(0, 0, 255),
          Color.FromArgb(0, 0, 128),
          LinearGradientMode.ForwardDiagonal);
        Pen pen = new Pen(
          brush,
          (this.IsDefault ? 6 : 2) * gr.DpiX / 72);
          gr.DrawEllipse(pen, rect);
        
        // Отображение надписи в центре прямоугольника кнопки
        // Для неактивной кнопки цвет надписи должен быть бледно-серым
        StringFormat strFormat = new StringFormat();
        // Определение точки привязки в центре текстового блока
        strFormat.Alignment = 
          strFormat.LineAlignment = 
          StringAlignment.Center;
        brush = this.Enabled ? SystemBrushes.WindowText // Для активной
          : SystemBrushes.GrayText;// Для неактивной
        gr.DrawString(this.Text, this.Font, brush, rect, strFormat);
        
        // Отображение пунктира вокруг текста, если кнопка имеет фокус
        if (this.Focused)
          {
          SizeF sizeF = gr.MeasureString(
            this.Text, 
            this.Font,
            PointF.Empty, 
            StringFormat.GenericTypographic);
          pen = new Pen(this.ForeColor);// Текущий цвет
          pen.DashStyle = DashStyle.Dash;// Пунктир
          gr.DrawRectangle(
            pen,
            rect.Left + rect.Width / 2 - sizeF.Width / 2,
            rect.Top + rect.Height / 2 - sizeF.Height / 2,
            sizeF.Width,
            sizeF.Height);
            }
        
          if (this.Paint != null)// Если есть хоть одна подписка
            this.Paint(this, args);// Инициируем событие
          }
        
        new public event PaintEventHandler Paint;// Объявляем новое событие
        }
           
        // Форма приложения
        class MyClass : Form
        {
        public MyClass()
          {
          this.Text = "Круглые кнопки";
          this.Font = new Font("Times New Roman", 18);
          this.MaximizeBox = false;
          // Подстраиваться под кнопки
          this.AutoSize = true;
          this.AutoSizeMode = AutoSizeMode.GrowAndShrink;
        
          // Панель для размещения элементов интерфейса в столбец
          FlowLayoutPanel flow = new FlowLayoutPanel();
          flow.Parent = this;
          flow.AutoSize = true;
          flow.FlowDirection = FlowDirection.TopDown;// В столбец
        
          // Встроенная компоновочная панель для верха
          FlowLayoutPanel flowTop = new FlowLayoutPanel();
          flowTop.Parent = flow;
          flowTop.FlowDirection = FlowDirection.LeftToRight;// По умолчанию
          flowTop.AutoSize = true; 
          // Выравнивание в столбец по центру родительской панели
          flowTop.Anchor = AnchorStyles.None; 
        
          // Помещаем в верхнюю компоновочную панель метку
          Label lbl = new Label();
          lbl.Parent = flowTop;
          lbl.AutoSize = true;
          lbl.Text = "Введите текст: ";
          lbl.Anchor = AnchorStyles.None;// Выравнивание по центру строки
        
          // Помещаем в верхнюю компоновочную панель поле ввода
          TextBox txtBox = new TextBox();
          txtBox.Parent = flowTop;
          txtBox.Width = 8 * this.Font.Height;// Длина поля ввода
          txtBox.TabStop = false;// Отключить клавишу Tab
        
          // Встроенная компоновочная панель для низа
          FlowLayoutPanel flowBottom = new FlowLayoutPanel();
          flowBottom.Parent = flow;
          flowBottom.FlowDirection = 
        FlowDirection.LeftToRight;// По умолчанию
          flowBottom.AutoSize = true;
          // Выравнивание в столбец по центру родительской панели
          flowBottom.Anchor = AnchorStyles.None;
        
          // Помещаем в нижнюю компоновочную панель кнопки
          CircleButton btn = new CircleButton();
          btn.Parent = flowBottom;
          btn.AutoSize = true;// Подстраиваемая
          btn.Text = "Кнопка 1";
          btn.Anchor = AnchorStyles.None;// Выравнивать по горизонтали
          btn.Click += btn_Click;
        
          btn = new CircleButton();
          btn.Parent = flowBottom;
          btn.AutoSize = true;// Подстраиваемая
          btn.Text = "Кнопка 2";
          btn.Anchor = AnchorStyles.None;// Выравнивать по горизонтали
          btn.Click += btn_Click;
        
          btn = new CircleButton();
          btn.Parent = flowBottom;
          btn.AutoSize = true;// Подстраиваемая
          btn.Text = "Кнопка 3";
          btn.Anchor = AnchorStyles.None;// Выравнивать по горизонтали
          btn.Click += btn_Click;
          }
        
            void btn_Click(object sender, EventArgs e)
            {
                //this.Close();// Завершить приложение
            }
        }
        
        // Запуск
        class Program
        {
            static void Main()
            {
                Application.EnableVisualStyles();
                // Создали форму и запустили цикл сообщений Windows
                Application.Run(new MyClass());
            }
        }
    }

    Результат выполнения будет таким

    Поскольку в классе расширения кнопки мы переопределили виртуальный метод OnPaint() и не предусмотрели его вызов для базового класса, то событие Paint никогда инициировано не будет. Потому что оно вызывается в методе OnPaint() именно базового класса, который мы перехватили. Чтобы не потерять событие Paint, мы его заново объявили в классе расширения, а в переопределенный метод OnPaint() вставили инициализацию этого события с предварительной проверкой, подписано ли оно в вызывающем коде.

    Комбинирование готовых элементов управления

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

    Теперь мы разработаем свой элемент управления на основе нескольких библиотечных элементов. Применим прием многоэтапного последовательного расширения классов на основе наследования.

    using System;
    using System.Drawing;
    using System.Windows.Forms;
        
    namespace Test
    {
        //******************************************************
        
        // Кнопка, генерирующая множество событий Click
        // При нажатии и удержании на ней курсора мыши
        class ClickmaticButton : Button
        {
        // Определяем поля класса
        // Создаем таймер
        Timer timer = new Timer();
        // Задержка срабатывания кнопки Button после нажатия мыши
        int iDelay = 250 * (1 + SystemInformation.KeyboardDelay);
        // Скорость для срабатывания таймера, генерирующего события повторных
        // нажатий кнопки компонента при удержании нажатой левой кнопки мыши
        int iSpeed = 405 - 12 * SystemInformation.KeyboardSpeed;
        
        // Конструктор
        public ClickmaticButton()
        {
        timer.Tick += TimerOnTick;// Подписались на тики
        }
        
        // Перехватываем нажатие левой кнопки мыши и запускаем таймер
        // для генерации событий Click кнопки Button
        protected override void OnMouseDown(MouseEventArgs args)
        {
        base.OnMouseDown(args);
        
        // Если нажата левая кнопка мыши
        if ((args.Button  MouseButtons.Left) != 0)
        {
        timer.Interval = iDelay;// Задержка до первого тика
        timer.Start();// Включили таймер
        }
        }
        
        // Отработка тиков
        void TimerOnTick(object sender, EventArgs e)
        {
        // Программно генерируем повторное 
        // нажатие Button при каждом тике
        this.OnClick(EventArgs.Empty);
        // Меняем скорость таймера
        timer.Interval = iSpeed;
        }
        
        // Контролируем, находится ли нажатый курсор мыши
        // над кнопкой и управляем приостановкой таймера
        protected override void OnMouseMove(MouseEventArgs args)
        {
        base.OnMouseMove(args);
                
        // Приостанавливаем таймер
        timer.Enabled = this.Capture 
          this.ClientRectangle.Contains(args.Location);
        }
        
        // При отпускании кнопки таймер останавливаем
        protected override void OnMouseUp(MouseEventArgs args)
        {
        base.OnMouseUp(args);
        
        timer.Stop();
        }
        }
        
        //******************************************************
        
        // Рисование на генерирующей событие Click кнопке 
        // изображения стрелки
        class ArrowButton : ClickmaticButton
        {
            // Поле для хранения ориентации кнопки
            // По умолчанию острый угол треугольника направлен вправо
            ScrollButton scrollButton = ScrollButton.Right;
        
            // Конструктор
            public ArrowButton()
            {
                // Задание стиля кнопки как не получающей фокус ввода
                this.SetStyle(ControlStyles.Selectable, false);
            }
        
            // Свойство для поддержки переменной перечисления
            public ScrollButton ScrollButton
            {
                get { return scrollButton; }
                set
                {
                    scrollButton = value;
                    this.Invalidate();// Перерисовать
                }
            }
        
            // Переопределяем метод отрисовки кнопки
            protected override void OnPaint(PaintEventArgs args)
            {
                Point point = PointToClient(Control.MousePosition);
                bool mouseInButton = this.Capture 
                    this.ClientRectangle.Contains(point);
                ButtonState state = mouseInButton ?
                    ButtonState.Pushed : ButtonState.Normal;
        
                state = !this.Enabled ?
                    ButtonState.Inactive : state;
        
                Graphics gr = args.Graphics;
                // Рисуем кнопку с треугольником внутри
                ControlPaint.DrawScrollButton(
                    gr,
                    this.ClientRectangle,
                    scrollButton,
                    state);
            }
        
            protected override void OnMouseCaptureChanged(EventArgs args)
            {
                base.OnMouseCaptureChanged(args);
        
                this.Invalidate();// Перерисовать
            }
        }
        
        //******************************************************
        
        // Самодельный элемент управления - поле с кнопками
        public class NumericScan : UserControl
        {
            // Объявили событие
            public event EventHandler ValueChanged;
        
            // Объявили ссылки-поля для видимости объектов
            TextBox txtBox;
            ArrowButton btn1, btn2;
        
            // Объявили базовые закрытые поля для открытых свойств
            int iDecimalPlaces = 0;
            decimal mValue = 0;
            decimal mIncrement = 1;
            decimal mMinimum = 0;
            decimal mMaximum = 100;
        
            // Доступные свойства-обертки полей
            public int DecimalPlaces
            {
                get { return iDecimalPlaces; }
                set { iDecimalPlaces = value; }
            }
            public decimal Value
            {
                get { return mValue; }
                set { txtBox.Text = ValueToText(mValue = value); }
            }
            public decimal Increment
            {
                get { return mIncrement; }
                set { mIncrement = value; }
            }
            public decimal Minimum
            {
                get { return mMinimum; }
                set
                {
                    // Заодно отслеживаем диапазон Value
                    if (Value < (mMinimum = value))
                        Value = mMinimum;
                }
            }
            public decimal Maximum
            {
                get { return mMaximum; }
                set
                {
                    // Заодно отслеживаем диапазон Value
                    if (Value > (mMaximum = value))
                        Value = mMaximum;
                }
            }
        
            // Конструктор
            public NumericScan()
            {
            txtBox = new TextBox();
            txtBox.Parent = this;
            // Текст у правой границы поля
            txtBox.TextAlign = HorizontalAlignment.Right;
            txtBox.Text = ValueToText(mValue);// Инициализируем
            txtBox.TextChanged += TextBoxOnTextChanged;
            // При вводе Enter обновить Value элемента
            txtBox.KeyDown += TextBoxOnKeyDown;
            // Не принимать лишние символы
            txtBox.KeyPress += new KeyPressEventHandler(txtBox_KeyPress);
        
            btn1 = new ArrowButton();
            btn1.Parent = this;
            // Ориентация треугольничка влево
            btn1.ScrollButton = ScrollButton.Left;
            btn1.Click += ButtonOnClick;
        
            btn2 = new ArrowButton();
            btn2.Parent = this;
            // Ориентация треугольничка вправо
            btn2.ScrollButton = ScrollButton.Right;// По умолчанию
            btn2.Click += ButtonOnClick;
            }
        
            string ValueToText(decimal mValue)
            {
            // Возвращаем с форматированием
            return mValue.ToString("F" + iDecimalPlaces);
            }
        
            // Обработчик события изменения содержимого текстового поля
            void TextBoxOnTextChanged(object sender, EventArgs args)
            {
            try
            {
            // Преобразуем текст в число
            mValue = Decimal.Parse(txtBox.Text);
            }
            catch { }// На исключения не реагируем
            }
        
            // Обрабатывает каждую клавишу, но ждет клавишу Enter
        
           void TextBoxOnKeyDown(object sender, KeyEventArgs args)
            {
            switch (args.KeyCode)
            {
            // При нажатии Enter генерируем наше событие ValueChanged 
            case Keys.Enter:
              OnValueChanged(EventArgs.Empty);
            break;
            }
        
          // Обнаруживаем цифру
          isNumeric =
            // Основная клавиатура
            args.KeyCode >= Keys.D0  args.KeyCode <= Keys.D9
            // Дополнительная клавиатура
            || args.KeyCode >= Keys.NumPad0  
      args.KeyCode <= Keys.NumPad9
            // Клавиша BackSpace
            || args.KeyCode == Keys.Back;
          }
        
          // Отфильтровываем лишние символы из поля ввода
          bool isNumeric;// Видимое поле
          void txtBox_KeyPress(object sender, KeyPressEventArgs args)
          {
          if (isNumeric != true)// Пропускаем только цифры
            args.Handled = true;// Не принимаем в TextBox
          }
        
          // Срабатывает автоматически и устанавливает
          // размеры компонента this.Width, this.Height
          public override Size GetPreferredSize(Size proposedSize)
          {
          const float weight = 1.2f;// Вес ширины
          // Высота компонента
          int height = txtBox.PreferredHeight +
            SystemInformation.HorizontalScrollBarHeight;
          // Устанавливаем размеры компонента
          return new Size((int)(weight * height), height);
          }
        
          // Срабатывает автоматически, позиционирует
          // кнопки и устанавливает их размеры
          protected override void OnResize(EventArgs args)
          {
          base.OnResize(args);
        
          txtBox.Height = txtBox.PreferredHeight;
          txtBox.Width = this.Width;// Текстовое поле по ширине компонента
          btn1.Location = new Point(0, txtBox.Height);// Слева снизу
          btn2.Location = new Point
        (this.Width / 2, txtBox.Height);// Середина низ
          // Приравниваем значения полей структур
          btn1.Size = btn2.Size = new Size(txtBox.Width / 2, 
          this.Height - txtBox.Height);
          }
            
          // Когда элемент управления уходит из фокуса ввода, надо
          // проверить введенное и сгенерировать событие ValueChanged
          protected override void OnLeave(EventArgs args)
          {
          base.OnLeave(args);
          this.OnValueChanged(EventArgs.Empty);// Аргумент не используется
          }
        
          // Изменения величины пользователем по щелчкам мыши
          void ButtonOnClick(object sender, EventArgs e)
          {
          // Если не было, то дать фокус тексовому полю
          if (!txtBox.Focused)
            txtBox.Focus();
         
          // Повышаем полномочия ссылки
          ArrowButton btn = sender as ArrowButton;
          // Пробная величина для изменения значения на шаге
          decimal mNewValue = Value;
        
          // Выявляем кнопку и проверяем выход за границу
          if (btn == btn1)
            if ((mNewValue -= Increment) < Minimum)
              return;
        
          if (btn == btn2)
            if ((mNewValue += Increment) > Maximum)
              return;
        
          // Принимаем изменение и вызываем обработчик,
          // который сгенерирует событие ValueChanged
          Value = mNewValue;
          OnValueChanged(EventArgs.Empty);
          }
        
          // Ввели свою дополнительную функцию
          // Изменение величины прямым вводом в текстовое поле
          protected virtual void OnValueChanged(EventArgs args)
          {
          // Последовательно проверяем выход величины за левую и правую
          // границы, и если выходит за границу, то обрезаем по границе
          Value = Math.Min(Maximum, Value);
          Value = Math.Max(Minimum, Value);
          //Value = Decimal.Round(Value, DecimalPlaces);
        
          // Генерируем событие
          if (ValueChanged != null)
            ValueChanged(this, args);
          }
        }
        
        //******************************************************
        
        // Тест для проверки нашего элемента управления
        class MyClass : Form
        {
        // Внутренние поля для видимости в методах
        Label lbl1, lbl2;// Заполняются обработчиком события ValueChanged
        NumericScan numscan1, numscan2;// Ссылки на компоненты
        
        public MyClass()
        {
        this.Text = "Тест пользовательского компонента";
        // Размеры формы
        this.Width = (int)(1.3f * this.Width);
        this.Height = (int)(.8f * this.Height);
        
        FlowLayoutPanel flow = new FlowLayoutPanel();
        flow.Parent = this;// Привязали к форме
        // Размещение по столбцам
        flow.FlowDirection = FlowDirection.TopDown;
        // Внутреннее обрамление в пикселах
        flow.Padding = new Padding(20);
        // Растянули по форме
        flow.Dock = DockStyle.Fill;
        
        numscan1 = new NumericScan();
        numscan1.Parent = flow;
        numscan1.AutoSize = true;
        // Элементы между собой выравнивать по центру
        numscan1.Anchor = AnchorStyles.None;
        numscan1.ValueChanged += NumericScanOnValueChanged;
        
        lbl1 = new Label();
        lbl1.Parent = flow;
        lbl1.AutoSize = true;
        // Элементы между собой выравнивать по центру
        lbl1.Anchor = AnchorStyles.None;
        
        // Пустая текстовая метка для отступа элементов
        (new Label()).Parent = flow;
        
        numscan2 = new NumericScan();
        numscan2.Parent = flow;
        numscan2.AutoSize = true;
        // Элементы между собой выравнивать по центру
        numscan2.Anchor = AnchorStyles.None;
        numscan2.ValueChanged += NumericScanOnValueChanged;
        
        lbl2 = new Label();
        lbl2.Parent = flow;
        lbl2.AutoSize = true;
        // Элементы между собой выравнивать по центру
        lbl2.Anchor = AnchorStyles.None;
        
        // Сами принудительно выполняем обработчик
        // для начальной инициализации текстовых меток
        NumericScanOnValueChanged(numscan1, EventArgs.Empty);
        NumericScanOnValueChanged(numscan2, EventArgs.Empty);
        }
        
        void NumericScanOnValueChanged(object sender, EventArgs args)
            {
                NumericScan ob = sender as NumericScan;// Приводим ссылку
                if (ob == numscan1)
                    lbl1.Text = "Первый: " + numscan1.Value;
                else if (ob == numscan2)
                    lbl2.Text = "Второй: " + numscan2.Value;
            }
        }
        
        //******************************************************
        
        // Запуск
        class Program
        {
            static void Main()
            {
                Application.EnableVisualStyles();
                // Создали форму и запустили цикл сообщений Windows
                Application.Run(new MyClass());
            }
        }
    }
    Страницы:
    Файлы к лекции Вы можете скачать здесь.

    Элементы управления являются посредниками между приложением и пользователем. Они составляют так называемый графический интерфейс пользователя (GUI - Graphics User Interface ). Их задача состоит в отображении информации для пользователя, обеспечении удобного ввода и генерации событий, отрабатывающих предпочтения пользователя.

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

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

    Упражнение 1. Построение элемента управления BeepButton

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

    Создание заготовки библиотеки компонентов

  • Создайте новый пустой проект командой File/New/Project и заполните окно мастера так
  • В панели Solution Explorer выделите корневой узел проекта UserControls и через контекстное меню выполните команду Properties
  • Откройте вкладку Application свойств оболочки и настройте поля Default namespace и Output type как показано на рисунке
  • В панели Solution Explorer вызовите для корневого узла проекта UserControls контекстное меню и выполните команду Add/Component
  • Добавьте к проекту новый файл компонента с именем BeepButton.cs
  • В рабочей области конструктора форм щелкните на ссылке click here to switch to code view или исполните команду контекстного меню View Code, чтобы перейти в режим редактирования кода
  • В панели Solution Explorer для узла References, где уже должна находиться ссылка на библиотечную сборку System, выполните команду Add Reference
  • Удерживая клавишу Ctrl через окно Add Reference выделите и добавьте к проекту ссылки на библиотечные сборки System.Drawing и System.Windows.Forms
  • Убедитесь, что теперь узел References проекта стал выглядеть так
  • Перейдите в режим редактирования файла BeepButton.cs и откорректируйте его так
  • using System;
    using System.ComponentModel;
    using System.Drawing;
    using System.Windows.Forms;
      
    using System.Media; // Для звука 
        
    namespace MyCompany.UserControls
    {
        public partial class BeepButton : Button 
        {
            public BeepButton()
            {
                InitializeComponent();
            }
        
            public BeepButton(IContainer container)
            {
                container.Add(this);
        
                InitializeComponent();
            }
        }
    }

    Класс Button является наследником класса Component, поэтому мы спокойно применяем его в качестве базового, продолжая цепочку наследования.

    Переопределение наследуемого от Control виртуального метода диспетчеризации события Click

    Большинство библиотечных классов, в которых определены события, имеют соответствующие виртуальные методы диспетчеризации с приставкой On. Эти методы можно переопределять в классах-наследниках, дополняя нужным кодом. Воспользуемся этой возможностью в нашем классе расширения кнопки, упаковав переопределение метода в отдельную часть класса BeepButton.

  • Дополните конец файла BeepButton.cs следующим кодом
  • namespace MyCompany.UserControls
    {
        partial class BeepButton
        {
            // Переопределяем виртуальный метод диспетчеризации
            // класса Control (задействуется наследуемый метод
            // диспетчеризации класса кнопки)
            protected override void OnClick(EventArgs e)
            {
                // Проиграть системный звук 
                SystemSounds.Exclamation.Play();// Сочный через внешние динамики
                //SystemSounds.Beep.Play();     // Простой через внешние динамики
                //System.Console.Beep();        // Простой через внутренний динамик
        
                // Подняться до базового класса Control,
                // чтобы сгенерировать его событие Click и 
                // запустить наш будущий обработчик ButtonOnClick
                base.OnClick(e);
            }
        }
    }

    В переопределенном методе диспетчеризации события Click кнопки мы предусмотрели вызов базового метода диспетчеризации непосредственно кнопки Button, чтобы дать возможность клиентам нашего класса BeepButton подписать свои обработчики на это событие. Событие Click, в свою очередь, унаследовано классом Button от класса Control и генерируется его методом OnClick примерно так

    protected virtual void OnClick(EventArgs args)
            {
                //...
                // Если хоть один обработчик зарегистрирован
                if (Click != null)
                    Click(this, args);
                //...
            }

    Испытание кнопки BeepButton мы проведем позднее, разработав для этого тестовую исполнимую Windows -сборку.

    Упражнение 2. Проектирование диалогового окна сообщений MyDialogBox

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

    Создание диалогового окна будет состоять из трех шагов:

  • Создание самой формы диалогового окна (класс MyDialogForm )
  • Размещение на форме интерфейсных элементов пользователя
  • Создание класса MyDialogBox.cs, управляющего диалоговым окном
  • В панели Solution Explorer выделите узел проекта UserControls и командой Add/Windows Form добавьте к компоненту файл формы с именем MyDialogForm.cs
  • Удалите из узла References лишние добавленные мастером ссылки на библиотечные сборки System.Data и System.Xml
  • Удалите соответствующие инструкции using подключения пространств имен в начале файла MyDialogForm.cs
  • Настройте форму MyDialogForm в соответствии с таблицей
  • Таблица свойств формы MyDialogForm
    Свойство Значение Пояснения
    StartPosition CenterScreen Открывать в центре экрана
    FormBorderStyle FixedDialog Запретить пользователю менять размер
    MaximizeBox false Отключить системную кнопку развертывания
    MinimizeBox false Отключить системную кнопку свертывания
    ShowInTaskbar false Запретить появление в панели задач
    AutoSize true Подстраиваться под содержимое
    AutoSizeMode GrowAndShrink Подстраиваться под содержимое (расширяться и сжиматься)
    Text Пусто Заголовок окна
  • Из свитка Containers панели инструментов Toolbox поместите на форму компоновочную панель FlowLayoutPanel и настройте ее в соответствии с таблицей
  • Таблица свойств компоновочной панели FlowLayoutPanel
    Свойство Значение Пояснения
    Name flow Имя компоновочной панели
    AutoSize true Подстраиваться под содержимое
    FlowDirection TopDown Размещать сверху вниз
    WrapContents false Не переносить дочерние элементы в следующий столбец
    Padding.All 8 Внутренний отступ дочерних элементов по периметру окна
    Dock Fill Распахнуть на всю клиентскую область родителя (формы)

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

  • Поместите внутрь экземпляра компоновочной панели flow из свитка Common Controls текстовую метку Label и настройте ее свойства в соответствии с таблицей
  • Таблица свойств текстовой метки Label
    Свойство Значение Пояснения
    Name lblMessage Имя текстовой метки
    AutoSize true Подстраиваться под содержимое
    Anchor None Расположить посредине
    Margin.All 8 Отступ по наружному периметру текстовой метки
    Modifiers Public Публичный доступ для клиентов формы (для класса MyDialogBox )

    Для того, чтобы объект lblMessage был доступен из метода класса MyDialogBox, при его создании мы сделали его общедоступным.

  • Поместите внутрь компоновочной панели flow из свитка Common Controls кнопку Button и настройте ее свойства в соответствии с таблицей
  • Таблица свойств кнопки Button
    Свойство Значение Пояснения
    Name btnShowDialog Имя кнопки
    AutoSize true Подстраиваться под содержимое
    Anchor None Расположить посредине
    Margin.All 8 Отступ по наружному периметру кнопки
    Text OK Надпись
    DialogResult OK Назначить статус "кнопки OK" в родительской форме

    Свойству дочерних кнопок формы DialogResult можно присвоить любое значение из перечисления System.Windows.Forms.DialogResult ( Abort, Cancel, Ignore, No, None, OK, Retry, Yes ). Оно и будет возвращено функцией ShowDialog() формы, если пользователь выберет именно эту кнопку.

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

  • В панели Solution Explorer выделите узел проекта UserControls и, выполнив команду меню Project/Add Class, добавьте к проекту новый класс с именем MyDialogBox
  • Заполните файл MyDialogBox.cs следующим кодом
  • using System;
    using System.Windows.Forms;
        
    namespace MyCompany.UserControls
    {
        // Наше диалоговое окно сообщения
        public class MyDialogBox
        {
        public static DialogResult Show(string strMessage, string strCaption)
        {
          // Создаем диалоговое окно
          MyDialogForm frm = new MyDialogForm();
          // Заголовок окна
          frm.Text = strCaption;
          // Содержимое текстовой метки
          frm.lblMessage.Text = strMessage;
        
          // Отобразить диалоговое окно в модальном режиме,
          // а после его закрытия вернуть выбор пользователя 
          // вызывающему коду
          return frm.ShowDialog();// Выполняем диалоговое окно
                                        // и возвращаем статус нажатой кнопки
          }
        }
    }

    Класс MyDialogBox нужно объявить с модификатором public, чтобы он был виден в клиентских сборках. Объявление метода Show() в классе MyDialogBox как статического позволяет его вызывать без создания экземпляра этого класса в клиенте. Через аргументы метода Show() можно задавать заголовок диалогового окна и содержимое текстовой метки lblMessage, как это принято в библиотечном классе System.Windows.Forms.MessageBox.

    Теперь осталось собрать все вместе и испытать на тестовом приложении, но прежде нужно откомпилировать проект UserControls и убедиться, что нет синтаксических ошибок.

  • Выполните команду меню оболочки Build/Build UserControls, чтобы откомпилировать проект нашей библиотеки пользовательских элементов управления
  • Тестирование пользовательских элементов BeepButton и MyDialogBox

  • Добавьте к решению MySolution новый проект с именем Test типа Windows Application. Для этого выполните команду меню оболочки File/Add/New Project и заполните окно мастера примерно так
  • В панели Solution Explorer выделите узел проекта Test и назначьте его стартовым командой Project/Set as StartUp Project меню оболочки или аналогичной командой контекстного меню
  • Выделите форму Form1 и настройте ее свойства как показано в таблице
  • Таблица свойств формы тестового приложения
    Свойство Значение Пояснения
    Text Кнопка со звуком Заголовок тестового окна
    StartPosition CenterScreen Открывать в центре экрана
  • Поместите из панели Toolbox на верхнюю часть формы экземпляр textBox1 текстового поля TextBox
  • Поместите из панели Toolbox на середину формы разработанную нами кнопку BeepButton и настройте ее в соответствии с таблицей
  • Таблица свойств кнопки BeepButton
    Свойство Значение Пояснения
    AutoSize true Подстраиваться под содержимое
    Text Звукнуть Надпись
    Location 100; 100 Позиционировали
  • Для выделенного экземпляра кнопки BeepButton переведите панель Properties оболочки в режим Events и в поле события Click задайте имя обработчика ButtonOnClick
  • Заполните обработчик кодом вызова нашего диалогового окна MyDialogBox так
  • private void ButtonOnClick(object sender, EventArgs e)
    {
    string msg;
        
    if (textBox1.Text.Trim().Length == 0)
      msg = "Щелкнули на кнопке";
    else
      msg = textBox1.Text.Trim();
        
    // Запускаем наше диалоговое окно статическим методом
    MyDialogBox.Show(msg, this.Text);
    }
  • Добавьте в начало файла Form1.cs строку кода подключения пространства имен нашей библиотеки элементов
  • using MyCompany.UserControls;

    для того, чтобы иметь возможность написать сокращенное имя класса MyDialogBox

  • Выполните приложение и получите примерно такой результат
  • В производном от кнопки классе BeepButton мы только переопределили виртуальный метод OnClick(), в который встроили вызов звукового сигнала. В вызывающем коде тестового приложения, использующем экземпляр класса расширения кнопки, мы подписались на событие Click, чтобы щелчок на кнопке запускал самодельное диалоговое окно.

    Но мы этот метод перехватили в нашем расширении, и если не предусмотреть передачу вызова base.OnClick(e) дальше по цепочке наследования в класс Control, то событие Click инициировано не будет и наш зарегистрированный обработчик ButtonOnClick() никогда не сработает. А значит и наше диалоговое окно вызываться не будет (попробуйте закоментировать вызов base.OnClick(e) в файле BeepButton.cs ).

    Таким образом, в переопределенном методе OnClick(), который срабатывает автоматически при щелчке на кнопке, вызывается звуковой сигнал. А при вызове этого метода в базовом классе генерируется событие Click, которое заставляет выполниться все зарегистрированные для него обработчики. В нашем случае это один обработчик, косвенно запускающий самодельное диалоговое окно. Метод OnClick() для кнопок вызывается и в тех случаях, когда кнопка имеет текущий фокус ввода и нажимается клавиша Enter или Space (пробел).

    Если в переопределенном методе OnClick() класса BeepButton поменять местами строки вызова звука и события Click, то звук появится только после закрытия диалогового окна (попробуйте).

    Обратите внимание, что при формировании интерфейса диалогового окна мы не расставляли по форме кнопку и текстовую метку. Но за счет применения библиотечного компонента FlowLayoutPanel эти элементы на этапе выполнения позиционируются автоматически.

    Упражнение 3. Проектирование музыкальной кнопки SoundButton

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

    Добавление компонента кнопки SoundButton в проект библиотеки

  • Закройте все окна редактора оболочки, выполнив команду Window/Close All Documents, чтобы они не мешали при выполнении нового упражнения
  • В панели Solution Explorer к узлу UserControls добавьте новый компонент с именем SoundButton
  • Откорректируйте заготовку файла SoundButton.cs следующим образом
  • using System;
    using System.ComponentModel;
    using System.Drawing;
    using System.Windows.Forms;
        
    using System.Media; // Для проигрывателя wav-файлов SoundPlayer
                        // и системного звука SystemSounds
        
    namespace MyCompany.UserControls
    {
        public partial class SoundButton : Button
        {
            public SoundButton()
            {
                InitializeComponent();
            }
        
            public SoundButton(IContainer container)
            {
                container.Add(this);
        
                InitializeComponent();
            }
        }
    }

    Добавление свойств загрузки звука

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

  • Добавьте в конец класса SoundButton файла SoundButton.cs следующий код, определяющий свойства загрузки звука
  • // Создаем объект-проигрыватель как 
            // поле класса для управления звуком
            SoundPlayer soundPlayer = new SoundPlayer();
        
            // Свойство подключения звука к проигрывателю из файла
            public string WaveFile
            {
                get
                {
                    // Что загружено для проигрывания
                    return soundPlayer.SoundLocation;
                }
                set
                {
                    // Подключить файл с диска или URL
                    soundPlayer.SoundLocation = value;
                }
            }
        
            // Свойство подключения звука к проигрывателю из потока
            public System.IO.Stream WaveStream
            {
                get
                {
                    // Вернуть ссылку на загруженный в поток файл
                    return soundPlayer.Stream;
                }
                set
                {
                    // Подключить файл к потоку
                    soundPlayer.Stream = value;
                    // Загрузить асинхронно (в фоновом режиме)
                    soundPlayer.LoadAsync();
                }
            }

    В классе расширения кнопки мы создали объект класса SoundPlayer как поле, чтобы он был виден в членах этого класса - в свойствах и переопределенном методе. Свойства WaveFile и WaveStream настраивают объект проигрывания звука и загружают в него указанный в вызывающем коде звук для выполнения в асинхронном (фоновом) режиме в отдельном потоке. Для синхронного проигрывания звука нужно использовать метод Load(), который будет держать приложение, пока звук полностью не проиграется. Свойство WaveFile используется для проигрывания звука из отдельного файла, хранящегося на диске или в Web по URL -адресу.

    Переопределение стандартного метода диспетчеризации щелчка на кнопке

    Как и в случае с кнопкой BeepButton, перехватим событие щелчка на кнопке SoundButton в переопределенном виртуальном методе диспетчеризации библиотечного класса Control и запустим в нем проигрывание адресуемого в свойствах файла

  • Добавьте в конец класса SoundButton файла SoundButton.cs следующий код, переопределяющий виртуальный метод диспетчеризации OnClick() класса Control и запускающий проигрывание звука
  • // Переопределяем виртуальный метод класса Control,
            // чтобы начать проигрывание по щелчку на кнопке
            protected override void OnClick(EventArgs e)
            {
                // Если имя экземпляра кнопки - "Web", 
                // то вместо адресации к удаленному звуку
                // в Internet проигрываем стандартный звуковой
                // файл локального компьютера, встроенный
                // в класс SystemSounds библиотеки .NET Framework 
                if (this.Name.ToUpper() == "Web".ToUpper())
                {
                    SystemSounds.Asterisk.Play();
                    return;
                }
        
                // Загрузить асинхронно - в фоновом режиме.
                // Поток для кнопки Stream открывается 
                // один раз в конструкторе клиента
                if (this.Name.ToUpper() != "Stream".ToUpper())
                    soundPlayer.LoadAsync();
        
                // Если загрузка закончена, то начинаем проигрывание
                if (soundPlayer.IsLoadCompleted)
                    soundPlayer.Play();
                else
                    MessageBox.Show("Проигрыватель пока не готов!");
        
                // Подняться до класса Control,
                // чтобы сгенерировать событие Click.
                // Но в этом упражнении не нужно,
                // поскольку не планируем создавать обработчики
                // (хотя можно и оставить незакомментированной)
                // base.OnClick(e);
            }
  • Перекомпилируйте библиотеку пользовательских элементов вызовом команды Rebuild через контекстное меню узла UserControls панели Solution Explorer
  • Все! Музыкальная кнопка готова. Теперь осталось упаковать ее экземпляры в блок, жестко подключающий проигрываемые файлы, а также поместить его в тестирующее приложение.

    Создание компонента блока музыкальных кнопок SoundButtonBox

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

  • Выделите в панели Solution Explorer узел UserControls и добавьте к проекту новый файл с именем SoundButtonBox.cs командой контекстного меню Add/User Control
  • В режиме редактирования SoundButtonBox.cs[Design] перейдите в панель инструментов Toolbox и двойным щелчком на компоненте FlowLayoutPanel свитка Containers создайте экземпляр компоновочной панели
  • Настройте компоновочную панель согласно таблице свойств
  • Таблица свойств экземпляра компонента FlowLayoutPanel
    Свойство Значение Пояснения
    Name flow Имя экземпляра компоновочной панели
    AutoSize true Подстраиваться под содержимое
    FlowDirection TopDown Размещать сверху вниз
    WrapContents false Не переносить дочерние элементы в следующий столбец
    Dock Fill Распахнуть на всю клиентскую область родителя (формы)
    Location 0; 0 Позиционировать в клиентской области родителя
  • В режиме редактирования SoundButtonBox.cs[Design] выделите экземпляр flow, перейдите в панель инструментов Toolbox и двойным щелчком на компоненте SoundButton добавьте в коллекцию объекта flow четыре экземпляра музыкальной кнопки
  • В силу настроек компоновочной панели, которая является родителем кнопок, они расположатся в столбец друг за другом в порядке их создания.

  • В раскрывающемся списке верхней части панели Properties выберите элемент SoundButtonBox, после чего он будет выделен в визуальном редакторе оболочки
  • Настройте класс SoundButtonBox согласно таблице свойств
  • Таблица свойств класса SoundButtonBox
    Свойство Значение Пояснения
    AutoSize true Подстраиваться под содержимое
    AutoSizeMode GrowAndShrink Подстраиваться под содержимое (расширяться и сжиматься)

    После этих действий вид компонента SoundButtonBox в визуальном конструкторе должен стать таким

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

    Настройка части свойств объектов музыкальных кнопок через панель свойств оболочки

  • Откройте на редактирование файл SoundButtonBox.cs в режиме [Design] и, выделяя последовательно экземпляры кнопки, настройте их свойства в соответствии с таблицами свойств, приведенными ниже
  • Таблица свойств первой музыкальной кнопки
    Свойство Значение Пояснения
    Name soundButton1 Имя экземпляра кнопки (сгенерировано автоматически оболочкой)
    AutoSize true Подстраиваться под надпись
    Anchor None Выравнивать по центру родителя
    Text Файл tada.wav в системном каталоге Надпись на кнопке
    Margin.All 8 Наружный отступ по контуру кнопки
    WaveFile Пусто Будет определен в коде конструктора класса для загрузки звука, расположенного в системном каталоге

    Таблица свойств второй музыкальной кнопки
    Свойство Значение Пояснения
    Name soundButton2 Имя экземпляра кнопки (сгенерировано автоматически оболочкой)
    AutoSize true Подстраиваться под надпись
    Anchor None Выравнивать по центру родителя
    Text Файл SOUND827.WAV в каталоге исполняемой сборки Надпись на кнопке
    Margin.All 8 Наружный отступ по контуру кнопки
    WaveFile Пусто Будет определен в коде конструктора класса для загрузки звука, расположенного в файле

    Таблица свойств третьей музыкальной кнопки
    Свойство Значение Пояснения
    Name Stream Имя экземпляра кнопки. Помечаем, чтобы распознать в переопределенном методе деспетчеризации OnClick() и не открывать уже открытый в конструкторе поток
    AutoSize true Подстраиваться под надпись
    Anchor None Выравнивать по центру родителя
    Text Файл Trumpet1.wav в ресурсе исполняемой сборки Надпись на кнопке
    Margin.All 8 Наружный отступ по контуру кнопки
    WaveStream Пусто Будет определен в коде конструктора класса для загрузки звука, расположенного в файле ресурсов

    Таблица свойств четвертой музыкальной кнопки
    Свойство Значение Пояснения
    Name Web Имя экземпляра кнопки. Помечаем, чтобы распознать в переопределенном методе деспетчеризации OnClick() и не обращаться в Web (считаем, что Internet пока что не подключен)
    AutoSize true Подстраиваться под надпись
    Anchor None Выравнивать по центру родителя
    Text Файл azlinsnd.wav, загружаемый из Web по URL (отключено) Надпись на кнопке
    Margin.All 8 Наружный отступ по контуру кнопки
    WaveFile http://www.oaklandzoo.org/atoz/azlinsnd.wav Подключаем для загрузки из Web, но в переопределенном методе деспетчеризации OnClick() распознаем по имени нажатой кнопки и заменяем другим звуком, встроенным в класс SystemSounds библиотеки .NET Framework

    Интерфейсная часть блока кнопок после их настройки должна выглядеть так

    Настройка части свойств объектов музыкальных кнопок в конструкторе класса SoundButtonBox

  • Добавьте в конец конструктора класса SoundButtonBox одноименного файла C# код, чтобы класс стал таким
  • using System;
    using System.Collections.Generic;
    using System.ComponentModel;
    using System.Drawing;
    using System.Data;
    using System.Text;
    using System.Windows.Forms;
        
    namespace MyCompany.UserControls
    {
        public partial class SoundButtonBox : UserControl
        {
        public SoundButtonBox()
        {
        InitializeComponent();
        
        // Подключить первую кнопку к звуковому файлу
        soundButton1.WaveFile = System.IO.Path.Combine(     // Склеить два пути
        Environment.GetEnvironmentVariable("windir"),   // Каталог системы
          @"Media\tada.wav");
        
        // Подключить вторую кнопку к звуковому файлу
        soundButton2.WaveFile = "SOUND827.WAV";
        
        // Подключить третью кнопку к звуковому файлу из файла ресурсов сборки
        Stream.WaveStream = this.GetType().Assembly.GetManifestResourceStream(
          "MyCompany.UserControls.Trumpet1.wav");
        }
        }
    }

    Ниже приведен код, формируемый оболочкой при настройке компонента SoundButtonBox в визуальном режиме

    namespace MyCompany.UserControls
    {
    partial class SoundButtonBox
      {
      /// <summary> 
      /// Required designer variable.
      /// </summary>
      private System.ComponentModel.IContainer components = null;
        
      /// <summary> 
      /// Clean up any resources being used.
      /// </summary>
      /// <param name="disposing">true 
        if managed resources should be disposed; 
        otherwise, false.</param>
      protected override void Dispose(bool disposing)
      {
      if (disposing  (components != null))
        {
        components.Dispose();
        }
      base.Dispose(disposing);
      }
        
      #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()
        {
        this.components = new System.ComponentModel.Container();
        this.flow = new System.Windows.Forms.FlowLayoutPanel();
        this.soundButton1 = new MyCompany.UserControls.SoundButton
        (this.components);
        this.soundButton2 = new MyCompany.UserControls.SoundButton
        (this.components);
        this.Stream = new MyCompany.UserControls.SoundButton
        (this.components);
        this.Web = new MyCompany.UserControls.SoundButton
        (this.components);
        this.flow.SuspendLayout();
        this.SuspendLayout();
        // 
        // flow
        // 
        this.flow.AutoSize = true;
        this.flow.Controls.Add(this.soundButton1);
        this.flow.Controls.Add(this.soundButton2);
        this.flow.Controls.Add(this.Stream);
        this.flow.Controls.Add(this.Web);
        this.flow.Dock = System.Windows.Forms.DockStyle.Fill;
        this.flow.FlowDirection = 
        System.Windows.Forms.FlowDirection.TopDown;
        this.flow.Location = new System.Drawing.Point(0, 0);
        this.flow.Name = "flow";
        this.flow.Size = new System.Drawing.Size(440, 172);
        this.flow.TabIndex = 0;
        this.flow.WrapContents = false;
        // 
        // soundButton1
        // 
        this.soundButton1.Anchor = System.Windows.Forms.AnchorStyles.None;
        this.soundButton1.AutoSize = true;
        this.soundButton1.Location = new System.Drawing.Point(88, 8);
        this.soundButton1.Margin = new System.Windows.Forms.Padding(8);
        this.soundButton1.Name = "soundButton1";
        this.soundButton1.Size = new System.Drawing.Size(264, 27);
        this.soundButton1.TabIndex = 0;
        this.soundButton1.Text = "Файл tada.wav в системном каталоге";
        this.soundButton1.UseVisualStyleBackColor = true;
        this.soundButton1.WaveFile = "";
        this.soundButton1.WaveStream = null;
        // 
        // soundButton2
        // 
        this.soundButton2.Anchor = System.Windows.Forms.AnchorStyles.None;
        this.soundButton2.AutoSize = true;
        this.soundButton2.Location = new System.Drawing.Point(28, 51);
        this.soundButton2.Margin = new System.Windows.Forms.Padding(8);
        this.soundButton2.Name = "soundButton2";
        this.soundButton2.Size = new System.Drawing.Size(384, 27);
        this.soundButton2.TabIndex = 1;
        this.soundButton2.Text = "Файл SOUND827.WAV 
        в каталоге исполняемой сборки";
        this.soundButton2.UseVisualStyleBackColor = true;
        this.soundButton2.WaveStream = null;
        // 
        // Stream
        // 
        this.Stream.Anchor = System.Windows.Forms.AnchorStyles.None;
        this.Stream.AutoSize = true;
        this.Stream.Location = new System.Drawing.Point(41, 94);
        this.Stream.Margin = new System.Windows.Forms.Padding(8);
        this.Stream.Name = "Stream";
        this.Stream.Size = new System.Drawing.Size(358, 27);
        this.Stream.TabIndex = 2;
        this.Stream.Text = "Файл Trumpet1.wav 
        в ресурсе исполняемой сборки";
        this.Stream.UseVisualStyleBackColor = true;
        this.Stream.WaveFile = "";
        this.Stream.WaveStream = null;
        // 
        // Web
        // 
        this.Web.Anchor = System.Windows.Forms.AnchorStyles.None;
        this.Web.AutoSize = true;
        this.Web.Location = new System.Drawing.Point(8, 137);
        this.Web.Margin = new System.Windows.Forms.Padding(8);
        this.Web.Name = "Web";
        this.Web.Size = new System.Drawing.Size(424, 27);
        this.Web.TabIndex = 3;
        this.Web.Text = "Файл azlinsnd.wav, 
        загружаемый из Web по URL (отключено)";
        this.Web.UseVisualStyleBackColor = true;
        this.Web.WaveFile = 
      "http://www.oaklandzoo.org/atoz/azlinsnd.wav";
        this.Web.WaveStream = null;
        // 
        // SoundButtonBox
        // 
        this.AutoScaleDimensions = new System.Drawing.SizeF(8F, 16F);
        this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
        this.AutoSize = true;
        this.AutoSizeMode = 
        System.Windows.Forms.AutoSizeMode.GrowAndShrink;
        this.Controls.Add(this.flow);
        this.Name = "SoundButtonBox";
        this.Size = new System.Drawing.Size(440, 172);
        this.flow.ResumeLayout(false);
        this.flow.PerformLayout();
        this.ResumeLayout(false);
        this.PerformLayout();
        
        }
        
        #endregion
        
        private System.Windows.Forms.FlowLayoutPanel flow;
        private SoundButton soundButton1;
        private SoundButton soundButton2;
        private SoundButton Stream;
        private SoundButton Web;
        }
    }

    Подготовка файлов звука к проигрыванию компонентом SoundButtonBox

    Присоединим адресуемые звуки к проекту нашей библиотеки компонентов UserControls.

    Первый звук: Первый звук tada.wav хранится в системном каталоге Windows и путь к нему мы формируем с использованием библиотечного класса Environment.

    Второй звук: Второй звук SOUND827.WAV (или любой другой) код компонента ищет в каталоге размещения самой сборки. Для того, чтобы он там оказался, выполните следующее:

  • В панели Solution Explorer выделите узел проекта UserControls и командой оболочки Project/Add Existing Item загрузите нужный файл в каталог проекта. Файл будет скопирован в каталог размещения проекта (звук SOUND827.WAV находится в прилагаемом к данной работе каталоге Source )
  • Выполненным действием мы скопировали файл звука пока-что в каталог размещения проекта. А он должен находиться в каталоге размещения самой сборки кода компонента. Нам нужно настроить оболочку для работы с этим файлом так, чтобы при компиляции кода компонента она автоматически копировала звук в каталог размещения сборки самого компонента.

  • В панели Solution Explorer вызовите контекстное меню для скопированного файла звука SOUND827.WAV и выполнить команду Properties. Установите опцию Copy to Output Directory в значение Copy if newer (копировать при обновлении файла звука) или Copy always (копировать всегда). Теперь оболочка сама при компиляции проекта разместит файл в одном со сборкой компонента каталоге
  • Третий звук: Свойство WaveStream используется для проигрывания звука, присоединенного к исполняемому файлу как внедренный ресурс. Файл Trumpet1.wav внедряемого как ресурс звука нужно добавить к проекту, а затем настроить его свойства так, чтобы компилятор поместил его в сборку компонента.

  • В панели Solution Explorer выделите узел проекта UserControls и командой Add/Add Existing Item контекстного меню загрузите файл Trumpet1.wav в каталог проекта из прилагаемого к работе каталога Source
  • Через команду Properties контекстного меню, вызванного в панели Solution Explorer для файла Trumpet1.wav, установите в таблице свойств Properties поле Build Action в значение Embedded Resource (назначить внедренным ресурсом)
  • Теперь звук Trumpet1.wav на этапе компиляции будет внедрен в исполнимую сборку и готовое приложение может далее выполняться отдельно от первоисточника звука. Так удобно поступать с файлами небольших размеров. Для загрузки звука из ресурса нужно указывать полный путь к ресурсу, включая пространство имен проекта.

    Четвертый звук: Специальных подготовительных действий не требует, поскольку извлекается из библиотеки .NET Framework.

  • Откомпилируйте проект с компонентами, выполнив команду Rebuild контекстного меню узла UserControls в панели Solution Explorer
  • Запустите проводник Windows Explorer и убедитесь, что размер сборки UserControls существенно увеличился за счет внедренного звука Trumpet1.wav.
  • Тестирование компонента SoundButtonBox

  • Добавьте к решению командой File/Add/New Project новый проект типа оконного приложения ( Windows Application ) с именем SoundButtonsTest и назначьте его стартовым командой Project/Set as StartUp Project меню оболочки (или аналогичной командой контекстного меню)
  • Поместите на форму из панели Toolbox экземпляр компонента SoundButtonBox и выровняйте его по размеру формы
  • Подключите динамики, запустите приложение и испытайте его работу
  • Отметьте, что разные исполнимые сборки используют одну и ту же библиотеку компонентов UserControls.dll при условии, что эта библиотека находится в том же каталоге, что и исполнимая сборка. При добавлении к нашему решению MySolution, содержащему проект библиотеки компонентов UserControls, нового проекта SoundButtonsTest исполнимого приложения оболочка автоматически копирует эту библиотеку в каталог, где будет находиться исполнимая сборка. При любом изменении библиотеки компонентов при ее компиляции оболочка обновляет файл библиотеки во всех каталогах, где находятся файлы исполнимых сборок.

    Упражнение 4. Рисование круглой кнопки

    Вид стандартного элемента управления можно изменить, переопределив его виртуальные методы OnPaint() и OnResize(). Предпочтимые размеры элемента управления устанавливаются в переопределении наследуемого метода GetPreferredSize(). Для прорисовки элементов управления можно использовать класс System.Windows.Forms.ControlPaint. Системная палитра, перья и кисти находятся в трех классах пространства имен SystemDrawing:

  • SystemColors
  • SystemPens
  • SystemBrushes
  • Пространство имен System.Windows.Forms также содержит цвета в классах

  • ProfessionalColors - цвета в статических свойствах класса
  • ProfessionalColorTable - цвета в виртуальных экземплярных свойствах класса
  • В этом упражнении мы создадим кнопку непрямоугольной формы, которая будет отрисовываться по пользовательскому алгоритму. На этом примере будет показано, как создавать пользовательские элементы управления непрямоугольной формы.

    Создание заготовки компонента CircleButton

  • В панели Solution Explorer выделите узел UserControls и добавьте к проекту библиотеки новый компонент командой Project/Add Component с именем CircleButton
  • В заголовке класса CircleButton файла CircleButton.cs замените базовый класс Component на Button
  • Добавьте в начало файла CircleButton.cs инструкции подключения пространств имен
    using System.Drawing;
    using System.Windows.Forms;
  • Добавьте в класс CircleButton закрытый метод Init() и поместите его вызовы в перегруженные конструкторы компонента
  • using System;
    using System.ComponentModel;
    using System.Collections.Generic;
    using System.Diagnostics;
        
    using System.Drawing;
    using System.Windows.Forms;
        
    namespace MyCompany.UserControls
    {
        public partial class CircleButton : Button
        {
            public CircleButton()
            {
                InitializeComponent();
        
                Init();
            }
        
            public CircleButton(IContainer container)
            {
                container.Add(this);
        
                InitializeComponent();
        
                Init();
            }
        
            // Начальные настройки для выполнения в конструкторе
            private void Init()
            {
                // Извещаем операционную систему, что будем сами
                // заботиться об отрисовке компонента
                this.SetStyle(ControlStyles.UserPaint, true);
        
                // Игнорировать системное сообщение WM_ERASEBKGND 
                // очистки окна, чтобы уменьшить нежелательное мерцание 
                this.SetStyle(ControlStyles.AllPaintingInWmPaint, true);
        
                // Наружный отступ по контуру кнопки
                this.Margin = new Padding(this.Font.Height);
        
                // Подстраиваться под размер надписи
                this.AutoSize = true;
            }
        }
    }

    Установка флага ControlStyles.UserPaint извещает операционную систему, что отрисовка окна компонента будет выполняться в переопределении его метода OnPaint().

    Вычисление размеров кнопки

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

  • Добавьте в класс CircleButton код переопределения метода GetPreferredSize()
  • // Вычисляем размер описывающего квадрата кнопки
       // в зависимости от размера надписи на ней
       public override Size GetPreferredSize(Size proposedSize)
       {
       // Получаем ссылку на контекст графического устройства
       Graphics gr = CreateGraphics();
                    
       // Измеряем размер описывающего текст прямоугольника
       SizeF box = gr.MeasureString(this.Text, this.Font);
                    
       // Вычисляем диагональ текстового блока надписи
       int diagonal = (int)Math.Sqrt(Math.Pow(box.Width, 2) 
                    + Math.Pow(box.Height, 2));
                    
       // Возвращаем объект с размерами описывающего квадрата кнопки
       return new Size(diagonal, diagonal);
    }

    Метод GetPreferredSize() вызывается диспетчером размещения для определения предпочтительных размеров компонента, когда значение его свойства AutoSize=true (мы его установили в методе Init() ).

    Задание круглой формы для кнопки

    using System;
    using System.Drawing;
    using System.Windows.Forms;
        
    using System.Drawing.Drawing2D;
        
    namespace Test
    {
        // Класс круглой кнопки
        class CircleButton : Button
        {
        public CircleButton()
        {
        // Сами отрисуем вместо операционной системы
        this.SetStyle(ControlStyles.UserPaint, true);
        // Отключить от системы автоматическую отрисовку
        this.SetStyle(ControlStyles.AllPaintingInWmPaint, true);
        // Наружный отступ по контуру кнопки
        this.Margin = new Padding(this.Font.Height);
        }
        
        // Возвращается предпочтимый размер кнопки в зависимости от текста
        public override Size GetPreferredSize(Size proposedSize)
        {
        // Создаем временный контекст устройства
        Graphics gr = CreateGraphics();
        // Вычисляем размер текстового блока надписи
        SizeF sizeF = gr.MeasureString(this.Text, this.Font);
        // Освобождаем контекст
        gr.Dispose();
        // Теорема Пифагора
        int radius = (int)Math.Sqrt(
          Math.Pow(sizeF.Width / 2, 2)
          + Math.Pow(sizeF.Height / 2, 2));
        
        // Возвращаем диаметр
        return new Size(2 * radius, 2 * radius);
        }
        
        // Вычисление контура кнопки при изменении ее размеров
        protected override void OnResize(EventArgs args)
        {
        // Рисуем кнопку круглой, используя класс Region
        GraphicsPath path = new GraphicsPath();// Объект для региона
        path.AddEllipse(this.ClientRectangle);// Преобразовать в элипс
        this.Region = new Region(path);
        
        // Вызываем предка для инициации OnPaint()
        base.OnResize(args);
        }
        
        // Отрисовка круглой кнопки
        protected override void OnPaint(PaintEventArgs args)
        {
        Graphics gr = args.Graphics;// Адресовались к контексту
        // Качество рисования - сглаживать контуры
        gr.SmoothingMode = SmoothingMode.AntiAlias;
        Rectangle rect = this.ClientRectangle;
        
        // Флаг попадания курсора внутрь кнопки при нажатии
        bool bPressed = this.Capture 
          ((Control.MouseButtons  MouseButtons.Left) != 0) 
          this.ClientRectangle.
          Contains(this.PointToClient(Control.MousePosition));
        
        // Отрисовка внутренней части кнопки
        // При нажатии она становится темнее
        GraphicsPath path = new GraphicsPath();
        path.AddEllipse(rect);
        PathGradientBrush gradient = new PathGradientBrush(path);
        int k = bPressed ? 2 : 1;// Для нажатой увеличим вес градиента
        gradient.CenterPoint = new PointF(
          k * (rect.Left + rect.Right) / 3,
          k * (rect.Top + rect.Bottom) / 3);
        gradient.CenterColor = bPressed ? Color.Blue : Color.White;
        gradient.SurroundColors = new Color[] { Color.SkyBlue };
        gr.FillRectangle(gradient, rect);
        
        // Отрисовка контура кнопки, утолщенного для активных кнопок
        Brush brush = new LinearGradientBrush(
          rect,
          Color.FromArgb(0, 0, 255),
          Color.FromArgb(0, 0, 128),
          LinearGradientMode.ForwardDiagonal);
        Pen pen = new Pen(
          brush,
          (this.IsDefault ? 6 : 2) * gr.DpiX / 72);
          gr.DrawEllipse(pen, rect);
        
        // Отображение надписи в центре прямоугольника кнопки
        // Для неактивной кнопки цвет надписи должен быть бледно-серым
        StringFormat strFormat = new StringFormat();
        // Определение точки привязки в центре текстового блока
        strFormat.Alignment = 
          strFormat.LineAlignment = 
          StringAlignment.Center;
        brush = this.Enabled ? SystemBrushes.WindowText // Для активной
          : SystemBrushes.GrayText;// Для неактивной
        gr.DrawString(this.Text, this.Font, brush, rect, strFormat);
        
        // Отображение пунктира вокруг текста, если кнопка имеет фокус
        if (this.Focused)
          {
          SizeF sizeF = gr.MeasureString(
            this.Text, 
            this.Font,
            PointF.Empty, 
            StringFormat.GenericTypographic);
          pen = new Pen(this.ForeColor);// Текущий цвет
          pen.DashStyle = DashStyle.Dash;// Пунктир
          gr.DrawRectangle(
            pen,
            rect.Left + rect.Width / 2 - sizeF.Width / 2,
            rect.Top + rect.Height / 2 - sizeF.Height / 2,
            sizeF.Width,
            sizeF.Height);
            }
        
          if (this.Paint != null)// Если есть хоть одна подписка
            this.Paint(this, args);// Инициируем событие
          }
        
        new public event PaintEventHandler Paint;// Объявляем новое событие
        }
           
        // Форма приложения
        class MyClass : Form
        {
        public MyClass()
          {
          this.Text = "Круглые кнопки";
          this.Font = new Font("Times New Roman", 18);
          this.MaximizeBox = false;
          // Подстраиваться под кнопки
          this.AutoSize = true;
          this.AutoSizeMode = AutoSizeMode.GrowAndShrink;
        
          // Панель для размещения элементов интерфейса в столбец
          FlowLayoutPanel flow = new FlowLayoutPanel();
          flow.Parent = this;
          flow.AutoSize = true;
          flow.FlowDirection = FlowDirection.TopDown;// В столбец
        
          // Встроенная компоновочная панель для верха
          FlowLayoutPanel flowTop = new FlowLayoutPanel();
          flowTop.Parent = flow;
          flowTop.FlowDirection = FlowDirection.LeftToRight;// По умолчанию
          flowTop.AutoSize = true; 
          // Выравнивание в столбец по центру родительской панели
          flowTop.Anchor = AnchorStyles.None; 
        
          // Помещаем в верхнюю компоновочную панель метку
          Label lbl = new Label();
          lbl.Parent = flowTop;
          lbl.AutoSize = true;
          lbl.Text = "Введите текст: ";
          lbl.Anchor = AnchorStyles.None;// Выравнивание по центру строки
        
          // Помещаем в верхнюю компоновочную панель поле ввода
          TextBox txtBox = new TextBox();
          txtBox.Parent = flowTop;
          txtBox.Width = 8 * this.Font.Height;// Длина поля ввода
          txtBox.TabStop = false;// Отключить клавишу Tab
        
          // Встроенная компоновочная панель для низа
          FlowLayoutPanel flowBottom = new FlowLayoutPanel();
          flowBottom.Parent = flow;
          flowBottom.FlowDirection = 
        FlowDirection.LeftToRight;// По умолчанию
          flowBottom.AutoSize = true;
          // Выравнивание в столбец по центру родительской панели
          flowBottom.Anchor = AnchorStyles.None;
        
          // Помещаем в нижнюю компоновочную панель кнопки
          CircleButton btn = new CircleButton();
          btn.Parent = flowBottom;
          btn.AutoSize = true;// Подстраиваемая
          btn.Text = "Кнопка 1";
          btn.Anchor = AnchorStyles.None;// Выравнивать по горизонтали
          btn.Click += btn_Click;
        
          btn = new CircleButton();
          btn.Parent = flowBottom;
          btn.AutoSize = true;// Подстраиваемая
          btn.Text = "Кнопка 2";
          btn.Anchor = AnchorStyles.None;// Выравнивать по горизонтали
          btn.Click += btn_Click;
        
          btn = new CircleButton();
          btn.Parent = flowBottom;
          btn.AutoSize = true;// Подстраиваемая
          btn.Text = "Кнопка 3";
          btn.Anchor = AnchorStyles.None;// Выравнивать по горизонтали
          btn.Click += btn_Click;
          }
        
            void btn_Click(object sender, EventArgs e)
            {
                //this.Close();// Завершить приложение
            }
        }
        
        // Запуск
        class Program
        {
            static void Main()
            {
                Application.EnableVisualStyles();
                // Создали форму и запустили цикл сообщений Windows
                Application.Run(new MyClass());
            }
        }
    }

    Результат выполнения будет таким

    Поскольку в классе расширения кнопки мы переопределили виртуальный метод OnPaint() и не предусмотрели его вызов для базового класса, то событие Paint никогда инициировано не будет. Потому что оно вызывается в методе OnPaint() именно базового класса, который мы перехватили. Чтобы не потерять событие Paint, мы его заново объявили в классе расширения, а в переопределенный метод OnPaint() вставили инициализацию этого события с предварительной проверкой, подписано ли оно в вызывающем коде.

    Комбинирование готовых элементов управления

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

    Теперь мы разработаем свой элемент управления на основе нескольких библиотечных элементов. Применим прием многоэтапного последовательного расширения классов на основе наследования.

    using System;
    using System.Drawing;
    using System.Windows.Forms;
        
    namespace Test
    {
        //******************************************************
        
        // Кнопка, генерирующая множество событий Click
        // При нажатии и удержании на ней курсора мыши
        class ClickmaticButton : Button
        {
        // Определяем поля класса
        // Создаем таймер
        Timer timer = new Timer();
        // Задержка срабатывания кнопки Button после нажатия мыши
        int iDelay = 250 * (1 + SystemInformation.KeyboardDelay);
        // Скорость для срабатывания таймера, генерирующего события повторных
        // нажатий кнопки компонента при удержании нажатой левой кнопки мыши
        int iSpeed = 405 - 12 * SystemInformation.KeyboardSpeed;
        
        // Конструктор
        public ClickmaticButton()
        {
        timer.Tick += TimerOnTick;// Подписались на тики
        }
        
        // Перехватываем нажатие левой кнопки мыши и запускаем таймер
        // для генерации событий Click кнопки Button
        protected override void OnMouseDown(MouseEventArgs args)
        {
        base.OnMouseDown(args);
        
        // Если нажата левая кнопка мыши
        if ((args.Button  MouseButtons.Left) != 0)
        {
        timer.Interval = iDelay;// Задержка до первого тика
        timer.Start();// Включили таймер
        }
        }
        
        // Отработка тиков
        void TimerOnTick(object sender, EventArgs e)
        {
        // Программно генерируем повторное 
        // нажатие Button при каждом тике
        this.OnClick(EventArgs.Empty);
        // Меняем скорость таймера
        timer.Interval = iSpeed;
        }
        
        // Контролируем, находится ли нажатый курсор мыши
        // над кнопкой и управляем приостановкой таймера
        protected override void OnMouseMove(MouseEventArgs args)
        {
        base.OnMouseMove(args);
                
        // Приостанавливаем таймер
        timer.Enabled = this.Capture 
          this.ClientRectangle.Contains(args.Location);
        }
        
        // При отпускании кнопки таймер останавливаем
        protected override void OnMouseUp(MouseEventArgs args)
        {
        base.OnMouseUp(args);
        
        timer.Stop();
        }
        }
        
        //******************************************************
        
        // Рисование на генерирующей событие Click кнопке 
        // изображения стрелки
        class ArrowButton : ClickmaticButton
        {
            // Поле для хранения ориентации кнопки
            // По умолчанию острый угол треугольника направлен вправо
            ScrollButton scrollButton = ScrollButton.Right;
        
            // Конструктор
            public ArrowButton()
            {
                // Задание стиля кнопки как не получающей фокус ввода
                this.SetStyle(ControlStyles.Selectable, false);
            }
        
            // Свойство для поддержки переменной перечисления
            public ScrollButton ScrollButton
            {
                get { return scrollButton; }
                set
                {
                    scrollButton = value;
                    this.Invalidate();// Перерисовать
                }
            }
        
            // Переопределяем метод отрисовки кнопки
            protected override void OnPaint(PaintEventArgs args)
            {
                Point point = PointToClient(Control.MousePosition);
                bool mouseInButton = this.Capture 
                    this.ClientRectangle.Contains(point);
                ButtonState state = mouseInButton ?
                    ButtonState.Pushed : ButtonState.Normal;
        
                state = !this.Enabled ?
                    ButtonState.Inactive : state;
        
                Graphics gr = args.Graphics;
                // Рисуем кнопку с треугольником внутри
                ControlPaint.DrawScrollButton(
                    gr,
                    this.ClientRectangle,
                    scrollButton,
                    state);
            }
        
            protected override void OnMouseCaptureChanged(EventArgs args)
            {
                base.OnMouseCaptureChanged(args);
        
                this.Invalidate();// Перерисовать
            }
        }
        
        //******************************************************
        
        // Самодельный элемент управления - поле с кнопками
        public class NumericScan : UserControl
        {
            // Объявили событие
            public event EventHandler ValueChanged;
        
            // Объявили ссылки-поля для видимости объектов
            TextBox txtBox;
            ArrowButton btn1, btn2;
        
            // Объявили базовые закрытые поля для открытых свойств
            int iDecimalPlaces = 0;
            decimal mValue = 0;
            decimal mIncrement = 1;
            decimal mMinimum = 0;
            decimal mMaximum = 100;
        
            // Доступные свойства-обертки полей
            public int DecimalPlaces
            {
                get { return iDecimalPlaces; }
                set { iDecimalPlaces = value; }
            }
            public decimal Value
            {
                get { return mValue; }
                set { txtBox.Text = ValueToText(mValue = value); }
            }
            public decimal Increment
            {
                get { return mIncrement; }
                set { mIncrement = value; }
            }
            public decimal Minimum
            {
                get { return mMinimum; }
                set
                {
                    // Заодно отслеживаем диапазон Value
                    if (Value < (mMinimum = value))
                        Value = mMinimum;
                }
            }
            public decimal Maximum
            {
                get { return mMaximum; }
                set
                {
                    // Заодно отслеживаем диапазон Value
                    if (Value > (mMaximum = value))
                        Value = mMaximum;
                }
            }
        
            // Конструктор
            public NumericScan()
            {
            txtBox = new TextBox();
            txtBox.Parent = this;
            // Текст у правой границы поля
            txtBox.TextAlign = HorizontalAlignment.Right;
            txtBox.Text = ValueToText(mValue);// Инициализируем
            txtBox.TextChanged += TextBoxOnTextChanged;
            // При вводе Enter обновить Value элемента
            txtBox.KeyDown += TextBoxOnKeyDown;
            // Не принимать лишние символы
            txtBox.KeyPress += new KeyPressEventHandler(txtBox_KeyPress);
        
            btn1 = new ArrowButton();
            btn1.Parent = this;
            // Ориентация треугольничка влево
            btn1.ScrollButton = ScrollButton.Left;
            btn1.Click += ButtonOnClick;
        
            btn2 = new ArrowButton();
            btn2.Parent = this;
            // Ориентация треугольничка вправо
            btn2.ScrollButton = ScrollButton.Right;// По умолчанию
            btn2.Click += ButtonOnClick;
            }
        
            string ValueToText(decimal mValue)
            {
            // Возвращаем с форматированием
            return mValue.ToString("F" + iDecimalPlaces);
            }
        
            // Обработчик события изменения содержимого текстового поля
            void TextBoxOnTextChanged(object sender, EventArgs args)
            {
            try
            {
            // Преобразуем текст в число
            mValue = Decimal.Parse(txtBox.Text);
            }
            catch { }// На исключения не реагируем
            }
        
            // Обрабатывает каждую клавишу, но ждет клавишу Enter
        
           void TextBoxOnKeyDown(object sender, KeyEventArgs args)
            {
            switch (args.KeyCode)
            {
            // При нажатии Enter генерируем наше событие ValueChanged 
            case Keys.Enter:
              OnValueChanged(EventArgs.Empty);
            break;
            }
        
          // Обнаруживаем цифру
          isNumeric =
            // Основная клавиатура
            args.KeyCode >= Keys.D0  args.KeyCode <= Keys.D9
            // Дополнительная клавиатура
            || args.KeyCode >= Keys.NumPad0  
      args.KeyCode <= Keys.NumPad9
            // Клавиша BackSpace
            || args.KeyCode == Keys.Back;
          }
        
          // Отфильтровываем лишние символы из поля ввода
          bool isNumeric;// Видимое поле
          void txtBox_KeyPress(object sender, KeyPressEventArgs args)
          {
          if (isNumeric != true)// Пропускаем только цифры
            args.Handled = true;// Не принимаем в TextBox
          }
        
          // Срабатывает автоматически и устанавливает
          // размеры компонента this.Width, this.Height
          public override Size GetPreferredSize(Size proposedSize)
          {
          const float weight = 1.2f;// Вес ширины
          // Высота компонента
          int height = txtBox.PreferredHeight +
            SystemInformation.HorizontalScrollBarHeight;
          // Устанавливаем размеры компонента
          return new Size((int)(weight * height), height);
          }
        
          // Срабатывает автоматически, позиционирует
          // кнопки и устанавливает их размеры
          protected override void OnResize(EventArgs args)
          {
          base.OnResize(args);
        
          txtBox.Height = txtBox.PreferredHeight;
          txtBox.Width = this.Width;// Текстовое поле по ширине компонента
          btn1.Location = new Point(0, txtBox.Height);// Слева снизу
          btn2.Location = new Point
        (this.Width / 2, txtBox.Height);// Середина низ
          // Приравниваем значения полей структур
          btn1.Size = btn2.Size = new Size(txtBox.Width / 2, 
          this.Height - txtBox.Height);
          }
            
          // Когда элемент управления уходит из фокуса ввода, надо
          // проверить введенное и сгенерировать событие ValueChanged
          protected override void OnLeave(EventArgs args)
          {
          base.OnLeave(args);
          this.OnValueChanged(EventArgs.Empty);// Аргумент не используется
          }
        
          // Изменения величины пользователем по щелчкам мыши
          void ButtonOnClick(object sender, EventArgs e)
          {
          // Если не было, то дать фокус тексовому полю
          if (!txtBox.Focused)
            txtBox.Focus();
         
          // Повышаем полномочия ссылки
          ArrowButton btn = sender as ArrowButton;
          // Пробная величина для изменения значения на шаге
          decimal mNewValue = Value;
        
          // Выявляем кнопку и проверяем выход за границу
          if (btn == btn1)
            if ((mNewValue -= Increment) < Minimum)
              return;
        
          if (btn == btn2)
            if ((mNewValue += Increment) > Maximum)
              return;
        
          // Принимаем изменение и вызываем обработчик,
          // который сгенерирует событие ValueChanged
          Value = mNewValue;
          OnValueChanged(EventArgs.Empty);
          }
        
          // Ввели свою дополнительную функцию
          // Изменение величины прямым вводом в текстовое поле
          protected virtual void OnValueChanged(EventArgs args)
          {
          // Последовательно проверяем выход величины за левую и правую
          // границы, и если выходит за границу, то обрезаем по границе
          Value = Math.Min(Maximum, Value);
          Value = Math.Max(Minimum, Value);
          //Value = Decimal.Round(Value, DecimalPlaces);
        
          // Генерируем событие
          if (ValueChanged != null)
            ValueChanged(this, args);
          }
        }
        
        //******************************************************
        
        // Тест для проверки нашего элемента управления
        class MyClass : Form
        {
        // Внутренние поля для видимости в методах
        Label lbl1, lbl2;// Заполняются обработчиком события ValueChanged
        NumericScan numscan1, numscan2;// Ссылки на компоненты
        
        public MyClass()
        {
        this.Text = "Тест пользовательского компонента";
        // Размеры формы
        this.Width = (int)(1.3f * this.Width);
        this.Height = (int)(.8f * this.Height);
        
        FlowLayoutPanel flow = new FlowLayoutPanel();
        flow.Parent = this;// Привязали к форме
        // Размещение по столбцам
        flow.FlowDirection = FlowDirection.TopDown;
        // Внутреннее обрамление в пикселах
        flow.Padding = new Padding(20);
        // Растянули по форме
        flow.Dock = DockStyle.Fill;
        
        numscan1 = new NumericScan();
        numscan1.Parent = flow;
        numscan1.AutoSize = true;
        // Элементы между собой выравнивать по центру
        numscan1.Anchor = AnchorStyles.None;
        numscan1.ValueChanged += NumericScanOnValueChanged;
        
        lbl1 = new Label();
        lbl1.Parent = flow;
        lbl1.AutoSize = true;
        // Элементы между собой выравнивать по центру
        lbl1.Anchor = AnchorStyles.None;
        
        // Пустая текстовая метка для отступа элементов
        (new Label()).Parent = flow;
        
        numscan2 = new NumericScan();
        numscan2.Parent = flow;
        numscan2.AutoSize = true;
        // Элементы между собой выравнивать по центру
        numscan2.Anchor = AnchorStyles.None;
        numscan2.ValueChanged += NumericScanOnValueChanged;
        
        lbl2 = new Label();
        lbl2.Parent = flow;
        lbl2.AutoSize = true;
        // Элементы между собой выравнивать по центру
        lbl2.Anchor = AnchorStyles.None;
        
        // Сами принудительно выполняем обработчик
        // для начальной инициализации текстовых меток
        NumericScanOnValueChanged(numscan1, EventArgs.Empty);
        NumericScanOnValueChanged(numscan2, EventArgs.Empty);
        }
        
        void NumericScanOnValueChanged(object sender, EventArgs args)
            {
                NumericScan ob = sender as NumericScan;// Приводим ссылку
                if (ob == numscan1)
                    lbl1.Text = "Первый: " + numscan1.Value;
                else if (ob == numscan2)
                    lbl2.Text = "Второй: " + numscan2.Value;
            }
        }
        
        //******************************************************
        
        // Запуск
        class Program
        {
            static void Main()
            {
                Application.EnableVisualStyles();
                // Создали форму и запустили цикл сообщений Windows
                Application.Run(new MyClass());
            }
        }
    }
    Вернуться к учебному плану