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

Разработка комбинированного компонента

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

Файлы к лабораторной работе Вы можете скачать здесь.

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

В данной работе мы продолжим выполнять упражнения по созданию собственных пользовательских компонентов, обладающих необходимыми нам свойствами. Мы создадим решение ( MySolution ), которое будет содержать проект DLL -библиотеки создаваемых компонентов ( UserControls ) и проект необходимых тестов ( Test ). Для удобства презентации выполненной работы создадим стартовую форму ( Start ) с кнопками-ссылками ( LinkLabel ), за каждой из которых закрепим вызов теста соответствующего упражнения.

В упражнениях мы последовательно разработаем свой (пользовательский) элемент управления на основе нескольких библиотечных элементов путем их комбинирования. Применим прием многоэтапного последовательного расширения классов на основе наследования. Вначале создадим из библиотечного класса Button класс расширения кнопки ClickmaticButton такой, чтобы в нажатом состоянии кнопка непрерывно генерировала событие Click с заданным интервалом. Затем вновь последовательно расширим класс ClickmaticButton классом ArrowButton, в котором на кнопке нарисуем стрелку. И наконец, скомбинируем два экземпляра созданной кнопки-стрелки и экземпляр библиотечного элемента TextBox в единый пользовательский компонент NumericScan по аналогии с библиотечным компонентом NumericUpDown - поле со счетчиком.

Разработка стартовой формы

Это небольшое приложение, которое обеспечит удобный способ демонстрации результатов всех других упражнений работы. Попутно решим проблему, которая состоит в предотвращении повторного запуска уже запущенной формы. Мы эту проблему уже решали в лабораторной работе №25 с использование коллекции ArrayList, которую сами создавали и поддерживали. Здесь мы применим уже готовую коллекцию запущенных форм приложения System.Windows.Forms.FormCollection.

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

  • В панели Solution Explorer вызовите контекстное меню для узла Test проекта, выполните команду Properties и настройте на вкладке Application текстовые поля Assembly name и Default namespace в соответствии с рисунком, где на на место второй составляющей имени MyCompany. StudName подставьте свою фамилию (можно кириллицей)
  • Тем самым мы указали оболочке имя исполнимой сборки и пространство имен, которое будет автоматически генерироваться при добавлении к проекту каждого нового элементы. Теперь, чтобы задействовать новые настройки оболочки, мы удалим старый файл Form1.cs и создадим его заново.

  • Откройте на редактирование файл Program.cs и замените в нем пространство имен Test на MyCompany. StudName
  • В панели Solution Explorer удалите файл Form1.cs, в котором используется прежнее пространство имен Test, и на его место командой Project/Add Windows Form вновь добавьте файл с прежним именем Form1.cs
  • Последнее действие мы выполнили для того, чтобы вручную не исправлять пространство имен в двух местах: Form1.cs и Form1.Designer.cs. Мы еще не написали ни строчки кода в этих файлах, поэтому нам не жалко их быстро перегенерировать, но уже с другим пространством имен.

  • В панели Solution Explorer переименуйте файл Form1.cs в Start.cs и откройте его в режиме View Designer
  • Сейчас мы займемся формированием стартовой формы для запуска всех будущих упражнений этой работы с использованием коллекции System.Windows.Forms.FormCollection. Вначале мы просто добавим к проекту 3 формы, чтобы испытать работоспособность подхода, а впоследствии мы их скорректируем в соответствии с выполненными упражнениями.

  • Добавьте к проекту 3 формы командой Project/Add Windows Form с именами Form1, Form2 и Form3
  • В панели Solution Explorer вызовите для корневого узла проекта Test контекстное меню и командой Add/New Folder добавьте новую папку с именем ChildrenForms
  • Удерживая клавишу Ctrl, выделите файлы Form1.cs, Form2.cs, Form3.cs и переместите их мышью в папку ChildrenForms
  • Мы пытаемся группировать файлы проекта по категориям решаемых задач. Но если бы мы вначале создали папку, а потом стали генерировать в нее новые формы, то оболочка упаковала бы их в пространство имен MyCompany.StudName.ChildrenForms. Это привело бы к тому, что классы дочерних форм оказались бы в другом пространстве имен и мы вынуждены были бы явно подключить его в файле Start.cs инструкцией using для видимости компилятору.

  • Откройте файл Start.cs в режиме View Designer и поместите на форму 3 компонента LinkLabel из панели Toolbox
  • Расположите компоненты LinkLabel на форме и выполните настройки в соответствии с таблицей свойств
  • Таблица свойств формы Start
    Элемент Свойство Значение
    Form Name По умолчанию
      Text Lab47. Студент Name
      StartPosition CenterScreen
      FormBorderStyle FixedDialog
    LinkLabel Name Form1
      Tag Form1
      Text Упражнение 1. ClickmaticButton
    LinkLabel Name Form2
      Tag Form2
      Text Упражнение 2. ArrowButton
    LinkLabel Name Form3
      Tag Form3
      Text Упражнение 3. NumericScan

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

    Свойство Tag любых компонентов, в том числе LinkLabel, служит для простого сохранения объектов произвольного типа, поскольку имеет тип Object. Мы разместили в нем метки имени формы для распознавания в общем обработчике щелчка на объекте LinkLabel.

  • Откройте файл Start.cs в режиме View Code и добавьте в код класса поле объявления ссылки типа Form
  • namespace MyCompany.StudName
    {
        public partial class Start : Form
        {
            public Start()
            {
                InitializeComponent();
            }
        
            Form frm;
        }
    }
  • Откройте файл Start.cs в режиме View Designer, выделите на форме одновременно все экземпляры LinkLabel и создайте через панель Properties для их события LinkClicked общий обработчик с именем OnLinkClicked
  • Заполните обработчик OnLinkClicked следующим кодом
  • using System;
    using System.Collections.Generic;
    using System.ComponentModel;
    using System.Data;
    using System.Drawing;
    using System.Text;
    using System.Windows.Forms;
    
    namespace MyCompany.StudName
    {
        public partial class Start : Form
        {
            public Start()
            {
                InitializeComponent();
            }
        
            Form frm;
        
            private void OnLinkClicked(object sender, LinkLabelLinkClickedEventArgs e)
            {
                // Повышаем полномочия ссылки и извлекаем закрепленное имя формы
                string formName = (string)((LinkLabel)sender).Tag;
        
                // Читаем коллекцию запущенных форм
                FormCollection fc = Application.OpenForms;
                bool IsRun = false;
                // Перебираем коллекцию
                foreach (Form form in fc)
                {
                    // Имя очередной запущенной формы сравниваем с закрепленным за кнопкой
                    if (form.Name == formName)
                    {
                        IsRun = true;   // Нашли, что форма запущена, поднимаем флаг
                        frm = form;     // Сохраняем ссылку на запущенную форму для фокуса
                        break;          // Выходим из цикла
                    }
                }
        
                // Если форма не запущена - запускаем
                if (!IsRun)
                {
                    switch (formName)
                    {
                        case "Form1":
                            frm = new Form1();
                            break;
                        case "Form2":
                            frm = new Form2();
                            break;
                        case "Form3":
                            frm = new Form3();
                            break;
                    }
        
                    this.AddOwnedForm(frm);     // Сделать новую frm подчиненной Start
                    // frm.Owner = this;        // Алтернативный способ назначить владельца
                    frm.ShowInTaskbar = false;  // Не отображать метку окна в панели задач
                    frm.Show();                 // Показать новую форму
                }
                else
                    frm.Focus();                // Передать фокус запущенной форме
            }
        }
    }

    Инструкция добавления подчиненной формы

    this.AddOwnedForm(frm);

    назначает для созданного экземпляра frm в качестве владельца текущую форму (в нашем случае - стартовую). Это означает, что подчиненное окно будет всегда размещаться поверх владельца, принудительно закрываться и разворачиваться тогда, когда это делает владелец. Таким поведением обладают, например, окна поиска и замены, или дочерние окна в многодокументных интерфейсах ( MDI ).

    Если известна ссылка frm на подчиненную форму, то освободить ее позволяет метод владельца RemoveOwnedForm(frm). Альтернативным способом назначить или сменить владельца у подчиненной формы можно через ее свойство Owner, а для освобождения от владельца нужно этому свойству присвоить нулевую ссылку Owner=null.

  • Запустите приложение и убедитесь, что на данном этапе наш механизм предотвращения повторного запуска форм не работает!!!
  • Дело в том, что имена форм по умолчанию в коллекции считаются пустыми до тех пор, пока явно не прописаны в теле класса формы. Оболочка сделает это явно в файле *.Designer.cs только после первого помещения на форму какого-либо компонента. Только тогда она посчитает, что может возникнуть необходимость их различать, и явно присвоит имя самой форме. Заставим оболочку явно прописать имена наших форм.

  • Поместите на каждую форму в каталоге ChildrenForms любой компонент пользовательского интерфейса (например, Button ) и сразу же его удалите
  • Опять запустите приложение и убедитесь, что все заработало правильно
  • Добавление объектов всплывающей подсказки

    Для пущей красоты (и для знакомства) добавим к LinkLabelвсплывающие подсказки (оперативные справки) - объекты класса ToolTip, генерирующие окно сообщений при наведении курсора на прикрепленный объект. К сожалению, компонент LinkLabel не имеет встроенного свойства ToolTip, которое бы прикрепляло саму справку к компоненту. Поэтому мы вначале создадим саму подсказку - объект типа ToolTip, а затем уже прикрепим объект LinkLabel к этой подсказке. В коде стартовой формы нашей лабораторной работы имеются 3 объекта LinkLabel, поэтому создадим 3 объекта всплывающей подсказки ToolTip.

  • В начале класса Start (удобнее перед конструктором класса) создайте и инициализируйте два одномерных массива
  • public partial class Start : Form
        {
            // Массив сообщений всплывающих подсказок для LinkLabel
            String[] strTips =
                {
                    "Тест для компонента кнопки,\n" +
                    "генерирующей щелчки мыши",
                    "Тест для компонента скроллирующей кнопки \n" +
                    "со стрелками, генерирующей щелчки мыши",
                    "Тест для компонента текстового \n" +
                    "поля со счетчиком"
                };
        
            // Массив ссылок для объектов всплывающих подсказок
            ToolTip[] tips = 
                {
                    new ToolTip(),
                    new ToolTip(),
                    new ToolTip()
                };
        
            public Start()
            {
                InitializeComponent();
            }
        
            Form frm;
        
            .......................................................
        }
  • В конструкторе класса Start после вызова функции инициализации компонентов формы вставьте код, связывающий объекты подсказок с кнопками-ссылками и настраивающий эти подсказки
  • public Start()
            {
                InitializeComponent();
        
                // Привязка всплывающих подсказок к запускающим кнопкам
                tips[0].SetToolTip(linkLabel1, strTips[0]);
                tips[1].SetToolTip(linkLabel2, strTips[1]);
                tips[2].SetToolTip(linkLabel3, strTips[2]);
        
                // Настройка ToolTip
                for (int i = 0; i < tips.Length; i++)
                {
                    tips[i].IsBalloon = true;// Использовать окно подсказки
                    tips[i].ToolTipIcon = ToolTipIcon.Info;// Иконки
                    tips[i].ToolTipTitle = "Упражнение " + (i + 1); // Заголовок
                    tips[i].ForeColor = Color.Magenta;  // Цвет текста
                    tips[i].BackColor = Color.LightGoldenrodYellow; // Цвет фона
                }
            }

    Таблица цветов структуры Color

    Цвета для объектов подсказки выберите из приведенной таблицы по своему вкусу.

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

    Упражнение 1. Разработка кнопки ClickmaticButton

  • Добавьте к решению MySolution новый пустой проект командой File/Add/New Project и заполните окно мастера так
  • В панели Solution Explorer для корневого узла проекта UserControls выполните команду Properties контекстного меню и настройте вкладку Application свойств проекта следующим образом
  • В панели Solution Explorer вызовите для корневого узла проекта UserControls контекстное меню и командой Add/Component добавьте элемент ClickmaticButton
  • В панели Solution Explorer для узла References проекта UserControls, где уже должна находиться ссылка на библиотечную сборку System, выполните команду Add Reference
  • Удерживая клавишу Ctrl через окно Add Reference выделите и добавьте к проекту UserControls ссылки на библиотечные сборки System.Drawing и System.Windows.Forms
  • Убедитесь, что теперь узел References проекта UserControls стал выглядеть так
  • Переведите файл ClickmaticButton.cs [Design] в режим редактирования кода командой контекстного меню View Code
  • Добавьте в начало файла ClickmaticButton.cs необходимые инструкции using подключения нужных нам пространств имен и замените базовый класс Component на Button, чтобы код файла стал таким
  • using System;
    using System.ComponentModel;
    using System.Drawing;
    using System.Windows.Forms;
        
    namespace MyCompany.StudName
    {
        public partial class ClickmaticButton : Button // : Component
        {
            public ClickmaticButton()
            {
                InitializeComponent();
            }
        
            public ClickmaticButton(IContainer container)
            {
                container.Add(this);
        
                InitializeComponent();
            }
        }
    }

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

    Наполнение класса ClickmaticButton функциональностью генерации щелчков мыши

  • Выполните команду контекстного меню View Designer и поместите из свитка Components панели Toolbox на форму ClickmaticButton.cs [Design] компонент Timer. Присвойте созданному экземпляру имя timer
  • Двойным щелчком на экземпляре timer создайте обработчик timer_Tick()
  • Добавьте в класс ClickmaticButton два поля целого типа для задания задержки на момент запуска таймера и интервала повторения тиков. Для этого используйте соответствующие библиотечные свойства класса System.Windows.Forms.SystemInformation, предусмотренные для управления клавиатурой
  • public partial class ClickmaticButton : Button // : Component
        {
            .....................................................
        
            // Поля характеристик работы таймера 
            readonly int DELAY = 250 * (1 + SystemInformation.KeyboardDelay);
            readonly int SPEED = 405 - 12 * SystemInformation.KeyboardSpeed;
        
            private void timer_Tick(object sender, EventArgs e)
            {
        
            }
        }

    Ключевое слово readonly дает указание компилятору следить за тем, чтобы где-нибудь в коде мы случайно не попытались повторно переприсвоить этим полям новое значение. Оно позволяет выполнить отложенную инициализацию один раз, причем можно вычислимым выражением в любом методе класса. Ключевое слово const требует немедленной инициализации переменной, причем константной величиной, что в данном случае нам не подходит.

  • Начав ввод с ключевого слова override, переопределите в классе ClickmaticButton унаследованный метод OnMouseMove() для управления приостановкой работы таймера, когда курсор при наличии установленной связи "разрабатываемый элемент кнопки - устройство мыши" будет выведен пользователем за пределы области чувствительности элемента кнопки
  • // Переопределение унаследованного метода диспетчеризации 
            // события MouseMove увода курсора за область чувствительности элемента
            protected override void OnMouseMove(MouseEventArgs mevent)
            {
                base.OnMouseMove(mevent);
        
                // Приостанавливаем или возобнавляем запущенный таймер логическим выражением
                timer.Enabled = this.Capture  // Связь с мышью установлена
                    //  (MouseButtons  MouseButtons.Left) != 0   // Распознавать левую кнопку необязательно
                     this.ClientRectangle.Contains(mevent.Location);   // Курсор над кнопкой 
                    //  this.ClientRectangle.Contains(this.PointToClient(MousePosition))// То же самое!
            }
  • Начав ввод с ключевого слова override, переопределите в классе ClickmaticButton унаследованный метод OnMouseDown() диспетчеризации события MouseDown для запуска таймера при нажатии левой кнопки мыши
  • // Переопределение унаследованного метода диспетчеризации 
            // события MouseDown нажатия кнопки мыши для запуска таймера
            protected override void OnMouseDown(MouseEventArgs mevent)
            {
                base.OnMouseDown(mevent);   // Отправляем к базовому методу
        
                // Если нажата левая кнопка мыши (побитовое умножение)
                if ((mevent.ButtonMouseButtons.Left) != 0)
                {
                    timer.Interval = DELAY;     // Задержка для момента нажатия
                    timer.Start();              // Запустить таймер
                }
            }

    Если мы не отправим вызов перехваченного виртуального метода далее к базовому классу, то хоть таймер и запустится, но интерфейс кнопки не будет меняться привычным для пользователя образом и подписанные на событие MouseDown обработчики не сработают. Запущенный таймер до первого срабатывания выдержит паузу DELAY, затем начнет вызывать обработчик timer_Tick(), в котором мы установим новый интервал срабатывания SPEED и будем принудительно вызывать метод диспетчеризации события Click элемента кнопки, имитируя тем самым генерацию щелчков пользователя на нем.

  • Заполните ранее созданный обработчик timer_Tick() тиков таймера следующим кодом
  • private void timer_Tick(object sender, EventArgs e)
            {
                timer.Interval = SPEED; // Устанавливаем новый интервал генерации тиков
                this.OnClick(EventArgs.Empty);  // Генерируем щелчок на элементе кнопки
                                                // с пустым фактическим параметром
            }

    Остановку запущенного таймера и, соответственно, прекращение генерации щелчков пользователя на элементе кнопки предусмотрим в переопределении метода OnMouseUp() диспетчеризации события MouseUp отпускания кнопки мыши.

  • Начав ввод с ключевого слова override, переопределите в классе ClickmaticButton унаследованный метод OnMouseUp() диспетчеризации события MouseUp для остановки таймера при отпускании кнопки мыши
  • // Переопределение унаследованного метода диспетчеризации 
            // события MouseUp отпускания кнопки мыши для остановки таймера
            protected override void OnMouseUp(MouseEventArgs mevent)
            {
                base.OnMouseUp(mevent);     // Отправляем к базовому методу
        
                timer.Stop();   // Стоп таймер
            }

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

    Тестирование генерирующей кнопки ClickmaticButton

  • Закройте все редактируемые текущие документы командой меню Window/Close All Documents
  • В панели Solution Explorer вызовите на редактирование в режиме View Designer форму Form1 двойным щелчком по одноименному узлу
  • В панели Solution Explorer вызовите контекстное меню для узла UserControls проекта пользовательских DLL -компонентов и выполните команду Rebuild, чтобы заново откомпилировать единственный пока компонент ClickmaticButton
  • Откройте панель Toolbox, найдите в верхней ее части свиток для категории UserControls Components и двойным щелчком на расширенном компоненте ClickmaticButton создайте на форме Form1 его экземпляр. Присвойте его свойству Name значение btnClickGen, а свойству Text - значение ClickGen
  • Поместите на форму библиотечный компонент Label для счетчика тиков и присвойте его свойству Name значение lblTickCount
  • Двойным щелчком на экземпляре кнопки ClickmaticButton создайте обработчик события Click, который заполните так
  • private void btnClickGen_Click(object sender, EventArgs e)
            {
                int tickCount;
                if (!Int32.TryParse(lblTickCount.Text, out tickCount))
                    tickCount = 1;  // При некорректном преобразовании установить 1
                else
                    tickCount++;    // При корректоном преобразовании увеличить на 1
        
                lblTickCount.Text = tickCount.ToString();
            }

    Статический метод TryParse() структуры Int32 преобразует строковый тип в целый без генерации исключения в случае невозможности выполнить эту операцию. В нашем тесте при первом вызове этого метода свойство Text метки не содержит числовых символов, следовательно преобразование невозможно и метод возвращает false. Мы такую ситуацию контролируем и устанавливаем tickCount=1, иначе увеличиваем значение счетчика.

  • Поместите на форму экземпляр обычного библиотечного компонента Button и присвойте его свойству Text значение Simple
  • Выделите на форме экземпляр обычного компонента Button, переведите панель Properties оболочки в режим Events и через раскрывающийся список в поле события Click присоедините к обычной кнопке тот же обработчик btnClickGen_Click()
  • В итоге пользовательский интерфейс тестовой формы должен быть примерно таким

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

  • Откройте файл Form1.cs (если он еще не открыт) в режиме View Designer и удалите на ней все экземпляры пользовательского интерфейса, созданные ранее для тестирования кнопки ClickmaticButton
  • С помощью контекстного меню для формы откройте Form1.cs в режиме View Code и удалите вручную обработчик btnClickGen_Click(), поскольку при удалении элементов заполненные обработчики автоматически не удаляются
  • Убедитесь, находясь в режиме Designer формы, что в панели Toolbox компонент ClickmaticButton пока присутствует и готов к размещению на форме
  • Откройте файл ClickmaticButton.cs в режиме View Code и добавьте перед объявлением класса ClickmaticButton ключевое слово abstract
  • namespace MyCompany.StudName
    {
        abstract public partial class ClickmaticButton : Button // : Component
        {
            ......................................
        }
    }

    Таким действием мы запретили создание экземпляров этого класса. Теперь его можно только наследовать.

  • В панели Solution Explorer вызовите контекстное меню для узла UserControls и выполните команду Build
  • Откройте файл Form1.cs в режиме View Designer и убедитесь, что пиктограмма компонента ClickmaticButton исчезла из панели Toolbox
  • Правильно, если создавать экземпляры нельзя, значит и компоненту там делать нечего.

  • Потренировались! А теперь восстановите тестовую форму Form1 для " Упражнения 1 ", убрав объявление abstract
  • Упражнение 2. Разработка кнопки в стиле полос прокрутки

    Наследуем от ClickmaticButton еще один класс ArrowButton, в котором переопределим виртуальный метод OnPaint() для отрисовки кнопки как в полосах прокрутки, где надпись будет замещена треугольной стрелкой. Образ скроллирующей кнопки будем отрисовывать методом ControlPaint. DrawScrollButton() из пространства имен System.Windows.Forms. Направление рисования функцией стрелки регулируется ее параметром типа библиотечного перечисления ScrollButton из того же пространства имен.

  • В панели Solution Explorer вызовите контекстное меню для узла UserControls и добавьте к проекту командой Add/Component класс для размещения нового компонента с именем ArrowButton
  • Перейдите в режим View Code, замените наследуемый класс Component на ClickmaticButton, чтобы не потерять ту функциональность, которую мы сделали ранее
  • Теперь нужно установить такой стиль для кнопки, чтобы в конечном пользовательском элементе (NumericScan - поле с кнопками) она никогда бы не получала фокус ввода. Фокус должен быть только у текстового поля.

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

    using System;
    using System.ComponentModel;
    using System.Drawing;
    using System.Windows.Forms;
        
    namespace MyCompany.StudName
    {
        public partial class ArrowButton : ClickmaticButton
        {
            public ArrowButton()
            {
                InitializeComponent();
        
                // Отключение фокуса ввода 
                this.SetStyle(ControlStyles.Selectable, false);
            }
        
            public ArrowButton(IContainer container)
            {
                container.Add(this);
        
                InitializeComponent();
        
                // Отключение фокуса ввода 
                this.SetStyle(ControlStyles.Selectable, false);
            }
        }
    }

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

  • Дополните класс ArrowButton следующим кодом
  • // Управление ориентацией стрелки на кнопке
            System.Windows.Forms.ScrollButton scrollButton =
                System.Windows.Forms.ScrollButton.Right;
            public System.Windows.Forms.ScrollButton ScrollButton
            {
                set
                {
                    scrollButton = value;
                    this.Invalidate();  // Перерисовать
                }
                get { return scrollButton; }
            }
  • Начав ввод с ключевого слова override, переопределите в классе ArrowButton унаследованный метод OnPaint() для отрисовки кнопки в стиле скроллирующей и заполните его следующим кодом
  • // Отрисовка кнопки в стиле со стрелкой
            protected override void OnPaint(PaintEventArgs pevent)
            {
                base.OnPaint(pevent);       // Отправляем к базовому методу 
                                            // или вообще убираем!!!
        
                Graphics gr = pevent.Graphics;  // Извлекаем контекст устройства
        
                // Кнопка в контакте с мышью и курсор находится в области чувствительности
                bool mouseInButton = this.Capture 
                    this.ClientRectangle.Contains(this.PointToClient(MousePosition));
        
                // Флаг состояния кнопки для управления ее стилем отрисовки
                System.Windows.Forms.ButtonState buttonState =
                    !this.Enabled ? ButtonState.Inactive        // Если кнопка недоступна
                    : (mouseInButton ? ButtonState.Pushed       // Если доступна и нажата
                    : ButtonState.Normal);                      // Если доступна и отпущена
        
                // Рисуем кнопку как кнопку со стрелкой
                ControlPaint.DrawScrollButton(gr, this.ClientRectangle,
                    scrollButton,   // В какую сторону рисовать стрелку
                    buttonState);   // Каким стилем рисовать
            }

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

  • Начав ввод с ключевого слова override, переопределите в классе ArrowButton унаследованный метод OnMouseCaptureChanged() для повторения отрисовки кнопки в стиле со стрелкой и заполните его следующим кодом
  • // Повторяем отрисовку при потере или восстановлении контакта с мышью
            protected override void OnMouseCaptureChanged(EventArgs e)
            {
                base.OnMouseCaptureChanged(e);
        
                this.Invalidate();  // Инициировать перерисовку
            }

    Тестирование скроллирующей кнопки ArrowButton

  • Откройте файл Form2.cs в режиме View Designer
  • Откомпилируйте проект пользовательской библиотеки UserControls, чтобы в панели Toolbox появился разработанный компонент ArrowButton
  • Поместите на Form2 четыре компонента ArrowButton
  • Переведите форму в режим View Code и настройте программно (в отличие от декларативного способа через панель Properties ) свойство ScrollButton компонентов на разные значения в конструкторе класса Form2
  • namespace MyCompany.StudName
    {
        public partial class Form2 : Form
        {
            public Form2()
            {
                InitializeComponent();  // Выполнение кода, сгенерированного
                                        // визуальным конструктором оболочки
                                        // в декларативном режиме проектирования
                                        // и расположенного в файле Form2.Designer.cs
        
                // Настройка ориентации стрелок на скроллирующих кнопках
                arrowButton1.ScrollButton = ScrollButton.Right;
                arrowButton2.ScrollButton = ScrollButton.Left;
                arrowButton3.ScrollButton = ScrollButton.Up;
                arrowButton4.ScrollButton = ScrollButton.Down;
            }
        }
    }
  • Запустите приложение и посмотрите на результат, который будет таким
  • Упражнение 3. Комбинирование элементов в единый компонент NumericScan

    Теперь мы готовы сконструировать аналог библиотечного компонента NumericUpDown - поле со списком, только с горизонтальным расположением скролирующих кнопок.

  • В панели Solution Explorer вызовите контекстное меню для узла UserControls и добавьте к проекту командой Add/User Control форму для размещения нового комбинированного компонента с именем NumericScan. Отметьте для себя, что в качестве базового в заготовке класса наследуется UserControl
  • Перенесите на форму NumericScan.cs в режиме View Designer из панели Toolbox два наших компонента ArrowButton и один библиотечный компонент TextBox. На данном этапе особо не заботьтесь о размещение экземпляров компонентов на форме. Мы это действие выполним позже программным способом. А пока расположите объекты компонента примерно так
  • Далее мы часть настроек выполним декларативно через панель Properties, а часть - программно в конструкторе компонента.

  • Через панель Properties выполните декларативные настройки объектов в соответствии с приведенной таблицей свойств
  • Таблица свойств декларативной настройки объектов компонента NumericScan
    Тип Свойство Значение Пояснения
    TextBox Name txtBox Имя экземпляра компонента текстового поля
      TextAlign Right Расположение текста
    ArrowButton Name btnLeft Имя объекта скроллирующей кнопки
      ScrollButton Left Ориентация стрелки влево
    ArrowButton Name btnRight Имя объекта скроллирующей кнопки
      ScrollButton Right Ориентация стрелки вправо ( по умолчанию )
  • Через панель Properties в режиме Events зарегистрируйте обработчики для составляющих объектов в соответствии с таблицей
  • Таблица событий декларативной настройки объектов компонента NumericScan
    Объект Событие Обработчик Пояснения
    txtBox TextChanged TextBoxOnTextChanged Возбуждается при завершении ввода клавишей Enter или потери фокуса
      KeyDown TextBoxOnKeyDown Возбуждается при нажатии любой клавиши, когда объект имеет фокус ввода
    btnLeft Click ButtonOnClick Возбуждается при шелчке на кнопке курсором мыши
    btnRight Click ButtonOnClick События двух кнопок подписаны на один обработчик

    Все остальные настройки составляющих объектов и самого компонента выполним программно.

  • Переведите редактирование файла NumericScan.cs в режим View Code и добавьте в конструктор компонента код вычисления размеров формы
  • using System;
    using System.Collections.Generic;
    using System.ComponentModel;
    using System.Drawing;
    using System.Data;
    using System.Text;
    using System.Windows.Forms;
        
    namespace MyCompany.StudName
    {
        public partial class NumericScan : UserControl
        {
            public NumericScan()
            {
                InitializeComponent();
        
                // Вычисляем и устанавливаем размеры компонента
                // Четыре высоты текущего шрифта 
                this.Width = 4 * this.Font.Height;  
                // Предпочтительная высота текстового поля +
                // высота горизонтальных линеек прокрутки
                this.Height = txtBox.PreferredHeight + 
                    SystemInformation.HorizontalScrollBarHeight;
            }
        
            // При завершении ввода клавишей Enter или потери фокуса 
            private void TextBoxOnTextChanged(object sender, EventArgs e)
            {
        
            }
        
            // При нажатии любой клавиши, когда объект имеет фокус ввода 
            private void TextBoxOnKeyDown(object sender, KeyEventArgs e)
            {
        
            }
        
            // Щелчки на скроллирующих кнопках
            private void ButtonOnClick(object sender, EventArgs e)
            {
        
            }
        }
    }
  • Добавьте в класс NumericScan закрытые поля для сохранения настроек компонента, а также оберните их общедоступными свойствами
  • public partial class NumericScan : UserControl
        {
            // Объявили базовые закрытые поля для открытых свойств
            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 = (mValue = value).ToString(); }
            }
            public decimal Increment
            {
                get { return mIncrement; }
                set { mIncrement = value; }
            }
            public decimal Minimum
            {
                get { return mMinimum; }
                set
                {
                    // Контроль нижней границы диапазона
                    if (Value < (mMinimum = value))
                        Value = mMinimum;
                }
            }
            public decimal Maximum
            {
                get { return mMaximum; }
                set
                {
                    // Контроль верхней границы диапазона
                    if (Value > (mMaximum = value))
                        Value = mMaximum;
                }
            }
        
            ..........................................
        }

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

  • Начните с ввода ключевого слова override и переопределите в классе NumericScan унаследованную виртуальную функцию GetPreferredSize() вычисления начальных размеров компонента при создании его экземпляра
  • // Срабатывает автоматически и устанавливает начальные размеры 
            // экземпляра компонента this.Width, this.Height при его 
            // создании или помещении на форму в режиме проектирования 
            public override Size GetPreferredSize(Size proposedSize)
            {
                return new Size(4 * this.Font.Height,   // Ширина
                    txtBox.PreferredHeight +
                    SystemInformation.HorizontalScrollBarHeight);
            }
  • Начните с ввода ключевого слова override и переопределите в классе NumericScan унаследованную виртуальную функцию OnResize() вычисления локализации и размеров дочерних объектов компонента
  • // Срабатывает автоматически, позиционирует
            // дочерние объекты и устанавливает их размеры
            protected override void OnResize(EventArgs e)
            {
                base.OnResize(e);
        
                txtBox.Location = new Point(0, 0); // В левом верхнем углу
                txtBox.Size = new Size(this.Width, txtBox.PreferredHeight); // По всей ширине
                btnLeft.Location = new Point(0, txtBox.Height); // Позиция
                btnRight.Location = new Point(this.Width / 2, txtBox.Height); // Позиция
                btnLeft.Size = btnRight.Size = new Size(this.Width / 2,
                    this.Height - txtBox.Height); // Одинаковый размер
            }

    Поддержку интерфейсного вида компонента мы обеспечили. Теперь нужно согласовать логику работы кнопок и текстового поля. Начнем с обработки события щелчка на кнопках. Для этого мы предусмотрели общий обработчик ButtonOnClick(). В нем нужно распознать нажатую кнопку и изменить значение текстового поля, а также сохранить это значение во внутреннем поле mValue класса. Но прежде объявим событие, которое будет информировать клиента, который будет создавать экземпляр компонента, об изменении значения поля компонента, а также функцию, которая будет инициировать генерацию этого события.

  • Объявите в классе NumericScan с помощью библиотечного делегата EventHandler событие с именем ValueChanged и определите функцию OnValueChanged() диспетчеризации этого события. Функцию диспетчеризации объявите виртуальной на случай дальнейшего наследования нашего компонента
  • // Объявили пользовательское событие
            public event EventHandler ValueChanged;
        
            // Ввели свою функцию диспетчеризации события ValueChanged
            // изменения величины прямым вводом в текстовое поле
            protected virtual void OnValueChanged(EventArgs args)
            {
                // Последовательно проверяем выход величины за левую и правую
                // границы, и если выходит за границу, то обрезаем по границе
                Value = Math.Min(mMaximum, mValue);
                Value = Math.Max(mMinimum, mValue);
                // Округляет вещественный денежный тип до заданного
                // количества значащих цифр после запятой
                Value = Decimal.Round(mValue, iDecimalPlaces);
        
                // Генерируем событие
                if (ValueChanged != null)
                    ValueChanged(this, args);
            }
  • Задайте в теле обработчика ButtonOnClick() код, который будет контролировать изменения внутреннего и текстового полей компонента при щелчках на кнопках, а также извещать подписавшегося клиента об этих изменениях
  • // Щелчки на скроллирующих кнопках
            private void ButtonOnClick(object sender, EventArgs e)
            {
                // Повышаем полномочия ссылки на объект скролирующей кнопки 
                ArrowButton btn = (ArrowButton)sender;
        
                // Создаем пробную переменную
                decimal tmpValue = mValue;
        
                // Идентифицируем кнопку и меняем значение поля
                if (btn == btnLeft)
                {
                    if ((tmpValue -= mIncrement) < mMinimum)
                        return;
                }
                else // Других кнопок нет 
                {
                    if ((tmpValue += mIncrement) > mMaximum)
                        return;
                }
        
                // Обновляем внутреннее и текстовое поля
                this.Value = tmpValue;
        
                // Генерируем событие, извещающее подписавшегося
                // клиента о произошедшем изменении значения счетчика
                OnValueChanged(EventArgs.Empty);
            }
  • Заполните обработчик TextBoxOnTextChanged() кодом, который будет обновлять внутреннее поле при его корректном изменении или восстанавливать из него корректное значение текстового поля при неправильном заполнении
  • // При завершении ввода клавишей Enter или потери фокуса 
            private void TextBoxOnTextChanged(object sender, EventArgs e)
            {
                if (txtBox.Text.Length == 0)
                    return;
        
                // При неудачной попытке преобразования восстанавливаем старое
                decimal tmpValue;
                if (!Decimal.TryParse(txtBox.Text, out tmpValue))
                    txtBox.Text = mValue.ToString();
                else
                    mValue = tmpValue;
            }

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

  • Переопределите в классе компонента виртуальный метод OnLeave() и заполните его следующим кодом
  • // При уходе с компонента
            protected override void OnLeave(EventArgs e)
            {
                base.OnLeave(e);
        
                // Возбуждаем событие и обновление состояния компонента
                OnValueChanged(EventArgs.Empty);
            }
  • Заполните обработчик TextBoxOnKeyDown() кодом, который будет контролировать нажатие клавиши Enter в текстовом поле и обновлять состояние компонента
  • // При нажатии любой клавиши, когда объект имеет фокус ввода 
            private void TextBoxOnKeyDown(object sender, KeyEventArgs e)
            {
                switch (e.KeyCode)
                {
                    case Keys.Enter:
                        OnValueChanged(EventArgs.Empty);
                        break;
                }
            }
  • В панели Solution Explorer выберите узел UserControls и командой Rebuild контекстного меню откомпилируйте готовый компонент
  • Тестирование компонента NumericScan

  • Поместите из панели Toolbox на форму Form3 два экземпляра компонента NumericScan и одну текстовую метку Label
  • Выделите одновременно оба объекта NumericScan и в панели Properties для их события ValueChanged, введенное нами при разработке компонента, зарегистрируйте общий обработчик с именем NumericScanOnValueChanged, который заполните так
  • using System;
    using System.Collections.Generic;
    using System.ComponentModel;
    using System.Data;
    using System.Drawing;
    using System.Text;
    using System.Windows.Forms;
        
    namespace MyCompany.StudName
    {
        public partial class Form3 : Form
        {
            public Form3()
            {
                InitializeComponent();
            }
        
            private void NumericScanOnValueChanged(object sender, EventArgs e)
            {
                label1.Text = "Первый: " + numericScan1.Value +
                    "; Второй: " + numericScan2.Value;
            }
        }
    }
  • Настройте в конструкторе класса Form3 экземпляры компонентов NumericScan
  • public Form3()
            {
                InitializeComponent();
        
                numericScan1.DecimalPlaces = 
                    numericScan2.DecimalPlaces = 2;
                numericScan1.Increment = 
                    numericScan2.Increment = 0.01M;
            }
  • Запустите " Упражнение 3 " и испытайте работу кода. Поизменяйте значения текстового поля клавиатурой и мышью, а также попользуйтесь клавишей Tab для смены фокуса ввода, испытав работу перегруженного метода OnLeave().
  • Замечание. Текстовое поле, ожидающее клавиатурный ввод числа типа Decimal, символом разделителя дробной части считает запятую.

    Внешний результат будет таким

    Упражнение 4. Использование компонента NumericScan для управления текстом

    В этом упражнении мы проиллюстрируем использование компонентов как классов с динамическим созданием их экземпляров прямо в коде, без использования графического редактора оболочки. Для автоматического размещения компонентов расширим библиотечный класс System.Windows.Forms.TableLayoutPanel, который представляет аналог невидимой таблицы для локализации элементов пользовательского интерфейса. Расширение опишет таблицу 6x2 (шесть строк - два столбца), левый столбец которой будет содержать текстовые метки, а правый - экземпляры компонентов NumericScan.

    Экземпляр этого расширения будет представлять панель управления для искажения текста, который будет помещен в панели отображения. Искажение текста определяется свойством Transform контекста устройства, представленного объектом Graphics. Это свойство имеет тип Matrix из пространства имен System.Drawing.Drawing2D. Экземпляр класса Matrix перед рисованием текста мы будем создавать с параметрами, которые пользователь будет задавать в объектах NumericScan панели управления. Для обмена данными между элементами управления и матрицей преобразования предусмотрим в классе-расширении свойство Matrix.

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

  • В панели Solution Explorer вызовите контекстное меню для узла Test и добавьте командой Add/Class новый файл с именем AuxForm4.cs
  • В панели Solution Explorer вызовите контекстное меню для папки ChildrenForms и командой Add/New Folder добавьте подпапку с именем AuxForms
  • В панели Solution Explorer переместите мышью файл AuxForm4.cs в подпапку AuxForms, чтобы "не мешалась под ногами"
  • Откройте на редактирование файл AuxForm4.cs и преобразуйте его так
  • using System;
    using System.Drawing;
    using System.Windows.Forms;
    using System.Drawing.Drawing2D;
        
    namespace MyCompany.StudName
    {
        // Панель размещения элементов управления текстом
        class MatrixPanel : TableLayoutPanel
        {
        }
    }
        
    namespace MyCompany.StudName
    {
        // Панель отображения текста
        class DisplayPanel : Panel
        {
        }
    }
  • Добавьте в класс MatrixPanel конструктор класса, в котором заполняются ячейки унаследованной таблицы динамически созданными объектами, чтобы класс стал таким
  • // Панель размещения элементов управления текстом
        class MatrixPanel : TableLayoutPanel
        {
            // Заготовили массив ссылок для будущей адресации
            NumericScan[] numscan = new NumericScan[6];
        
            // Конструктор для наполнения таблицы размещения
            public MatrixPanel()
            {
                // Параметры таблицы
                this.AutoSize = true;   // Подстраиваться под содержимое
                this.Padding = new Padding(this.Font.Height);   // Внутренний отступ
                // дочерних элементов от ячеек таблицы
                this.ColumnCount = 2;   // Количество столбцов таблицы размещения
        
                this.SuspendLayout();   // Приостановить механизм размещения элементов таблицы
                // до завершения создания содержимого ячеек 
        
                // Заполнить ячейки элементами интерфейса слева-направо сверху вниз
                for (int i = 0; i < 6; i++)
                {
                    // Создаем и настраиваем текстовые метки в левом столбце таблицы
                    Label lbl = new Label();    // Создаем неадресуемый экземпляр
                    lbl.Parent = this;      // Отдали таблице
                    lbl.AutoSize = true;    // Подстраиваться под содержимое
                    lbl.Anchor = AnchorStyles.Left; // Привязываем к ячейке слева
                    // Нерациональный способ многократно создавать 
                    // массив, а использовать только один элемент 
                    lbl.Text = new string[]{
                        "X Scale:", "Y Shear:", "X Shear:",
                        "Y Scale:", "X Translate:", "Y Translate:"}[i];
        
                    // Создаем и настраиваем экземпляры компонента NumericScan
                    numscan[i] = new NumericScan(); // Создаем адресуемый экземпляр
                    numscan[i].Parent = this;       // Отдали таблице
                    numscan[i].AutoSize = true;     // Подстраиваться под содержимое
                    numscan[i].Anchor = AnchorStyles.Right;// Привязываем к ячейке справа
                    numscan[i].Minimum = -1000; // Нижняя граница диапазона
                    numscan[i].Maximum = 1000;  // Верхняя граница диапазона
                    numscan[i].DecimalPlaces = 2;   // Число значащих цифр после запятой
                }
        
                this.ResumeLayout();   // Включить механизм размещения элементов таблицы
            }
        }

    Для искажения текста в панели представления DisplayPanel мы воспользуемся матрицей преобразования, представленной классом System.Drawing.Drawing2D.Matrix, которая имеет следующий вид

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

  • Добавьте к классу MatrixPanel новое свойство с именем Matrix, которое будет представлено следующим кодом
  • // Свойство для взаимодействия экземпляров NumericScan и объекта Matrix 
            public System.Drawing.Drawing2D.Matrix Matrix
            {
                set
                {
                    // Извлекаем элементы матрицы преобразования в интерфейсные элементы
                    for (int i = 0; i < 6; i++)
                        numscan[i].Value = (decimal)value.Elements[i];
                }
                get
                {
                    // Создаем матрицу преобразования по значениям интерфейсных элементов
                    return new System.Drawing.Drawing2D.Matrix(
                        (float)numscan[0].Value,    // X Scale:
                        (float)numscan[1].Value,    // Y Shear:
                        (float)numscan[2].Value,    // X Shear:
                        (float)numscan[3].Value,    // Y Scale:
                        (float)numscan[4].Value,    // X Translate:
                        (float)numscan[5].Value);   // Y Translate:
                }
            }
  • Добавьте в конец конструктора класса код начального заполнения интерфейсных элементов значениями единичной матрицы преобразования через свойство класса
  • // Конструктор для наполнения таблицы размещения
            public MatrixPanel()
            {
                .......................................................
        
                this.ResumeLayout();   // Включить механизм размещения элементов таблицы
        
                // Заполнение интерфейсных элементов единичной 
                // матрицей преобразования через свойство класса
                this.Matrix = new System.Drawing.Drawing2D.Matrix();
            }

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

  • Добавьте в класс MatrixPanel объявление нового события с именем Changed и создайте метод его диспетчеризации OnChanged() - защищенный и виртуальный для переопределения в возможных потомках, если наследование продолжится
  • // Панель размещения элементов управления текстом
        class MatrixPanel : TableLayoutPanel
        {
            ......................................................
        
            // Объявляем событие изменения состояния интерфейсных элементов
            // по типовому библиотечному делегату EventHandler
            public event EventHandler Changed;
        
            // Метод диспетчеризации события Changed 
            protected virtual void OnChanged(EventArgs args)
            {
                // Проверяем наличие подписанных обработчиков и генерируем событие
                if (Changed != null)
                    Changed(this, EventArgs.Empty);
            }
        }
  • В конструкторе класса MatrixPanel в код создания и настройки интерфейсных элементов NumericScan добавьте код регистрации общего обработчика. Код вводите вручную, чтобы оболочка создала обработчик с правильной сигнатурой, соответствующей делегату события
  • // Конструктор для наполнения таблицы размещения
            public MatrixPanel()
            {
                .......................................
        
                // Заполнить ячейки элементами интерфейса слева-направо сверху вниз
                for (int i = 0; i < 6; i++)
                {
                    // Создаем и настраиваем текстовые метки в левом столбце таблицы
                    ...............................................
        
                    // Создаем и настраиваем экземпляры компонента NumericScan
                    numscan[i] = new NumericScan(); // Создаем адресуемый экземпляр
                    numscan[i].Parent = this;       // Отдали таблице
                    numscan[i].AutoSize = true;     // Подстраиваться под содержимое
                    numscan[i].Anchor = AnchorStyles.Right;// Привязываем к ячейке справа
                    numscan[i].Minimum = -1000; // Нижняя граница диапазона
                    numscan[i].Maximum = 1000;  // Верхняя граница диапазона
                    numscan[i].DecimalPlaces = 2;   // Число значащих цифр после запятой
                    // Регистрируем общий обработчик на изменения состояния объекта
                    // НАБИРАТЬ ВРУЧНУЮ !!!!!!!
                    numscan[i].ValueChanged += new EventHandler(MatrixPanel_ValueChanged);
                }
        
                this.ResumeLayout();   // Включить механизм размещения элементов таблицы
        
                // Заполнение интерфейсных элементов единичной 
                // матрицей преобразования через свойство класса
                this.Matrix = new System.Drawing.Drawing2D.Matrix();
            }
        
            void MatrixPanel_ValueChanged(object sender, EventArgs e)
            {
                throw new Exception("The method or operation is not implemented.");
            }
  • Замените код генерации исключения в теле заготовки обработчика на код вызова метода диспетчеризации события
  • void MatrixPanel_ValueChanged(object sender, EventArgs e)
            {
                OnChanged(EventArgs.Empty);
            }

    На этом создание панели с интерфейсными элементами управления текстом завершено.

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

    Теперь нужно создать класс DisplayPanel расширения класса панели Panel, который будет рисовать заданный текст по установленному в матрице преобразования алгоритму. Заготовка этого класса у нас уже создана.

  • Добавьте в заготовку класса DisplayPanel контсруктор по умолчанию, в котором разместите код включения свойства перерисовки панели при изменении ее размеров
  • // Панель отображения текста
        class DisplayPanel : Panel
        {
            // Конструктор класса
            public DisplayPanel()
            {
                // Перерисовываться самостоятельно при изменении размеров
                this.ResizeRedraw = true;
            }
        }
  • Добавьте к классу DisplayPanel поле ссылки на объект матрицы преобразования и переопределите унаследованную функцию OnPaint() рисования содержимого свойства Text панели, начав ввод с ключевого слова override
  • // Создание экземпляра матрицы преобразования текста 
            // и инициализация ее единичной матрицей 
            System.Drawing.Drawing2D.Matrix matrix =
                new System.Drawing.Drawing2D.Matrix();  // Единичная матрица
        
            // Переопределение функции прорисовки панели
            protected override void OnPaint(PaintEventArgs e)
            {
                Graphics gr = e.Graphics;   // Извлекаем контекст устройства
                Brush brush = new SolidBrush(this.ForeColor);
        
                // Защищенно отображаем трансформированный текст
                try
                {
                    gr.Transform = matrix;
                    gr.DrawString(this.Text, this.Font, brush, Point.Empty);
                }
                catch (Exception ext)
                {
                    gr.Transform = new Matrix();  // Сбрасываем искажения
                    StringFormat strfrm = new StringFormat();
                    strfrm.Alignment = strfrm.LineAlignment = StringAlignment.Center;
                    gr.DrawString(ext.Message, Font, brush, ClientRectangle, strfrm);
                }
        
                //Освободили ограниченный ресурс  
                //gr.Dispose();
                brush.Dispose();
        
                base.OnPaint(e);    // Спустили предкам
            }
  • Оберните в классе DisplayPanel поле matrix свойством доступа с именем Matrix
  • // Свойство доступа к матрице преобразования
            public System.Drawing.Drawing2D.Matrix Matrix
            {
                set
                {
                    matrix = value;
                    this.Invalidate();  // Перерисовать
                }
                // Аксессор get нам не нужен
            }

    На этом создание элемента панели отображения текста мы закончили.

    Тестирование панелей управления текстом

    Начнем с заготовки тестирующей формы и обеспечения механизма ее корректного вызова.

  • В панели Solution Explorer вызовите для узла Test контекстное меню и командой Add/Windows Form добавьте к проекту файл Form4.cs
  • В панели Solution Explorer переместите мышью файл Form4.cs в подпапку ChildrenForms
  • Откройте файл Start.cs в режиме View Designer и поместите на форму компонент LinkLabel с именем по умолчанию linkLabel4
  • Подпишите событие LinkClicked ссылки linkLabel4 на общий обработчик OnLinkClicked()
  • В панели Properties задайте для linkLabel4 свойство Tag=Form4 для различения связанной формы в стартовом окне Start
  • Откройте файл Start.cs в режиме View Code и откорректируйте код для запуска формы, предотвращения повторного запуска и отображения всплывающей подсказки, как мы это сделали для трех предыдущих упражнений
  • Заготовка формы Form4 готова для тестирования классов отображения искаженного текста.

  • Откройте файл Form4.cs в режиме View Code и заполните конструктор класса следующим кодом
  • using System;
    using System.Collections.Generic;
    using System.ComponentModel;
    using System.Data;
    using System.Drawing;
    using System.Text;
    using System.Windows.Forms;
        
    namespace MyCompany.StudName
    {
        public partial class Form4 : Form
        {
            // Поля для видимости в методах класса
            MatrixPanel matrixPanel;
            DisplayPanel displayPanel;
        
            // Конструктор класса
            public Form4()
            {
                InitializeComponent();
        
                TableLayoutPanel panel = new TableLayoutPanel();
                panel.Parent = this;
                panel.Dock = DockStyle.Fill;
                panel.ColumnCount = 2;
        
                matrixPanel = new MatrixPanel();
                matrixPanel.Parent = panel;
                matrixPanel.Anchor = AnchorStyles.Left | AnchorStyles.Right;
                matrixPanel.Changed += new EventHandler(matrixPanel_Changed);
        
                displayPanel = new DisplayPanel();
                displayPanel.Parent = panel;
                displayPanel.Dock = DockStyle.Fill;
                displayPanel.BackColor = Color.White;
                displayPanel.ForeColor = Color.Black;
                displayPanel.Text = "Некоторый текст";
                displayPanel.Font = new Font(FontFamily.GenericSerif, 24);
        
                this.Width = 3 * matrixPanel.Width;
                this.Height = 3 * matrixPanel.Height / 2;
            }
        
            void matrixPanel_Changed(object sender, EventArgs e)
            {
                displayPanel.Matrix = matrixPanel.Matrix;
            }
        }
    }
  • Запустите приложение и испытайте работу "Упражнения 1", его окно в запущенном состоянии имеет, примерно, такой вид
  • Продемонстрируйте преподавателю все упражнения выполненной работы
  • Вернуться к учебному плану