Задача: сделать нестандартный элемент управления (control) для Windows Froms. Какой именно? Есть несколько разных вариантов, темой этой статьи будет собственный DateTimePicker, способный работать с пустыми датами. В качестве пустых дат будет активно использоваться описанный ранее класс DatePlus. Что мы хотим? По сути дела нам нужен Masked TextBox под ввод даты, кнопка для вызова всплывающего календаря и ряд служебных функций - упакованные в одну простую оболочку. Плюс на кнопке должна быть стрелка, разворачивающаяся вверх или вниз в зависимости от наличия календаря.
Добрый MSDN говорит, что нестандартные управляторы бывают трех типов:
- составные (composite), совмещающие несколько уже существующих, делаются наследованеим от UserControl
- расширенные, добавляющие что-то новое в существующий control, делаются наследованием от соотвествующего класса
- нестандартные (custom), в которых переделывается отрисовка на экран
Очевидно, что нам нужен составной элемент управления. Последний кусочек теории перед переходом к коду - с точки зрения операционной системы каждый элемент Windows Forms представляет собой отдельное окно. То есть у нас есть главное окно - форма, внутри которого сидят окна поменьше, внутри которых могут сидеть еще более мелкие окна (например GroupBox и несколько кнопок внутри).
В студии мы можем просто добавить в проект "Пользовательский элемент управления" и получить дизайнер, очень похожий на дизайнер обычной формы и кинуть на него MaskedTextBox, кнопку и календарь. По клику на кнопке должен появляться и исчезать календарь - добиться этого можно простым изменением размера control'а.
Здесь появляется первая проблема - стандартный календарь не выходит за пределы родительской формы. То есть если мы расположим наш замечательный датавзятор рядом с нижним или правым краем формы, календарь обрежется. Хочется, чтобы он красиво парил в виртуальном воздухе подобно всплывающему меню. Кликаем на кнопку - появляется, кликаем на кнопку или вне ее - исчезает. Вот в этих кликах собака и зарыта.
На Stackoverflow нам советуют наследоваться от базового элемента всплывающего меню ToolStripDropDown. Все замечательно всплывает и вылезает, но возникает масса проблем с автоматически закрытием всплывающего меню - оно запрятано где-то очень глубоко в недрах системы. Отслеживать его надо чтобы корректно отрабатывать ручное открытие и закрытие при клике на кнопку. Автозакрытие ведет себя очень странно и нелогично, последней каплей для меня стало то, что клике на кнопку оно происходит то раньше то позже собственно события клика на кнопку. В итоге я плюнул на извращенный код обхода всех этих глюков и нарисовал в дизайнере мини-форму с календарем внутри, которую уже и показываю/закрываю по клику.
Но куда именно это форму выводить? Ведь местоположения нашего управлятора мы заранее не знаем. Координаты для вывода формы придется пересчитывать из относительных кординат внутри родительского окна для контрола и обратно.
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 и тестовой формой
Developing Custom Windows Forms Controls with the .NET
А как внести данные в элемент управления программно?
Например мне нужно выгрузить данные с SQL
А как выгрузить данные в элемент программно?
Например с SQL.