Как сделать собственный составной элемент управления (composite control) в WinForms

Задача: сделать нестандартный элемент управления (control) для Windows Froms. Какой именно? Есть несколько разных вариантов, темой этой статьи будет собственный DateTimePicker, способный работать с пустыми датами. В качестве пустых дат будет активно использоваться описанный ранее класс DatePlus. Что мы хотим? По сути дела нам нужен Masked TextBox под ввод даты, кнопка для вызова всплывающего календаря и ряд служебных функций - упакованные в одну простую оболочку. Плюс на кнопке должна быть стрелка, разворачивающаяся вверх или вниз в зависимости от наличия календаря.

Добрый MSDN говорит, что нестандартные управляторы бывают трех типов:

- составные (composite), совмещающие несколько уже существующих, делаются наследованеим от UserControl
- расширенные, добавляющие что-то новое в существующий control, делаются наследованием от соотвествующего класса
- нестандартные (custom), в которых переделывается отрисовка на экран

Очевидно, что нам нужен составной элемент управления. Последний кусочек теории перед переходом к коду - с точки зрения операционной системы каждый элемент Windows Forms представляет собой отдельное окно. То есть у нас есть главное окно - форма, внутри которого сидят окна поменьше, внутри которых могут сидеть еще более мелкие окна (например GroupBox и несколько кнопок внутри).

В студии мы можем просто добавить в проект "Пользовательский элемент управления" и получить дизайнер, очень похожий на дизайнер обычной формы и кинуть на него MaskedTextBox, кнопку и календарь. По клику на кнопке должен появляться и исчезать календарь - добиться этого можно простым изменением размера control'а.

date-plus-picker

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

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

calendar-form

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

Point parentScreenPoint = this.Parent.PointToScreen(new Point(this.Location.X, this.Location.Y + this.Height)); 
_calendarForm.Location = parentScreenPoint;

Автозакрытие имитируется очень просто - вешаем вызов Hide на событие Deactivate

Так же на стоит забывать, что в MonthCalendar по умолчанию выбирается диапазон дат, а нам нужна только одна - при выборе даты диапазон надо схлопывать. Удобнее всего делать это изнутри формы.

private void Calendar_DateSelected(object sender, DateRangeEventArgs e)
{
    Calendar.SelectionStart = Calendar.SelectionEnd;
    this.Hide();
}

Отлично. Все не просто всплывает и вылезает, но еще и открывается-закрывается. Начинаем тестировать с разным размером шрифта на форме и сталкиваемся с еще одним интересным глюком - MaskedTextBox масштабируется непропорционально своему содержимому. Проще говоря при увеличении размера шрифта строка содержимого перестает в него влезать и обрезается (можно конечно прокрутить до конца курсором). Лечится этот глюк и вовсе неожиданным способом - установкой прозрачного фонового цвета общего UserControl-контейнера.

Еще один глюк можно будет увидеть прямо в дизайнере - при копировании элемента через Ctrl + перетаскивание мышью базовый контейнер нашего элемента управления будет растягиваться. Кроме того его сложно позиционировать относительно других элементов. Лечим следующим образом:

1. Обнуляем в внутренних элементов свойство Margin редактируем Location чтобы поставить их вплотную к границам и поджимаем сам UserControl

2. Ставим AutoScaleMode в Front, AutoSize -> true, AutoSizeMode -> GrowAndShrink

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

_calendarForm.Font = new Font(_calendarForm.Font.Name, this.Font.Size);

Что нам осталось? Стрелка на кнопке! Придется менять картинку во время выполнения программы и проще всего это сделать, добавив в элемент управления ImageList через дизайнер и там же загрузить картинки. Код для смены тривиален.

_calendarForm.VisibleChanged += new EventHandler(_calendarForm_VisibleChanged);
// .... 
void _calendarForm_VisibleChanged(object sender, EventArgs e)
{
    _calendarForm.Font = new Font(_calendarForm.Font.Name, this.Font.Size);
    if (_calendarForm.Visible)
    {
        _calendarButton.BackgroundImage = _arrowsImageList.Images["arrow-01-up.png"];
    }
    else
    {
        _calendarButton.BackgroundImage = _arrowsImageList.Images["arrow-01-down.png"];
    }
}

Чем больше свободы, тем больше проблем. Если пользователь вводит дату текстом, то он может ввести ее неправильно! Причем встает отдельный вопрос о том, что надо считать правильным - например можно ли пользователю вводить дату из будущего? Иногда можно иногда нельзя. Поэтому имеет смысл ввести отдельный параметр для регулировки ввода даты из будущего и вывести его в панель свойств дизайнера VisualStudio

private bool _allowFuture = false;

[Description("AllowFuture"), Category("Behavior")] 
public bool AllowFuture
{
    get { return _allowFuture; }
    set { _allowFuture = value; }
}

Добавить функцию для проверки даты на правильность.

public bool DateCorrect
{
    get
    {
        bool dateCorrect = false;

        dateCorrect = DatePlus.DateCorrect(EnteredDateString);

        if (dateCorrect  && !AllowFuture)
        {
             DatePlus date = new DatePlus(EnteredDateString);
             if (date.InFuture)
             {
                 dateCorrect = false;
             }
        }

        return dateCorrect;
    }
}

И наконец корректно обработать проставление даты в календарь, учитывая что диапазон дат календаря тоже ограничен и если дата вылезает за его пределы, то в календарь ее ставить не надо

if (DateCorrect && !Empty && Date.DateTime > _calendarForm.Calendar.MinDate && Date.DateTime < _calendarForm.Calendar.MaxDate)
{
                _calendarForm.Calendar.SelectionStart = Date.DateTime;
                _calendarForm.Calendar.SelectionEnd = Date.DateTime;
}

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

if (!DateCorrect && !_wrongDateBox.Visible)
{
    _wrongDateBox.ShowDialog();
    _dateMaskedTextBox.Focus();
    _dateMaskedTextBox.SelectAll();
}

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

Чтобы собрать все вместе приведу тестовый проект WinForms со всеми вышеописанными классами, собранными в отдельный dll и тестовой формой

date-plus-picker-full

DatePlusPickerTest.zip

Developing Custom Windows Forms Controls with the .NET

Developing a Composite Windows Forms Control

Writing your Custom Control: step by step


Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *


*

Можно использовать следующие HTML-теги и атрибуты: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>