Файлы к лабораторной работе Вы можете скачать здесь.
Всегда пользовательский элемент управления лучше создавать
на основе уже существующих, преимущественно библиотечных, элементов
управления, которые хорошо отлажены и работают привычным образом.
В предыдущих примерах мы усовершенствовали библиотечный класс Button,
приспосабливая его к нашим задачам путем расширения в другом
классе.
В данной работе мы продолжим выполнять упражнения по
созданию собственных MySolution ),
которое будет содержать проект DLL -библиотеки
создаваемых компонентов ( ) и проект
необходимых тестов ( Test ). Для удобства презентации
выполненной работы создадим стартовую форму ( Start )
с кнопками-ссылками ( LinkLabel ), за каждой
из которых закрепим вызов теста соответствующего упражнения.
В упражнениях мы последовательно разработаем свой (пользовательский)
элемент управления на основе нескольких библиотечных элементов
путем их комбинирования. Применим прием многоэтапного последовательного
Button класс расширения
кнопки ClickmaticButton такой, чтобы в нажатом
состоянии кнопка непрерывно генерировала событие с
заданным интервалом. Затем вновь последовательно расширим класс ClickmaticButton классом ArrowButton,
в котором на кнопке нарисуем стрелку. И наконец, скомбинируем
два экземпляра созданной кнопки-стрелки и экземпляр библиотечного
элемента TextBox в единый пользовательский
компонент NumericScan по аналогии с библиотечным
компонентом NumericUpDown - поле со счетчиком.
Это небольшое приложение, которое обеспечит удобный способ
демонстрации результатов всех других упражнений работы. Попутно
решим проблему, которая состоит в предотвращении повторного
запуска уже запущенной формы. Мы эту проблему уже решали
в лабораторной работе №25 с использование
коллекции ,
которую сами создавали и поддерживали. Здесь мы применим уже
готовую коллекцию запущенных форм приложения 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 на форме и выполните
настройки в соответствии с таблицей свойств| Элемент | Свойство | Значение |
|---|---|---|
Form |
Name |
По умолчанию |
Text |
Lab47. Студент Name | |
StartPosition |
CenterScreen | |
FormBorderStyle |
FixedDialog | |
LinkLabel |
Name |
Form1 |
|
Form1 | |
Text |
Упражнение 1. ClickmaticButton | |
LinkLabel |
Name |
Form2 |
|
Form2 | |
Text |
Упражнение 2. ArrowButton | |
LinkLabel |
Name |
Form3 |
|
Form3 | |
Text |
Упражнение 3. NumericScan |
Интерфейс главной формы должен стать примерно таким

Свойство любых компонентов, в том числе LinkLabel,
служит для простого сохранения объектов произвольного типа,
поскольку имеет тип Object. Мы
разместили в нем метки имени формы для распознавания в общем
обработчике щелчка на объекте LinkLabel.
Откройте
файл Start.cs в режиме View Code и добавьте в код класса
поле объявления ссылки типа Formnamespace 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 в
качестве владельца ).
Если известна ссылка frm на
подчиненную форму, то освободить ее позволяет
метод владельца RemoveOwnedForm(frm).
Альтернативным способом назначить или сменить владельца у
подчиненной формы можно через ее свойство ,
а для освобождения от владельца нужно этому свойству
присвоить нулевую ссылку .
Запустите
приложение и убедитесь, что на данном этапе наш механизм
предотвращения повторного запуска форм не работает!!!Дело в том, что имена форм по умолчанию в коллекции
считаются пустыми до тех пор, пока явно не прописаны в теле
класса формы. Оболочка сделает это явно в файле *.Designer.cs только
после первого помещения на форму какого-либо компонента. Только
тогда она посчитает, что может возникнуть необходимость их
различать, и явно присвоит имя самой форме. Заставим оболочку
явно прописать имена наших форм.
Поместите
на каждую форму в каталоге ChildrenForms любой
компонент пользовательского интерфейса (например, Button )
и сразу же его удалите
Опять
запустите приложение и убедитесь, что все заработало правильноДля пущей красоты (и для знакомства) добавим к LinkLabelвсплывающие
подсказки (оперативные справки) - объекты класса ,
генерирующие окно сообщений при наведении курсора на прикрепленный
объект. К сожалению, компонент LinkLabel не
имеет , которое бы прикрепляло саму справку
к компоненту. Поэтому мы вначале создадим саму подсказку -
объект типа , а затем уже прикрепим
объект LinkLabel к этой подсказке. В коде
стартовой формы нашей лабораторной работы имеются 3 объекта LinkLabel, поэтому создадим 3 объекта всплывающей
подсказки .
В начале
класса 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; // Цвет фона
}
}
Цвета для объектов подсказки выберите из приведенной таблицы по своему вкусу.
|
|
|
|
Запустите
тестовую форму и проверьте механизм работы всплывающих подсказок
Теперь для запуска тестов нескольких упражнений мы подготовились
и можно приступать к разработке
Добавьте
к решению 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();
}
}
}
Мы создали заготовку расширения библиотечного класса кнопки с целью изменить ее функциональность таким образом, чтобы в нажатом состоянии кнопка генерировала имитацию щелчков пользователя.
Выполните
команду контекстного меню 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)
{
}
}
Ключевое слово дает указание компилятору
следить за тем, чтобы где-нибудь в коде мы случайно не попытались
повторно переприсвоить этим полям новое значение. Оно позволяет
выполнить отложенную инициализацию один раз, причем можно вычислимым
выражением в любом методе класса. Ключевое слово 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 обработчики не сработают. Запущенный
таймер до первого срабатывания выдержит паузу , затем
начнет вызывать обработчик timer_Tick(), в котором мы установим
новый интервал срабатывания и будем принудительно вызывать
метод диспетчеризации события элемента кнопки, имитируя
тем самым генерацию щелчков пользователя на нем.
Заполните
ранее созданный обработчик 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.
Но прежде, чем двигаться дальше, проверим работоспособность
сконструированного расширения кнопки.
Закройте
все редактируемые текущие документы командой меню 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 Наследуем
от 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(); // Инициировать перерисовку
}
Откройте
файл Form2.cs в режиме View Designer
Откомпилируйте
проект пользовательской библиотеки UserControls ,
чтобы в панели Toolbox появился разработанный компонент ArrowButton
Поместите
на Form2 четыре компонента ArrowButton
Переведите
форму в режим View Code и
настройте программно (в
отличие от декларативного способа через панель Properties )
свойство ScrollButton компонентов
на разные значения в Form2namespace 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;
}
}
}
Запустите
приложение и посмотрите на результат, который будет таким
Теперь мы готовы сконструировать аналог библиотечного компонента NumericUpDown - поле со списком, только с горизонтальным расположением
скролирующих кнопок.
В панели Solution
Explorer вызовите контекстное меню
для узла UserControls и
добавьте к проекту командой Add/User
Control форму
для размещения нового комбинированного компонента с именем NumericScan.
Отметьте для себя, что в качестве базового в заготовке класса
наследуется UserControl
Перенесите
на форму NumericScan.cs в режиме View Designer из
панели Toolbox два наших компонента ArrowButton и
один библиотечный компонент TextBox. На
данном этапе особо не заботьтесь о размещение экземпляров
компонентов на форме. Мы это действие выполним позже программным
способом. А пока расположите объекты компонента примерно
так
Далее мы часть настроек выполним декларативно через панель Properties, а часть - программно в конструкторе компонента.
Через
панель Properties выполните декларативные
настройки объектов в соответствии с приведенной таблицей
свойств| Тип | Свойство | Значение | Пояснения |
|---|---|---|---|
TextBox |
Name | txtBox | Имя экземпляра компонента текстового поля |
| TextAlign | Right | Расположение текста | |
ArrowButton |
Name | btnLeft | |
| ScrollButton | Left | Ориентация стрелки влево | |
ArrowButton |
Name | btnRight | |
| ScrollButton | Right | Ориентация стрелки вправо ( по умолчанию ) |
Через
панель Properties в режиме Events зарегистрируйте
обработчики для составляющих объектов в соответствии с таблицей| Объект | Событие | Обработчик | Пояснения |
|---|---|---|---|
txtBox |
TextChanged |
TextBoxOnTextChanged |
Возбуждается при завершении ввода клавишей Enter или потери фокуса |
KeyDown |
TextBoxOnKeyDown |
Возбуждается при нажатии любой клавиши, когда объект имеет фокус ввода | |
btnLeft |
|
ButtonOnClick |
Возбуждается при шелчке на кнопке курсором мыши |
btnRight |
|
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;
}
Когда компонент теряет фокус, возбуждается событие ,
которое инициируется методом диспетчеризации 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 контекстного меню откомпилируйте готовый компонент
Поместите
из панели 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 экземпляры компонентов NumericScanpublic Form3()
{
InitializeComponent();
numericScan1.DecimalPlaces =
numericScan2.DecimalPlaces = 2;
numericScan1.Increment =
numericScan2.Increment = 0.01M;
}
Запустите " Упражнение
3 " и
испытайте работу кода. Поизменяйте значения текстового
поля клавиатурой и мышью, а также попользуйтесь клавишей Tab для
смены фокуса ввода, испытав работу OnLeave().Замечание. Текстовое поле, ожидающее клавиатурный ввод числа
типа , символом разделителя дробной части считает запятую.
Внешний результат будет таким

В этом упражнении мы проиллюстрируем использование компонентов
как классов с динамическим созданием их экземпляров прямо в
коде, без использования System.Windows.Forms.TableLayoutPanel,
который представляет аналог невидимой таблицы для NumericScan.
Экземпляр этого расширения будет представлять панель управления
для искажения текста, который будет помещен в панели отображения.
Искажение текста определяется свойством контекста
устройства, представленного объектом .
Это свойство имеет тип из пространства
имен System.Drawing.Drawing2D. Экземпляр класса перед
рисованием текста мы будем создавать с параметрами, которые
пользователь будет задавать в объектах NumericScan панели
управления. Для обмена данными между элементами управления
и матрицей преобразования предусмотрим в классе-расширении
свойство .
В панели 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. которая имеет
следующий вид

При создании экземпляра класса с помощью конструктора
без параметров создается 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", его окно в
запущенном состоянии имеет, примерно, такой вид
Продемонстрируйте
преподавателю все упражнения выполненной работы
Для получения официальных документов о завершении программы дополнительного профессионального образования (удостоверения о повышении квалификации, дипломов о профессиональной переподготовке и MBA) необходимо предоставить:
Внимание! Вы можете не заказывать доставку бумажной версии официального документы, а скачать его в электронном виде и распечатать самостоятельно. Информация о выданном документе в течение 1 месяца загружается в Федеральную информационную систему «Федеральный реестр сведений о документах об образовании и (или) о квалификации, документах об обучении» - ФИС ФРДО.
Доступ на новый сайт осуществляется с использованием адреса электронной почты, который был указан вами при регистрации на "старом". Мы постарались перенести все ваши данные с прежнего ресурса, однако не исключена вероятность потери части информации.
При возникновении проблемы со входом, воспользуйтесь функцией сброса пароля
Если вы обнаружите несоответствия, пожалуйста, сообщите нам.