Текст
                    Чарльз Петцольд
с использованием
Microsoft*
Windows Forms
Новые возможности технологии Windows Forms
РУССКАЯ РЕДАКЦИЯ Microsoft £^ППТЕР
Программирование


Charles Petzold Programming Microsoft Windows Forms Microsoft Press
Чарльз Петцольд Программирование с использованием Microsoft* Windows Forms МАСТЕР-КЛАСС Москва * Санкт-Петербург * Нижний Новгород * Воронеж Новосибирск * Ростов-на-Дону * Екатеринбург * Самара Киев * Харьков * Минск 2006
УДК 004.45 ББК 32.973.26-018.2 П23 Петцольд Ч. П23 Программирование с использованием Microsoft Windows Forms. Мастер-класс / Пер. с англ. — М.: Русская Редакция; СПб.: Питер, 2006. — 432 стр.: ил. ISBN 5-7502-0284-4 ISBN 5-91180-041-1 В этой книге подробно рассказывается о создании программ для Microsoft Windows с использованием языка С# и библиотеки классов Windows Forms, входящей в Microsoft .NET Framework 2.0. Вы научитесь создавать новые нестандартные и комбинировать существующие элементы управления, а также разрабатывать панели инструментов, меню и строки состояния, используя появившиеся в .NET Framework 2.0 новинки, узнаете о новом механизме динамического размещения элементов управления на форме и о привязке элементов управления к данным. Книга состоит из 7 глав и адресована программистам, стремящимся освоить новые возможности, связанные с выходом .NET Framework 2.0, а также разработчикам, имеющим значительный опыт программирования для Windows и желающим максимально быстро и эффективно освоить Windows Forms. УДК 004.45 ББК 32.973.26-018.2 Подготовлено к печати по лицензионному договору с Microsoft Corporation, Редмонд, Вашингтон, США. Microsoft, ActiveX, IntelliSense, JScript, Microsoft Press, MSDN, MS-DOS, Visual Basic, Visual C#, Visual Studio, Win32, Windows и Windows Media являются товарными знаками или охраняемыми товарными знаками корпорации Microsoft в США и/или других странах. Все другие товарные знаки являются собственностью соответствующих фирм. Все названия компаний, организаций и продуктов, а также имена лиц, используемые в примерах, вымышлены и не имеют никакого отношения к реальным компаниям, организациям, продуктам и лицам. О Оригинальное издание на английском языке, ISBN 0-7356-2153-5 (англ.) Charles Petzold, 2006 ISBN 5-7502-0284-4 («Русская Редакция») ° ПеРевод ш Русский язъщ Microsoft CoiP°ration>2006 О Оформление и подготовка к изданию, издательство ISBN 5-91180-041-1 («Питер») «Русская Редакция,, 2006
Введение XI ГЛАВА 1 Создание приложений 1 Обзор 1 Инструменты программирования 3 Документация 4 Разработка приложений 5 Самые короткие программы 5 Проекты Visual Studio 7 Ссылки 9 От консольных к Windows-приложениям 10 Убираем шероховатости 12 События и обработчики 13 Наследники класса Form 17 Свойства и события в Visual Studio 21 Наследование класса Form 24 Создание подклассов элементов управления 28 Аппаратно-независимое кодирование 31 Информация о сборке 36 Диалоговые окна 37 DLL-библиотеки 47 ГЛАВА 2 Этот плодовитый класс Control! 51 Обзор элементов управления 5 2 Родители и потомки 53 Видимость и отклик 5 5 Расположение и размер 5 5 Шрифты и цвет 56 Отслеживание элементов управления 57
Списки изображений (ImageList) 58 Всплывающие подсказки (ToolTip) 59 Статические (и не совсем) элементы управления 59 Группа элементов управления (GroupBox) 60 Метка (Label) 60 Метка-ссылка (LinkLabel) 61 Графическое окно (PictureBox) 64 Индикатор хода процесса (ProgressBar) 64 Кнопки и двоичные переключатели 65 Кнопка (Button) 66 Флажок (CheckBox) 66 Переключатель (RadioButton) 61 Полоса прокрутки 69 Горизонтальная и вертикальная полосы прокрутки (ScrollBar) 70 Ползунок (TrackBar) 72 Элементы управления с поддержкой редактирования текста 72 Текстовое окно с маской (MaskedTextBox) 73 Текстовое поле (TextBox) 74 Поле ввода с форматированием (RichTextBox) 74 Список и поле со списком 76 Список (ListBox) 76 Список с флажками (CheckedListBox) 78 Поле со списком (ComboBox) 79 Абстрактный класс наборного счетчика (UpDownBase) 80 Числовой наборный счетчик (NumericUpDown) 80 Счетчик выбора из диапазона (DomainUpDown) 81 Дата и время 81 Календарь на месяц (MonthCalendar) 82 Элемент выбора даты и времени (DateTimePicker) 83 Древовидное и списковое представление 87 Древовидное представление (TreeView) 87 Списковое представление (ListView) 95
ГЛАВА 3 Панели и динамическое размещение 103 Разные способы решения задачи размещения элементов управления 103 Сложности с размещением 105 Свойство AutoSize 106 Панели и контейнеры 109 Стыковка и привязка 110 Стыковка ПО Простые панели 113 Привязка 115 Разделители 118 Свойства Padding и Margin 120 Размещение в панели FlowLayoutPanel 122 Стыковка и привязка в панели FlowLayoutPanel 123 Прощай, GroupBox 130 Панели TableLayoutPanel 137 Автоматическое расширение таблицы 137 Позиции ячеек 143 Стили строк и столбцов 145 Свойства Dock и Anchor 146 Диапазоны столбцов и строк 150 Пример программы: диалоговое окно выбора шрифта 150 Тестирование размещения 162 ГЛАВА 4 Пользовательские элементы управления 163 Совершенствование существующих элементов управления 164 Переопределение методов 164 Добавление новых свойств 169 Прорисовка элементов управления 172 Комбинирование готовых элементов управления 179 Это сладкое слово — «автопрокрутка» 195 Создание элементов управления «с нуля» 202 Интерактивная линейка 202 Сетка выбора цветов 223
ГЛАВА 5 Меню и панели инструментов 233 Меню и его команды 234 Элементы меню: общая картина 237 Сборка меню 239 Поля и обращение к элементам меню 244 Элементы управления, элементы меню и владельцы 245 Установка и снятие флажков 247 Добавление изображений 252 Нестандартные элементы меню 260 Контекстное меню 2б4 Панели инструментов и их компоненты 267 Кнопки на панели инструментов 268 Элементы управления как элементы ToolStrip 269 Панель инструментов для форматирования текста 269 Создание нескольких панелей инструментов 283 Строка состояния 284 Метки состояния 285 ГЛАВА 6 Привязка и представление данных 289 Связывание элементов управления и данных 289 Как это работает 291 Элемент управления меняет данные 292 И снова о ColorScroll 296 Отличительные особенности ComboBox 300 Основы ввода данных 305 Традиционный подход 305 Сериализация в XML 308 Когда привязка не работает 313 Посредничество BindingSource 315 Просмотр данных 320 Работаем с данными 325 Элемент управления DataGridView 325 DataGridView и текст 325 Иерархия классов 329 Расширяем возможности по работе с данными 330
Сохранение в XML-формате 334 Проверка корректности данных и инициализация 337 Реализация столбца Calendar 340 DataGridView и привязка данных 341 ГЛАВА 7 Два настоящих приложения 347 Пример LControlExplorer 348 Иерархия классов Control 349 Свойства «только для чтения» 351 Динамический перехват события 355 Обертка 363 Установка ClickOnce 370 Вопросы безопасности 373 Публикация приложения 374 Пример 2: MdiBrowser 375 MDI-интерфейс 376 Решение и проект 377 Избранное и параметры настройки 378 Дочернее окно 381 Форма приложения 383 Меню File 387 Меню View 393 Меню Favorites 396 Меню Window 398 Меню Help .• 400 Два элемента управления ToolStrip 402 Справочная система в формате HTML Help 407
Введение В этой книге рассказывается о создании программ для Microsoft Windows — обычных автономных Windows-приложений или внешних интерфейсных компонентов распределенных прикладных систем (их еще называют клиентскими приложениями). Есть несколько способов создания таких программ. Здесь рассказывается об одном из методов программирования — с использованием языка С# и библиотеки классов Windows Forms, которая входит в каркас Microsoft .NET Framework. Много внимания уделяется расширениям Windows Forms, которые появились вместе с выходом .NET Framework 2.0 осенью 2005 года. Эта книга дополняет (а не заменяет собой) ранее написанную мной книгу по программированию с использованием Windows Forms — Programming Microsoft Windows with C# (Microsoft Press, 2002) [Программирование для MS Windows на С#. M.: Русская Редакция, 2002]. Предыдущая книга в три раза объемнее этой и детально описывает программирование с применением Windows Forms в .NET Framework версий 1.0 и 1.1 — эта информация остается полностью в силе и для .NET Framework 2.0. Кому адресована эта книга Я ориентировался на две основных группы читателей: ■ уже знакомых с Windows Forms (возможно, после прочтения моей предыдущей книги). Этой группе рекомендую считать эту книгу простым продолжением предыдущей. Можно опустить главы 1 и 2 и стразу окунуться в главу 3 с ее захватывающим новым миром динамического размещения. Начиная с главы 3, книга содержит совершенно новый материал о новинках, появившихся в Windows Forms с выходом .NET Framework 2.0; ■ имеющих значительный опыт программирования для Windows (например, с использованием MFC-классов или «родного» API-интерфейса Windows) и желающих максимально быстро и эффективно освоить Windows Forms. Я считаю эту книгу одним из элементов программы совершенствования и «корректировки» методов программирования с использованием Windows Forms, и не только потому, что она небольшая и лаконичная, но и по той причине, что усовершенствования Windows Forms в .NET Framework 2.0 значительно упростили разработку и развертывание Windows-приложений, которые стали мощнее и понятнее.
XII Введение Конечно, в такой короткой книге невозможно рассказать обо всех нюансах программирования с применением Windows Forms. В частности, пришлось отказаться от программирования графики — подробно об отображении форматированного текста, векторной графики и точечных рисунков рассказывается в предыдущей моей книге. (К слову сказать, на Programming Microsoft Windows with С* часто жалуются, что в книге слишком много графики!) Также, я бы посоветовал обратиться к предыдущей книге мало знакомым с основами программирования с применением Windows Forms — тем, кому показалось, что в этом издании слишком мало сведений об основах этой технологии. В книге совсем немного общей информации о языке программирования С#. Программисты, уже знакомые с C++ и Java, смогут быстро освоить С#. Остальных отсылаю к одному из имеющихся на рынке учебников по С#. Моя книга Programming in the Key of С* (Microsoft Press, 2003) [Программирование в тональности С#. М.: Русская Редакция, 2004] ориентирована на новичков, изучающих С# в качестве своего первого языка программирования, но она может быть полезной и опытным программистам. Структура книги Первые две главы содержат очень краткий обзор основных принципов и инструментов программирования с использованием Windows Forms. Читатели, имеющие опыт такого программирования могут пропустить эти главы. В главе 3 рассказывается о поддержке нового механизма динамического размещения в .NET Framework 2.0. Новые элементы управления FlowLayoutPanel и Table- LayoutPanel теперь позволяют проектировать формы и диалоговые окна, явно не указывая координаты и размеры элементов управления на формах. Некоторые читатели книги Programming Microsoft Windows with C# [Программирование для MS Windows на С#. М.: Русская Редакция, 2002] жаловались на недостаточный объем материала о создании нестандартных (пользовательских) элементов управления. Поэтому в этой книге я отвел этой теме целую главу — это глава 4. Имеющиеся в .NET Framework 1.0 меню, инструментальная панель и строка состояния прекрасно работают и в .NET Framework 2.0, но в версии 2.0 появились два новых элементах управления — MenuStrip, ToolStrip и StatusStrip. О них и пойдет речь в главе 5. В Programming Microsoft Windows with С* ничего не говорится о привязке элементов управления к данным. Я восполнил этот недостаток в главе 6, а также описал два новых элемента управления для работы с данными — DataGridView и Binding- Navigator.
Введение XIII В главе 7 все описанное в предыдущих главах используется для демонстрации возможностей .NET Framework 2.0 на примере двух «настоящих» приложений: Web-браузера и средства для исследования возможностей элементов управления Windows Forms. Я был недавно поражен, увидев следующее примечание во введении одной из книг, вышедших в издательстве Microsoft Press1: «Чтобы максимально сократить объем кода примеров в этой книге, мы написали их в петцольдовском стиле, то есть без использования конструкторов форм Microsoft Visual Studio .NET». Эта книга также написана в «петцольдовском стиле». Я считаю, что программисты должны учиться самостоятельно писать свой код, не полагаясь на средства автогенерации кода в Visual Studio. Технология Windows Forms значительно облегчила создание приложений с нуля. Более того, одна из основных функций Visual Studio — предоставление программисту средств проектирования форм или диалоговых окон, позволяющих обойтись без явного определения координат и размеров — стала фактически ненужной благодаря новым возможностям динамического размещения в .NET Framework 2.0. Системные требования Для сборки и исполнения примеров этой книги вам потребуются следующее оборудование и ПО: ■ Microsoft Windows XP с пакетом исправлений Service Pack 2, Microsoft Windows Server 2003 с Service Pack 1 или Windows 2000 с Service Pack 4; ■ Visual Studio 2005 Standard Edition или Visual Studio 2005 Professional Edition2; ■ процессор Pentium с частотой 600 МГц или аналогичный (рекомендуется частота 1 ГГц); ■ 192 Мбайт оперативной памяти (рекомендуется 256 Мбайт или больше); ■ монитор с разрешением не менее 800 х 600 и количеством цветов не менее 256 (рекомендуется разрешение 1024 х 768 и качество цветопередачи в 16 бит); ■ привод компакт- или DVD-дисков; ■ мышь или другое совместимое указательное устройство. 1 Rob Jarrett, Philip Su, Building Tablet PC Applications, Microsoft Press, 2002. 2 Для большинства примеров подойдет и Microsoft Visual C# 2005 Express Edition, однако в этом продукте отсутствует ряд компонентов (например, набор значков и растровых картинок), которые нужны для работы с остальными примерами. Скачав и установив .NET Framework 2.0 Software Development Kit, вы сможете компилировать программы Windows Forms даже в командной строке MS-DOS или использовать другие среды разработки, например мою программу «Key of С#», которую можно скачать с моего сайта (см. главу 1).
XIV Введение Предварительная версия ПО Эта книга редактировалась и тестировалась в представленной в августе 2005 года версии Community Technical Preview (CTP) среды Visual Studio 2005, которая была последней оценочной версией перед выпуском коммерческой версии Visual Studio 2005. Считается, что материал этой книги полностью верен и в отношении последнего выпуска Visual Studio 2005. Любые обнаруженные неточности или замечания в отношении этой книги будут собраны и опубликованы в виде отдельной статьи базы знаний Microsoft Knowledge Base. Подробнее см. раздел «Материалы издательства Microsoft Press» ниже. Обновление технологий По мере обновления упомянутых в этой книге технологий будут обновляться ссылки на Web-странице Microsoft Press Technology Updates {http://vuwwmicrosoft.com/mspress/ updates). Рекомендуем периодически обращаться к этой странице за обновлениями Visual Studio 2005 и других технологий Microsoft. Вспомогательные материалы Все приведенные в этой книге примеры кода можно загрузить со специальной страницы на сайте издательства Microsoft Press: http://www.microsoft.com/mspress/ companion/0 - 7356-2153 -5- Материалы издательства Microsoft Press Я предпринял все усилия, чтобы обеспечить точность информации, содержащейся в этой книге и во вспомогательных материалах. По мере накопления корректировок или поправок к книге они будут публиковаться в отдельной статье базы знаний Microsoft Knowledge Base (http://support.microsoft.com/kb/905047). Издательство Microsoft Press постоянно обновляет список исправлений и дополнений к своим книгам, он доступен на Web-узле hUp://ivww.microsoft.com/learning/support/books. Вопросы и комментарии Если у вас есть комментарии, вопросы или идеи по этой книге, направляйте их в издательство Microsoft Press по одному из перечисленных ниже адресов. Почтовый адрес: Microsoft Press Attn: Programming Microsoft Windows Forms Editor One Microsoft Way Redmond,WA 98052-6399
Введение XV Электронная почта: mspinput@microsoft.com Учтите, что по указанным почтовым адресам техническая поддержка не предоставляется. Web-сайт автора Информация об этой книге (и остальных моих книгах) также доступна на моем Web-сайте http://www.charlespetzold.com/. Там же вы найдете материалы по программированию, математике и истории — моих хобби, которыми я занимаюсь в редкие часы, свободные от написания книг. Благодарности Я содрогаюсь всякий раз, когда слышу о грядущей «революции» в издательском деле — говорят, что автор сможет обойтись без традиционных издательств, публикуя свои труды непосредственно на Web-сайте. Неужели эти «прорицатели» не понимают важность работы, выполняемой редакторами? Огромную помощь в том, что мои проза и код стали пригодны для широкой аудитории, мне оказали Валери Вулли (Valerie Woolley), Роберт Лайон (Robert Lyon) и Роджер ЛеБланк (Roger LeBlanc). Вместе с тем понятно, что вся вина за любые ошибки или пробелы в конечном итоге лежит на мне. Я также хочу сказать «спасибо» трем самым важным женщинам в моей жизни. В порядке возрастания важности это: мой литературный агент Клодетт Мур (Claudette Moore) из агентства Moore Literary Agency, моя мать и моя невеста Диердр (Deirdre). Чарльз Петцольд г. Нью-Йорк Сентябрь, 2005 г.
Глава 1 Создание приложений Платформа Microsoft .NET — это набор программных технологий, призванных упростить разработку современных Web- и Windows-приложений. В этой книге рассматривается часть платформы .NET под названием Windows Forms, используемая для написания программ, которые обычно называют клиентскими Windows-приложениями (альтернативные названия: «прикладное ПО рабочего стола» или просто «стандартные Windows-приложения»). Эти приложения состоят из обычных исполняемых файлов (с расширением .ехе), иногда в них входят файлы динамических библиотек (с расширением .dll) и другие необходимые файлы (например, файлы справочной системы). Windows Forms считается современной альтернативой прежних методов создания Windows-приложений, таких как использование языка программирования С и «родного» 32-битного интерфейса прикладного программирования для Windows (Win32 API), а также программирование на C++ с использованием классов библиотеки MFC (Microsoft Foundation Classes). Обзор Windows Forms реализована в нескольких DLL-библиотеках, составляющих платформу .NET Платформа .NET должна присутствовать и на компьютере, где разрабатываются программы Windows Forms, и на машинах, где они выполняются. Различные версии платформы .NET можно скачать с Web-сайта Microsoft http://msdn. microsoft.com/netframework/downloads/updates. Программы этой книги основаны на платформе .NET Framework 2.0, выпущенной в 2005 году. Хотя программы на основе Windows Forms внешне более привычны для пользователей Windows, у них есть одно важное отличие от Win32- и MFC-приложений: файлы Windows Forms содержат не микропроцессорный машинный код, а двоичные инструкции на языке MSIL (Microsoft Intermediate Language), который играет роль универсального языка сборки. (Самые смелые программисты пишут приложения сразу на MSIL.) В процессе выполнения программы компонент платформы .NET под названием Common Language Runtime (CLR) преобразует промежуточный язык в машинный микропроцессорный код и подключает все нужные DLL-библиотеки .NET
2 ГЛАВА 1 Компиляция программы в процессе выполнения, в принципе, позволяет приложениям .NET не зависеть от платформы. Операционная система также защищена от ошибочного или случайного кода, который нечаянно или преднамеренно может вывести из строя клиентский компьютер. Например, исполняющая среда .NET может отслеживать обращение конкретного приложения Windows Forms к локальным файлам и немедленно оповещать об этом пользователя. Поэтому теперь можно без опаски запускать исполняемый файл Windows Forms напрямую из Интернета. Использование промежуточного языка — это одно из проявлений более общего явления -•■ управляемого кода (managed code). Важную роль также играют строгий контроль типов и жесткие ограничения на использование указателей. Приложения .NET структурированы, что позволяет CLR-среде выполнять сборку мусора, то есть при недостатке памяти удалять все объекты, на которые нет ссылок. Программистам больше не нужно беспокоиться об утечке памяти. При необходимости в программах Windows Forms можно использовать Win32 API, но чаще всего библиотек .NET Windows Forms вполне хватает. Большинство этих библиотек — это наборы объектно-ориентированных классов, содержащих обычные поля и методы, а также члены классов — свойства (properties) и события (events). (Подробнее о них — чуть ниже). Ясно, что язык программирования, в котором используются эти классы, должен поддерживать свойства и события, а также все базовые типы, используемые в этих классах (целые числа, числа с плавающей точкой, строки и др.). Строки и массивы должны храниться в нем так же, как и в соответствующих классах. Это значит, что строки не завершаются символом с нулевым кодом и содержат информацию о длине строки, а размер массивов фиксирован. Из сказанного выше следует, что язык программирования, используемый для создания приложений Windows Forms, должен отвечать спецификации .NET Common Language Specification. Например, C++ пригоден для этих целей только при наличии управляемых расширений (managed extensions), что, увы, не очень-то повысило его шансы. Более тесно интегрированы с платформой .NET языки С# и Microsoft Visual Basic .NET. Язык С# (произносится «си шарп») основан на синтаксисе С и C++, но разработан с учетом требований CLS. Это стандартный (универсальный) язык программирования для .NET, и именно его мы будем использовать. Есть несколько книг, которые помогут программистам, работающим на С и C++, освоить С#. Моя книга «Programming in the Key of C#» [Программирование в тональности С#. М.: Русская Редакция, 2004] адресована новичкам, но может пригодиться и опытным программистам. Программисты, работающие на С#, должны освоить спецификацию С# Language Specification — официальное формальное описание языка. Ее можно найти на Web- странице Microsoft, посвященной С# (http://msdn.microsoft.com/library/en-us/cscon/ html/vcoriCStartPagexisp), или в книге «The C# Programming Language» (Anders Hejlsberg, Scott Wiltamuth Peter Golde, издательство Addison-Wesley, 2003). В справочнике «С# Programmer's Reference» (который также можно скачать с указанной Web-страни-
Создание приложений 3 цы) та же информация изложена в более простой и доступной форме, что облегчает поиск нужного раздела. Выбрав С#, я ничуть не умаляю достоинства других языков программирования для .NET При работе с .NET выбор языка программирования всецело зависит от самого программиста. Например, программист, работающий с Visual Basic .NET и имеющий общее представление о С#, сможет преобразовать весь код примеров этой книги. Microsoft предлагает еще два языка, поддерживающих CLS, — J# и Jscript; они основаны на других популярных языках программирования сторонних разработчиков, и название этих языков также начинается с буквы «J». Инструменты программирования Большинство программистов создает приложения Windows Forms с помощью Microsoft Visual Studio, развитой интегрированной среде разработки с длинной историей. Visual Studio 2005 — первая версия Visual Studio, поддерживающая платформу .NET 2.0. Microsoft также предлагает урезанные версии (express edition), с поддержкой отдельных языков, в том числе Visual C# и Visual Basic .NET. Использовать Visual Studio совсем необязательно, более того — в длительной перспективе, чем реже вы будете полагаться на возможности этой среды, тем выше будет ваша квалификация в области программирования для .NET. Visual Studio не притупляет мышление, однако из-за автоматической генерации кода технология Windows Forms кажется сложнее и загадочнее, чем на самом деле. И не исключено, что программист не справится с такой рядовой задачей, как добавление пункта меню во время выполнения программы, потому что ему никогда не приходилось самостоятельно писать код для создания меню. Некоторые — от бедности ли, из принципа или просто так — создают программы Windows Forms полностью в обычном текстовом редакторе (например, в блокноте) и компилируют в командной строке MS-DOS. Для этого нужно всего лишь скачать и установить пакет для разработки программ .NET (.NET Framework SDK) с указанной ранее Web-страницы. SDK распространяется бесплатно вместе с документацией и инструментами программирования, в том числе компиляторами командной строки esc (для С#) и vbc (для Visual Basic .NET). Есть и другие варианты. Microsoft предлагает компилятор С#, реализованный в виде DLL-библиотеки .NET. Он позволяет программистам создавать собственные среды разработки для С#, более простые и не такие масштабные, как Visual Studio, например «Key of С#» (ее можно бесплатно скачать с Web-сайта автора: http:// www.cbarlesspetzold.com/keycs). Для работы с «Key of C#» на компьютере должна присутствовать платформа .NET (устанавливать пакет .NET Framework SDK необязательно). Это значит, что на любой компьютер с платформой .NET можно скачать «Key of C#» и программировать в .NET, не устанавливая .NET Framework SDK или Visual Studio («Key of C#» устанавливается и удаляется в мгновение ока).
4 ГЛАВА 1 В Visual Studio можно запретить автоматическую генерацию кода, чтобы создавать код самому и при этом пользоваться некоторыми популярными функциями Visual Studio, например Microsoft InteiliSense, которая помогает запоминать имена методов, свойств и событий, но об этом чуть позже. Документация Вместе с Visual Studio и пакетом .NET Framework SDK устанавливается программа с технической документацией для платформы .NET. Она также доступна в библиотеке MSDN по адресу {http://msdnmicrosoft.com/library). Прежде всего, документация содержит описание всех классов, доступных для использования в приложениях .NET К сожалению, местоположение этих важных сведений в иерархической структуре документации часто зависит от продукта и его версии. Обычно их следует искать по оглавлению в разделах «.NET Framework» и «Class Library». Искомый раздел содержит длинный алфавитный список пространств имен (namespace), большинство из которых начинается со слова System (а некоторые — со слова Microsoft). Пространства имен делят все классы платформы .NET на функциональные группы, что упрощает поиск нужного класса. Но это не главное их предназначение. Имена классов в пространстве имен уникальны, но могут повторяться в других пространствах имен. Например, в платформе .NET есть три класса Timer. Они находятся в разных пространствах имен, поэтому никакой путаницы не возникает. Аналогично, использование DLL-библиотеки стороннего производителя, также содержащей класс Timer, не вызовет конфликта имен. Пространство имен System — самое важное для любого типа программирования в .NET Оно определяет все основные типы данных в приложениях .NET Также вам, наверняка, пригодятся System.Collections и System.Collections.Generic, в которых реализованы стандартные способы хранения данных (стеки, очереди, хэш-таблицы), и SystemJO, содержащее классы для работы с дисковыми файлами. В пространствах имен SystemData и SystemXml есть полезные классы для работы с базами данных и XML. Для программирования с использованием Windows Forms необходимы пространства имен System.WindowsJForms, включающее классы всех элементов управления в программах (кнопок, меню, полей ввода и т. д.), и SystemDrawing, содержащее все классы для работы с графикой. Документацию для классов .NET практически невозможно изучить досконально, даже посвятив этому все свободное время и проведя над ней бессонные ночи. Каждое пространство имен представлено в виде иерархической структуры, состоящей из пяти типов элементов. Они перечислены далее с указанием ключевых слов, соответствующих им в языке С#: ■ class — как и в большинстве объектно-ориентированных языков класс представляет собой основной структурный элемент, инкапсулирующий код и данные;
Создание приложений 5 ■ struct — структура в .NET отличается от класса лишь тем, что имеет значимый, а не ссылочный тип. Структуры хранятся в стеке, а не в куче и лучше всего подходят для небольших элементов и элементов, обычно хранящихся в массивах. Наследование структур невозможно; ■ interface — интерфейс немного похож на класс. В нем есть методы, но код в методах не определяется. Класс может наследовать нескольким интерфейсам и содержать код методов, определенных в этих интерфейсах. Интерфейсы оказываются полезными для приведения типов; ■ delegate — делегат, в сущности, представляет собой прототип метода. Пример делегата приводится далее в этой главе; ■ епит — перечисление определяет набор возможных значений констант. В классах и структурах есть не только конструкторы, поля и методы (как в обычных объектно-ориентированных языках), но также свойства и события. В классах и структурах также могут определяться интерфейсы, делегаты и перечисления, а вот вложение одних классов и структур в другие встречается реже. Разработка приложений Различные понятия, связанные с программированием в .NET и с использованием Windows Forms, становятся более ясными на примере создания конкретных программ. Перефразировав старую поговорку, можно сказать, что «лучше один раз увидеть код, чем сто раз о нем услышать». Самые короткие программы В .NET можно создавать программы текстового режима — так называемые консольные приложения. Это лучшая отправная точка для программирования в .NET Пространство имен System содержит класс Console, в котором есть методы вывода текста Write и WriteLine. (WriteLine отличается от Write лишь тем, что вывод текста завершается возвратом каретки). Для чтения информации с клавиатуры в Console есть методы Read, ReadLine и ReadKey (последний появился в .NET 2.0). Это статические (static) методы — при ссылке на них сначала указывается имя класса (Console), а не объекта типа Console. Первая программа — вариант традиционной программы «Hello, world»: FirstConsoleProgram.es И // FirstConsoleProgram.es (с) 2005 by Charles Petzold // class FirstConsoleProgram {
6 ГЛАВА 1 public static void Main() { System.Console.WriteLine("Hello .NET Console"); } } Из комментария видно, что имя файла — FirstConsoleProgram.cs, его расширение указывает на С# (cs расшифровывается как «С Sharp»). Программа состоит из одного класса с именем FirstConsoleProgram. В С# имя файла не должно обязательно совпадать с именем класса, но некоторые программисты (и я из их числа) предпочитают именно такой способ организации кода. В С# в файле с исходным текстом можно использовать несколько классов (иногда это, конечно, удобно), а начиная с С# 2.0 один класс можно распределить по разным файлам с исходным текстом, воспользовавшись ключевым словом partial Входная точка программы на С# — метод Main, аналог похожих методов в С и C++. Язык С# чувствителен к регистру, поэтому имя Main обязательно пишется с заглавной буквы. Дополнительно у Main может быть возвращаемое значение и параметр — массив строк, в котором хранятся аргументы командной строки, введенные пользователем в процессе выполнения программы. public static int Main(string[] args) Метод Main определяется как статический, потому что он должен существовать на момент начала работы программы. Иначе говоря, его наличие не должно зависеть от создания объекта типа FirstConsoleProgram. Поскольку Main — статический метод, любой класс в программе может ссылаться на него таю Fi rstConsolePгод ram.Main() В Main содержится один вызов метода System.Console.WriteLine — его имя указывается полностью и состоит из имени пространства имен, класса и метода. Если вызовов несколько или если в программе применяются другие классы и методы из пространства имен System, текст программы можно сократить, добавив в начале директиву using. SecondConsoleProgram.cs И // SecondConsoleProgram.cs (с) 2005 by Charles Petzold // using System; class SecondConsoleProgram { public static void Main() {
Создание приложений 7 Console.WriteLinef'Hello again, .NET Console"); } } Директивы using помогают компилятору определить полные имена всех классов, используемых в программе. В случае неопределенности (например, когда using подключает и System.Threading, и System.Timers и используется класс Timer одного из них) нужно указывать полное имя класса. Эту программу можно скомпилировать в командной строке с помощью компилятора С# csc.exe или воспользоваться программой «Key of С#». Но, скорее всего, вы воспользуетесь Visual Studio, поэтому я рассмотрю этот вариант подробнее. Проекты Visual Studio В Visual Studio работа ведется в проектах (projects). При программировании с применением Windows Forms проект представляет собой набор файлов, из которых создаются исполняемые файлы и файлы DLL-библиотек. Проект состоит из одного или нескольких файлов с исходным кодом. В последнем случае все файлы должны быть на одном языке, поэтому можно говорить о «проектах на С#» или «проектах на Visual Basic .NET». В Visual Studio информация о проекте хранится в XML- файле с расширением .csproj (для С#) или .vbproj (для Visual Basic .NET). Visual Studio позволяет объединять проекты (даже написанные на разных языках) в решения (solution), которые позволяют работать над несколькими проектами одновременно и компилировать их все вместе — этот процесс называется сборка решения. Обычно проекты решения взаимосвязаны, в этом случае их зависимости можно указать при создании решения в Visual Studio. Допустим, в решении три проекта: DLL-библиотека на С# и две использующие ее программы на Visual Basic .NET и C++ .NET (Если вместо Visual Studio задействовать Visual C#, все проекты решения придется писать только на С#). Нужно, чтобы DLL-библиотека компилировалась раньше, чем две программы. В Visual Studio информация о решениях хранится в текстовых файлах с расширением .sin. В Visual Studio немного вариантов создания простого консольного приложения. Выберите команду New Project в меню File. В диалоговом окне New Project последовательно выберите С# и Windows слева. (В Visual C# диалоговое окно New Project проще, и эта операция не нужна.). Чтобы отключить автоматическую генерацию кода (это полезно, когда учишься программировать самостоятельно), выберите тип Empty Project. Также укажите имя проекта и его местоположение. Имя проекта используется для создания подпапки проекта в указанном местоположении. Также Visual Studio всегда создает файл решения (с расширением .sin). Если в решении лишь один проект, снимите флажок Create Directory For Solution, в противном случае Visual Studio создаст подпапку решения, в которую войдет под- папка проекта, а лишние подпапки никому не нужны. (В Visual C# местоположение проекта указывается при его первом сохранении.)
8 ГЛАВА 1 В проекте типа Empty Project нет даже файла с исходным текстом. В меню Project выберите Add New Item или правой кнопкой мыши щелкните имя проекта в правой панели. В открывшемся диалоговом окне выберите Code File (это пустой файл с исходным текстом для С#). Теперь можно вводить текст программы. Часто при вводе исходного текста Visual Studio предлагает список возможных классов и ключевых слов для С#. Так, после ввода точки после Console, откроется список членов класса Console. Из списка можно выбрать, например, WriteLine. Динамические подсказки в процессе ввода обеспечиваются механизмом IntelliSense. Это одна из самых любимых у пользователей функций, предлагаемых Microsoft. (Управлять поведением IntelliSense легко: выберите в меню Tools команду Options, а затем в левой части диалогового окна — Text Editor, C# и IntelliSense). Когда программа готова к компиляции, выберите в меню Build команду Build Solution или нажмите F6. Ошибки отображаются в окне под исходным текстом. Чтобы выполнить программу, выберите Start Debugging или Start Without Debugging в меню Debug или нажмите F5 (Ctrl + F5). Если с момента последних изменений программа не скомпилировалась, Visual Studio сначала ее скомпилирует. Давайте закроем решение, выбрав Close Solution в меню File, и попробуем другой вариант — посмотрим, в действии автоматическую генерацию кода в Visual Studio. В меню File выберите New Project. В диалоговом окне New Project выберите Console Application. При создании проекта консольного приложения Visual Studio по умолчанию присваивает ему имя ConsoleApplicationl и создает файл, как в следующем коде: Program.cs using System; using System.Collections.Generic; using System.Text; namespace ConsoleApplicationl { class Program { static void Main(string[] args) { } } } Visual Studio добавила в код наиболее полезные — по ее мнению — директивы using, а выделила классу отдельное пространство имен. Пространства имен нужны при написании больших программ с повторяющимися именами классов, в осталь-
Создание приложений 9 ных случаях они излишни. (В DDL-библиотеках пространства имен играют более важную роль). После создания и загрузки решения в Visual Studio, в него можно добавлять новые или имеющиеся проекты. Для этого выберите New/Project в меню File или щелкните правой кнопкой имя решения (в правом верхнем углу окна Solution Explorer) и выберите New Project (для нового проекта) или Existing Project (для имеющегося проекта) в меню Add. Чтобы добавить в проект другие файлы, выберите Add New Item или Add Existing Item в меню Project или щелкните имя проекта правой кнопкой и выберите New Item или Existing Item в меню New. В файл Program.cs можно дописать свой код на С#, а затем скомпилировать и запустить его, как описано выше. Ссылки Как вы уже, наверное, заметили, в приложениях .NET не нужны заголовочные файлы, сообщающие компилятору С# о библиотечных функциях. Компилятору также не нужны библиотечные файлы для добавления кода в исполняемый файл или для создания ссылок на DLL-библиотеки. Откуда же компилятор знает, что пространство имен System содержит класс Console, в котором есть метод ReadLine, принимающий строковый аргумент? Компилятор С# получает эти сведения напрямую из DLL-библиотеки, в которой хранится класс Console. Во всех DLL-библиотеках .NET есть «метаданные» — информация об их содержимом. (Приложение .NET самостоятельно извлекает эту информацию с помощью классов пространства имен SystemReflection). В верхней части первой страницы документации для класса Console есть строка, начинающаяся со слова Assembly (сборка). В ней указано, что класс Console хранится в сборке mscorlib (в файле mscorlib.dll). Раньше имя mscorlib расшифровывалось как «Microsoft Common Object Runtime Library», а теперь у него другая интерпретация: «Multilanguage Standard Common Object Runtime Library». Эта библиотека содержит самые распространенные классы. Любому приложению Windows Forms нужны классы из разных DLL-библиотек. Например, все классы пространства имен System.WindowsJForms хранятся в сборке SystemWindowsJForms, то есть в файле SystemWindowsForms.dll. Чтобы успешно скомпилировать программу, где используются эти классы, компилятору С# необходимо знать, к каким DLL-библиотекам обратиться. DLL-библиотека, к которой компилятор обращается за метаданными, называется ссылка (reference). Есть несколько причин, зачем Visual Studio нужны данные о ссылках. Ясно, что эти ссылки передаются компилятору С#. Но для Visual Studio надо явно указать используемые в программе ссылки, потому что она обращается за метаданными к DLL-библиотекам задолго до запуска компилятора. Именно на этом основан Intelli- Sense, работающий даже с пользовательскими DLL-библиотеками.
1 О ГЛАВА 1 От консольных к Windows-приложениям Простое приложение Windows Forms создается почти так же легко, как консольное. Создадим в Visual Studio проект Empty Project с именем FirstWinFormsProgram. Но прежде чем вводить исходный текст, выберите команду Add Reference в меню Project или щелкните правой кнопкой слово References справа в окне Solution Explorer и выберите Add Reference. В диалоговом окне Add Reference выберите вкладку .NET. Для любого более-менее сложного приложения Windows Forms нужны ссылки System, SystemDrawing и SystemWindowsJForms. Некоторым приложениям (но не этому) также часто требуются SystemData и SystemXml. Чтобы выбрать несколько ссылок, щелкните их, удерживая клавишу Ctrl. После выбора ссылок, наступает очередь IntelliSense. Если при вводе программы IntelliSense не работает, перепроверьте ссылки и директивы using. В проекте FirstWinFormsProgram добавьте новый элемент Code File с именем FirstWinFormsProgram.cs и введите следующий код: FirstWinFormsProgram.cs И // FirstWinFormsPrografn.es (с) 2005 by Charles Petzold // using System.Windows.Forms; class FirstWinFormsProgram { public static void Main() { Application.Run(new Form()); } } Обратите внимание на директиву using. В программе используются классы Application и Form из пространства имен System.WindowsForms. He путайте ссылки проекта и директивы using из файла с исходным текстом, хотя у них могут быть одинаковые имена. Ссылки — это динамически подключаемые библиотеки, они необходимы всегда. Директивы using указывают на пространства имен, их использовать необязательно (но в таком случае нужно указывать полные имена классов). В этой программе нужны ссылки на SystemWindowsFormsApplication и SystemWindowsForms.- Form. Пространств имен намного больше, чем библиотек. Например, в библиотеке SystemDrawingdll есть классы из пространств имен SystemDrawing, SystemDrawing.- Drawing2D1 SystemDrawinglmaging и SystemDrawingPrinting, также часто используемые в программировании с применением Windows Forms. Компилируют и запускают созданную программу так же, как и консольное приложение — комбинацией Ctrl + F5. Появится маленькое окно.
Создание приложений 11 И хотя его клиентская область совершенно пустая, а в строке заголовка нет текста, все же это полноценное окно, которое можно перемещать, сворачивать и разворачивать, изменять размеры и закрывать. Как вы заметите, вместе с этим окном появляется окно командной строки — точно такое же, как при запуске консольного приложения. Но об этом чуть позже. Следующее выражение создает объект типа Form (это класс Windows Forms, инкапсулирующий стандартное окно приложения): new Form() Программа передает объект Form статическому методу Run класса Application. Несмотря на свое незамысловатое имя, именно этот метод превращает консольное приложение в приложение Windows. С точки зрения Windows-программирования этот метод создает очередь для получения сообщений, которые Windows отправляет приложениям. Метод Application Run не возвращает управление программе, пока пользователь не закроет переданный этому методу объект Form. Порядок таков: если ApplicationRun возвращает управление программе после отображения формы, Main завершается, программа прекращает работу и в процессе очистки системы окно уничтожается. Это можно проверить, добавив в программу после ApplicationRun оператор Console. WriteLine (придется указать полное имя класса Console или вставить еще одну директиву using для System). В любом Windows-приложении можно направлять вывод в консоль. Любые выводимые в консоль данные из программы отображаются в окне командной строки, которое открывается вместе с окном приложения. ConsoleWriteLine — один из лучших инструментов для изучения Windows Forms и создания и отладки приложений Windows Forms. Поэтому мне нравится, когда программы Windows Forms сопровождаются окном командной строки. Это также замечательный «спасательный круг». Когда действия пользователя приводят к бесконечному циклу или зависанию программы, достаточно щелчком активизировать окно командной строки и нажать Ctrl+C, чтобы программа моментально завершила работу.
1 2 ГЛАВА 1 Разумеется, приложение Windows Forms с окном командной строки не предназначено для конечных пользователей. Поэтому от окна командной строки легко избавиться. Им управляет параметр компилятора (в компиляторе командной строки это target), определяемый в окне Project Properties в Visual Studio. В меню Project выберите Properties или щелкните правой кнопкой имя проекта в окне Solution Explorer, а затем — Properties. В поле со списком Output Type выберите Windows Application вместо Console Application и перекомпилируйте программу — окно командной строки исчезнет. На первый взгляд, кажется, что для создания приложения Windows Forms нужно присвоить параметру Project Type значение Windows Application. Это не так. Этот параметр влияет только на способность программы отображать вывод в консоль. А Windows-приложение отличается от других программ тем, что в его коде вызывается метод ApplicationRun. Убираем шероховатости В показанной ранее программе FirstWinFormsProgram есть грубая ошибка — она создает объект типа Form и передает его напрямую методу Application.Run. Поскольку этот метод возвращает управление программе только после закрытия формы, у программы нет доступа к форме и с этим ничего сделать нельзя. Проблема решается просто — сначала нужно создать объект Form, затем назначить ему свойства и лишь потом передать объект методу Application.Run. FormProperties.cs И // FormProperties.es (с) 2005 by Charles Petzold // using System; using System.Drawing; using System.Windows.Forms; class FormProperties { public static void Main() { Form frm = new Form(); frm.Text = "My WinForms Program"; frm.Width *= 2; Application.Run(frm); } }
Создание приложений 13 Text и Width — члены класса Form. Это не методы, потому что аргументы методов заключаются в скобки. Они похожи на поля, но это не поля, потому что оба реализованы в коде. На самом деле это свойства — иллюстрация важной особенности платформы .NET. В большинстве классов определено много свойств, служащих для определения характеристик объекта. Поскольку свойства похожи на поля, но реализованы в коде, их иногда называют «смарт-полями» (smart fields). Свойство Text ссылается на текст, отображаемый в строке заголовка программы. Width содержит значение ширины окна программы в пикселах. Программа получает ширину объекта Form, удваивает ее и присваивает полученное значение свойству Width. Ширина отображаемого окна в два раза превышает исходное значение. j*; My WinForms Progr В системе без свойств в объекте Form пришлось бы реализовать методы get* и set* (например, get_Width и set_Width), а ширину окна удваивать так: frm.set_Width(2 * frm.get_Width()); Именно так реализованы свойства в промежуточном языке MSIL, и так на них ссылаются при программировании на ]*. Понятно, что использование синтаксиса свойств делает код яснее и понятнее. А в Visual Studio свойства класса отображаются в виде таблицы, что позволяет программисту изменять их и затем генерировать нужный код (подробнее об этом далее). В документации для платформы .NET в иерархии Form вы не найдете свойств Text и Width. Они определены в классе Controls, подклассом которого является Form. На странице Form Members приводится полный список методов и свойств класса Form, в том числе наследуемых и тех, которые он реализует сам. События и обработчики Программы не только задают различные свойства, но еще должны откликаться на ввод пользователя. Для этого служат события — еще один важный элемент языков .NET. Событие — это универсальный механизм, позволяющий одному классу (объекту) сигнализировать другому классу (объекту) путем вызова метода, это формали-
1 4 ГЛАВА 1 зованный, структурированный и более безопасный вариант функции обратного вызова. Вызываемый метод называется обработчиком события (event handler). Обработчик безопаснее, чем обратный вызов, потому что у метода должны быть только правильные аргументы и возвращаемое значение, которые не переопределяются даже после многократных приведений к типу. В документации для класса есть список реализованных в нем событий, а также перечислены конструкторы, поля, свойства и методы. Класс также наследует события. Класс Control определяет множество событий, наследуемых классом Form. Например, класс Control реализует (а класс Form наследует) событие Click, которое инициируется всякий раз, когда пользователь щелкает кнопкой мыши элемент управления (или клиентскую область в форме). В исходном тексте класса Control событие Click определяется примерно так: public event EventHandler Click; Ключевое слово event языка С# определяет этот член как событие. В определении события Click есть ссылка на EventHandler. EventHandler — это делегат (delegate) (также ключевое слово в С#). В пространстве имен System оно определяется так: public delegate void EventHandler(object sender, EventArgs e); Делегат — это прототип функции, диктующий определение обработчика события Click программе. Обработчику событий можно присвоить любое имя, но у него должно быть то же возвращаемое значение, что и у делегата, и те же два аргумента, которые также можно переименовать. void MyClicker(object objSrc, EventArgs args) { } Обработчик можно подключить к событию, указав: 1) объект, реализующий событие, 2) само событие, 3) делегат, связанный с событием, и 4) обработчик событий. У этого выражения особый синтаксис: frm.Click += new EventHandler(MyClicker); Обратите внимание на составной знак присвоения (+=). Знак -= в этом выражении позволяет отключить (detach) обработчик событий, но это редко требуется. Начиная с С# 2.0 можно использовать сокращенный синтаксис, чем я и воспользуюсь в этой книге. frm.Click += MyClicker; И, конечно, метод MyClicker должен также определяться как делегат EventHandler.
Создание приложений 15 Теперь при каждом щелчке кнопкой мыши в клиентской области формы объект Form инициирует событие Click, что приводит к вызову метода MyClicker в программе. ObjSrc, первый аргумент MyClicker, — это объект, инициирующий событие. В данном случае это созданный программой объект Form. В обработчике событий его можно привести к типу Form-. Form frm = (Form) objSrc; или Form frm = objSrc as Form; Затем обработчик событий может обращаться к свойствам и методам объекта Form. А вот аргумент EventArgs малоинтересен, так как событие Click не несет в себе никакой дополнительной информации (Другие события, связанные с мышью, например MouseDown и MouseHover, несут дополнительные сведения во втором аргументе обработчика событий). Еще одно важное событие, Paint, указывает, когда клиентская область элемента управления или формы требует прорисовки. Первое событие Paint инициируется при создании формы, а последующие — при ее свертывании и восстановлении, при изменении размеров или когда она выводится на передний план из-под другой формы. Обработка события Paint не ограничена только отрисовкой поверхности формы — обычно ее также используют для восстановления отображаемых в форме текста или графики. Обработчик события Paint определен в классе Control следующим образом: public event PaintEventHandler Paint; Делегат PaintEventHandler определен в пространстве имен SystemWindoivs.Forms-. public delegate void PaintEventHandler(object sender, PaintEventArgs e); Таким образом, в определении обработчика события Paint должен присутствовать в качестве второго аргумента PaintEventArgs, а не EventArgs-. void MyPainter(object objSrc, PaintEventArgs args) { } Программа устанавливает этот обработчик событий с помощью делегата PaintEventHandler. frm.Paint += new PaintEventHandler(MyPainter); или frm.Paint += MyPainter;
1 6 ГЛАВА 1 Класс PaintEventArgs определен в пространстве имен SystemWindowsForms. Он наследует классу EventArgs и содержит два свойства — Graphics и ClipRectangle. Обработчик событий использует объект Graphics из-за определенных в нем методов прорисовки графических фигур. Свойство ClipRectangle означает прямоугольник, содержащий недействительную область, ограничивающую объект Graphics. Следующая программа содержит реализации обработчиков событий Click и Paint Она реагирует на событие Click выводом окна с сообщением, а на событие Paint — отображением определенного текста в клиентской области. FormEvents.cs ,, // FormEvents.cs (с) 2005 by Charles Petzold // using System; using System.Drawing; using System.Windows.Forms; class FormEvents { public static void Main() { Form frm = new Form(); frm.Text = "My Events Program"; frm.Width *= 2; frm.Click += MyClicker; frm.Paint += MyPainter; Application.Run(frm); } static void MyClicker(object objSrc, EventArgs args) { MessageBox.Show("The form has been clicked!", "Click"); } static void MyPainter(object objSrc, PaintEventArgs args) { Form frm = (Form)objSrc; Graphics grfx = args.Graphics; grfx.DrawStringC'Hello, Windows Forms", frm.Font, SystemBrushes.ControlText, 0, 0); } } В этой программе обработчики событий должны определяться как статические (static). Программа не создает объект типа FormEvents, поэтому нестатические
Создание приложений 17 методы этого класса будут бесполезны. Хотя обработчики событий вызываются за пределами класса FormEvents, их не нужно определять как открытые (public). В ответ на щелчок мышью в клиентской области программа выводит окно с сообщением. Код в MyPainter выводит некий текст в клиентской области с помощью метода DrawString класса Graphics. Аргументы этого метода — отображаемый текст, его шрифт и цвет (в виде графической кисти) и координаты хиу, задающие положение левого верхнего угла строки текста относительно левого верхнего угла клиентской области. В MyPainter можно создать любой шрифт из имеющихся гарнитур и размеров, но в нем используется шрифт по умолчанию для формы, который хранится в свойстве Font класса Form. Чтобы получить доступ к этому свойству, метод приводит объект objSrc к типу Form и получает его свойство Font. Объект Form, переданный обработчику событий, — это тот же объект, который был создан в Main. Создаваемый объект Form можно сохранять в программе как статическое поле, чтобы он был доступен всем методам программы. Наследники класса Form В предыдущей программе показан способ создания и применения объекта Form, но обычно это делается по-другому. Более гибкий и привлекательный подход — создание класса-наследника Form. class MyForm: Form { } Затем можно создать объект этого класса как аргумент ApplicationRun в Main. Или определить в программе еще один класс, предназначенный только для статического метода Maim
1 8 ГЛАВА 1 class MyProgram { public static void Main() { Application.Run(new MyFormO); } } Поскольку MyForm — класс-потомок Form, у него есть доступ ко всем открытым и защищенным методам, свойствам и событиям класса Form. Следовательно, в своем конструкторе он может задавать свойства формы, class MyForm: Form { public MyFormO { Text = "My Inherited Form"; Width *= 2; } } В С# у конструктора такое же имя, как и у класса, но нет возвращаемого значения. Это первый метод в этой главе, который не определен как статический. Конструктор применяется к объекту типа MyForm. Перед свойствами не нужно указывать имя объекта. На самом деле, объект MyForm не существует, пока не создан в Main. Для ссылки на текущий объект в конструкторе, методе или свойстве можно использовать ключевое слово this из С#. this.Text = "My Inherited Form"; Ввод с клавиатуры слова this с точкой в Visual Studio активизирует механизм IntelliSense, который выводит список всех нестатических членов класса. Также можно установить обработчики событий. Учтите, что они не статичны, так как относятся к объекту типа MyForm: class MyForm: Form { public MyFormO { Text = "My Inherited Form"; Width *= 2; Click += MyClicker; Paint += MyPainter; } void MyClicker(object objSrc, EventArgs args) { MessageBox.Show("The button has been clicked!", "Click");
Создание приложений 19 } void MyPainter(object objSrc, PaintEventArgs args) { Graphics grfx = args.Graphics; grfx.DrawString("Hello, Windows Forms", Font, SystemBrushes.ControlText, 0, 0); } } И все же есть более простой способ работы с событиями при наследовании классу Form. Для каждого события, реализованного в классе Control (и, следовательно, — в Form), есть соответствующий метод. У метода то же имя, что и у события, только с приставкой On-, например OnPaint и OnClick. У этих методов один аргумент, совпадающий со вторым аргументом обработчика событий. (Первый аргумент не нужен, потому что ко всем свойствам и методам объекта можно обращаться напрямую или с помощью ключевого слова this). Эти методы определяются как виртуальные (virtual). Это значит, что программа, происходящая от Control или Form, может переопределить эти методы ключевым словом override. Поскольку эти виртуальные методы в Control определяются как защищенные (protected), переопределенные методы тоже надо определять как защищенные. class MyForm: Form { public MyForm() { Text = ""; Width *= 2; } protected override void OnClick(EventArgs args) { MessageBox.Show("The button has been clicked!", "Click"); } protected override void OnPaint(PaintEventArgs args) { Graphics grfx = args.Graphics; grfx.DrawString("Hello, Windows Forms", Font, SystemBrushes.ControlText, 0, 0); } } Ранее я показал отдельный класс MyProgram со статическим методом Main, создающим объект типа MyForm. Также можно (и часто это удобно) помещать метод Main прямо в класс, который наследует Form, что позволяет избавиться от необходимости создавать отдельный класс для Main.
20 ГЛАВА 1 class MyForm: Form { ublic static void Main() { Application.Run(new MyFormO); } Этот код может показаться странным, потому что Main является членом класса MyForm и одновременно отвечает за создание экземпляра MyForm. Код работает лишь потому, что Main определяется как статический и, следовательно, не зависит от любых возможных объектов типа MyForm. Вот полный текст программы: lnheritFromForm.es И // InheritFromForm.es (с) 2005 by Charles Petzold // using System; using System.Drawing; using System.Windows.Forms; class InheritFromForm: Form { public static void Main() Application.Run(new InheritFromForm()); public InheritFromFormO Text = "Inherit from Form"; Width *= 2; protected override void OnClick(EventArgs args) MessageBox.Show("The form has been clicked!", "Click"); protected override void OnPaint(PaintEventArgs args) Graphics grfx = args.Graphics; grfx.DrawString("Hello, Windows Forms", Font, SystemBrushes.ControlText, 0, 0); } }
Создание приложений 21 Есть одно предостережение. Методы OnClick и OnPaint класса Control отвечают за инициирование событий Click и Paint. Поскольку InheritFromForm переопределяет эти методы, события Click и Paint для InheritFromForm фактически отключены. Если в InheritFromForm (или любом наследующем ему классе) попытаться подключить обработчики событий Click и Paint, они работать не будут. В таком случае код переопределяемых методов OnClick и OnPaint должен начинаться со строки, вызывающей переопределенные методы, например: base.OnPaint(args) Свойства и события в Visual Studio Теперь у вас достаточно знаний, чтобы разобраться с тем, что происходит в Visual Studio при создании проекта типа Windows Application. Visual Studio генерирует базовый, готовый для компиляции код в виде нескольких файлов, а также отображает Design View, панель с заготовкой формы, которую можно изменять и расширять новыми элементами управления. Visual Studio создает файл Program.cs, который содержит метод Main в классе Program, входящем в избыточное пространство имен. Имя последнего совпадает с именем проекта (по умолчанию WindowsApplicationl). Program.cs using System; using System.Collections.Generic; using System.Windows.Forms; namespace WindowsApplicationl { static class Program { /// <summary> /// Главная входная точка приложения. /// </summary> [STAThread] static void MainQ { Application.EnableVisualStyles(); Application.SetCompatibleTextRenderingDefault(false); Application.Run(new Form1()); } } }
22 ГЛАВА 1 Комментарии над методом Main — пример XML-документации для кода. В параметре компиляции можно указать файл, в котором будет собрана вся подобная XML-документация для программы. Это прекрасный способ документирования кода. В этой книге я его не использую лишь для экономии бумаги. Методу Main непосредственно предшествует метод attribute, состоящий из слова STATbread в квадратных скобках. Он указывает исполняющей среде .NET запускать программу в отделенном потоке, что обеспечить корректное ее взаимодействие с СОМ (подробнее см. документацию к STATbreadAttribute в пространстве имен System). Странно, но работающие с буфером обмена программы Windows Forms без этого атрибута часто работают неустойчиво. STATbread далее используется во всех программах этой книги. Метод EnableVisualStyles класса Application позволяет задать внешний вид некоторых элементов управления. Подробнее об этом — в документации для перечисления FlatStyle. Вызов этого метода позволяет задать более «современный» вид элементов управления, поэтому этот метод далее использован во всех программах. Класс Forml определен в файле Forml.cs. Form1.cs using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Windows.Forms; namespace WindowsApplicationl { partial class Forml : Form { public Forml() { InitializeComponent(); } } } Класс Forml — производный от Form. Ключевое слоъо partial в определении класса позволяет распределить код класса среди нескольких файлов. Метод Initialize- Component также относится к классу Forml, но находится в другом файле с исходным текстом — Forml Designer.cs.
Создание приложений 23 Form 1 .Designer.cs namespace WindowsApplicationl { partial class Forml { /// <summary> /// Обязательная переменная конструктора форм. /// </summary> private System.ComponentModel.IContainer components = null; /// <summary> /// Очистка всех используемых ресурсов. /// </summary> /// <param name="disposing">true, если ресурсы нужно освободить; /// иначе - false.</param> protected override void Dispose(bool disposing) { if (disposing && (components != null)) { components.Dispose(); } base.Dispose(disposing); } «region Windows Form Designer generated code /// <summary> /// Метод необходим для работы конструктора форм - не изменяйте /// содержимое этого метода средствами редактора кода. /// </summary> private void InitializeComponentQ { this.components = new System.ComponentModel.Contained); this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; this.Text = "Forml"; } } } #endregion Код, относящийся к объекту components, создан по шаблону Visual Studio. Класс Container является набором объектов Component. Класс Control наследует классу Component, как и некоторые другие классы в Windows Forms, инкапсулирующие ресурсы Windows, на которые ссылаются обработчики. В документации по Component
24 ГЛАВА 1 рекомендуется очищать такие объекты явно, а не автоматически (поручая эту работу процессу сбора мусора) или при завершении программы. Объединение таких объектов в Container ускоряет этот процесс. Когда Visual Studio отображает внешний вид формы в конструкторе, в правом нижнем углу появляется окно свойств, где можно изменить любые свойства формы, а также (щелкнув значок молнии) просмотреть события формы. Например, чтобы изменить размер, найдите свойство Size, у которого есть два свойства — Width (ширина) и Height (высота). Размер формы также можно изменять напрямую в окне Design View, перемещая один из маленьких квадратиков на границах формы. В любом случае, Visual Studio вставит в Forml.Designer.cs код следующего вида: this.ClientSize = new System.Drawitig.Size(565, 266); Наследование класса Form В клиентской области формы приложение отображает визуальную информацию. Программа Windows Forms заполняет клиентскую область текстом и графикой, помещает на нее элементы управления (стандартные и пользовательские) или комбинирует графику с элементами управления. Из элементов управления в программах Windows Forms доступны привычные кнопки, поля ввода, окна списков, полосы прокрутки и более сложные элементы. (В следующей главе обсуждаются элементы управления Windows Forms в .NET 2.0). Теперь клиентские области приложений часто разделяют на логические области, при этом также применяются такие элементы управления, как панели и разделители (они рассматриваются в главе 3). Все элементы управления восходят к классу Control вместе со всеми его свойствами, методами и событиями, которые есть у всех его наследников. Одно из таких свойств — Parent, которое также является объектом типа Control Элемент управления всегда отображается на видимой поверхности родителя. Расположение элемента управления указывается относительно левого верхнего угла родительского элемента (или когда родительским элементом является объект Form — относительно левого верхнего угла клиентской области формы). Элемент управления без родительского элемента отображается только в том случае, если он сам является формой. Элемент управления можно явно сделать невидимым, присвоив свойству Visible значение false. Но чаще используется свойство Enabled. Элемент управления, у которого это свойство имеет значение false, отображается тусклым цветом и не реагирует на операции пользователя. Класс Button реализует обычную нажимаемую кнопку, которая часто встречается в диалоговых окнах со словами ОК или Cancel. В следующей программе показано, как создать объект Button, поместить его на поверхность формы и подключить обработчик к событию Click этой кнопки. Как и остальные программы этой книги, я написал эту, сначала создав проект Empty Project в Visual Studio, а затем добавив пустой файл с исходным текстом Code File на С*.
Создание приложений 25 Form WithButton.es И // FormWithButton.es (с) 2005 by Charles Petzold // using System; using System.Drawing; using System.Windows.Forms; class FormWithButton: Form { [STAThread] public static void MainQ { Application.EnableVisualStyles(); Application.Run(new FormWithButton()); } public FormWithButtonO { Text = "Form with Button"; Button btn = new ButtonO; btn.Parent = this; btn.Text = "Click me!"; btn.Location = new Point(50, 25); btn.AutoSize = true; btn.Click += ButtonOnClick; } void ButtonOnClick(object objSrc, EventArgs args) { MessageBox.Show("The button was clicked!", "Button"); } } Конструктор формы создает объект типа Button и присваивает свойству Parent значение this, что указывает на объект Form. Свойство Text — это текст на кнопке. В свойстве Location указывается, что верхний левый угол кнопки находится на расстоянии 50 пикселов правее и 25 пикселов ниже верхнего левого угла клиентской области формы. Свойству AutoSize присвоено значение true. Это значит, что размер кнопки будет меняться в соответствии с отображенным на ней текстом. И, наконец, метод ButtonOnClick использован в качестве обработчика события Click. Событие и все эти свойства определены в классе Control и наследуются классом Button и всеми остальными классами элементов управления. Свойство AutoSize удобно применять для выбора оптимального размера кнопки.
26 ГЛАВА 1 Чтобы получить объект Button, инициировавший событие, обработчик должен привести первый аргумент к типу Button-. Button btn = (Button) objSrc; или таю Button btn = objSrc as Button; Один обработчик событий может обрабатывать события Click нескольких кнопок. Он различает кнопки несколькими способами. Например, по свойству Text, но это не лучший вариант-, обработчик событий придется обновлять при каждом изменении текста кнопки. Лучше воспользоваться свойством Name, которому можно присвоить любое строковое значение. А свойство Tag, также пригодное для этих целей, можно полностью контролировать: оно больше ни для чего не используется и ему можно назначить любой объект. Оказывается, что обработчики событий не нужно устанавливать для каждого создаваемого элемента управления. Например, каждый щелчок элемента управления CbeckBox инициирует событие Click. Но по умолчанию элемент сам снимает/ устанавливает свою «галочку». В общем, нужно лишь определить его текущее состояние по значению его логического свойства Checked. В форме с несколькими элементами управления они могут ссылаться друг на друга. Например, щелчок одного элемента управления влияет на включение/отключение других. Чтобы обработчик событий одного элемента управления ссылался на другие элементы управления, проще всего сохранить каждый объект Control в виде поля, вот так: Button btnCancel; В конструкторе создается объект, и назначаются его свойства: btnCancel = new ButtonQ;
Создание приложений 27 Или можно определить и создать объект в поле: Button btnCancel = new Button(); Компилятор С# подключает специальный метод, который сначала исполняет весь код в полях и лишь затем код в конструкторе класса. Принципы объектно-ориентированного программирования требуют скрывать данные и объекты и делать их доступными для внешнего кода только при необходимости. Я предпочитаю сохранять объект как поле, только если нет другого способа сделать его видимым для нескольких методов. Я не люблю поля, но у создателей Visual Studio прямо противоположное мнение. Среда разработки хранит все объекты элементов управления в виде полей. В проекте Windows Application в Visual Studio можно создать кнопку на форме, выбрав Button слева в окне инструментария, а затем поместив кнопку в форму. Visual Studio добавит закрытое поле в класс Forml, содержащий соответствующий экземпляр класса Button-. private System.Windows.Forms.Button button"!; Visual Studio также создает следующий код (ну, или нечто подобное) для объекта Button-. this.buttonl = new System.Windows. Forms. ButtonO; this.button"!.Location = new System.Drawing.Point(92, 55); this.buttonl. Name = "button"!"; this.buttonl.Size = new System.Drawing.Size(104, 47); this, button"!. Tablndex = 0; this.buttonl. Text = "button"!"; this.Controls.Add(this.buttonl); Кнопку можно переместить и задать ее размер напрямую или изменить ее свойства в таблице в правом нижнем углу окна Visual Studio. Обратите внимание, что в Visual Studio свойство кнопки Parent не ссылается на объект Form. Вместо этого нечто подобное выполняется в последнем из шести вышеприведенных выражений: this.Controls.Add(this.buttonl); Controls — это свойство класса Form. (На самом деле Form наследует его от Control?) Тип свойства Controls — Control.ControlCollection — означает, что класс ControlCollection определен в классе Control, но все же он является открытым, и на него можно ссылаться извне класса Control. Класс ControlCollection реализует интерфейсы ICollection, ШпитегаЫе и Hist, предназначенные для работы с наборами объектов — для добавления, удаления и поиска объектов набора. Свойство Controls класса Control — это, в сущности, набор дочерних элементов управления. Добавление элемента управления в свойство Controls объекта Form
28 ГЛАВА 1 равносильно присвоению объекта Form свойству Parent элемента управления, а удаление из набора равносильно приравниванию свойства Parent элемента управления нулю. Программа может проиндексировать свойство Controls как массив — это позволяет получить все дочерние элементы формы (или другого элемента управления). Также, для поиска объекта Control по его свойству Name можно применить метод Find класса Control.ControlCollection. Пусть два дочерних объекта одной формы расположены так, что перекрывают друг друга. Какой из них будет находиться на переднем плане? Это зависит от 2- порядка элементов управления. (Этот термин восходит к трехмерной графике: z — еще одна координата помимо обычных х и у) Z-порядок — это то же, что и индекс элемента управления в наборе Controls. Это порядок, назначенный элементам управления при добавлении в набор или присвоенный их свойству Parent. У созданных раньше элементов управления меньший индекс, но более высокий z-порядок, потому что на экране они располагаются поверх элементов управления, созданных позже. Методы BringToTop и SendToBack позволяют изменить Z-порядок элементов управления в процессе выполнения программы. При создании элементов управления в Visual Studio генерируется код, присваивающий свойству Tablndex последовательные номера, начиная с 0. Это свойство указывает порядок перемещения фокуса между дочерними элементами управления, когда для этого используется клавиша Tab. В Visual Studio это свойство задано явно, поэтому последовательность перехода можно изменить, выбрав команду Tab Order в меню View. Если значения свойства Tablndex нескольких элементов управления равны нулю (так бывает при отсутствии кода, присваивающего значения свойствам), последовательность перехода между элементами управления при нажатии клавиши Tab определяется z-порядком, то есть порядком добавления элементов управления в набор. При бессистемном создании формы в Visual Studio, скорее всего, порядок индексов табулятора придется менять. А при внимательном подходе к разработке формы порядок элементов управления в коде будет определять последовательность перехода между ними при нажатии клавиши Tab. Visual Studio также вставляет вызовы SuspendLayout и ResumeLayout. Они необязательны, но их лучше добавить, чтобы избежать перегрузки формы при задании значений свойств элементов управления. Создание подклассов элементов управления В программах Windows Forms обычно определяется класс, производный от Form. А вместо того чтобы определять новые классы, наследующие имеющимся классам элементов управления, в программах просто создаются экземпляры элементов управления. Вместе с тем, создание новых производных классов элементов управления полезно, когда нужно расширить элемент управления или сделать так, чтобы в нем хранилось чуть больше информации, чем это позволяет свойство Tag.
Создание приложений 29 Допустим, нужно создать кнопку, при каждом щелчке которой появляется окно с сообщением. Пусть имя этого класса — MessageButton. Заголовок окна с сообщением должен совпадать с текстом на кнопке. Класс получает эту текстовую строку из свойства Text кнопки. Но у каждой кнопки должен быть свой текст сообщения. Для этого текст нужно сохранить как открытое поле в новом классе: class MessageButton: Button { public string MessageBoxText = ""; } Разумеется, у метода OnClick есть доступ к этому полю. А поскольку оно открытое, создающая экземпляр MessageButton программа сможет задать значение этого ПОЛЯ: MessageButton mbbtn = new MessageButtonO; mbbtn.MessageBoxText = "Text in the message box."; MessageBoxText можно сделать полем класса, но в документации для классов платформы .NET полей не так много, и большинство из них является полями для наборов констант. Зато в ней много свойств. Очень часто свойство дает общий доступ к закрытому полю. Свойство часто полезно тем, что оно также включает код, например для контроля правильности или для выполнения определенных действий при изменении объекта. Так, при изменении свойства Text или цвета класса Control элемент управления сразу же перерисовывается. С полями такое поведение не получится. Допустим, нам надо, чтобы кнопка была активной, только когда в поле MessageBoxText содержится текстовая строка длиной не менее одного символа. Если MessageBoxText — простое поле, класс не сможет определить момент его изменения. А если MessageBoxText — свойство, тогда при его изменении можно задать свойству Enabled значение true. В коде для MessageButton есть закрытое поле, конструктор, инициализирующий свойство Enabled как false, и свойство MessageBoxText, активирующее кнопку, только когда в тексте не менее одного символа. MessageButton.cs И // MessageButton.cs (с) 2005 by Charles Petzold // using System; using System.Drawing; using System.Windows.Forms;
30 ГЛАВА 1 class MessageButton: Button { string strMessageBoxText; public MessageButton() { Enabled = false; } public string MessageBoxText { set { StrMessageBoxText = value; Enabled = value != null && value.Length > 0; } get { return strMessageBoxText; } } protected override void OnClick(EventArgs args) { MessageBox.Show(MessageBoxText, Text); } } Обратите внимание на определение MessageBoxText: отсутствие скобок после MessageBoxText и наличие аксессоров set и get говорит о том, что это свойство. В аксессоре set специальное слово value указывает на значение, присваиваемое свойству. Поскольку свойство MessageBoxText определяется как строка, value — тоже строка, длина которой указана в свойстве Length. В Visual Studio я создал пустой проект с именем MessageButtonDemo, а затем добавил пустой файл кода на С#, MessageButton.cs. Создав класс MessageButton, я добавил второй пустой файл кода на С# — MessageButtonDemo.cs. Это программа, демонстрирующая использование кнопки. MessageButtonDemo.cs И // MessageButtonDemo.cs (с) 2005 by Charles Petzold // using System; using System.Drawing; using System.Windows.Fo rms;
Создание приложений 31 class MessageButtonDemo: Form { [STAThread] public static void Main() { Application.EnableVisualStyles(); Application.Run(new MessageButtonDemo()); } public MessageButtonDemo() { Text = "MessageButton Demo"; MessageButton msgbtn = new MessageButtonQ; msgbtn.Parent = this; msgbtn.Text = "Calculate 10,000,000 digits of PI"; msgbtn.MessageBoxText = "This button is not yet implemented!"; msgbtn.Location = new Point(50, 50); msgbtn.AutoSize = true; } } Для проверки работы активизирующего кнопку кода можно закомментировать выражение, присваивающее значение свойству MessageBoxText или присвоить свойству значение null или пустую строку Вводя код MessageButtonDemo.cs в Visual Studio, вы, наверняка, заметили, что IntelliSense обращается с переменной msgbtn как с экземпляром «официального» класса .NET. Именно поэтому я вначале создал класс MessageButton — иначе IntelliSense не знал бы, что делать с MessageButton. Программисты издавна спорят о том, как лучше писать программу: по нисходящей (сначала создать общую структуру, а затем код всех подзадач) или по восходящей (сначала написать подзадачи, а затем выстраивать структуру вверх — вплоть до метода Main). В Visual Studio спорить незачем — чтобы нормально работал IntelliSense, программу лучше писать по восходящей. Теперь класс MessageButton можно использовать в любой программе, добавив MessageButton.cs в проект и скомпилировав все файлы вместе. Чтобы добавить файл с исходным текстом в проект Visual Basic, выберите Add Existing Item в меню Project и укажите ссылку на нужный файл — в диалоговом окне Add Existing Item щелкните маленькую стрелку на кнопке Add и выберите Add As Link. Благодаря ссылке файл не будет скопирован, и его многочисленные копии не помешают, когда его нужно будет изменить. Аппаратно-независимое кодирование В классе Control определено два свойства — Location и Size — для явного указания расположения и размера элементов управления. (Как это сделать по-другому, об-
32 ГЛАВА 1 суждается в главе 3) Свойство Location — это структура Point, содержащая два целочисленных поля X и Y. Они задают положение левого верхнего угла элемента управления относительно левого верхнего угла родительского элемента (в пикселах). Свойство Size задает размер элемента управления в пикселах и содержит структуру Size с двумя полями — Width и Height. (Структуры Point и Size находятся в пространстве имен SystemJDrawing.) Иногда в Windows-программах элементы управления еле видны или не вмещают весь текст. Это значит, что программист не освоил аппаратно-независимое кодирование. Разрешающая способность принтера равна 300,600 или 1200 точек на дюйм, по умолчанию разрешение монитора равно 96 точек на дюйм, но его можно изменить, открыв диалоговое окно Свойства: Экран (Display Properties) в панели управления Windows или щелкнув правой кнопкой на экране. На вкладке Параметры (Settings) щелкните кнопку Дополнительно (Advanced) и выберите нужное разрешение — от 19 до 480 точек на дюйм. (Даже это диалоговое окно не закодировано с учетом такого широкого диапазона разрешений.) Есть лишь два варианта: 9б и 120 точек на дюйм. Последнее делает текст на 25% крупнее — его выбирают ретрограды. Разрешения 96 и 120 точек на дюйм иногда называют мелким (Small Fonts) и крупным (Large Fonts) шрифтом соответственно, а в диалоговом окне Свойства: Экран (Display Properties) они называются Обычный размер (Normal Size) и Крупный размер (Large Size). Свойства DpiX и DpiYкласса Graphics передают программе Windows Forms разрешение любого устройства графического вывода, в том числе монитора. Размер шрифта Windows Forms по умолчанию — примерно 8 точек. Его можно узнать по значению свойства SizelnPoints класса Font. (Точка составляет У72 дюйма, размер в точках — это высота шрифта с учетом подстрочных элементов и без учета диакритических знаков). Свойство Height класса Form задает высоту шрифта в пикселах с небольшим запасом для разделения строк. Для мелкого шрифта его значение равно 13, а для крупного шрифта — 16. Ъ среднем ширина символов шрифта составляет примерно половину их высоты. Вооруженные этими знаниями, мы готовы размещать и задавать размер элементов управления так, чтобы они не зависели от устройства. Есть хорошее правило: высота кнопок должна равняться 13/4 высоты шрифта. А ширина кнопки определяется количеством отображаемых на ней символов плюс две (или, для верности, четыре) средних ширины символа или половина высоты шрифта. Вот небольшая программа, задающая размер клиентской области и кнопки в зависимости от высоты шрифта и помещающая кнопку в центр клиентской области. DevicelndependentButton.cs И // DeviceIndependentButton.es (с) 2005 by Charles Petzold // using System;
Создание приложений 33 using System.Drawing; using System.Windows.Forms; class DevicelndependentButton: Form { [STAThread] public static void Main() { Application.EnableVisualStyles(); Application.Run(new DevicelndependentButtonQ); } public DevicelndependentButtonO { Text = "Device-Independent Button"; int fntht = Font.Height; ClientSize = new Size(fntht * 30, fntht * 10); Button btn = new ButtonO; btn.Parent = this; btn.Text = "Lookin' good!"; btn.Size = new Size(17 * fntht / 2, 7 * fntht / 4); btn.Location = new Point((ClientSize.Width - btn.Width) / 2, (ClientSize.Height - btn.Height) / 2); } } Первый аргумент конструктора Size рассчитан на 13 плюс 4 символа текста, умноженные на половину высоты шрифта, а второй — на 13/4 высоты шрифта. Вот так выглядит программа при разрешении экрана 96 точек на дюйм: ^...^........^^^^^дп,,,|.^.^ А вот та же программа при разрешении 120 точек на дюйм: rice-Independent Button I- IIBTXI |111Ш1111^И111 \ШШШШШ$^ШШШШШ1ШШШШШШШШШШ ^^^^^^^^^^^M'7.u^J^-y^'. Я^^^Щ Естественно, изображение крупнее, но все пропорции сохранены.
34 ГЛАВА 1 В Visual Studio аппаратная независимость достигается за счет пары свойств: AutoScaleDimensions и AutoScaleMode (взамен свойства AutoScaleBaseSize в .NET 1.x.). Они определены в производном от Control классе ContainerControl, которому напрямую наследует Form. Свойство AutoScaleMode принимает значения из перечисления AutoScaleMode —AutoScaleModeDpi,AutoScaleModeFont шм. AutoScaleMode Inherit. Если свойству AutoScaleMode присвоено значение AutoScaleMode Font, класс Form меняет масштаб всех размеров с учетом отношения между значением свойства Font этой формы и размером, указанным в свойстве AutoScaleDimensions. При создании формы в Visual Studio ее расположение и все размеры сохраняются в аппаратных координатах, а в программу добавляется два выражения, задающие свойства AutoScaleDimensions и AutoScaleMode. В Windows для мелкого шрифта (Small Fonts) эти выражения будут следующими: this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); this.AutoScaleMode = System.Windows.Forms.AutoScaleMode. Font; Структура SizeF похожа на Size, но в ней вместо целочисленных используются размеры с плавающей точкой. Средняя ширина и высота стандартного шрифта Windows Forms равны 6 и 13 пикселов соответственно. При запуске этой программы в сеансе Windows с крупным шрифтом (Large Fonts) класс Form масштабирует свои размер и дочерние элементы управления с учетом среднего размера стандартного шрифта Form, равного 8 пикселов в ширину и 16 пикселов в высоту. Горизонтальные координаты и ширина масштабируются с коэффициентом 8/6, а вертикальные координаты и высота — 16/15. Конечно, при создании формы в сеансе Windows с крупным шрифтом (Large Fonts) в код будут вставлены следующие строки: this.AutoScaleDimensions = new System.Drawing.SizeF(8F, 16F); this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; Если часть формы создана с мелким, а остальная форма — с крупным шрифтом, Visual Studio соответственно изменит расположение и размеры всех имеющихся элементов управления. Другой параметр свойства AutoScaleMode — значение из перечисления AutoScale- Mode.Dpi, указывающее разрешение монитора. При создании формы с использованием мелких шрифтов (что эквивалентно разрешению 96 точек на дюйм) в код также могут вставляться следующие строки: this.AutoScaleDimensions = new System.Drawing.SizeF(96, 96); this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Dpi; А для крупных шрифтов:
Создание приложений 35 this.AutoScaleDimensions = new System.Drawing.SizeF(120, 120); this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Dpi; Чтобы закодировать положение и размеры элементов управления, можно более гибко задействовать функцию автоматического масштабирования. Например, опытные программисты в Windows обычно задают расположение диалоговых окон с помощью описания ресурсов. В сценариях ресурсов есть особая система координат: цена деления по горизонтали равна !/4 средней ширины стандартного шрифта, а по вертикали — 78 высоты шрифта. Поэтому если средняя ширина равна примерно половине высоты, цена деления по горизонтали и по вертикали одинаковая. Следующие выражения воспроизводят эту систему координат: AutoScaleDimensions = new Size(4, 8); AutoScaleMode = AutoScaleMode.Font; А эти выражения позволяют задать расположение и размер элементов управления в единицах, равных l/lQ дюйма: AutoScaleDimensions = new Size(10, 10); AutoScaleMode = AutoScaleMode.Dpi; Для правильной работы AutoScaleDimensions и AutoScaleMode должны задаваться точно в указанном порядке и размещаться ближе к концу конструктора, после того, как задан размер клиентской области формы и все элементы управления были созданы и назначены дочерними элементами формы. Вот программа автоматического масштабирования формы и кнопки, видимое поведение которой аналогично поведению программы DevicelndependendButton: AutoScaleButton.cs И // AutoScaleButton.cs (с) 2005 by Charles Petzold // using System; using System.Drawing; using System.Windows.Forms; class AutoScaleButton: Form { [STAThread] public static void Main() { Application.EnableVisualStyles(); Application.Run(new AutoScaleButtonO); } public AutoScaleButtonO
36 ГЛАВА 1 { Text = "Auto-Scale Button"; ClientSize = new Size(240, 80); Button btn = new ButtonO; btn.Parent » this; btn.Text = "Lookin1 good!"; btn.Size = new Size(17 * 4, 14); btn.Location = new Point((ClientSize.Width - btn.Width) / 2, (ClientSize.Height - btn.Height) / 2); AutoScaleDimensions = new Size(4, 8); AutoScaleMode = AutoScaleMode.Font; } } Информация о сборке В процессе разработки стандартного приложения Windows Visual Studio создает еще один файл — AssemblyInfo.cs. Он содержит ряд атрибутов, которые используются компилятором для указания информации об авторских правах на программу и об ее версии в ЕХЕ и DLL-файлах. Эти данные отображаются в диалоговом окне свойств в проводнике и имеют смысл, только если программа предназначена для продажи или передачи другим людям. Если же программа написана для себя, чтобы научиться программировать с помощью Windows Forms, эти сведения не нужны. Одну или все эти записи можно вставить в любое место исходного текста программы. Такие записи я использую в своей программе «Key of C#»: [assembly: AssemblyTitle("Key of C#")] [assembly: AssemblyDescription("Small C# IDE for .NET")] [assembly: AssemblyCompany("www.charlespetzold.com")] [assembly: AssemblyProduct("Key of C#")] [assembly: AssemblyCopyright("(c) Charles Petzold, " + Version.Copyright)] [assembly: AssemblyVersion(Version.Major + "." + Version.Minor + ".*")] Эти атрибуты ссылаются на структуру Version, которая применяется и в других местах программы «Key of C#», например, в диалоговом окне About. При изменении версии или данных об авторских правах нужно всего лишь изменить структуру Version. Слово assembly с двоеточием в этих атрибутах указывает на их цель. Оно означает, что следующий за ним атрибут относится к сборке (обычно это ЕХЕ- или DLL- файл). Когда в файл с исходным текстом добавляются эти атрибуты, в нем надо директивой using подключить SystemReflection, поскольку атрибуты используют
Создание приложений 37 классы в этом пространстве имен. Например, AssemblyTitle ссылается на класс As- semblyTitleAttribute. Наверное, самый важный из этих атрибутов — AssemblyVersion. Версия указывается в виде последовательности из четырех цифр, разделенных точками: основной номер версии (Major Version), дополнительный номер версии (Minor Version), номер сборки (Build Number) и редакция (Revision). Если вместо номера сборки и редакции стоят звездочки, компилятор С# в качестве номера сборки укажет число дней с 1 января 2000 г., а в качестве номера редакции — число секунд с полночи, деленное на два. Такой порядок гарантирует, что номер каждой последующей сборки будет автоматически выше предыдущего. Версия файла и версия продукта совпадают с номером версии AssemblyVersion (несмотря на ее название) или при добавлении атрибута AssemblyFileVersion — с его значением. Диалоговые окна Диалоговые окна служат для получения информации от пользователя, когда это неудобно делать с помощью элементов управления, меню и панелей инструментов в окне приложения. Чаще всего диалоговое окно вызывается с помощью пунктов меню или панели инструментов. Знак многоточия в названии команды меню обычно указывает на то, что она вызывает диалоговое окно. Есть два типа диалоговых окон: модальные и немодальные. Модальные диалоговые окна Это самый распространенный тип диалоговых окон. При появлении такого окна пользователь может вернуться в окно приложения, только закрыв это диалоговое окно. Пользователь работал в приложении, а теперь он находится в другом режиме. Чтобы вернуться из режима диалогового окна в режим приложения, придется закрыть диалоговое окно. В модальных диалоговых окнах обычно есть набор элементов управления, в том числе две кнопки: ОК и Cancel (Отмена). (В диалоговом окне вполне могут быть меню и панели инструментов, но такое очень редко встречается.) Кнопки ОК и Cancel закрывают диалоговое окно, убирая его с экрана и возвращая пользователя в приложение. При щелчке кнопки Cancel приложение игнорирует все выполненное пользователем в диалоговом окне. В Windows Forms диалоговое окно — это просто потомок класса Form: class MyDialogBox: Form { }
38 ГЛАВА 1 Программа открывает диалоговое окно, создавая экземпляр этого класса и вызывая метод ShowDialog, — обычно это происходит в ответ на щелчок пункта меню: MyDialogBox dig = new MyDialogBoxO; dlg.ShowDialogO; Метод ShowDialog не возвращает управление программе, пока пользователь не закроет окно. Часто диалоговое окно внешне немного отличается от окна приложения. В нем обычно нет кнопки минимизации и максимизации и рамки установки размеров окна. Впрочем, сейчас такие рамки встречаются в диалоговых окнах все чаще. О том, как их добавить, чуть позже. Когда-то в диалоговых окнах не было строки заголовка, теперь это в прошлом. Строка заголовка позволяет переместить диалоговое окно в другое место экрана, например, чтобы увидеть закрытую им область. Приложение должно получить от диалогового окна информацию, по щелчку какой кнопки оно было закрыто — ОК или Cancel. Диалоговое окно закрывается присвоением свойству DialogResult значения одного из членов перечисления Dialog- Result — DialogResult.OK или DialogResult.Cancel. Метод ShowDialog, изначально вызванный формой приложения для открытия диалогового окна, передает управление приложению и возвращает значение свойства DialogResult. Таким образом, как минимум в диалоговом окне надо реализовать обработчики события Click кнопок ОК и Cancel, присваивающие свойству DialogResult соответствующее значение. Вот обработчик для кнопки ОК. void OkButtonOnClick(object objSrc, EventArgs args) { DialogResult = DialogResult.OK; } В следующем коде форма приложения выводит диалоговое окно и обрабатывает результаты: if (dlg.ShowDialogO == DialogResult.OK) { } В Windows Forms этот процесс немного упрощен благодаря реализации свойства DialogResult в классе Button. Хотя это свойство, конечно, связано со свойством DialogResult класса Form, они разные. В классах Form и Button реализованы различные свойства DialogResult — они не наследуют его от Control. Если свойству кнопки DialogResult присвоено значение одного из членов перечисления DialogResult, при щелчке кнопки свойству формы DialogResult автоматически присваивается значение и форма закрывается.
Создание приложений 39 Рассмотрим все вышесказанное на примере простого приложения. Программа состоит из двух классов — главной формы приложения и диалогового окна. Вот класс диалогового окна: ModalDialogBox.es И // ModalDialogBox.cs (с) 2005 by Charles Petzold // using System; using System.Drawing; using System.Windows.Forms; class ModalDialogBox: Form { CheckBox cbGrayShades; public ModalDialogBoxO { Text = "Change Color"; FormBorderStyle = FormBorderStyle.FixedDialog; ControlBox = false; MinimizeBox = false; MaximizeBox = false; ShowInTaskbar = false; StartPosition = FormStartPosition.Manual; Location = ActiveForm.Location + Systemlnformation.CaptionButtonSize + Systemlnformation.FrameBorderSize; ClientSize = new Size(144, 56); cbGrayShades = new CheckBoxQ; cbGrayShades.Parent = this; cbGrayShades.Text = "Gray Shades Only"; cbGrayShades.Location = new Point(16, 8); cbGrayShades.Size = new Size(80, 12); Button btn = new Button(); btn.Parent = this; btn.Text = "OK"; btn.Location = new Point(16, 32); btn.Size = new Size(48, 14); btn.DialogResult = DialogResult.OK; AcceptButton = btn;
40 ГЛАВА 1 btn = new ButtonO; btn.Parent = this; btn.Text = "Cancel"; btn.Location = new Point(80, 32); btn.Size = new Size(48, 14); btn.DialogResult = DialogResult.Cancel; CancelButton = btn; AutoScaleDimensions = new Size(4, 8); AutoScaleMode = AutoScaleMode.Font; } public bool GrayShades { set { cbGrayShades.Checked = value; } get { retu rn cbGrayShades.Checked; } } } Сначала в конструкторе создается текст строки заголовка диалогового окна — он должен иметь отношение к команде меню, вызвавшей диалоговое окно. А описанные далее свойства устанавливают неизменяемую рамку диалогового окна (стандартную для диалоговых окон) и скрывают экранные кнопки управления окном, минимизации и максимизации. Значение false свойства ShowInTaskBar запрещает отображение диалогового окна в панели задач пользователя. Если не задать следующих двух свойств, диалоговое окно будет отображаться на экране в месте, определяемом Windows по умолчанию. Мне нравится, когда диалоговое окно отображается немного правее и ниже левого верхнего угла окна родительского приложения. Конструктор ссылается на свойство ActiveForm, чтобы узнать текущую активную форму, в которой должно появиться диалоговое окно. (Поэкспериментируйте со свойствами StartPosition и Location, чтобы узнать их возможности.) Затем программа создает флажок с текстом «Gray Shades Only» и две кнопки: ОК и Cancel. Код конструктора присваивает свойству этих двух кнопок DialogResult значения DialogResult.OKи DialogResult.Cancel соответственно. Конструктор также присваивает свойства AcceptButton и CancelButton формы диалогового окна объектам кнопок ОК и Cancel. Эти два свойства также играют
Создание приложений 41 важную роль в правильной разметке диалогового окна. Свойство AcceptButton превращает кнопку ОК в кнопку по умолчанию — у нее утолщенная граница и она откликается на нажатие клавиши Enter, даже если активен другой элемент управления. Поэтому пользователь может закрыть диалоговое окно и сообщить приложению о выборе кнопки ОК, просто нажав Enter. Однако в фокусе другая кнопка диалогового окна, она становится кнопкой по умолчанию и откликается на нажатие клавиши Enter. Свойство CancelButton позволяет в любой момент закрыть диалоговое окно нажатием клавиши Esc. И, наконец, класс диалогового окна включает открытое свойство, которое позволяет коду, внешнему по отношению к данному классу, получать доступ к текущему состоянию элемента управления Checkbox. Это булево свойство, и называется оно GrayShades. В сущности, диалоговое окно рассматривается как объект для получения булевого значения от пользователя. Обработчики событий не установлены: для этого диалогового окна они не нужны. Класс, использующий это диалоговое окно, называется ModalDialogDemo. Файлы ModalDialogBox.cs и ModalDialogDemo.cs составляют проект ModalDialogDemo. ModalDialogDemo.cs И // ModalDialogDemo.es (с) 2005 by Charles Petzold // using System; using System.Drawing; using System.Windows.Forms; class ModalDialogDemo: Form { [STAThread] public static void MainQ { Application.EnableVisualStyles(); Application.Run(new ModalDialogDemo()); } public ModalDialogDemoO { Text = "Modal Dialog Demo"; Button btn = new ButtonO; btn.Parent = this; btn.Text = "Change Color"; btn.Location = new Point(16, 16); btn.AutoSize = true; btn.Click += ButtonOnClick;
42 ГЛАВА 1 } void ButtonOnClick(object objSrc, EventArgs args) { ModalDialogBox dig = new ModalDialogBoxO; if (dlg.ShowDialogO == DialogResult.OK) { Random rnd = new RandomQ; int iShade = rnd.Next(255); if (dlg.GrayShades) BackColor = Color.FromArgb(iShade, iShade, iShade); else BackColor = Color.FromArgb(iShade, rnd.Next(255), rnd.Next(255)); } } } Конструктор обычно просто создает кнопку и устанавливает обработчик события Click. Хотя обычно диалоговое окно вызывается щелчком команды меню или кнопки на панели инструментов, в данном случае оно открывается по щелчку кнопки. Сначала обработчик Click создает объект типа ModalDialogBox, а затем вызывает его метод ShowDialog, чтобы отобразить это диалоговое окно. По закрытии диалогового окна ShowDialog возвращает управление приложению. Если возвращается результат DialogResult.OK, программа окрашивает фон формы в случайный цвет. Если пользователь отметил флажок Gray Shades Only, фон окрашивается в случайный оттенок серого (в большинстве программ с диалоговыми окнами цвет задается не так произвольно). Ссылаясь на свойство dlg.GrayShades, форма приложения на самом деле ссылается на состояние флажка. И хотя диалоговое окно уже закрыто и исчезло с экрана, форма и все ее элементы управления все еще хранятся в памяти. Эти элементы управления существуют до тех пор, пока dig не уйдет из области видимости и не попадет в распоряжение механизма сборки мусора. Точно так же приложение может инициализировать флажок после создания объекта диалогового окна, но до вызова ShowDialog. Приложение может инициализировать диалоговое окно с текущими параметрами приложения или с теми, которые были на момент последнего вызова окна. Немодальные диалоговые окна Такие окна встречаются гораздо реже. «Немодальными» их называют из-за того, что пользователь может переключаться между диалоговым окном и главным окном приложения (но визуально диалоговое окно всегда расположено поверх приложения). Немодальные диалоговые окна обычно используются для операций поиска и
Создание приложений 43 замены. Пользователь может внести изменения в документ, не закрывая диалоговое окно. В немодальных диалоговых окнах нет кнопок ОК и Cancel, но есть стандартная экранная кнопка Close (Закрыть) справа в строке заголовка. Такой внешний вид диалогового окна получается, если свойству FormBorderStyle присвоить значение Fixed- Dialog, свойствам MinimizeBox и MaximixeBox —false (как и в случае модальных диалоговых окон), а свойству ControlBox задать значение true. И хотя свойство ControlBox на самом деле ссылается на значок меню слева в строке заголовка, в результате сочетания этих свойств отображается только кнопка закрытия, а значок меню — нет. Чтобы расположить немодальное диалоговое окно поверх окна приложения, нужно в свойстве Owner диалогового окна задать основную форму приложения. Это выполняется после создания диалогового окна: dig.Owner = this; Программа активирует немодальное диалоговое окно вызовом метода Show класса Form, а не ShowDialog. Как вы наверняка помните, SbowDialog возвращает управление программе только после закрытия диалогового окна, a Show делает это сразу, поэтому пользователь может продолжить работу в главном окне. Когда Show возвращает управление, возникает некоторая путаница: обычно диалоговое окно должно сообщать главному окну о щелчке одной из его кнопок. Эта проблема решается с помощью события. В диалоговом окне определено событие, инициируемое каждый раз, когда нужно передать главному окну информацию об изменениях, а главное окно содержит обработчик этого события. Пусть это событие называется Change и пусть все обработчики событий, установленные для Change, будут согласованы с делегатом EventHandler. В классе диалогового окна появится следующий код: public event EventHandler Change; При желании можно задать свои делегаты, чтобы события были основаны на них. Всякий раз, когда класс инициирует это событие, он вызывает событие Change как метод, передавая аргументы, определенные делегатом: if (Change != null) Change(this, new EventArgsO); Change будет равным null, если для него не были установлены обработчики и вызов Change не требуется. Первый аргумент — это объект, инициировавший событие. Класс ModelessDialogBox создает флажок (как и ModalDialogBox), но в нем есть лишь одна кнопка Change.
44 ГЛАВА 1 ModelessDialogBox.es И // ModelessDialogBox.es (с) 2005 by Charles Petzold // using System; using System.Drawing; using System.Windows.Forms; class ModelessDialogBox: Form { CheckBox cbGrayShades; public event EventHandler Change; public ModelessDialogBoxO { Text = "Change Color"; FormBorderStyle = FormBorderStyle.FixedDialog; MinimizeBox = false; MaximizeBox = false; ShowInTaskbar = false; StartPosition = FormStartPosition.Manual; Location = ActiveForm.Location + Systemlnformation.CaptionButtonSize + Systemlnformation.FrameBorderSize; ClientSize = new Size(144, 56); cbGrayShades = new CheckBoxO; cbGrayShades.Parent = this; cbGrayShades.Text = "Gray Shades Only"; cbGrayShades.Location = new Point(32, 8); cbGrayShades.Size = new Size(80, 12); Button btn = new ButtonO; btn.Parent = this; btn.Text = "Change"; btn.Location = new Point(48, 32); btn.Size = new Size(48, 14); btn.Click += ButtonOnClick; AcceptButton = btn; AutoScaleDimensions = new Size(4, 8); AutoScaleMode = AutoScaleMode.Font;
Создание приложений 45 } public bool GrayShades { set { cbGrayShades.Checked = value; } get { return cbGrayShades.Checked; > > void ButtonOnClick(object objSrc, EventArgs args) { if (Change != null) Change(this, new EventArgsO); } } Обратите внимание, как диалоговое окно инициирует событие Change при каждом щелчке кнопки Change. Класс ModelessDialogDemo отображает это немодальное диалоговое окно. Оба файла входят в проект ModelessDialogDemo. ModelessDialogDemo.cs И // ModelessDialogDemo.es (с) 2005 by Charles Petzold // using System; using System.Drawing; using System.Windows.Forms; class ModelessDialogDemo: Form { [STAThread] public static void Main() { Application.EnableVisualStyles(); Application.Run(new ModelessDialogDemoQ); } public ModelessDialogDemoQ { Text = "Modeless Dialog Demo";
46 ГЛАВА 1 Button btn = new ButtonO; btn.Parent = this; btn.Text = "Change Color"; btn.Location = new Point(16, 16); btn.AutoSize = true; btn.Click += ButtonOnClick; } void ButtonOnClick(object objSrc, EventArgs args) { ModelessDialogBox dig = new ModelessDialogBoxO; dig.Owner = this; dig.Change += DialogOnChange; dlg.Show(); > void DialogOnChange(object objSrc, EventArgs args) { ModelessDialogBox dig = (ModelessDialogBox) objSrc; Random rnd = new RandomQ; int iShade = rnd.Next(255); if (dlg.GrayShades) BackColor = Color.FromArgb(iShade, iShade, iShade); else BackColor = Color.FromArgb(iShade, rnd.Next(255), rnd.Next(255)); > > В процессе работы обработчика события ButtonOnClick программа создает диалоговое окно, назначает владельцем главную форму приложения, устанавливает обработчик для события Change этого диалогового окна и отображает диалоговое окно методом Show. После отображения диалогового окна на экране, Show возвращает управление программе, и, как и прежде, главное окно приложения доступно для пользователя. Когда пользователь щелкает кнопку Change в диалоговом окне, оно инициирует событие Change. Главная форма реагирует на это событие, запуская обработчик DialogOnChange. Обработчик события может получить объект «диалоговое окно», приведя первый аргумент к объекту типа ModelessDialogBox. Экспериментируя с DialogBoxDemo, многократно щелкая кнопку Change Color, вы получите несколько копий диалогового окна, что нежелательно в реальном приложении. Есть много решений этой проблемы. Во-первых, объект dig можно сохранить как поле и просто игнорировать создание диалогового окна, если dig не равно null. Во-вторых, (и это лучший вариант) можно отключить кнопку Change Color в ButtonOnClick при создании диалогового окна:
Создание приложений 47 ((Button) objSrc).Enabled = false; Теперь же нужно вновь активизировать кнопку, когда диалоговое окно закроется. Для этого в форме можно установить обработчик для события Closed диалогового окна: dig.Closed += DialogOnClosed; Обработчик события активизирует кнопку при закрытии диалогового окна: void DialogOnClosed(object objSrc, EventArgs args) { Controls[0].Enabled = true; } В реальной программе объект кнопки, скорее всего, будет сохранен как поле. Я же просто обратился к первому элементу набора Controls данной формы. В платформе .NET Framework 2.0 определено семь стандартных диалоговых окон, восходящих к CommonDialog: ColorDialog, FolderBrowserDialog, FontDialog, OpenFile- Dialog и SaveFileDialog (оба производные от FileDialog), PageSetupDialog и PrintDialog. DLL-библиотеки Динамически подключаемые библиотеки содержат код и данные, используемые другими DLL-библиотеками и программами. Исходный текст DLL-библиотек очень похож на исходный текст программ, но есть три основных отличия: ■ в DLL-библиотеках нет метода Main; ■ чтобы классы были видимы за пределами DLL-библиотек, их нужно определять как открытые (public); ■ хорошим тоном считается определение пространства имен в DLL-библиотеке. Пространства имен позволяют избежать конфликтов на уровне имен при использовании DLL-библиотек с совпадающими именами классов. Рекомендуемый формат пространства имен — «название компании + точка + имя продукта». В следующем исходном тексте класс MessageButton реализован как DLL-библиотека. MessageButtonLib.cs И // MessageButtonLib.cs (с) 2005 by Charles Petzold // using System; using System.Drawing; using System.Windows.Forms;
48 ГЛАВА 1 namespace Petzold.ProgrammingWindowsForms { public class MessageButton: Button { string strMessageBoxText; public MessageButtonO { Enabled = false; > public string MessageBoxText { set { StrMessageBoxText = value; Enabled = value != null && value.Length > 0; > get { return strMessageBoxText; } > protected override void OnClick(EventArgs args) { base.OnClick(args); MessageBox.Show(MessageBoxText, Text); > > } Этот файл отличается от MessageButton.cs тем, что в нем есть определение пространства имен, а класс определяется как public. В метод OnClick также добавлено выражение для вызова этого же метода в базовом классе (Button). Без этого выражения программа не смогла бы подключить обработчик события Click к Message- Button. При компиляции MessageButtonLib.cs в командной строке нужно указать параметр /targetdibrary, тогда компилятор создаст файл MessageButtonLib.dll. В Visual Studio сначала можно создать проект Class Library, или в диалоговом окне свойств проекта в поле Output Type выбрать Windows Class Library. Я создал проект MessageButtonLib в решении Visual Studio с именем Message- ButtonLibraryDemo. В этом же решении я создал еще один проект ProgramUsingLibraiy, состоящий из одного файла с исходным текстом, похожего на программу Message- ButtonDemo.
Создание приложений 49 ProgramUsingLibrary.cs И // ProgramUsingLibrary.cs (с) 2005 by Charles Petzold // using System; using System.Drawing; using System.Windows.Forms; using Petzold.ProgrammingWindowsForms; class ProgramUsingLibrary: Form { [STAThread] public static void Main() { Application.EnableVisualStyles(); Application.Run(new ProgramUsingLibrary()); > public ProgramUsingLibraryO { Text = "Program Using Library"; MessageButton msgbtn = new MessageButtonO; msgbtn.Parent = this; msgbtn.Text = "Calculate 10,000,000 digits of PI"; msgbtn.MessageBoxText = "This button is not yet implemented!"; msgbtn.Location = new Point(50, 50); msgbtn.AutoSize = true; > } Обратите внимание на новую директиву using, которая ссылается на пространство имен в DLL-библиотеке. Директивы using использовать необязательно, но в противном случае нужно ссылаться на полное имя класса (в данном примере — Petzold.ProgrammingWindowsFormsMessageButtori). И все же необходима новая ссылка. До этого программы Windows Forms компилировались со ссылкой на сборки System, System.Drawing и SystemWindows.Forms, которые являются DLL-библиотеками. Теперь же нужна ссылка на MessageButtonLib.dll, чтобы компилятор получил необходимые метаданные для компиляции этой программы. В командной строке местоположение файла MessageButtonLib.dll указывается параметром /reference. А в Visual Studio удобнее выбрать сборку MessageButtonLib из списка на вкладке Projects диалогового окна Add Reference. При сборке решения в Visual Studio желательно, чтобы DLL-библиотека ском- пилировалась раньше использующей ее программы. После указания ссылки на DLL-
50 ГЛАВА 1 библиотеку из программы, Visual Studio считает, что программа от нее зависит. Поэтому на вкладке Build Order диалогового окна Project Dependencies (которое вызывается из меню Project или щелчком правой кнопкой имени решения) отображается правильный порядок сборки. Если решение состоит из нескольких проектов, Visual Studio компилирует все, но запустит на исполнение только один. В окне Solution Explorer этот проект выделен полужирным. Чтобы сделать ProgramUsingLibrary стартовым проектом, щелкните имя проекта правой кнопкой и выберите Save As Startup Project. Теперь можно перейти к сборке решения. Visual Studio сначала компилирует DLL- библиотеки, а затем саму программу. Поскольку для работы файлов ProgramUsing- Library.exe необходима библиотека MessageButtonLib.dll, Visual Studio скопирует их в одну папку. Верится с трудом, но когда-то создание DLL-библиотеки считалось высшим пилотажем в программировании для Windows.
Глава 2 Этот плодовитый класс Control] Control — самый важный класс в Windows Forms — он один позволяет получить несчетное количество инструментов для создания приложений Windows Forms. Control не только базовый класс для таких элементов управления, как кнопки, деревья, панели инструментов и меню, но и для Form — класса, инкапсулирующего главное окно приложения Windows Forms и выполняющего другие функции в диалоговых окнах. Объект Form по умолчанию содержит строку заголовка с системным меню слева и кнопки свертки, развертки и закрытия окна — справа. По периметру окна проходит рамка определения размера окна. Внутри окна расположена клиентская область — в ней отображается информация, и содержатся инструменты для пользователя. Программы могут отображать текст и графическую информацию непосредственно в клиентской области и получать команды с клавиатуры или мыши с применением таких методов, как OnKeyPress и OnMouseDown. Но в большинстве современных программ в клиентской области располагаются дочерние элементы управления — обработка ввода/вывода информации о действиях пользователя делегируется им. Кроме того, программисты вправе создавать собственные элементы управления, расширяя либо комбинируя имеющиеся. В этой главе рассказывается об элементах управления, определенных в пространстве имен System.WindowsForms, и наиболее важных их свойствах и событиях (и, попутно, методах), необходимых для использования этих элементов управления. Ясно, что одна глава (не книга целиком) вряд ли заменит документацию по .NET Framework (для этого нужна отдельная книга). В этой главе не обсуждаются некоторые возможности класса Control, поскольку они описаны в следующих главах. Глава 3 содержит описание элемента управления SplitContainer, а также элементов, производных от Panel, в том числе FlowLayoutPanel, SplitterPanel и TableLayout- Panel В книге не обсуждается Splitter, поскольку ему на смену пришли SplitContainer и SplitterPanel. (Splitter описан в начале главы 22 моей книги Programming Microsoft
52 ГЛАВА 2 Windows with C# [Программирование для MS Windows на С#. М.: Русская Редакция, 2002]). Глава 5 описывает ToolStrip, MenuStrip, ContextMenuStrip, StatusStrip и все элементы управления, названия которых начинаются с ToolStrip. Я обхожу вниманием меню .NET Framework 1.x, а также элементы управления ToolBar к StatusBar, поскольку они заменены MenuStrip, ToolStrip и StatusStrip. (Создание меню в .NET Framework 1.x описано в главе 14, a StatusBar и ToolBar — в главе 20 книги Programming Microsoft Windows with C#.) В главе б рассказывается об элементах управления BindingNavigator и DataGridView, а также всех элементах управления, названия которых начинаются с префикса DataGridView. DataGrid и DataGridTextBox не обсуждаются, поскольку им на смену пришли DataGridView и DataGridViewTextBoxEditingControl В главе 7 описаны программы, в которых используются MdiClient, PropertyGrid и WebBrowser. Обзор элементов управления Практически все на экране Microsoft Windows состоит из элементов управления. Действительно, в общем случае, элемент управления можно определить как визуальный объект. Обычно элементы управления занимают прямоугольную область экрана, хотя могут быть непрямоугольными или даже скрытыми и выполнять команды, поступающие с клавиатуры или мыши. Класс Control поддерживает множество событий (и соответствующих методов ввода) для обработки ввода информации, например событий клавиатуры KeyDown, KeyUp и KeyPress и мыши — MouseDown, MouseUp и MouseMove. Также элементы управления должны «перерисовывать» себя на экране — для этого служит метод OnPaint. Превращая информацию об операциях пользователя в простые события, элементы управления служат уровнем абстрагирования между пользователем и приложением. Например, для нормальной работы элемента Button достаточно определить отображаемый на кнопке текст и обработчик события Click. Об остальном позаботится элемент управления. Чтобы разобраться, как работает тот или иной элемент управления, полезно поэкспериментировать с ним. Конечно, это можно сделать как с помощью Microsoft Visual Studio, так и программы ControlExplorer, которую я написал специально для этих целей. Поскольку в ControlExplorer используются методы программирования, описываемые в следующих главах, ее описание можно найти в главе 7. В меню ControlExplorer перечислены все открытые классы сборки System.Windows. Forms, производные от Control. Иерархия меню соответствует иерархии наследования. При выборе элемента в меню программа создает его в левом верхнем углу клиентской области. Кроме этого, ControlExplorer отображает диалоговое окно,
Этот плодовитый класс Controll 53 позволяющее изменять свойства элемента управления, проверять свойства «только для чтения» и просматривать инициируемые элементом события. ControlExplorer пригодится и следующих главах, где описываются другие элементы управления. Родители и потомки Одно из самых важных определяемых классом Control свойств — Parent; оно указывает на другой объект типа Control. В процессе исполнения приложения на экране отображаются только элементы управления с корректно определенным свойством Parent. Положение элемента всегда задается относительно «родителя» и отображается на поверхности последнего. Часть элемента управления, выступающая за края родителя, не видна. Класс Form является исключением из этого правила. Значение свойства Parent объекта типа Form обычно равно null, то есть родительским свойством формы является рабочий стол. Однако объект Form, у которого значение свойства TopLevel равно false (то есть родитель формы — не рабочий стол), может быть потомком любого другого элемента управления. Такая методика используется в приложениях с многодокументным интерфейсом (Multiple Document Interface, MDI). (В классе Form есть свойство Owner, в котором можно задать другой объект типа Form. Форма на экране всегда отображается поверх своего владельца, и если последний сворачивается или закрывается, то же происходит с формой. Этот механизм полезен в немодальных диалоговых окнах. Владелец немодального диалогового окна должен сопоставляться создавшему его приложению.) В простейшем случае, объект типа Form создает набор элементов управления и «прописывает» себя в свойстве Parent элементов управления. Вот соответствующий код в конструкторе форм: Button btn = new Button(); btn.Parent = this; Оператор, присваивающий значение свойству Parent кнопки, функционально эквивалентен оператору: this.Controls.Add(btn); или Controls.Add(btn); Controls - это свойство, определяемое классом Control и унаследованное классом Form. Это объект типа ControlControlCollection — то есть, экземпляр класса Control- Collection, определенного в классе Control. Свойство Controls содержит всех потомков класса Control. Метод Add добавляет элемент управления в набор, a Remove — удаляет:
54 ГЛАВА 2 Cont rols.Remove(btn); Этот оператор функционально эквивалентен коду: btn.Parent = null; Свойство Controls может представляться в виде массива, где нумерация элементов начинается с нуля. Например, так обращаются к четвертому элементу набора: Controls[3] Это выражение возвращает объект типа Control. Если форма «знает», что, к примеру, это объект Button, его можно привести к объекту Button таким оператором: Controls[3] as Button или (Button) Controls[3] Между этими двумя вариантами приведения есть разница. Если Controls[3] не является объектом Button, выражение as возвратит null Во втором случае задейству- ется приведение в стиле С, поэтому программа вернет исключение. В программе можно проверить, является ли конкретный Control объектом Button, воспользовавшись выражением is: if (Controls[3] is Button) { Button btn = Controls[3] as Button; } Форма позволяет перечислить все элементы управления своего набора, используя цикл for. for (int i = 0; i < Controls.Count; i++) { Control Ctrl = Controls[i]; } Для этого также часто задействуют цикл/oreach: foreach (Control Ctrl in Controls) { } Хотя любой элемент управления может быть родителем по отношению к другим элементам, в действительности, немногие наследники Control используются в
Этот плодовитый класс Control] 55 качестве родителей. Обычно это Form, Panel и производные классы, а также GroupBox. Вобще говоря, элементы управления — это дочерние элементы формы, но иерархия «родитель-потомок» может распространяться гораздо дальше. Например, Form может содержать в своей клиентской области несколько элементов управления Panel, а эти панели могут иметь собственные дочерние элементы управления. Элементы управления, производные от Panel и ContainerControl (включая Form), самостоятельно управляют фокусом среди своих потомков. Как правило, перемещение между элементам управления выполняется клавишей Tab. Фокус ввода способны получать только элементы управления с заданным свойством TabStop. Порядок получения фокуса задается свойством Tablndex производных элементов управления, или выполняется в так называемом z-порядке, то есть в порядке, в котором элементы были добавлены в набор. Дочерние элементы управления одного родителя (и, поэтому, члены одного набора) называются родственными. Видимость и отклик По умолчанию все элементы управления видимы и активны. Если свойству Visible присвоить значение false, элемент управления исчезнет с поверхности своего родителя. Он никуда не пропадет и по-прежнему будет оставаться членом родительского набора элементов управления, но картина будет такой, как будто его нет. Все потомки невидимого элемента управления также невидимы. Однако наиболее часто меняют свойство Enabled. Элемент со свойством Enabled равным/я/se отображается на экране, но выглядит обесцвеченным, или отключенным. Такой элемент управления не получает фокус ввода и не реагирует на команды клавиатуры или мыши. Часто для активизации и отключения элементов управления используются параметры других элементов. Например, в диалоговом окне File Open кнопка Open будет отключена, пока пользователь не задаст имя файла. Расположение и размер Каждому элементу управления задается определенный размер и положение относительно левого верхнего угла его родителя. Расположение определяется свойством Location — объектом Point с двумя свойствами — XwY. Координаты задаются в пикселах, и определяют положение верхнего левого угла элемента управления относительно верхнего левого угла родителя. Размер элемента задается свойством Size — объектом типа Size со свойствами Width и Height. Вот код, задающий кнопку с положением в точке (50,100) и размерами 75 пикселов по ширине и 25 — по высоте: btn.Location = new Point(50, 100); btn.Size = new Size(75, 25);
56 ГЛАВА 2 Конечно, существует сложности, связанные с работой программы на различных системах с различными разрешениями экрана. Кроме того, может потребоваться перевод программы на другой язык. В одних языках слова короче, в других — длиннее, и это влияет на размер элементов управления, отображающих текст. Усложнение элементов управления приводит к тому, что задавать должный размер становится все труднее. Проще позволить элементам управления самостоятельно определять нужный размер и располагаться в форме динамически в зависимости от требуемого размера. Динамическое определение компоновки становится очень важным для дизайна, поэтому я посвятил этой теме целиком следующую главу. Некоторые новые свойства .NET Framework 2.0 сильно помогают в работе. У Control есть новое свойство «только для чтения», PreferredSize, задающее размер элемента управления в зависимости от его содержимого и используемого шрифта. Очень мощное изменяемое свойство — AutoSize. Обычно в большинстве элементов управления его значение равно false, но если ему присвоить значение true, свойство Size примет значение PreferredSize. У некоторых (но не всех) элементов управления есть свойство AutoSizeMode, которому присваивается одно из значений перечисления AutoSizeMode. Параметры GrowOnly и GrowAndSbrink определяют, как элемент управления ведет себя во время исполнения, когда его содержимое меняется. Два новых свойства .NET Framework 2.0 — это Padding и Margin. Они используются для динамического размещения, и о них рассказывается в следующей главе. Еще два свойства, важных для процесса разметки формы — Dock и Anchor. Первое используется для растягивания и выравнивания элемента управления по определенному краю родителя. Для этого свойству задают одно из значений перечисления DockStyle — Left, Right, Top, Bottom или Fill. Например, совершенно естественно, что у панели инструментов значение свойства Dock равно DockStyle.Top (то есть панель расположена вверху родительской формы), а у строки состояния — DockStyle.- Bottom. DockStyleFill заставляет элемент управления заполнить всю клиентскую область родителя. Свойство Anchor заставляет элемент управления располагаться на определенном постоянном расстоянии от одной из сторон родителя при изменении размеров последнего. Свойству может присваиваться любая комбинация членов перечисления AnchorStyles. По умолчанию используется AnchorStylesLeft\AnchorStylesRight, то есть при изменении размера родителя элемент управления остается в том же положении относительно его левой и правой сторон. Более подробно о свойствах Dock и Anchor будет рассказано в следующей гдаве. Шрифты и цвет У всех элементов управления есть свойство Text, хотя некоторые из них (например, полоса прокрутки) не отображают никакого текста. Для объекта Form, свойство Text — это текст, отображаемый в строке заголовка формы.
Этот плодовитый класс Control! 57 Элемент управления наследует исходные свойства Font, BackColor и ForeColor у своего родителя. При изменении свойств родителя свойства «дочек» также изменяются. Однако если элементам управления явно задать другие свойства, они сохранят их и перестанут зависеть от родителей. В качестве значения свойства Font задают объект типа Font. Например, следующее выражение задает шрифт кнопки: курсив Times New Roman размером 24 пункта. btn.Font = new Font("Times New Roman", 24, FontStyle.Italic); Свойства BackColor и ForeColor задают цвет фона и изображения элемента управления. Цветом изображения часто является цвет текста элемента управления. Некоторые элементы управления игнорируют такие свойства либо используют их другим образом. В качестве значения этим свойствам можно присвоить любой созданный объект Color (обычно используется статический метод ColorFromArgb) или один из статических членов структуры Color. btn.ForeColor = Color.HotPink; По умолчанию свойствам BackColor и ForeColor элементов управления задаются значения SystemColors.Control и SystemColors.ControlText, соответственно. Объекту Form также по умолчанию назначаются эти цвета. Поскольку SystemColors.Control — это обычно серый цвет, фон клиентской области формы — серый, что обычно для диалоговых окон всех основных приложений. Если нужно, чтобы форма отображала черный текст на белом фоне, введите в конструктор форм код: BackColor = SystemColors.Window; ForeColor = SystemColors.WindowText; Чтобы не отменять персональные параметры пользователей, предпочитающих экран с белым текстом на черном фоне, лучше использовать системные, а не черно-белые цвета. В этом случае у таких пользователей SystemColors.Window будет черным, a SystemColors.WindowText — белым. Свойство Backgroundlmage позволяет задать изображение (либо растр или метафайл) на фоне элемента управления. Появившееся в .NET Framework 2.0 свойство BackgroundlmageLayout позволять справляться с ситуациями, когда изображение не совпадает с элементом управления по размеру. Отслеживание элементов управления У формы может быть много дочерних элементов управления. В качестве примера возьмем диалоговое окно с набором элементов управления и кнопкой ОК. Вполне может быть так, что диалоговое окно игнорирует любые действия пользователя, пока не нажата кнопка ОК. В этом случае, диалоговое окно опросит все элементы управления на предмет наличия текста, выбранных флажков и т. п. Программе, созданной таким образом, нет необходимости устанавливать обработчики событий
58 ГЛАВА 2 для всех остальных элементов управления, а нужен способ доступа ко всем элементам управления. Один из способов заключается в хранении всех объектов управления как полей, в этом случае элементы управления доступны по соответствующему классу. Однако есть и другие способы. Из класса форм, через набор Controls доступны все дочерние элементы управления. Выделить конкретный элемент из набора можно по его типу, свойствам Text или Name, которые специально созданы для этих целей. Вообще, вначале работы с .NET Framework 2.0 можно даже размечать (индексировать) набор Controls, используя свойство Name. Назовем одну кнопку таю btn.Name = "ok"; Используя в этой форме какой-либо другой метод, можно получить объект кнопки с помощью кода: Button btn = Controls["ok"] as Button; Набор Controls возвращает объект типа Control, ъ выражение as приводит его к объекту типа Button. Свойство Controls — это экземпляр класса ControlControlCollection, а этот класс предлагает множество методов, таких как Contains и Find, позволяющих управлять набором дочерних элементов управления. Произвольные объекты можно подключить к элементу управления при помощи свойства Tag. Оно полезно при обозначении набора сходных элементов управления. Например, при использовании элементов управления с независимой фиксацией для курсивного и полужирного начертания шрифта, можно обозначить такой элемент управления, присвоив свойству Tag значение, равное FontStyleJtalic и Font- StyleBold. Списки изображений (ImageList) Определенное в классе Control свойство Controls хранит набор дочерних элементов управления. В .NET Framework много подобных наборов, а некоторые классы, например ImageList, служат, главным образом, в качестве набора объектов. ImageList — это набор объектов Image (категория, включающая растры и метафайлы) одного размера и глубины цвета. Вообще говоря, ImageList — не элемент управления, но часто используется вместе с ними. Основное свойство ImageList называется Images. Это объект типа ImageListJmageCollection. Для начала необходимо создать объект типа ImageList: ImageList img 1st = new ImageListO; Затем можно добавлять изображения к списку, используя метод Add объекта ImageListJmageCollection. Если img — одна из разновидностей объекта Image, то код выглядит таю imglst.Images.Add(img);
Этот плодовитый класс Controll 59 В начале работы с .NET Framework 2.0 можно также сопоставить изображению текстовую клавишу: imglst.Images.Add("arrow", img); Затем можно будет получить это изображение, обозначив ключевым значением одно из изображений из списка свойства Images: imglst.Images["arrow"] Так удобно систематизировать набор используемых в программе значков. Где берутся эти изображения? Вы вправе сделать их самостоятельно, но можно использовать набор растров и значков пакета Visual Studio 2005 (к сожалению, их нет в Visual C# 2005 Express Edition). Значки хранятся в ZIP-файле, из папки \Program Files\Microsoft Visual Studio 8\Common7\VS2005ImageLibrary. В конце главы я продемонстрирую, как использовать значки из этого набора. Всплывающие подсказки (ToolTip) Как и ImageList, класс ToolTip не является элементом управления, но работает с ними в тесном контакте. Всплывающая подсказка — это информативный текст, обычно появляющийся при наведении курсора мыши на элемент управления. Класс ToolTip позволяет привести все контекстные окна к определенной форме. Сначала создают объект типа ToolTip в конструкторе форм: ToolTip tips = new ToolTipO; Затем, для каждого элемента управления формы, которой требуется присвоить всплывающую подсказку, вызывается SetToolTip. Вот пример для кнопки Button по имени Ып\ tips.SetToolTip(btn, "Displays the Help window"); В классе ToolTip несколько свойств управляющих временем появления и отображения всплывающей подсказки — AutomaticDelay, InitialDelay, ResbowDelay и Auto- PopDelay, но обычно используются параметры по умолчанию. Статические (и не совсем) элементы управления Интерфейс Microsoft Win32 API содержит пару статических прямоугольных элементов управления. Их реализация настолько проста, что я не считаю нужным рассказывать об этом. Вместо этого, я думаю коснуться тех «статических» элементов управления, которые имеют минимум интерактивности. (Тем не менее, они продолжают отвечать на запросы мыши.) В этом разделе, мы обсудим элементы управления, отображаемые в частичном классе иерархии Control.
60 ГЛАВА 2 Control GroupBox Label LinkLabel PictureBox ProgressBar Группа элементов управления (GroupBox) Группа элементов управления по периметру выделена линией и вверху содержит текст, определенный в свойстве Text. Рамка группы темнее цвета фона. Обычно группы используются в качестве «родителя» кнопок-переключателей (radio buttons). Все переключатели внутри одной группы взаимоисключающие. Нажатие клавиш со стрелками перемещает фокус ввода и метку выбора между переключателями. GroupBox — не потомок ContainerControl, поскольку в ContainerControl навигация выполняется клавишей Tab. Переключателям вовсе не обязательно быть потомками GroupBox. Общим родителем группы взаимоисключающих переключателей может служить любой элемент управления. Логикой активизации переключателей управляет не группа, а сами переключатели. В следующей главе я покажу замену GroupBox, не требующую явного размещения переключателей. Метка (Label) Label — это элемент управления, отображающий нередактируемый текст. Сам текст задается в свойстве Text. Хотя метка может отображать текст в нескольких строках, она не выводит полос прокрутки, если текст не умещается в элементе управления. Если в элементе управления надо разместить объемный текст, но делать его редактируемым нельзя, используйте элемент управления TextBox со свойством Readonly равным true. Помимо текста, метка может отображать объект Image, который можете быть как объектом Bitmap, так и Metafile. (Эти классы определены в пространствах имен SystemJDrawing и SystemDraivinglmaging) Один из способов заключается в присвоении свойству Image загруженного файла: lbl.Image = Image.Load("SillyCat.jpg"); Кроме этого, можно использовать изображения, заданные в качестве ресурсов и прикрепленные к исполняемому файлу программы. Как это сделать, я покажу чуть позже.
Этот плодовитый класс Controll 61 Метка-ссылка (LinkLabel) LinkLabel — без сомнений, не статический элемент управления. По функциональности он ближе к Button, но все же я включил его в этот раздел, потому что он происходит от Label. Этот элемент управления отображает текстовую строку, сильно или немного выделенную, чтобы показать, что щелчок этого элемента вызывает какое-то действие, например запуск приложения или открытие Web-страницы. Вдобавок к назначению стандартных значений свойств BackColor и ForeColor, в LinkLabel определены четыре свойства цвета: ■ LinkColor (по умолчанию синий); ■ DisabledLinkColor (серый); ■ ActiveLinkColor (красный при активизации ссылки); ■ VisitedLinkColor (пурпурный). Свойство LinkArea элемента LinkLabel — это объект типа LinkArea, представляющий собой структуру из двух целых свойств — Start и Length. Допустим, свойство Text элемента LinkLabel определено таю lnklbl.Text = "Click here to display the page." Пусть слово here надо сделать ссылкой. Зададим LinkArea так, чтобы ссылка начиналась с шестого знака и была длиной четыре знака: lnklbl.LinkArea = new LinkArea(6, 4); Вид этой части символьной строки определяется свойством LinkBehavior, которому присваивается значение перечисления LinkBehavior. Вот его четыре члена: ■ AlwaysUnderline; ■ HoverUnderline-, ■ NeverUnderline; ш SystemDefault. По умолчанию назначается SystemDefault, повторяющее поведение ссылки в соответствии с пользовательской настройкой Microsoft Internet Explorer. Одной метке можно назначить несколько ссылок. В этом случае применяется свойство LinkLabel по имени Links. Это объект LinkLabelLinkCollection, представляющий собой набор объектов LinkLabelLinks. Щелчок ссылки LinkLabel инициирует событие LinkClicked. Обработчик событий LinkClicked должен быть определен в соответствии с делегатом LinkLabelLinkCUcked EventHandler. Сопровождающий событие объект LinkLabelLinkClickedEventArgs содержит объект типа LinkLabelLinks, который служит для определения щелкнутой ссылки. Программа Windows Forms отвечает на нажатие ссылки запуском другого процесса, используя метод ProcessStart пространства имен SystemDiagnostics. Для сопо-
62 ГЛАВА 2 ставления с ссылкой нет необходимости определения исполняемого файла — можно задать имя файла или URL. * Вот небольшая программа, создающая один элемент управления LinkLabel с тремя различными ссылками, сопоставленными с тремя различными Web-страницами: LinkLabelDemo.es И // LinkLabelDemo.cs (с) 2005 by Charles Petzold // using System; using System.Diagnostics; using System.Drawing; using System.Windows.Forms; class LinkLabelDemo : Form { [STAThread] public static void Main() { Application.EnableVisualStyles(); Application.Run(new LinkLabelDemoO); } public LinkLabelDemoO { Text = "LinkLabel Demo"; Font = new Font("Times New Roman", 14); LinkLabel lnklbl = new LinkLabelQ; lnklbl.Parent = this; lnklbl.Dock = DockStyle.Fill; lnklbl.LinkClicked += LinkLabelOnLinkClicked; lnklbl.Text = "Jane Austen Societies exist in North America, the " + "United Kingdom, and Australia, among other places."; string str = "North America"; lnklbl.Links.Add(lnklbl.Text.IndexOf(str), str.Length, "http://www.jasna.org"); str = "United Kingdom"; lnklbl.Links.Add(lnklbl.Text.IndexOf(str), str.Length, "http://www.janeaustensoci.freeuk.com"); str = "Australia"; lnklbl. Links.Adddnklbl.Text.IndexOf(str), str. Length, "http://www.jasa.net.au");
Этот плодовитый класс Control* 63 } void LinkLabelOnLinkClicked(object objSrc, LinkLabelLinkClickedEventArgs args) { LinkLabel.Link Ink = args.Link; string strLink = Ink.LinkData as string; Process.Start(strLink); } Чтобы сделать текст заметнее, конструктор определяет в форме новое свойство, Font — шрифт, который наследуют все дочерние элементы управления формы. Единственный дочерний элемент этой формы — LinkLabel, которому присваивается значение свойства Dock объекта DockStyleJFill, чтобы размер формы совпадал с размером клиентской области. Если ширины элемента управления недостаточно, текст, отображаемый этим элементом управления, разбивается на строки автоматически. После определения свойства Text в программе задаются три ссылки, используя, принимающий три аргумента, метод Add свойства Links. Чтобы не считать знаки вручную, я определил короткую текстовую строку ссылки, а затем использовал метод IndexOf класса String для определения места этой короткой строки в свойстве метки Text. Определенное в классе String свойство Length возвращает длину строки. В качестве третьего аргумента свойству Add задается URL-адрес. Он становится свойством LinkData объекта LinkLabellink, созданного методом Add. Объект LinkLabel отображает ссылки способом, привычным для пользователя: Jane Austen Societies exist in North America, the United Kingdom, and Australia, among other places, Щелчок любой из ссылок инициирует событие LinkClick и вызов определенного в программе метода LinkLabelOnLinkClicked. Первый аргумент — элемент управления, генерирующий ссылку. Если обработчику событий нужно было узнать, откуда произошло событие, ему достаточно привести этот аргумент к объекту типа LinkLabel: LinkLabel lnklbl = objSrc as LinkLabel;
64 ГЛАВА 2 Но ему эта информация не нужна. Вместо этого, обработчик событий получает объект LinkLabelLink от второго аргумента, а затем приводит свойство LinkData к объекту типа string. Это URL-адрес ссылки. Строка передается ProcessStart, и запускается Web-браузер по умолчанию. Графическое окно (PictureBox) Элемент управления PictureBox служит для отображения изображения, представляющего собой растр или метафайл. Изображение задается свойством Image. Можно создать набор ImageList различных изображений и присвоить его свойству ImageList объекта PictureBox. Изображение задается либо по Imagelndex (числовому указателю изображения в списке), либо по ImageKey (ссылке на изображение в виде текстовой строки). Свойство SizeMode элемента PictureBox регулирует «растягивание» изображение в рамках родительского элемента управления. Свойству SizeMode задают значение одного из указанных ниже членов перечисления PictureBoxSizeMode. Первые три члена не меняют заданного в пикселах размера изображения — оно отображается без растяжения. ■ Normal — изображение размещается в верхнем левом углу графического окна. ■ Centerlmage — изображение размещается по центру элемента управления. ■ AutoSize — графическое окно принимает размер изображения. ■ Stretchlmage — изображение принимает размер элемента управления. ■ Zoom — изображение масштабируется без искажений. Элемент управления PictureBox описан в программе ImageDirectory главы 4 и программе ImageFiler главы 5. В .NET Framework 2.0 у PictureBox появились несколько новых свойств, позволяющих задать URL-адрес или имя локального файла при помощи методов Load или LoadAsync. Метод LoadAsync загружает файл в дополнительном потоке и информирует о завершении событием LoadCompleted. Можно также задать Initiallmage — «картинку», отображаемую во время загрузки нужного изображения, и Errorlmage — изображение, выводимое в случае ошибки загрузки. Есть еще одно событие — LoadProgressChanged. Оно предоставляет информацию о загрузке изображения в процентах. Его можно использовать для отображения индикатора выполнения — элемента управления ProgressBar. Индикатор хода процесса (ProgressBar) Основные свойства ProgressBar — это целочисленные Minimum, Maximum и Value. По мере выполнения процесса свойству Value присваивают возрастающие значения в диапазоне от Minimum до Maximum.
Этот плодовитый класс Control! 65 Есть другой способ: увеличивать значение Value, передавая целочисленный аргумент методу Increment. Также можно вызывать метод PerformStep, увеличивающий свойство Value на величину, заданную в свойстве Step. В .NET Framework 2.0 появилось свойство Style, которому задают одно из значений перечисления ProgressBarStyle: Blocks (по умолчанию), Continuous или Marquee. При выборе Marquee отображается анимация, ход процесса. Кроме того, этот вариант доступен только при выполнении программы в Windows XP. Конечно, если вы хотите потрафить пользователям, уделите больше внимания не созданию необычного вида индикатора выполнения, а максимально близкому к истине отражению выполняемого процесса. Индикатор, за 30 секунд «пробежавший» от 0 до 95%, а затем зависший на несколько минут на 95%, вряд ли можно назвать информативным и отвечающим требованиям качества. Кнопки и двоичные переключатели Элемент управления «кнопка» называется так не потому, что его можно щелкать, — в конечном итоге, все элементы по щелчку инициируют событие Click, а главным образом по той причине, что при щелчке он визуально ведет себя как кнопка. Все три типа кнопок происходят от абстрактного класса ButtonBase-. Control ButtonBase (абстрактный) Button СпескВох RadioButton Обычно Button используется для инициирования действий, CheckBox — для установки и снятия флажков, a RadioButton обычно служит для выбора одного из нескольких взаимоисключающих вариантов. На кнопке как правило присутствует текст, но некоторые из них — особенно экземпляры класса Button — содержат изображения, с текстом или без. Класс Button- Base содержит свойство Image, наследуемое кнопками всех трех типов и позволяющее назначить отображаемый на кнопке растр или метафайл. Кроме этого, можно задать свойство ImageList объекта ButtonBase и задать конкретное изображение, используя свойства Imagelndex или ImageKey. (Применение ImageList целесообразно, только если вы работаете со множеством кнопок с изображениями. Можно назначить один и тот же объект ImageList для всех кнопок, но с различными свойствами Imagelndex или ImageKey для каждой кнопки.) Если кнопка должна отображать и текст, и изображение, можно задать определенное в объекте ButtonBase свойство TextlmageRelation, которому присваивают одно из значений перечисления TextlmageRelation, состоящего из членов ImageAboveText, ImageBeforeText, TextAbovelmage, TextBeforelmage и Overlay.
66 ГЛАВА 2 Свойства TextAlign и ImageAlign задают положение текста или изображения в элементе управления. Оба принимают значения перечисления ContentAlignment, состоящего из девяти членов — комбинации Top, Middle и Bottom с Left, Center и Right. Значение по умолчанию обоих свойств — ContentAlignmentMiddleCenter. Кнопка (Button) По сравнению с ButtonBase класс Button привносит мало нового. В нем практически всегда определяется обработчик события Click. Единственное исключение — присвоение свойству DialogResult значения из перечисления DialogResult (обычно DialogResult.OK или DialogResult.Cancel), как уже рассказывалось в главе 1. Флажок (CheckBox) Обычно элемент управления CheckBox — это небольшой прямоугольник, расположенный слева от текста. Однако если свойству Appearance задать значение Арре- arance.Button (по умолчанию Appearance.Normal), элемент управления будет схож с кнопкой, за исключением того, что будет по щелчку «залипать» и «отлипать». Вид элемента управления CheckBox можно изменить, задав свойству CheckAlign значение из перечисления ContentAlignment. По умолчанию используется значение Content- Alignment MiddleLeft, то есть флажок по вертикали выравнивается по оси текста и располагается слева от него. Булево свойство Checked указывает текущее состояние флажка — «установлен» или «сброшен». Событие CheckedChanged информирует об изменении состояния. По умолчанию значение AutoCheck равно true, то есть по команде с клавиатуры или по щелчку мыши кнопка автоматически изменяет свое состояние и инициирует событие CheckedChanged. Если отклик необходим при любом изменении состояния флажка (например, для активизации и отключения других элементов управления), установите обработчик события CheckedChanged. Если отклик для каждого изменения не нужен, обычно достаточно получить значение свойства Checked, когда пользователь щелчком кнопку ОК закрывает форму. Свойству A utoCheck обычно задают false, когда планируют захватывать событие Click и управлять установкой/сбросом флажка самостоятельно, задавая значение свойства Checked из программы. Такое изменение свойства Checked также инициирует событие CheckedChanged. Иногда недостаточно одного состояния флажка — установленый или сброшенный, — требуется промежуточное состояние. Допустим, CheckBox управляет курсивным начертанием текста. Если в выбранном тексте есть и курсив, и обычный шрифт, CheckBox должен находиться в «неопределенном» состоянии. (С другой стороны, если выбранный текст не может выделяться курсивом, CheckBox должен быть отключен.) Для использования этого третьего состояния, прежде всего, надо булеву свойству ThreeState присвоить значение true. Вместо свойства Checked нужно задейство-
Этот плодовитый класс Control* 67 вать CheckState. Это значение из перечисления CheckState, состоящего из членов Unchecked, Checked и Indeterminate, Также, вместо установки обработчика Checked- Change, используют CheckStateChanged. Если вызвать метод EnableVisualStyles класса Application промежуточное состояние будет выглядеть как небольшой квадрат, в противном случае в элементе управления будет отображаться серый флажок. Если AutoCheck равно true, по щелчку элемент управления будет по циклу менять свое состояние: «установлен-сброшен-неопределенный». Если необходим другой порядок, присвойте AutoCheck значение false и вручную закодируйте в обработчике события Click порядок смены значений CheckState. Переключатель (RadioButton) Этот элемент управления обычно представляет собой кружок, расположенный слева от текста. Как и у Checkbox, у класса RadioButton есть свойство Appearance, которому можно задать значение AppearanceButton — тогда элемент управления выглядит как кнопка, а также свойство CheckAlign, позволяющее менять положение кружка относительно текста. Как и у CheckBox, у RadioButton есть булево свойство Checked, при изменении которого инициируется событие CheckedChanged. По умолчанию значение AutoCheck равно true, и в этом отличие RadioButton от CheckBox: если значение AutoCheck равно true и RadioButton «включен», щелчок кнопки ничего не изменит. Если AutoCheck равно true и RadioButton «выключен», щелчок мышью его «включит». Вдобавок, все «родственные» переключатели «выключатся». Таким образом, в каждый момент включен всего лишь один из группы переключателей RadioButton. Обычно наборы взаимоисключающих кнопок-переключателей создаются как потомки GroupBox, хотя «родитель» у них может быть любой. Ниже приведена программа с семью элементами RadioButton в клиентской области формы. ColorRadioButtons.es И // ColorRadioButtons.cs (с) 2005 by Charles Petzold // using System; using System.Drawing; using System.Windows.Forms; class ColorRadioButtons : Form { public static void Main() { Application.EnableVisualStyles();
68 ГЛАВА 2 Application.Run(new ColorRadioButtonsO); } public ColorRadioButtonsO { Text = "Color Radio Buttons"; Color[] aclr = { Color.Red, Color.Orange, Color.Yellow, Color.Green, Color.Blue, Color.Indigo, Color.Violet }; int у = Font.Height; foreach (Color clr in aclr) { RadioButton radio = new RadioButtonQ; radio.Parent = this; radio.Location = new Point(Font.Height, y); radio.Text = clr.Name; radio.AutoSize = true; radio.Tag = clr; radio.CheckedChanged += RadioButtonOnCheckedChanged; у += radio.Height; } } void RadioButtonOnCheckedChanged(object objSrc, EventArgs args) { RadioButton radio = objSrc as RadioButton; BackColor = (Color)radio.Tag; } } В массиве объектов Color есть все цвета радуги. В цикле foreach, задается текст кнопки, соответствующий нужному цвету. Свойству Tag (оно нужно для хранения произвольных данных) назначается сам объект Color. У всех переключателей единый обработчик события CheckedChanged. В первом аргументе в обработчик передается объект, инициировавший событие. Он приводится к типу RadioButton, а затем свойство Tag элемента управления приводится к объекту Color, который затем используется для определения свойства BackColor формы. Этот же цвет становится фоновым цветом самого переключателя:
Этот плодовитый класс Controll 69 :3 Color Radio Buttons О Red QQum 0 8Jwe О indigo OVWet Обратите внимание, что «родственные» переключатели поддерживают ввод с клавиатуры: переключение осуществляется клавишами со стрелками. Как и в ситуации с Checkbox, если не нужна немедленная реакция на изменения, нет необходимости устанавливать обработчик события CheckedChanged. Однако если потребуется определить, какой из переключателей выбран, надо в цикле проверить свойства Checked всех родственных переключателей. Если AutoCheck равно false, можно обрабатывать событие Click и самостоятельно включать/отключать переключатели. При желании можно сделать включенными несколько переключателей, но пользователя такая ситуация наверняка поставит в тупик. Полоса прокрутки Полосы прокрутки обычно устанавливаются с правой стороны или снизу таких элементов управления как ListBox, ComboBox и TextBox (при использовании в многострочном режиме, как в Windows Notepad). Эти элементы управления автоматически отображают полосы прокрутки. Производные от ScrollableControl элементы управления, в том числе Form и Panel, также отображают полосы прокрутки, когда их свойство AutoScroll приравнено true, а дочерние элементы управления организованы так, что занятая ими область выходит за рамки родителя. Полосы прокрутки можно создавать не только автоматически. Теоретически, полоса прокрутки позволяет выбирать определенное целое число из непрерывного ряда целых чисел. Указатель полосы прокрутки иллюстрирует положение в этом диапазоне. Поведение элемента управления «ползунок» {TrackBar) похоже, но отлично по виду. Иерархия класса такова: Control ScrollBar (абстрактный) HScrollBar VScrollBar TrackBar
70 ГЛАВА 2 У TrackBar есть свойство Orientation, определяющее положение элемента управления — горизонтальное или вертикальное. В ответ на действия пользователя оба элемента управления меняют позицию указателя автоматически, без вмешательства программными методами. Горизонтальная и вертикальная полосы прокрутки (ScrollBar) Наиболее важные свойства ScrollBar — Minimum, Maximum, SmallChange, LargeChange и Value. Все они — целые числа. Программа задает свойствам начальные значения, а свойство Value можно изменить щелчком или управляя полосой прокрутки. Щелчки стрелок по сторонам полосы прокрутки увеличивают или уменьшают Value с шагом, определенным в SmallChange (это свойство почти всегда равно 1); щелчок в области между стрелкой и указателем изменяет Value с большим шагом, который задан в свойстве LargeChange. Указатель также можно перемещать мышью. Если полоса прокрутки в фокусе ввода, она реагирует на клавиши со стрелками, а также клавиши Page Up, Page Down, Home и End. Диапазон значений Value не лежит в пределах от Minimum до Maximum — Value меняется от Minimum до {Maximum - LargeChange + 1). Чтобы понять, почему так происходит, посмотрим на работу текстового редактора. Допустим, в документе 1000 строк, пронумерованных от 0 до 999, а окно документа вмещает 20 строк. Используем эти значения для определения свойств полосы прокрутки: Mininum сделаем равным 0, Maximum — 999 и LargeChange — 20. Используем свойство Value полосы прокрутки для определения строки документа, отображаемой вверху окна. При прокрутке документа вниз, Value будет равняться {Maximum - LargeChange + 1) или 980. Строка экрана 980 будет расположена вверху, а строка 999 — внизу окна. Преимущество этой схемы в том, что размер указателя можно изменять пропорционально LargeChange и Maximum, таким образом наглядно показывая, какая часть всего документа отображается на экране. ScrollBar реализует два важных события — ValueChanged и Scroll. ValueChanged обрабатывать просто; оно инициируется (как ясно из самого названия) при изменении свойства Value. Получить новое значение в обработчике можно так: void ScrollOnValueChanged(object objSrc, EventArgs args) { int iValue = ((ScrollBar) objSrc).Value; } Однако бывает, что ValueChanged не предоставляет достаточно информации. Если «захватить» указатель полосы прокрутки мышью и быстро перемещать туда-обратно, программе не будет хватать времени для обработки данных. В таком случае можно задействовать событие Scroll. Обработчики Scroll должны определяться в соответствии с делегатом ScrollEventHandler. Обработчику события предоставляется объект типа
Этот плодовитый класс Control* 71 ScrollEventArgs, имеющий четыре свойства — OldValue, NewValue, ScrollOrientation и Туре. {OldValue и ScrollOrientation — появились только в .NET Framework 2.0). В сущности, в программе, где обрабатывается событие Scroll, можно запретить реакцию указателя полосы прокрутки на действия пользователя. Для этого свойству NewValue можно задать значение OldValue (которое также является текущим значением свойства Value). Свойство ScrollOrientation (ориентация полосы прокрутки) — это член перечисления ScrollOrientation, имеющего два значения — Horizon- talScroll (горизонтальная) и VerticalScroll (вертикальная). Это свойство позволяет использовать один обработчик событий для горизонтальной и вертикальной прокрутки, при этом легко различая их ориентацию. Свойство Туре — это объект типа ScrollEventType, перечисления, точно определяющее порядок взаимодействия пользователя с полосой прокрутки. В таблице описаны девять членов объекта ScrollEventType. Член Описание SmallDecrement Мышь: верхняя или левая кнопка со стрелкой Клавиатура: клавиша со стрелкой «вверх» или «влево» Smalllncrement Мышь: нижняя или правая кнопка со стрелкой Клавиатура: клавиша со стрелкой «вправо» или «вниз» LargeDecrement Мышь: левая или верхняя область Клавиатура: клавиша Page Up Largelncrement Мышь: правая или нижняя область Клавиатура: клавиша Page Down TbumbPosition Кнопка мыши, не удерживающая указатель полосы прокрутки ThumbTrack Нажатое состояние кнопки мыши на указателе полосы прокрутки или перемещение указателя First Клавиатура: клавиша Ноте Last Клавиатура: клавиша End EndScroll Прокрутка завершена Программа с медленным откликом полосы прокрутки может игнорировать все за исключением значения ScrollEventType.EndScroll свойства Туре. Например, если щелкнуть правую (нижнюю) стрелку полосы прокрутки и задержать ее в нажатом состоянии, полоса прокрутки будет инициировать попеременно два события для каждого движения указателя: событие Scroll со свойством Туре равным ScrollEventTypeSmalllncrement и NewValue, со значением, большим свойства Value. Если NewValue не изменяется программой (стандартный случай), инициируется событие ValueCbanged. Когда указатель достигает MaxValue, события ValueCbanged больше не инициируются, а инициирование события Scrol продолжится. В конце концов, когда пользователь отпустит кнопку мыши, будет инициировано последнее событие Scroll со свойством Туре равным ScrollEventType.EndScroll и NewValue равным Value.
72 ГЛАВА 2 Если захватить и переместить указатель мышью, полоса прокрутки будет инициировать события Scroll и ValueChanged. Об освобождении указателя сигнализируют два последних события Scroll со свойствами Туре, равными ScrollEventType.- ThumbPosition и ScrollEventTypeEndScroll. Ползунок (ТгаскВаг) Ползунок в принципе аналогичен двум другим элементами управления прокруткой, но выглядит и действует чуть иначе. Как и Scrollbar, у класса TrackBar есть свойства, выраженные целыми числами — Minimum, Maximum, SmallChange, LargeChange и Value. Minimum и Maximum определяют диапазон значений Value. Класс TrackBar реализует и вертикальный, и горизонтальный ползунки — ориентация задается свойством Orientation. Соответствующее перечисление Orientation содержит два члена — Horizontal и Vertical. Ползунок представляет собой шкалу с делениями. Свойство TickStyle определяет стиль делений, который выбирается из одноименного перечисления: None, TopLeft, BottomRight и Both. Значение по умолчанию — TickStyle.BottomRight, то есть метки делений отображаются под горизонтальным или справа от вертикального ползунка. Целочисленное свойство TickFreqaency определяет цену деления. Общее количество делений определяется таю {Maximum - Minimum + 1) / TickFrequency По умолчанию цена деления равна 1, поэтому нужно быть аккуратным: если, например, увеличить значение Maximum по умолчанию с 10 на 100, метки скорее всего сольются в одну черную полосу. Как и полоса прокрутки, ползунок реализует события Scroll и ValueChanged. Однако при обработке ввода между этими событиями различия не делают — они следуют один за другим и предоставляются в простых объектах EventArgs. При изменении свойства Value из программы, инициируется событие ValueChanged, но не Scroll. Ползунки лучше всего подходят для выбора значения из дискретного диапазона, поэтому неплохо размещать метку, указывающую выбранное значение. Классический пример — ползунок на вкладке Параметры (Settings) окна Свойства: Экран (Display Properties). Есть и альтернативное решение для выбора значения из дискретного набора — это элемент управления «наборный счетчик» {spin control). Элементы управления с поддержкой редактирования текста В Windows Forms есть несколько элементов управления, служащих для ввода или редактирования текста:
Этот плодовитый класс Control] 73 Control TextBoxBase (абстрактный) MaskedTextBox TextBox DataGridTextBox Da taGri dVi ewTextBoxEdi ti ngCon t rol RichTextBox Мы обсудим DataGridViewTextBoxEditingControl (заменивший устаревший элемент управления DataGridTextBox) в главе 6. Самое важное свойство элемента с поддержкой редактирования текста — это, конечно же, Text, содержащее текст, отображаемый в элементе управления. Программа инициализирует текст в элементе управления, простым определением значения Text, и получает текст, введенный или измененный пользователем, обращением к свойству Text. Если txtbox объект типа TextBox, удалить из него текст просто: txtbox.Text = ""; А добавляют строку в конец существующего текста таю txtbox.Text += " and that's the truth"; Многие важные свойства этих элементов управления определены в абстрактном классе TextBoxBase. Свойство Multiline определяет, может ли элемент управления получать и отображать многострочный текст. По умолчанию оно равно false в TextBox и MaskedTextBox и true — в RichTextBox. Свойство Wordwrap (перенос по словам) знакомо пользователям блокнота (Notepad). Оно применимо только к многострочным элементам управления и по умолчанию равно true. Чтобы элемент управления отображал нередактируемый текст, свойству Readonly присваивают значение true. Выбор текста выполняется с клавиатуры или мыши. Свойства SelectedStart и SelectedLength — целые числа, указывающие позицию первого символа выбранного текста и длину выбранного отрезка в символах соответственно. SelectedText — это и есть выбранный текст. TextBoxBase реализует собственные операции вырезки, копирования, вставки и отмены на основе стандартных комбинаций клавишей — Ctrl+X, Ctrl+C, Ctrl+V и Ctrl+Z соответственно. Кроме этого, методы Cut, Copy, Paste и Undo выполняют эти операции с буфером обмена программно, обычно в ответ на выбор команды меню. Самое важное событие в этих элементах управления — TextChanged, оно инициируется при изменении свойства Text. Текстовое окно с маской (MaskedTextBox) MaskedTextBox — новинка .NET Framework 2.0. Этот элемент управления создан для ввода одной строки текста в заранее заданном формате, например телефонных
74 ГЛАВА 2 номеров, денежных сумм, дат или адресов электронной почты. Понять, как использовать этот элемент управления, проще всего из описания свойства Mask, представляющего собой символьную строку, определяющую формат вводимой в поле строки. При попытке ввести не предусмотренный маской текст инициируется событие MasklnputRejected, вместе с которым передается объект MasklnputRejectedEventArgs, имеющий два свойства: Position — позиция символа, не совпадающего с маской, и RejectionHint — текстовая строка, отображаемая (например, в элементе управления Label) для прочтения пользователем. Текстовое поле (TextBox) Это простейший элемент управления с поддержкой редактирования текста, но вместе с тем он достаточно сложен, чтобы стать основой блокнота Windows. TextBox добавляет всего несколько свойств к имеющимся в TextBoxBase. Наверное, самое важное из них — это ScrollBars, которому присваиваются значения из перечисления ScrollBars.Vertical, ScrollBars.Horizontal или ScrollBars.Both. Значение по умолчанию — ScrollBars.None, то есть,полосы прокрутки не отображаются, даже если этого требует длина текста. (Если значение свойства Wordwrap равно true, горизонтальные полосы прокрутки не отображаются независимо от значения ScrollBars). Если TextBox используется для ввода пароля, нужно указать в PasswordChar символ, который будет отображаться при вводе. В этом отношении также полезно свойство CharacterCasing. Если присвоить этому свойству значение из перечисления CharacterCasinglower или CbaracterCasing.Upper, вводимые символы будут приводиться в нижний или верхний регистр соответственно. Поле ввода с форматированием (RichTextBox) В RichTextBox текст хранится в поддерживающем стилевое оформление формате RTF (Microsoft Rich Text Format); этот формат известен уже много лет1. В статье базы знаний Microsoft http://support.microsoft.com/kb/q86999 есть ссылка на спецификацию RTF 1.7. В своей №т32-форме элемент управления RichTextBox является основой программы Windows WordPad. RichTextBox по умолчанию многострочный. Как и в TextBox, в RichTextBox есть свойство ScrollBars, но здесь ему присваивается одно из значений перечисления RichTextScrollBars. По умолчанию это RichTextScrollBars.Both, предписывающий горизонтальные и вертикальные полосы прокрутки, но только если они нужны. Остальные члены перечисления — None, Horizontal, Vertical, ForcedHorizontal, ForcedVertical и ForcedBoth. Значения с префиксом Forced требуют отображать полосы прокрутки, даже если они не обязательны. Подробнее см. статью Нэнси Эндрюс (Nancy Andrews) «Rich Text Format Standard Makes Transferring Text Easier», Microsoft Systems Journal, Vol. 2, No. 1, March 1987.
Этот плодовитый класс Control* 75 RichTextBox разрешает определять программно форматирование только выделенного в данный момент текста. Такое форматирование выполняеется путем определения свойств, начинающихся со слова Selection. В некоторых случаях эта схема очень удобна, например когда пользователь выделяет текст, а затем выбирает Font из меню Format В программе создается новый объект Font, например по имени fnt, и присваивается свойству SelectionFont элемента управления RichTextBox-. rtb.SelectionFont = fnt; Однако если потребуется определить цветовое оформление не выделенного текста (например для визуального выделения ошибки в текстовом редакторе, используемом как простейший редактор кода), программисту придется изрядно попотеть. Нужно сохранить значения определяющих выделение свойств SelectionStart и Select- Length. Затем нужно методом Select задать новое выделение — текст, цвет которого следует изменить. Выделение окрашивается присвоением свойству SelectionColor соответствующего объекта Color. Процедура завершается методом Select, восстанавливающим исходное выделение. SelectionBackColor и SelectionColor — это свойства объекта Color, определяющие цвет фона и текста выделенного участка соответственно. SelectionFont — это объект типа Font. Еще один элемент форматирования символов — SelectionCharOffset, смещение уровня текста, выраженное в пикселах. Положительное значение используется для верхних, а отрицательное — для нижних индексов. Все остальное относится к форматированию абзацев. SelectionAlignment принимает значения из перечисления HorizontalAlignment, состоящего из членов Left, Right и Center. Выравнивания по правому и левому краям одновременно не предусмотрено. SelectionBullet — булево свойство, задающее маркер перед каждым абзацем. Selec- tionlndent, SelectionRightlndent и SelectionHanginglndent — отступ в пикселах. Selection- Rightlndent отмеряется от правой стороны элемента управления. Отступ первой строки абзаца указывается в Selectionlndent и отмеряется от левой стороны элемента управления. SelectionHanginglndent — «висячий отступ», то есть отступ остатка абзаца по отношению к Selectionlndent. (Здесь есть некоторое отличие от определения отступа в стандартных текстовых редакторах). SelectionTabs — это массив чисел, определяющих позиции табуляции. В главе 4 рассказывается как создать аналог используемой в WordPad управляющей линейки (ruler), служащей для определения отступов и табуляции. В главе 5 описан аналог панели инструментов для форматирования символов и выравнивания текста. В свойстве Text объекта RichTextBox задается только «пустой» текст без каких- либо RTF-тэгов. Для работы с полнофункциональным RTF-текстом служит свойство Rtf. В RichTextBox предусмотрены встроенные методы ввода/вывода файлов — LoadFile и SaveFile.
76 ГЛАВА 2 Список и поле со списком В наиболее общем и абстрактном виде элемент управления ListBox позволяет пользователю выбрать элемент из списка из одного или нескольких элементов. В дополнение к этому СотЬоВох позволяет вводить текст, не содержащийся в списке. Эти элементы управления наследуют абстрактному классу ListControl. Control ListControl (абстрактный) СотЬоВох DataGridViewComboBoxEditingControl ListBox CheckedListBox О DataGridViewComboBoxEditingControl рассказывается в главе 6. В последнее время элемент управления ListBox уступил место СотЬоВох, занимающему меньше экранного пространства. Единственная функция, которая есть у ListBox, но нет у СотЬоВох — возможность выделения нескольких элементов. Однако для этого визуально удобнее CheckedListBox. Тем не менее, я детально расскажу о ListBox, поскольку исторически он является предком множества элементов управления, которые унаследовали большинство используемых в ListBox принципов. Список (ListBox) Список содержит прокручиваемый набор объектов. По умолчанию пользователь враве выбрать один (или несколько) элементов, используя клавиатуру или мышь. Выбранные объекты выделяются. Отображаемые элементы списка задаются свойством Items — объектом типа ListBox.ObjectCollection. В классе ListBox.ObjectCollection определен метод Add, позволяющий добавлять новые объекты в набор. В списке отображаются текстовые названия элементов, возвращаемые методом ToString каждого объекта. Свойство Sorted объекта ListBox обеспечивает автоматическую сортировку элементов. После этого можно использовать свойство Items объекта ListBox для обращения к отдельным объектам наподобие того, как это делают с массивом. Список также можно «заселить» элементами из источника данных — объекта, реализующего интерфейс IList (например, массив) или объект DataSet. Если ostrStateNames является массивом имен состояний, окно списка можно заполнить оператором: Istbox.DataSource = astrStateNames; Подробнее об этом я расскажу в главе 6.
Этот плодовитый класс Control] 77 В свойстве Selectedlndex хранится индекс выбранного в данный момент элемента. Если выбранных элементов нет, его значение равно -1. Свойство Selectedltem представляет фактический выбранный элемент (объект). Оно может быть равным null При выборе какого-либо элемента списка выражение: lstbox.Selectedltem эквивалентно выражению: lstbox.Items[lstbox.Selectedlndex] Следующие два выражения также равносильны: lstbox.Text и lstbox. Selectedltem. ToStringO По умолчанию в списке разрешается выбрать только один элемент. Однако вы вправе разрешить множественное выделение, задав свойству SelectionMode одно из значений перечисления SelectionMode-. None, One, MultiSimple или MultiExtended. Вариант MultiSimple слегка меняет вид ListBox, делая различия между выбранными элементами (указанны выделением) и фокусом ввода (указан пунктиром). Фокус ввода перемещают клавишами-стрелками, а пробелом или мышью выбирают или отменяют выбор элементов. Вариант MultiExtended разрешает выбирать только диапазон последовательных элементов, для чего пользователь должен, придерживая клавишу Shift, стрелками выбрать нужный диапазон. В списках с возможностью выбора нескольких элементов используется свойство «только для чтения» Selectedlndices (объект типа ListBoxSelectedlndexCollection) или Selectedltems (объект типа ListBoxSelectedObjectCollection). Объекты ListBoxSelectedlndexCollection и ListBox.SelectedObjectCollection поддерживают индексы и доступ к отдельным элементам по механизму массива целых чисел. Для получения информации об изменении выбранного элемента ( или элементов) устанавливают обработчик события OnSelectedlndexCbanged или OnSelected- ValueCbanged. Если нужно, чтобы элементы списка отображались методом, отличным от ToString, можно использовать функцию элемента управления ListBox, называемую отрисов- ка владельцем (owner draw). В этом случае программа уведомляется посредством событий о необходимости отрисовки элемента списка. При использовании отрисовки владельцем первым делом надо присвоить свойству DrawMode значение DrawMode.OwnerDrawFixed (все пункты списка имеют одну высоту) или DrawMode.OwnerDrawVariable (пункты списка разной высоты). По умолчанию значение свойства DrawMode равно DrawMode.Normal, что возлагает обязанность отрисовки на сам элемент управления.
78 ГЛАВА 2 Если все элементы списка одинаковой высоты, ее нужно указать в свойстве ItemHeight. В противном случае необходимо задействовать событие Measureltem, обработчик которого задается в соответствии с делегатом MeasureltemEventHandler. Предоставляемый сообщением объект MeasureltemEventArgs имеет свойства для чтения Graphics и Index. Первое представляет собой объект типа Graphics, служащий для вычисления высоты элемента, а последнее — числовой индекс элемента списка. Высота и ширина элемента задается свойствами ItemHeight и ItemWidth. При отрисовке владельцем необходимо установить обработчик события Draw- Item. Обработчик вызывается для каждого отображаемого элемента, при этом ему передается объект типа DrawItemEventArgs с несколькими свойствами ListBox «только для чтения»: BackColor, ForeColor и Font. Свойство Bounds — это объект типа Rectangle, в котором выполняется отрисовка. Кроме этого, обработчик получает объекты Graphics и Index. Свойство State — это член перечисления DrawItemState, которое указывает, выбран ли элемент, имеет фокус ввода и т. д. Класс DrawItemEventArgs также включает два метода, DrawBackground и DrawFocusRectangle, помогающие в отрисовке элемента списка. Список с флажками (CheckedListBox) При необходимости выбора нескольких элементов списка вместо ListBox лучше использовать CheckedListBox. В этом варианте слева от элементов расположены флажки; при этом интерфейсы взаимодействия с помощью клавиатуры и мыши проще и понятнее для пользователя, чем в списках с возможностью выбора нескольких элементов. (В программе ControlExplorer главы 7 используется список с флажками для указания событий, информацию которых нужно отображать.) В списках с флажками SelectionMode может принимать только значение Selection- Mode.One или SelectionModeNone. (В последнем варианте поведение элемента управления довольно неочевидно, поэтому его лучше не использовать.) В каждый момент времени выбран и выделен цветом только один элемент, однако флажками могут быть отмечены несколько пунктов. Можно сделать так, чтобы флаговые окна выглядели «притопленными», задав свойству ThreeDCheckBoxes значение true. Перемещение фокуса выполняется клавишами со стрелками, а пробел служит для переключения флажка. Первый щелчок мыши выделяет элемент списка, а следующий — устанавливает флажок. При выборе другого элемента состояние флажка останется неизменным. Можно избавиться от необходимости два раза щелкать мышь, для этого нужно задать свойству CheckOnClick значение true. Теперь состояние флажка будет меняться одним щелчком. Два прилагающихся к набору элементов метода Add позволяют при добавлении новых элементов автоматически устанавливать флажки. Состояние флажка определяется значением второго аргумента — true или false-.
Этот плодовитый класс Control] 79 ckdlstbox.Add("New Jersey", true); Для определения состояния элемента управления можно использовать встречавшееся ранее при описании элементов управления Checkbox перечисление CheckState, содержащее члены Checked, Unchecked или Indeterminate-. ckdlstbox.Add("New York", CheckState.Indeterminate); Позже можно изменить состояние флажка, указав индекс элемента: SetItemChecked(5, true) Перечисление CheckState можно использовать при перегрузке метода: SetItemCheckState(7, CheckState.Unchecked) Свойства «только для чтения» Checkedlndices и Checkedltems предоставляют наборы целочисленных индексов или объекты, выбранные в данный момент. Чтобы получать информацию об изменении состояния флажка, нужно установить обработчик события ItemCheck в соответствии с делегатом ItemCheckEventHandler. Предоставляемый вместе с сообщением о событии объект ItemCbeckEventArgs содержит свойство Index элемента, а также значения CurrentValue и NewValue, выраженные членами перечисления CheckState. Поле со списком (ComboBox) Поле со списком ComboBox — это комбинация списка и текстового окна. В обычном состоянии отображается только текстовое окно со стрелкой «вниз» справа. По щелчку этой стрелки раскрывается список — внизу появляется окно со списком. По умолчанию пользователь вправе ввести текст в текстовое окно (чтобы выбрать элемент списка), редактировать отображаемый в окне текст или ввести совершенно новый текст. Введенный текст не добавляется автоматически в список — это делается из программы. Большинство программ не разрешают редактирование текста. Такое поведение управляется свойством DropDownStyle — членом перечисления ComboBoxStyle, содержащего следующие члены: ■ Simple — текст можно редактировать, список постоянно открыт; ■ DropDown — текст можно редактировать, список отображается по щелчку (по умолчанию); ■ DropDownList — текст редактировать нельзя, список отображается по щелчку. Для раскрытия списка (или определения его положения в текущий момент) можно использовать свойство DroppedDown. Как и в ListBox, в программе можно задействовать свойства Selectedlndex и Selected- Item для определения или получения выбранного элемента. Поля со списком не поддерживают множественный выбор. Выбранный элемент отображается в тексто-
80 ГЛАВА 2 вом окне элемента управления и доступен через свойство Text. В момент ввода текста в поле текстового окна свойство Selectedlndex равно -1, a Selectedltem — null. Как и в ListBox, для обнаружения момента изменения выбранного элемента задействуют обработчики событий SelectedlndexChanged или SelectedValueChanged. Событие TextChanged информирует об изменении текста в верхней части Combo- Box — из-за выбора другого элемента или ввода текста. Как и ListBox, ComboBox поддерживает отрисовку владельцем. Об этом я расскажу при рассмотрении программы FontDialogMimic в главе 3. Абстрактный класс наборного счетчика (UpDownBase) Наборные счетчики (spin controls) содержат как поле ввода, так и кнопки, поэтому они считаются контейнером с развитым «генеалогическим деревом»: Control ScrollableControl ContainerControl UpDownBase (абстрактный) NumericUpDown DomainUpDown Элемент управления NumericUpDown позволяет выбрать число из диапазона. DomainUpDown похож на ListBox тем, что позволяет выбрать один объект из набора. В обоих есть кнопки со стрелками, и оба реагируют на клавиши со стрелками. Числовой наборный счетчик (NumericUpDown) Самые важные свойства NumericUpdown — Minimum, Maximum, Increment и Value, все это десятичные величины. (Десятичный тип в С# соответствует структуре Decimal пространства имен System. В отличие от float и double, десятичный тип данных позволяет хранить числа с точностью до 28 разрядов.) DecimalPlaces — это еще одно важное свойство NumericUpDown. Элемент управления можно инициализировать так- updn.DecimalPlaces = 2; updn.Minimum = 5.25m; updn.Maximum = 5.50m; updn.Interval = 0.05m; Затем по нажатию клавиш или кнопок со стрелками свойство Value может принимать значения 5,25, 5,30, 5,35, 5,40, 5,45 и 5,50. Однако пользователю можно предоставить возможность вводить и промежуточные, лежащие между Minimum и Maximum, значения — например, 5,33- В этом случае при«ажатии клавиши со стрелкой вверх следующее значение будет равно 5,38. Два булевых свойства элемента управления по умолчанию равны/я/se. Это ТЪои- sandsSeparator, которое добавляет в больших числах разделитель (в зависимости
Этот плодовитый класс Controll 81 от региональных стандартов это может быть запятая или пробел), и Hexadecimal, которое отображает результаты в шестнадцатеричной системе. При попытке ввода данных, выходящих за пределы заданного диапазона, прозвучит сигнал, и значение Value станет равным Minimum или Maximum. Значение false свойства Readonly запрещает ввод данных пользователем. Наиболее важным событием элемента управления NumericUpDown является ValueChanged. Счетчик выбора из диапазона (DomainUpDown) Элемент управления DomainUpDown похож на NumericUpDown, но хранит объекты и отображает текстовые строки. Свойство Items — это набор объектов, хранящихся в экземпляре класса DomainUpDown.DomainUpDownltemCollection. Элементы в набор добавляются в точности, как в ListBox. domain.Items.Add("New Jersey"); domain.Items.Add("New York"); domain.Items.Add("New Hampshire"); domain.Items.Add("New Mexico"); domain.Items.Add("New Rochelle"); Об изменении выбранного элемента информирует событие SelectedltemChanged. Доступ к выбранному элементу получают, используя свойство Selectedltem или выбрав элемент из набора Items по его индексу, указанному в свойстве Selectedlndex. Дата и время В .NET Framework определена структура DateTime в пространстве имен System, которая широко используется для хранения и отображения дат и времени. Как правило новый объект DateTime создается одним из двух способов: с помощью статического свойства Now, создающего объект DateTime с текущими датой и временем, или с помощью одного из множества конструкторов, позволяющих задать дату, со временем или без. Вот пример создания объекта DateTime, представляющего дату 29 августа 2006 г.: DateTime dt = new DateTime(2006, 8, 29); У структуры DateTime есть свойства «только для чтения» — Year, Month, Day, Hour, Minute, Second и Millisecond, служащие для получения информации о дате и времени, а также несколько методов для форматирования даты и времени в соответствии с региональными стандартами. В Windows Forms есть два элемента управления для работы с датами. Month- Calendar предназначен для получения информации о дате от пользователя, a Date- TimePicker позволяет получать еще и время, но чаще всего используется только для ввода даты.
82 ГЛАВА 2 Календарь на месяц (MonthCalendar) Элемент управления MonthCalendar отображает календарь с текущей датой, выделенной красным кружком и отображаемой внизу элемента управления. С помощью мыши или клавиатуры пользователь может выбрать до семи последовательных дней. Для этого надо выбрать первую, а затем, удерживая клавишу Shift, выделить последнюю дату диапазона. Для просмотра другого месяца нужно щелкнуть одну из стрелок, расположенных вверху элемента управления, или нажать клавишу Page Up или Page Down. В программе узнать выбранный диапазон позволяют два свойства типа DateTime: SelectionStart и SelectionEnd. (В качестве альтернативы можно использовать свойство SelectionRange — объект типа SelectionRange со свойствами Start и End.) Чтобы получать уведомление об изменении выбранных дат, устанавливают обработчик события DateCbanged, основанный на DateRangeEventHandler. В сообщении о событии содержится объект типа DataRangeEventArgs с двумя свойствами типа DateTime — Start и End. (В классе MonthCalendar реализовано еще одно событие — DateSelected, но оно практически бесполезно.) Для увеличения/уменьшения количества последовательных дней, которые пользователь может выбрать в элементе управления, используется свойство MaxSelection- Count. Чтобы ограничить выбор одним днем, задайте свойству значение 1. Обычно MonthCalendar позволяет перемещаться сколь угодно далеко по времени (на нескольких столетий или даже тысячелетий). Для ограничения диапазона дат служат свойства MinDate и MaxDate типа DateTime. Можно запретить отображение текущей даты внизу элемента управления, задав свойству ShowToday значение/tfte. Чтобы запретить выделение текущей даты кратным кружком, задайте свойству ShowTodayCircle значение false. Установка текущей даты выполняется присвоением соответствующего значения свойству TodayDate. Свойство SbowWeekNumbers предлагает нумерацию недель, отображаемую слева от календаря, начиная с первой недели года. Если региональные стандарты требуют отображать календарь иначе, не нужно менять значение по умолчанию Day.De/ault свойства FirstDayOJWeek. Это позволит календарю корректно отображать названия дней недели. Свойства BoldedDates, MonthlyBoldedDates и AnnuallyBoldedDates являются массивами объектов DateTime. Класс MonthCalendar содержит методы Add и Remove, используемые для создания и обслуживания этих массивов. Элемент управления MonthCalendar может изменять свой размер. По умолчанию его ширина составляет 13-кратную, а высота — 11-кратную высоту шрифта. Можно изменить значение свойства Size, но нужно иметь в виду, что элемент управления проигнорирует изменения, если новые размеры недостаточны для размещения, по крайней мере, одного месяца. Если значение свойства Size таково, что в элементе управления можно разместить несколько месяцев (горизонтально, вер-
Этот плодовитый класс Control] 83 тикально или в обоих направлениях), MontbCalendar отобразит их, но скорректирует свойство Size так, чтобы в элементе управления умещалось именно это количество месяцев. Чтобы задать отображение заданного количества месяцев, во время исполнения проверьте свойство «только для чтения» SmgleMontbSize. Оно учитывает текущий шрифт и свойства SbowToday и SbowWeekNumbers. Общая ширина должна быть несколько больше числа, кратного SingleMontbSize.Widtb, поскольку в элементе управления месяцы разделяются небольшим промежутком. (Возможно, надежнее и проще просто добавить еще одну половину SingleMontbSize.Widtb) Высота должна быть чуть меньше числа, кратного SingleMontbSizeHeigbt, потому что размер одного месяца рассчитан на размещение текущей даты, а она одна на все хмесяцы и отображается только внизу элемента управления. Например, для отображения шести месяцев — трех в ширину и двух в высоту, нужно выражение: moncal.Size = new Size(7 * moncal.SingleMonthSize.Width / 2, 2 * moncal.SingleMonthSize.Height); Если надо узнать точный размер элемента управления, запросите значение свойства Size. Элемент выбора даты и времени (DateTimePicker) Хотя у DateTimePicker больше возможностей, чем у MontbCalendar, так как он позволяет выбирать дату и время, его обычно используют только для определения даты (впрочем, это и есть режим по умолчанию). В отличие от MontbCalendar, у DateTimePicker нет возможности выбора диапазона дат. С первого взгляда DateTimePicker походит на поле со списком с текстовым полем и стрелкой справа. При инициализации элемента управления из программы нужно задать свойству Value значение объекта типа DateTime, в противном случае ему будет присвоено значение DateTime.Now. Следующим после Value по важности свойством элемента DateTimePicker является Format, которому присваивается значение одного из членов перечисления DateTimePickerFormat. По умолчанию это Long, при выборе которого дата отображается в формате «Tuesday, August 02, 2005» (в региональном стандарте U.S. English). Если выбрать DateTimePickerFormatSbort дата отобразится в формате «8/1/2005», а при выборе DateTimePickerFormat.Time элемент управления будет содержать только время. Чтобы определить пользовательский формат отображения даты и времени, свойству Format задают значение DateTimePickerFormat.Custom, а формат даты и времени определяют в свойстве CustomFormat. Независимо от формата пользователь сможет напрямую редактировать поля (месяц, день, год и т. д.). Щелчок правой стрелки раскрывает календарь — так же, как и в MontbCalendar.
84 ГЛАВА 2 Событие ValueChanged информирует об изменении свойства Value. Следующая программа отображает два элемента управления DateTimePicker в небольшом окне и подсчитывает количество лет, месяцев и дней между двумя датами. Программа Life Years создана на основе аналогичной Win32-nporpaMMbi, написанной мною для друга, которому она нужна была для вычислений, связанных с составлением генеалогического дерева. В той версии два элемента управления назывались Birth Date (дата рождения) и Death Date (дата смерти). Но я подумал, что читатели этой книги захотят поэкспериментировать с программой, вычисляя как долго они проживут, и решил дать этим элементам управления менее мрачные названия. LifeYears.cs И // LifeYears.cs (с) 2005 by Charles Petzold // using System; using System.Drawing; using System.Windows.Forms; class LifeYears : Form { DateTimePicker dtpBeg, dtpEnd; Label lblResult; [STAThread] public static void Main() { Application.EnableVisualStyles(); Application.Run(new LifeYearsO); } public LifeYearsO { Text = "LifeYears"; Label lbl = new Label(); lbl.Parent = this; lbl.Text = "Begin Date: "; lbl.AutoSize = true; lbl.Location = new Point(Font.Height, Font.Height); int xDateTimePicker = lbl.Right; lbl = new Label(); lbl.Parent = this;
Этот плодовитый класс Control! 85 lbl.Text = "End Date: "; lbl.AutoSize = true; lbl.Location = new Point(Font.Height, 3 * Font.Height); xDateTimePicker = Math.Max(xDateTimePicker, lbl.Right); lbl = new LabelO; lbl.Parent = this; lbl.Text = "Life Years:"; lbl.AutoSize = true; lbl.Location = new Point(Font.Height, 5 * Font.Height); xDateTimePicker = Math.Max(xDateTimePicker, lbl.Right); dtpBeg = new DateTimePickerO; dtpBeg.Parent = this; dtpBeg.AutoSize = true; dtpBeg.Location = new Point(xDateTiraePicker, Font.Height); dtpBeg.ValueChanged += DateTimePickerValueChanged; dtpEnd = new DateTimePickerO; dtpEnd.Parent = this; dtpEnd.AutoSize = true; dtpEnd.Location = new Point(xDateTimePicker, 3 * Font.Height); dtpEnd.ValueChanged += DateTimePickerValueChanged; IblResult = new LabelO; IblResult.Parent = this; IblResult.AutoSize = true; IblResult.Location = new Point(xDateTimePicker, 5 * Font.Height); ClientSize = new Size(dtpEnd.Right + Font.Height, 7 * Font.Height); } void DateTimePickerValueChanged(object objSrc, EventArgs args) { if (dtpBeg.Value >= dtpEnd.Value) { IblResult.Text = ""; } else { DateTime dtBeg = dtpBeg.Value; DateTime dtEnd = dtpEnd.Value; int iYears = dtEnd.Year - dtBeg.Year; int iMonths = dtEnd.Month - dtBeg.Month; int iDays = dtEnd.Day - dtBeg.Day;
86 ГЛАВА 2 if (iDays < 0) { iDays += DateTime.DaysInMonth(dtEnd.Year, 1 + (dtEnd.Month + 10) % 12); iMonths -= 1; } if (iMonths < 0) { iMonths += 12; iYears -= 1; } lblResult.Text = String.Format("{0} year{1}, {2} month{3}, {4} day{5}", iYears, iYears == 1 ? "" : "s", iMonths, iMonths == 1 ? "" : "s", iDays, iDays == 1 ? "" : "s"); } Свойству AutoSize присвоено значение true, поэтому размер окна приложения и элементы управления определяются на основе высоты шрифта по умолчанию: Ш UfeYear* Begin Date: ("моп^Т "Sp™02J953 ~ Щ End Date: Г"т^^Т*^ид^' "о£ 2005 '.?:] Life Years: 52 years, 6 months, 0 days При изменении любой из дат инициируется событие ValueCbanged, которое обрабатывает единый для обоих элементов управления обработчик. Он различает элементы управления, поэтому устанавливает соответствующую метку IblResult. Объекты DateTime можно вычитать друг из друга, что позволяет получить истекшее время. Разница между двумя объектами DateTime — это объект TimeSpan, представляющий истекшее время в десятых долях микросекунды. Свойства структуры TimeSpan — Microseconds, Seconds, Minutes, Hours и Days — позволяют преобразовать эту цифру в нечто более понятное человеку. Однако в TimeSpan нет свойств Months (месяцы) и Years (годы), и на то есть своя причина. В месяцах и годах разное число дней. Я хотел, чтобы Life Years вычисляла результаты в годах, месяцах и днях, что более приемлемо для пользователя. Если начальная дата — это 10-е число месяца, а конечное — 15-е, я хотел, чтобы в метке стояло «5 дней», независимо от месяца и года. Сложность здесь заключается в подсчете количества дней, когда начальная дата больше конечной, например начальная дата — 20 мая, а конечная — 10 октября. Я хотел, чтобы результат выглядел так: 4 месяца (то есть, с 20 мая по 20 сентября) и
Этот плодовитый класс Control] 87 20 дней (с 20 сентября по 10 октября). В расчетах необходимо принимать во внимание количество дней месяца, предшествующего конечной дате. Статический метод DaysInMontb объекта DateTime и несколько вычислений по модулю позволили мне решить задачу. (Единственная причина того, что у DaysInMontb есть аргумент для года — необходимость получения верного числа дней в феврале. В противном случае, аргумент года был бы бесполезен). Древовидное и списковое представление Элементы управления TreeVieiv и ListView знакомы пользователям Microsoft Windows, как два основных компонента проводника (Windows Explorer). Элемент управления TreeVieiv отображает иерархический список и часто используется для отображения дерева папок или аналогичной структуры. ListView отображает список элементов в одном из нескольких форматов, в числе которых таблица, содержащая подробную информацию об элементах. В проводнике элемент ListView отображает файлы и подпапки папки, выбранной в TreeView. Оба элемента управления напрямую наследуют Control. Control TreeView ListView Древовидное представление {TreeView) Все элементы иерархического списка, отображаемые в TreeVieiv, называются узлами и являются объектами типа TreeNode. Каждый узел может быть родителем других узлов. Потомки узла указаны в свойстве Nodes класса TreeNode, которое является объектом типа TreeNodeCollection — набора других объектов TreeNode. В TreeView также есть свойство Nodes, содержащее информацию об узлах верхнего уровня. Объект TreeNode можно создать с помощью не получающего параметры конструктора, задав лишь значение свойству Text. TreeNode nodeCats = new TreeNodeQ; nodeCats.Text = "Cats"; Также можно передать отображаемый в узле текст непосредственно конструктору, принимающему один аргумент: TreeNode nodeSiamese = new TreeNode("Siamese"); Затем один из узлов делают потомком другого, используя метод Add узла TreeNodeCollection, доступный через свойство Nodes-. nodeCats.Nodes.Add(nodeSiamese);
88 ГЛАВА 2 Также можно создать узел неявно, передав текст узла другой версии метода Add. nodeCats.Nodes.Add("Calico"); Конечно, для отображения всех этих узлов нужно создать объект TreeView-. TreeView tree = new TreeView(); К элементу управления TreeView можно добавить узлы верхнего уровня, используя те же методы Add. t гее.Nodes.Add(nodeCats); tree.Nodes.Add("Dogs"); Позже можно получить доступ к этим узлам, обращаясь к ним по индексу с использованием свойства Nodes. Например, такое выражение возвращает созданный ранее узел Calico: TreeNode node = tree.Nodes[0].Nodes[1]; В TreeNode есть много свойств, призванных облегчить навигацию по узлам. Среди них Parent, родительский узел, а также FirstNode, LastNode, NextNode и PrevNode, все из которых возвращают «родственные» узлы, то есть расположенные на одном уровне иерархии с текущим узлом. Еще одно интересное свойство TreeNode называется FullPath, оно возвращает текстовую строку, состоящую из конкатенации значений свойств Text всех родительских узлов вплоть до верха иерархии, разделенных обратным слэшем. Например, следующее выражение возвращает строку Cats\Calico. nodeCalico.FullPath Обратный слэш можно заменить другим знаком — он задается значением свойства PathSeparator. Ясно, что свойство FullPath с обратным слэшем в качестве разделителя по умолчанию идеально подходит для использования TreeView в его обычной роли — для отображения папок. Однако, работая с TreeView, очень легко написать неэффективный код. Следующая программа создает древовидный список TreeView всех папок диска С, но время построения может оказаться очень большим, если структура папок достаточно развитая. NaiveDirectoryTree View.cs И // NaiveDirectoryTreeView.es (с) 2005 by Charles Petzold // using System; using System.Drawing; using System.10; using System.Windows.Forms;
Этот плодовитый класс Control 89 class NaiveDirectoryTreeView : Form { [STAThread] public static void Main() { Application.EnableVisualStyles(); Application.Run(new NaiveDirectoryTreeViewO); } public NaiveDirectoryTreeViewO { Text = "Naive Directory TreeView"; TreeView tree = new TreeView(); tree.Parent = this; tree.Dock = DockStyle.Fill; TreeNode nodeDriveC = new TreeNode("C:\\"); tree.Nodes.Add(nodeDriveC); AddDirectories(nodeDriveC); } void AddDirectories(TreeNode node) { string strPath = node.FullPath; Directorylnfo dirinfo = new Directorylnfo(strPath); DirectoryInfo[] adirinfo; try { adirinfo = dirinfo.GetDirectories(); } catch { return; } foreach (Directorylnfo di in adirinfo) { TreeNode nodeDir = new TreeNode(di.Name); node.Nodes.Add(nodeDir); AddDi recto ries(nodeDi r); } } }
90 ГЛАВА 2 На первый взгляд, программа кажется отлично структурированной. Конструктор создает элемент управления TreeView и объект типа TreeNode, представляющий диск С. Затем TreeNode передается в рекурсивный метод AddDirectories. AddDirectories использует свойство FullPatb узла, переданного этому методу, для создания объекта типа Directorylnfo (класса, определенного в пространстве имен SystemJO). Метод GetDirectoties этого класса возвращает массив объектов Directorylnfo, соответствующих всем подпапкам на этом пути. (Некоторые папки могут оказаться закрытыми для доступа, поэтому GetDirectories вызывается в блоке try.) Для каждой подпапки создается новый узел и добавляется к переданному методу узлу; далее AddDirectories рекурсивно вызывается с этим новым узлом в качестве аргумента. Проблема в том, что для отображения результатов программе NaiveDirectory- TreeView нужно «пройти» по всей иерархии диска. Поскольку программа выполняет эту работу в конструкторе формы, главное окно программы не видно на экране, пока не завершится проход по папкам. Тем не менее, после появления окна на экране у NaiveDirectoryTreeView нет активных заданий, поэтому вам предоставляется отличная возможность поэкспериментировать со стандартным поведением элемента управления TreeView. В каждый момент времени выбран только один узел. Узлы могут быть развернутыми (при этом отображаются дочерние узлы) и свернутыми. Узел, содержащий дочерние узлы, помечен знаком «плюс» (+) в свернутом и знаком «минус» (-) — в развернутом состоянии. (Для управления отображением этих символов и линий, соединяющих узлы, служат свойства ShowPlusMinus, ShowLines и SbowRootLines.) Развертыванием/свертыванием узлов можно управлять из программы, используя методы Expand, ExpandAll, Collapse и Toggle. У TreeNode есть также булевы свойства IsExpanded и IsSelected, информирующие о том, развернут или выбран ли узел соответственно. Свойство SelectedNode элемента TreeView позволяет выяснить, какой узел выбран, а также выбрать определенный узел. Кроме этого существует целый набор событий, сигнализирующих о происходящем с узлом. При развертывании/свертывании и изменении выбранного узла инициируется пара событий: BeforeExpandwAfterExpand, BeforeCollapse wAfterCollapse или BeforeSelect и AfterSelect. Сообщения о событиях Before содержат объект типа TreeViewCancelEventArgs. В обработчике событий можно отменить операцию, задав свойству Cancel значение true. Если Cancel оставить равным false, событие After будет сопровождаться объектом типа TreeViewEventArgs. TreeViewCancelEventArgs и TreeViewEventArgs есть, соответственно, свойства: Node, указывающее на задействованный в операции узел, и Action, являющееся членом перечисления TreeViewAction. Свойство Action предоставляет информацию о типе операции (развертывание или свертывание) и инструменте действия (клавиатура или мышь). Таким образом, вместо прохода по всему дереву папок сразу после создания элемента управления TreeView, можно дождаться события BeforeExpand и только после этого получать список всех подпапок соответствующего узла. Неплохое решение,
Этот плодовитый класс Control* 91 но не идеальное. Нужно быть на шаг впереди: если не получить все подпапки узла, нельзя будет отобразить знак «плюс», да и узел в таких условиях развернуть проблематично. (Ниже я представлю корректный код для выполнения этого задания.) Обычно узлы TreeView отмечаются небольшими значками. При выборе папки в проводнике ее значок меняется с «закрытого» на «открытый». Изображения, отображаемые элементом управления TreeView, заданы в свойстве ImageList. Свойства Imagelndex и Selectedlmagelndex элемента TreeView указывают на изображения по умолчанию для выбранных и невыбранных узлов. (Кроме этого, можно ссылаться на изображения набора ImageList по имени, используя ImageKey и SelectedlmageKey?) В классе TreeNode также есть свойства Imagelndex, Selectedlmagelndex, ImageKey и SelectedlmageKey, используемые для обозначения индивидуальных значков для отдельных узлов. Эти свойства всегда ссылаются на ImageList элемента TreeView, которому принадлежат эти узлы. Ниже представлен более эффективный код для отображения дерева папок. Класс DirectoryTreeView реализован как элемент управления, наследующий TreeView. DirectoryTree View.cs И // DirectoryTreeView.es (с) 2005 by Charles Petzold // using System; using System.Drawing; using System.10; using System.Windows.Forms; class DirectoryTreeView : TreeView { public DirectoryTreeView() { // Получение используемых в дереве значков. ImageList = new ImageListO; ImageList.Images.Add(new Icon(GetType(), "Resource.CLSDFOLD.ICO")) ImageList.Images.Add(new Icon(GetType() ImageList.Images.Add(new Icon(GetType() ImageList.Images.Add(new Icon(GetType() ImageList.Images.Add(new Icon(GetType() Imagelndex = 0; Selectedlmagelndex = 1; "Resource.0PENF0LD.ICO")) "Resource.35FL0PPY.ICO")) "Resource.CDDRIVE.ICO")); "Resource.DRIVENET.ICO")) DriveInfo[] drives = Drivelnfo.GetDrivesO; foreach (Drivelnfo drive in drives) {
92 ГЛАВА 2 // Создаем узел диска. TreeNode nodeDrive = new TreeNode(drive.RootDirectory.Name); // Задаем индекс значка в зависимости от типа диска, if (drive.DriveType == DriveType.Removable) nodeDrive.Imagelndex = nodeDrive.Selectedlmagelndex = 2; else if (drive.DriveType == DriveType.CDRom) nodeDrive.Imagelndex = nodeDrive.Selectedlmagelndex = 3; else nodeDrive.Imagelndex = nodeDrive.Selectedlmagelndex = 4; // Добавляем узел дерева и папки. Nodes.Add(nodeDrive); AddDi rectories(nodeDrive); // Выделяем диск С. if (drive.RootDirectory.Name[0] == 'С') SelectedNode = nodeDrive; > void AddDirectories(TreeNode node) { node.Nodes.Clear(); Directorylnfo dirinfo = new Directorylnfo(node.FullPath); DirectoryInfo[] adirinfo; try { adirinfo = dirinfo. GetDirectoriesO; } catch { return; } // Создаем узлы для каждой подпапки. foreach (Directorylnfo dir in adirinfo) { TreeNode nodeDir = new TreeNode(dir.Name); node.Nodes.Add(nodeDir); } } protected override void OnBeforeExpand(TreeViewCancelEventArgs args)
Этот плодовитый класс Controll 93 { base.OnBeforeExpand(args); BeginUpdateO; // Создаем подпапки для всех подузлов, которые предполагается отображать, fоreach (TreeNode node in args.Node.Nodes) AddDi rectories(node); EndUpdateO; } } DirectoryTreeView.es — это часть проекта DirectoryTreeViewDemo (о последнем мы вскоре поговорим). В проект добавлены пять файлов значков (лсд). Я взял их из набора, поставляемого в составе Visual Studio 2005, и добавил в свой проект командой Add Existing Item. Все файлы значков надо отметить как внедренный ресурс (Embedded Resource). Это делается таю сначала нужно выбрать файл в Solution Explorer. В окне Properties свойству Build Action задается значение Embedded Resource. При компиляции файл значка будет добавлен в ЕХЕ-файл в качестве ресурса. В процессе выполнения файл будет загружаться в оперативную память. В начале конструктора Direc- toryTreeView значки загружаются при помощи конструктора класса Icon: new Icon(GetType(), "Resource.CLSDFOLD.ICO") Первый аргумент — это объект типа Туре, который ссылается на любой, определенный в программе класс. Для его получения вполне подходит вызов GetTypeQ. Также задачу решит выражение typeof(DirectoryTreeView). Второй аргумент конструктора — это имя файла значка, начинающееся со слова Resource. Эта часть имени в реальности может быть любой, но ее нужно определить в свойствах проекта. Откройте свойства проекта и обратите внимание на поле Default namespace. По умолчанию Visual Studio проставляет в нем имя проекта — в данном случае DirectoryTreeViewDemo. Замените значение поля на Resource, иначе программа не сможет загрузить значки. Если оставить в поле Default Namespace значение DirectoryTreeViewDemo, значки придется загружать таю new Icon(6etType(), "DirectoryTreeViewDemo.CLSDFOLD.ICO") Все правильно, но файл DirectoryTreeView.cs будет использоваться в другом проекте (о нем — в конце главы), где значение поля Default Namespace другое. Не следует путать Default Namespace с другим пространством имен в программах .NET Это имя используется только по отношению к ресурсам. После загрузки значков конструктор присваивает свойствам Imagelndex и Selected- Imagelndex значения 0 и 1, соответственно — это означает, что значки закрытой и открытой папки являются для этого элемента управления изображениями по умолчанию.
94 ГЛАВА 2 Появившийся в .NET Framework 2.0 класс Drivelnfo используется для получения информации о дисководах компьютера. Свойство DriveType позволяет назначать дискам соответствующее изображения. Метод AddDirectories создает узлы всех подкаталогов верхнего уровня дисков. Это позволяет TreeView отображать знак «плюс», если диск содержит каталоги. Впоследствии, при открытии любого другого каталога, узлы его подкаталогов уже будут отображены, a AddDirectories добавит узлы для подкаталогов вновь появившихся каталогов. Небольшой файл DirectoryTreeViewDemo.cs — последний из файлов проекта DirectoryTreeViewDemo. DirectoryTree ViewDemo.cs И // DirectoryTreeViewDemo.cs (с) 2005 by Charles Petzold // using System; using System.Drawing; using System.Windows.Forms; class DirectoryTreeViewDemo : Form { [STAThread] public static void Main() { Application. EnableVisualStylesO; Application.Run(new DirectoryTreeViewDemo()); } public DirectoryTreeViewDemoO { Text = "DirectoryTreeView Demo"; DirectoryTreeView tree = new DirectoryTreeView(); tree.Parent = this; tree.Dock = DockStyle.Fill; } } Программа создает DirectoryTreeView и размещает в клиентской области элемент управления. Обратите внимание, что окно программы появляется гораздо быстрее, чем окно NaiveDirectoryTreeView. У TreeView есть и другие интересные возможности. Для отображения флажков рядом с узлами надо задать свойству CheckBoxes значение true. В таком виде TreeView похож на CheckedListBox, но содержит иерархическое дерево, а не простой список.
Этот плодовитый класс Control] 95 Булево свойство Checked элемента TreeNode указывает, отмечен ли данный узел флажком. Связаны с этой возможностью и новые свойства .NET Framework 2.0 — StatelmageList в TreeView и Statelmagelndex с StatelmageKey в TreeNode. Чтобы пользователи могли редактировать метки, прежде всего, задайте свойству LabelEdit элемента TreeView значение true. Далее, нужно определить, как пользователь будет активизировать режим редактирования. Обычно используется щелчок правой кнопки мыши, для этого нужно установить обработчик события Node- MouseClick, в котором убедиться, что значение свойства Button аргумента равно MouseButtons.Right, и затем вызвать для этого узла метод BeginEdit. Затем TreeView генерирует событие BeforeLabelEdit. Объект NodeLabelEditEventArgs этого события имеет три свойства: Node, Label и CancelEdit. Свойство «только для чтения» Node — это редактируемый объект-узел TreeNode. Свойство Label равно null Редактирование можно отметить, задав свойству CancelEdit значение true. В противном случае вы получите сообщение AfterLabelEdit с другим объектом NodeLabel- EventArgs. Возможно, потребуется обработка этого события. Свойство Node хранит состояние узла, а свойство Text объекта — текст до редактирования. Свойство Label содержит новый текст. (Если пользователь отменит редактирование, нажав клавишу Esc либо щелкнув в другом месте экрана, свойство Label станет равным null) На этом этапе проверяется правомочность редактирования. Например, проводник не позволит задать пустую строку в качестве имени каталога, а также предупредит о возможных последствиях при изменении расширения файла. Чтобы запретить изменение, надо задать CancelEdit значение true. Иначе свойству Text узла будет присвоено значение свойства Label. Списковое представление (ListView) В проводнике предоставляется несколько вариантов отображения списка файлов: только имена файлов со значками или подробная информация о файлах, включая размер, тип, дату изменения и другие параметры. Эти способы отображения связаны с View, одним из самых важных свойств ListView. Оно принимает значения одноименного перечисления, состоящего из членов Details, List, Largelcon, Smalllcon и Tile. ListView — сложный элемент управления, поэтому здесь мы коснемся его только поверхностно. Один из первых шагов настройки ListView — назначение колонок, отображающихся при выборе View.Details. Свойство Columns элемента ListView — это объект типа ColumnHeaderCollection, представляющего собой набор объектов ColumnHeader. Объекты ColumnHeader можно создавать по отдельности, затем, добавляя их к свойству Columns, или вы вправе воспользоваться методом Add набора ColumnHeaderCollection. Например, таю lstview.Columns.Add("File Name", 100, HorizontalAlignment.Left);
96 ГЛАВА 2 Первый аргумент определяет отображаемый в колонке текст, второй — ширину колонки, а последний аргумент задает выравнивание текста в заголовке и теле колонки — по левому или правому краю или по центру. В режиме просмотра Details каждый ряд представлен объектом типа ListViewItem. Как минимум, надо задать свойство Text, отображаемое в первой колонке. Свойство Subltems объекта ListViewItem представляет вторую и каждую из последующих колонок. Subltems — это объект типа ListViewItem.ListViewSubltemCollection, представляющий собой набор объектов ListViewSubltem. Все относящиеся к ListView объекты ListViewItem собраны в свойстве Items. Итак, элемент управления ListView — это набор объектов ListViewItem, каждый из которых в свою очередь содержит набор объектов ListViewSubltem. У ListView есть два свойства типа ImageList — SmalllmageList и LargelmageList, которые используются для хранения больших и маленьких значков. Вот класс, наследующий ListView и отображающий список файлов практически так же, как проводник. DirectoryListVlew.cs /I // DirectoryListView.es (с) 2005 by Charles Petzold // using System; using System.Diagnostics; using System.Drawing; using System.10; using System.Windows.Forms; class DirectoryListView : ListView { string strDirectory; public DirectoryListViewO { // Создаем столбцы ListView. Columns.Add("Name", 150, HorizontalAlignment.Left); Columns.Add("Size", 100, HorizontalAlignment.Right); Columns.Addf'Type", 100, HorizontalAlignment.Left); Columns.Add("Date Modified", 150, HorizontalAlignment.Left); // Создаем списки изображений ListView. SmalllmageList = new ImageListO; LargelmageList = new ImageListO; LargelmageList.ImageSize = new Size(32, 32); }
Этот плодовитый класс Controll 97 public string Directory { get { return strDirectory; } set { // Очищаем списки элементов и изображений. Items.Clear(); SmalllmageList.Images.Clear(); LargelmageList.Images.Clear(); // Загружаем значок папки. Icon icn = new Icon(GetType(), "Resource.Folder.ico"); SmalllmageList.Images.Add(icn); LargelmageList.Images.Add(icn); // Создаем объект Directorylnfo на основе информации требуемой папки. Directorylnfo dirinfo = new DirectoryInfo(strDirectory = value); // Получаем список всех подпапок и добавляем их в ListView. foreach (Directorylnfo dir in dirinfo.GetDirectoriesO) { ListViewItem item = new ListViewItem(dir.Name); item.Imagelndex = 0; item.Tag = "dir"; item.Subltems.Add(""); item.Subltems.Add("File Folder"); item. Subltems. Add(dir.LastAccessTime.ToStringO); Items.Add(item); } int ilmage = 1; // Получаем список всех файлов и добавляем их в ListView. foreach (Filelnfo file in dirinfo.GetFilesQ) { ListViewItem item = new ListViewItem(file.Name); icn = Icon.ExtractAssociatedIcon( Path.Combine(file.DirectoryName, file.Name)); SmalllmageList.Images.Add(icn); LargelmageList.Images.Add(icn); item.Imagelndex = ilmage++; item.Subltems.Add(file.Length.ToString("NO")); item.Subltems.Add(
98 ГЛАВА 2 Path.GetExtension(file.Name).ToUpperQ == ".EXE" ? "Executable" : "Document"); item.Subltems.Add(file.LastWriteTime.ToSt ring()); Items.Add(item); } > } protected override void OnMouseDown(MouseEventArgs args) { base.OnMouseDown(args); // Представление изменяется по щелчку правой кнопки мыши. if (args.Button == MouseButtons.Right) View = (View)(((int)View + 1) % 5); } protected override void OnItemActivate(EventArgs args) { base.OnltemActivate(a rgs); // По щелчку папки переходим к ней. if ((string)SelectedItems[0].Tag == "dir") { Directory = Path.Combine(Directory, SelectedItems[0].Text); } else { // В противном случае запускаем программу, // ассоциированную с файлом (или файлами). foreach (ListViewItem item in Selectedltems) { try Process.Start(Path.Combine(Directory, item.Text)); catch } > } } Этот файл входит в проект DirectoryListViewDemo, содержащий также файл со значком Folder.ico. Так же как и в предыдущем проекте, файл нужно пометить как
Этот плодовитый класс Controll 99 внедренный ресурс (Embedded Resource), а свойству Default Namespace проекта следует задать значение Resource. Конструктор создает четыре колонки, отображаемые в режиме просмотра Details. Затем он создает два объекта ImageList и присваивает их свойствам SmalllmageList и LargelmageList. В дальнейшем, список представления может обращаться к объектам ImageList по этим свойствам. По умолчанию свойство ImageSize объекта ImageList представляет собой объект Size с шириной и высотой по 16 пикселов. Если свойство ImageSize оставить неизменным, любое изображение, добавляемое в ImageList, будет принимать такие размеры. Квадрат размером 16x16 пикселов (его высота примерно равна высоте символа текста по умолчанию) хорошо подходит для небольших значков, а для больших — квадрат должен быть крупнее, поэтому свойство ImageSize объекта ImageList, хранящегося в свойстве LargelmageList объекта DirectoryListView, задает квадрат размером 32x32 пиксела. В DirectoryListView появилось новое открытое свойство Directory, указывающее на отображаемую папку. Основную работу в этом классе выполняет аксессор set этого свойства. Элемент управления должен хранить все файлы и подпапки заданной папки. Аксессор set начинает работу с очищения всех существующих элементов из самого элемента управления и двух списков изображений. Затем он загружает файл Folder.ico со значком папки. Поскольку это первое изображение в обоих списках изображений, на него ссылаются по индексу 0. Затем аксессор set создает объект типа Directorylnfo и получает все подпапки этой папки. Для каждой создается новый объект ListViewItem. Свойству Imagelndex присваивается значение 0 (указывающее на значок папки), а свойству Tag — значение «dir», обозначающее элемент как папку. У каждого элемента есть три подэлемента. Первый — это размер, для папки равный 0, второй — тип (File Folder), а третий — время последней операции доступа. Далее аксессор set получает все файлы каталога. Для каждого файла статический метод IconExtractAssociatedlcon получает значок, ассоциированный с файлом. Для файлов документов информация, касающаяся ассоциированных исполняемых файлов и их места в системе, поступает из реестра Windows. Значок папки добавляется в оба списка изображений, а индекс изображения становится свойством Imagelndex элемента. К сожалению, в .NET Framework мне не удалось найти такого метода, как Icon,- ExtractAssociatedlcon, который позволяет получить информацию о типе файла (например, в проводнике для CSPROJ-файлов в поле типа отображается строка «Visual Studio Project file»). Для получения этой информации приходится копаться в реестре Windows. Этот класс просто выбирает одну из строк — Executable (то есть «исполняемый файл») или Document (документ) — в зависимости от расширения файла. Поскольку нам пока не хватает знаний, чтобы создать панель инструментов или команду меню, позволяющую изменить свойство View, я решил изменять режим
100 ГЛАВА 2 отображения по щелчку правой кнопки мыши. Для этого я задействовал метод OnMouseDown. Наконец, метод OnltemActivate информирует о двойном щелчке или нажатии клавиши Enter. Только после этого класс предпринимает активные действия. Если свойство Tag указывает на папку, OnltemActivate присваивает свойству Directory эту папку Если выбран файл (их может быть несколько), вызовом метода ProcessStart запускается ассоциированное приложение. В проект DirectoryListViewDemo также входит следующая простая программа: DirectoryListViewDemo.es I/ // DirectoryListViewDemo.es (с) 2005 by Charles Petzold // using System; using System.Drawing; using System.Windows.Forms; class DirectoryListViewDemo : Form { [STAThread] public static void Main() { Application. EnableVisualStylesO; Application.Run(new DirectoryListViewDemoO); } public DirectoryListViewDemoO { Text = "DirectoryListView Demo"; Width *= 2; DirectoryListView dirlv = new DirectoryListViewO; dirlv.Parent = this; dirlv.Dock = DockStyle.Fill; dirlv.View = View.Details; dirlv.Directory = "C:"; } } Последний оператор задает свойству Directory элемента управления DirectoryListView значение корневой папки диска С. Перейти к другой папке можно, щелкнув ее имя в ListView, но вернуться к родительскому каталогу позже будет нельзя. Давайте устраним этот недостаток, объединив элементы DirectoryTreeView и DirectoryListView в простейшую программу типа проводника. В программу Primeval-
Этот плодовитый класс Control* 101 FileExplorer входят файлы DirectoryTreeView.cs, DirectoryListView.cs, а также файлы всех значков, используемых обоими классами. Эти значки надо отметить как внедренные ресурсы (Embedded Resource), а свойству Default Namespace проекта — задать значение Resource. Проект также содержит следующий файл: PrimevalFileExplorer.cs И // PrimevalFileExplorer.cs (с) 2005 by Charles Petzold // using System; using System.Drawing; using System.Windows.Forms; class PrimevalFileExplorer : Form { DirectoryTreeView tree; DirectoryListView list; public static void Main() { Application.EnableVisualStyles(); Application.Run(new PrimevalFileExplorerO); } public PrimevalFileExplorerO { Text = "Primeval File Explorer"; tree = new DirectoryTreeView(); tree.Parent = this; tree.Dock = DockStyle.Left; tree.AfterSelect += TreeViewOnSelect; list = new DirectoryListViewO; list.Parent = this; list.Dock = DockStyle.Right; Width *= 2; } protected override void OnResize(EventArgs args) { base.OnResize(args); tree.Width = list.Width = Width / 2; } void TreeViewOnSelect(object objSrc, TreeViewEventArgs args) {
102 ГЛАВА 2 list.Directory = args.Node.FullPath; > } Два элемента управления занимают по половине клиентской области программы. Переопределение метода OnResize позволяет из программы корректировать ширину элементов управления. В следующей главе я покажу, как правильно размещать разделитель, разбивающий клиентскую область на части. Класс PrimevalFileExplorer устанавливает обработчик события Select элемента DirectoryTreeView, а затем присваивает свойству Directory этого элемента значение свойства FullPath выбранного узла. Однако программа PrimevalFileExplorer не корректирует выбор узла в DirectoryTreeView в соответствии со сменой папки. Для этого надо разбить путь к новому каталогу на части, соответствующие отдельным папкам, а затем сопоставить их узлам.
Глава 3 Панели и динамическое размещение Иногда случается так, что тщательно размещенные на форме или в диалоговом окне элементы странным образом перемешиваются на экране компьютера другого пользователя. Это может произойти из-за нестандартного разрешения экрана или странного шрифта по умолчанию, но, конечно же, элементы управления не должны накладываться друг на друга, а текст — не вмещаться на кнопках. Одним из самых важных улучшений Windows Forms в .NET Framework 2.0 является поддержка динамического размещения (dynamic layout). Идея проста: настройка размеров и позиций элементов управления выполняется при запуске формы, и в том случае если пользователь изменяет размер формы. Динамическое размещение — полезный инструмент проектирования Windows-программ, а также важная составляющая будущей концепции проектирования пользовательского интерфейса в Windows. Разные способы решения задачи размещения элементов управления Разработчики программ для Windows традиционно размещали элементы управления на формах и в диалоговых окнах, жестко задавая их координаты и размеры. Чтобы избежать проблем с интерфейсом из-за использования программ на системах с другим разрешением экрана и размерами шрифтов, применялись разные методы. Программисты, использующие Microsoft Win32 или MFC (Microsoft Foundation Class), задают расположение и размеры элементов в особой аппаратно-независи- мой системе координат, в которой за основу взята одна восьмая высоты и одна четвертая ширины системного шрифта Windows. Обычно эта система координат ограничена шаблонами диалоговых окон в сценариях ресурсов программы. При загрузке и отображении такого диалогового окна Windows преобразует эти единицы в пикселы, исходя из размеров текущего системного шрифта. Но даже в аппаратно-независимой системе координат расположение всегда было малоприятной работой, поэтому Microsoft разработала одно из первых средств для
104 ГЛАВА 3 «комфортного» программирования — конструктор форм, или визуальный редактор диалоговых окон [см. статью Charles Petzold, «Latest Dialog Editor Speeds Windows Application Development», Microsoft Systems Journal, Vol. 1, No. 1 (October, 1986)]. С его помощью программисты могли проектировать диалоговые окна интерактивно, перемещая и изменяя размеры элементов управления. Редактор также позволял создавать соответствующие сценарии ресурсов. Позже такие редакторы стали неотъемлемой частью интегрированных сред разработки, таких как Microsoft Visual Basic и Microsoft Visual Studio. В Windows Forms представлена новая модель аппаратно-независимого размещения. В среде Visual Studio 2005, как и в более ранних ее версиях, формы и диалоговые окна можно проектировать интерактивно, но есть ряд изменений. Во-первых, при определении позиции и размера элемента управления Visual Studio вставляет координаты непосредственно в код на С# как часть логики создания этого элемента. Вот типичный пример кода, созданного Visual Studio: this.buttonl = new System. Windows. Forms. ButtonO; this.button"!. Location = new System.Drawing.Point(69, 52); this.buttonl.Size = new System.Drawing.Size(77, 27); Это код конструктора класса, производного от класса Form, a button 1 — поле этого класса. Конечно, код тяжеловат: ключевое слово this не требуется, многие программисты обошлись бы без полных имен классов и определили имена при помощи пары операторов using. Знакомых с «подводными камнями» размещения в этом коде несомненно поразит совершенно вопиющее определение размещения и размеров в пикселах. Ясно, что координаты в пикселах придется как-то утрясать в процессе исполнения. Наряду с жестко заданными положениями и размерами элементов, Visual Studio создает следующие операторы и определяет два свойства формы: this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; Эти свойства появились в .NET Framework 2.0 и замещают схожее свойство Auto- ScaleBaseSize из .NET Framework 1.x. Эти два операторы показывают, что программист спроектировал и разместил кнопку, исходя из размера шрифта по умолчанию, равного 6 пикселам в ширину и 13 пикселам в высоту. Этот размер рассчитан на разрешение экрана по умолчанию, то есть 96 точек на дюйм. Если программа работает в системе с другим разрешением и размером шрифта, все размеры и координаты элементов управления формы изменятся в соответствии с соотношением размера шрифта, установленного на компьютере, и размером шрифта, переданным свойству AutoScaleDimensions.
Панели и динамическое размещение 105 Сложности с размещением Хотя все эти способы обеспечения аппаратной независимости работоспособны, они не решают всех проблем с размещением. Определение точных размеров и координат — даже в аппаратно-независимом стиле — в последние годы становится все менее желательным. И на то есть ряд причин. Во-первых, приложения переводятся на другие языки, и часто оказывается, что элемент управления, подогнанный по размерам к тексту на одном языке, не способен уместить текст на другом языке. Вместо перепроектирования формы и диалоговых окон, лучше сразу сделать так, чтобы элементы управления и диалоговые окна «автоматически» подстраивались к размерам текста на другом языке. Во-вторых, со временем мониторы с высоким разрешением (200 или 300 точек на дюйм) без сомнения получат практически повсеместное распространение. Сегодняшние программы должны работать на системах с такими мониторами. Программисты могут думать, что их программы аппаратно независимы, но действительность намного богаче. Третья проблема, возможно, имеющая наиболее серьезные последствия, связана с самими элементами управления. Они становятся сложнее, и программистам становится труднее предугадать, сколько пространства потребуется для их размещения. Возможно, определение оптимального размера нужно делегировать самому элементу управления и выполняться это должно только во время выполнения. Форма или диалоговое окно, где находится этот элемент управления, должны изменять свои размеры в соответствии с его потребностями. В прошлом формы определяли размеры элементов управления. В будущем, которое начинается с этого момента, элементы управления будут сами определять свои размеры, а формы — подстраиваться к ним. Новая парадигма динамического размещения решает много проблем традиционного размещения. Во время выполнения программа размещает элементы управления, исходя из размера, затребованного этими элементами, разрешения, шрифта по умолчанию, размера окна программы и других факторов, известных только самим элементам управления. Такой подход сегодня воспринимается намного легче, чем десяток-второй лет назад, так как сейчас мы каждый день имеем дело с одним из видов динамического размещения — это HTML. Разметка HTML показала, что размещение может более или менее успешно подстраиваться к различным размерам шрифтов и окна браузера. Конечно, в царстве HTML далеко не все идеально, но нельзя не признать факт, что технология размещения, реализованная в каждом Web-браузере, намного богаче и функциональнее, чем та, что обычно используется в старых программах для Windows.
106 ГЛАВА 3 Свойство AutoSize Реализованное в классе Control и наследуемое всеми его потомками логическое свойство AutoSize — один из компонентов, позволяющих реализовать динамическое размещение в Windows Forms. В .NET Framework 1.x было всего несколько элементов управления со свойством AutoSize — теперь оно есть у каждого. По умолчанию AutoSize равно false, но если значение изменить на true, элемент управления станет менять свои размеры в зависимости от содержимого. Например, на кнопке всегда будет отображаться весь размещенный на ней текст. Некоторые элементы управления дополнительно имеют свойство AutoSizeMode, принимающее значение одного из двух членов перечисления AutoSizeMode: Groiv- AndShrink или GrowOnly (по умолчанию). В следующем примере демонстрируется использование свойства A utoSize кнопки и формы, на которой она расположена. Размер кнопки автоматически изменяется в зависимости от расположенного на ней текста, а размер формы изменяется в зависимости от размера кнопки. AutoSizeDemo.cs // // AutoSizeDemo (с) 2005 by Charles Petzold // using System; using System.Drawing; using System.Windows.Forms; class AutoSizeDemo : Form { [STAThread] public static void Main() { Application.EnableVisualStyles(); Application.Run(new AutoSizeDemoO); } public AutoSizeDemoO { Text = "AutoSize Demo"; AutoSize = true; AutoSizeMode = AutoSizeMode.GrowAndShrink; Button btn = new ButtonO; btn.Parent = this; btn.AutoSize = true; btn.Text = "Look back on time with kindly eyes,\n" + "He doubtless did his best;\n" +
Панели и динамическое размещение 107 "How softly sinks his trembling sun\n" + "In human nature's west!"; } } Форма содержит единственный элемент управления Button, на котором отображается текст короткого стихотворения Эмили Дикинсон (Emily Dickinson). Свойству AutoSize как формы, так и кнопки присвоено значение true. Кроме того, программа изменяет свойство AutoSizeMode формы на AutoSizeMode.GrowAndShrink (то есть размер может динамически увеличиваться и уменьшаться). В противном случае, форма сохранила бы размер по умолчанию, который больше, чем требуется для отображения кнопки. Хотя у формы обычные границы установки размера окна, изменить размер формы не удастся. При назначении свойству AutoSizeMode формы значения Grow- AndShrink лучше запретить любые изменения размера окна. Этот пример демонстрирует небольшую часть возможностей свойства AutoSize, но показанное не совсем динамическое размещение в полном смысле этого слова. Кнопка размещается в левом верхнем углу клиентской области формы, поскольку по умолчанию свойству Location присвоено значение (0,0). Если в программе появится вторая кнопка, придется что-то менять. Вот программа, отображающая вторую кнопку, размещенную без промежутка справа от первой. ManualLayoutDemo.cs // // ManualLayoutDemo.cs (с) 2005 by Charles Petzold // using System; using System.Drawing; using System.Windows.Forms; class ManualLayoutDemo : Form { [STAThread] public static void Main() { Application.EnableVisualStyles(); Application.Run(new ManualLayoutDemoO); }
108 ГЛАВА 3 public ManualLayoutDemoO { Text = "Manual Layout Demo"; AutoSize = true; AutoSizeMode = AutoSizeMode.GrowAndShrink; Button btn = new Button(); btn.Parent = this; btn.AutoSize = true; btn.Text = "Look back on time with kindly eyes,\n" + "He doubtless did his best;\n" + "How softly sinks his trembling sun\n" + "In human nature's west!"; Button btn2 = new Button(); btn2.Parent = this; btn2.AutoSize = true; btn2.Location = new Point(btn.Right, 0); btn2.Text = "He ate and drank the precious words,\n" + "His spirit grew robust;\n" + "He knew no more that he was poor,\n" + "Nor that his frame was dust.\n" + "He danced along the dingy days,\n" + "And this bequest of wings\n" + "Was but a book. What liberty\n" + "A loosened spirit brings!"; } Ко времени создания конструктором формы второй кнопки, координаты и размер первой уже известны. На основе этой информации конструктор определяет положение второй кнопки. 1 Manual Layout Demo Look back on time with kindly eyes. He doubtless <M hi* best; How softly sinks his trembling sun In human nature's west! He ate and drank the precious words. His spirit grew robust; He knew no more that he was poor. Nor that his frame was dust. He danced along the dingy days. And this bequest erf wings Was but a book. What liberty A loosened spirit brings! В этой программе показано кое-что из того, что встречается при динамическом размещении, но это далеко не все. Если текст или шрифт первой кнопки изменится, нужно будет переместить вторую кнопку, чтобы подстроить ее под новые размеры. В общем случае, при изменении размера элемента управления вызывается так называемый диспетчер размещения, который соответствующим образом сдвигает
Панели и динамическое размещение 109 остальные элементы. Если вам интересна эта тема, начните с события Layout, реализованного в классе Control, и класса LayoutEngine из пространства имен System. WindowsJFormslayout. Если создание собственной логики динамического размещения вас не прельщает, воспользуйтесь двумя новыми панелями — FlowLayoutPanel и TableLayoutPanel — которые обеспечат динамическое размещение. В первой используется модель плавающего размещения, а в другой — табличная модель. Опыт использования HTML подтверждает, что эти две модели вполне годятся для решения самого широкого диапазона задач по размещению. Конечная цель этой главы (и одна из моих личных целей) — научиться проектировать одновременно отлично выглядящие и достаточно гибкие формы без использования точно определенных координат и размеров. Мне не важно, будет ли внедрять эти координаты в код Visual Studio или мне придется задавать их самостоятельно, — я просто больше не хочу с ними морочится. Панели и контейнеры У каждого элемента управления есть свойство Controls, которое может хранить набор других, дочерних элементов управления. Однако в большинстве элементов управления это свойство не используется — возможность иметь дочерние элементы управления важна только для таких элементов, как формы и панели. Вот выборочная иерархия классов, в которой я перечислил только те элементы управления, о которых расскажу в этой главе: Control ScrollableControl ContainerControl Form SplitContainer Panel FlowLayoutPanel SplitterPanel TableLayoutPanel Возможность поддерживать набор дочерних элементов управления — основная особенность класса ScrollableControl Этот класс так называется потому, что может автоматически отображать полосы прокрутки, если размер не позволяет показать все дочерние элементы. Элемент управления, содержащий дочерние элементы, часто называют контейнером, несмотря на то, что класс ContainerControl производный от ScrollableControl Элемент управления ContainerControl состоит из нескольких других элементов управления. Класс Form попадает в эту категорию, поскольку (по крайней мере, по определению) содержит клиентскую и неклиентскую области. Хотя элементы управления Panel производны от ScrollableControl, а не ContainerControl, они тоже
11 О ГЛАВА 3 считаются контейнерами, поскольку их основное предназначение — содержать другие элементы управления. В этой главе обсуждаются элементы управления Panel, SplitContainer (и связанный с ним SplitterPanel), FlowLayoutPanel и TableLayoutPanel Сначала рассказывается о некоторых стандартных (принятых в .NET Framework 1.x) способах квазидинамического размещения, и лишь затем о новых. Стыковка и привязка В .NET Framework 1.0 появились два полезных свойства: Dock w Anchor. На первый взгляд они кажутся похожими, причем такое впечатление может остаться даже после их длительного применения. Свойство Dock чаще используется в традиционном размещении, а у свойства Anchor появились новые возможности, о чем мы узнаем далее. Стыковка Свойство Dock реализовано в классе Control и наследуется всеми производными классами. Оно может принимать значение из перечисления DockStyle: None, Top, Bottom, Left, Right и Fill У большинства элементов управления по умолчанию задано значение DockStyle .None. Если, например, свойству Dock задать значение DockStyle.Top, элемент управления разместится в верхней части своего контейнера (например, в клиентской области формы) и займет всю его ширину от левого до правого края. Значение Dock- Style.Top подходить для панелей инструментов, a DockStyle.Bottom — для строк состояния (именно такие значения по умолчанию назначаются элементам управления ToolBar и StatusBar, как и заменяющим их в .NET Framework 2.0 элементам ToolStrip и StatusStrip). Обычно часть элементов управления размещается по краям формы, остальные — располагаются в центре. В последнем случае свойству Dock задают значение DockStyle.Fill Вот основные правила. Если задается значение свойству Dock одного элемента управления, нужно задать этому свойству всех элементов того же уровня значение отличное от DockStyle.None. Только один элемент управления может иметь свойство Dock равное DockStyle.Fill Выполнение этих простых правил поможет избежать наложения элементов друг друга. (Вместе с тем, если контейнер слишком маленький, стыкованные элементы все равно будут перекрывать друг друга — от этого никак не избавиться). Ясно, что при задании различных (или одинаковых) значений свойству Dock разных элементов управления, нужно знать, как они будут взаимодействовать друг с другом.
Панели и динамическое размещение 111 При назначении элементам управлении свойства Parent (или при добавлении в родительский элемент управления методом Add свойства Controls), элементы управления выстраиваются в z-порядке (этот термин относится к третьей оси координат в трехмерной системе координат). Z-порядок — это последовательность элементов управления в наборе элементов родителя. Элемент управления, расположенный в начале z-порядка, обычно первый элемент, добавленный в набор. Ему соответствует индекс 0 родительского свойства Controls, и визуально он располагается над другими элементами, если с ними пересекается. Элемент управления в конце z-порядка — это последний элемент, добавленный в набор, на экране он располагается под остальными элементами. Z-порядок изменяют методами BringToFront и BringToBack. Если несколько элементов одного уровня пристыкованы к одному краю контейнера, непосредственно к краю будет примыкать элемент управления, находящийся ближе всех к концу z-порядка, далее, ближе к центру располагается элемент, стоящий на ступеньку выше и так далее. Поэтому верно простое правило: работайте от центра. То есть, начинать нужно с создания центральных элементов управления, задавая свойству Dock значение DockStyle.FilL Затем создают остальные элементы, а заканчивать надо элементом, который должен располагаться у самого края контейнера. Вот пример применения этого правила. Это простейший аналог программы Блокнот (Notepad) с элементом управления TreeVieiv в левой части элемента EditBox. Как и в большинстве программ из этой главы, элементы управления практически ничего не делают и наполнение их минимально. PrimevalNotepad.cs // // PrimevalNotepad.cs (с) 2005 by Charles Petzold // using System; using System.Drawing; using System.Windows.Forms; class PrimevalNotepad : Form { [STAThread] public static void Main() { Application.EnableVisualStyles(); Application.Run(new PrimevalNotepadO); } public PrimevalNotepadO { Text = "Primeval Notepad";
11 2 ГЛАВА 3 TextBox txtbox = new TextBox(); txtbox.Parent = this; txtbox.Dock = DockStyle.Fill; txtbox.Multiline = true; TreeView tree = new TreeViewQ; tree.Parent = this; tree.Dock = DockStyle.Left; tree.Nodes.Add("tree"); StatusStrip stat = new StatusStripO; stat.Parent = this; stat.Items.Add("status"); ToolStrip tool = new ToolStripO; tool.Parent = this; tool.Items.Add("tool"); MenuStrip menu = new MenuStripO; menu.Parent = this; menu.Items.Add("menu"); } В этой программе значения свойства Dock элементов управления StatusStrip, ToolStrip и MenuStrip не задаются явно. Соответствующие значения назначаются элементами самостоятельно. Обратите внимание, что программа определяет значение свойства Parent для элемента ToolStrip раньше, чем для MenuStrip. Так ToolStrip размещается под MenuStrip. I can type in the TextBox and it v wrap the text for me.| Обратите также внимание на то, что программа создает элемент TreeView раньше, чем три элемента управления над и под ним. Если создавать TreeView в после-
Панели и динамическое размещение 113 днюю очередь, он будет растянут на всю высоту клиентской области. Тогда меню, размер панели инструментов и строки состояния сократиться, и все это будет выглядеть более чем странно. Не буду возражать, если вы посоветуете мне вставить разделитель между элементами TreeView и TextBox. Этим я займусь чуть попозже. Простые панели Иногда нужно создать форму с меню, панелью инструментов и строкой состояния, но без большого элемента управления TextBox (или любого другого) в центре — вместо него требуется разместить группу элементов: метки, списки, наборные счетчики и другие. Конечно, можно сделать так, чтобы одни элементы управления (такие как Menu- Strip и ToolStrip) стыковались к краю, а остальные дочерние элементы — нет. Не- пристыкованные дочерние элементы можно явно разместить так, чтобы они отображались между панелью инструментов и строкой состояния. Но помните, что размещение выполняется относительно верхнего левого угла клиентской области. Часть клиентской области уже занята элементами управления MenuStrip и ToolStrip. Нужно позаботиться, чтобы элементы управления не перекрывали друг друга. Если же форма становится такой узкой, что меню или панель инструментов переходит на вторую строку, нужно немного сместить все элементы управления вниз. Гораздо лучше создать в центре клиентской области простой, не выполняющий никаких функций элемент управления, задать его свойству Dock значение Dock- Style.Fill и сделать его родителем всех остальных элементов управления небольшого размера (меток, списков, счетчиков и т. д.). Такой элемент управления называется панелью. Положение дочерних элементов панели задается относительно верхнего левого угла панели, а не формы. Вот пример программы, где вместо TextBox используется Panel. SimplePanel.cs // // SimplePanel.cs (с) 2005 by Charles Petzold // using System; using System.Drawing; using System.Windows.Forms; class SimplePanel : Form { [STAThread] public static void Main() {
114 ГЛАВА 3 Application. EnableVisualStylesO; Application.Run(new SimplePanelO); } public SimplePanelO { Text = "Simple Panel"; Panel pnl = new Panel(); pnl.Parent = this; pnl.Dock = DockStyle.Fill; pnl.AutoScroll = true; TreeView tree = new TreeViewQ; tree.Parent = this; tree.Dock = DockStyle.Left; tree.Nodes.Add("tree"); StatusStrip stat = new StatusStripO; stat.Parent = this; stat.Items.Add("status"); ToolStrip tool = new ToolStripO; tool.Parent = this; tool.Items.Add("tool"); MenuStrip menu = new MenuStripO; menu.Parent = this; menu.Items.Add("menu"); Label lbl = new Label(); lbl.Parent = pnl; lbl.AutoSize = true; lbl.Text = "Label control at top of panel"; lbl = new Label(); lbl.Parent = pnl; lbl.AutoSize = true; lbl.Text = "Label control at bottom of panel"; lbl.Location = new Point(300, 300); } } Элемент управления Panel, расположенный в центре, имеет два дочерних элемента Label один — в верхней части панели, а второй расположен на 300 пикселов правее и ниже первого. Если панель недостаточно велика, нижний элемент не
Панели и динамическое размещение 115 виден. Свойству AutoScroll панели в программе задается значение true, так что пользователь может прокрутить форму и увидеть этот элемент. I ЙЗЭ jtabd control *t bottom of panel Ч status Л Помните, что свойство AutoScroll есть у всех элементов управления, производных от класса ScrollableControl. В обычных формах оно применяется не часто, но оказывается очень кстати при использовании панели, на которой отображается много элементов управления (например, графических окон). В обычном элементе управления Panel нужно явно размещать элементы управления (или использовать стыковку). Однако в качестве альтернативы можно использовать элементы управления FlowLayoutPanel или TableLayoutPanel, которые мы рассмотрим позже в этой главе. Можно также задействовать элемент управления SplitContainer в качестве родителя других панелей. При планировании пользовательского интерфейса в Windows Forms надо мыслить в терминах иерархий панелей и элементов управления. Привязка Свойство Anchor схоже со свойством Dock в том, что оно связывает элемент управления с одним или несколькими краями контейнера. Однако это свойство не прикрепляет элемент управления к краю контейнера, а позволяет ему сохранять постоянную дистанцию от края. Еще одно отличие: свойству Dock программы можно задать значение только одного члена перечисления DockStyle. Со свойством Ancbor можно связывать несколько членов перечисления AnchorStyles, объединив их битовым оператором ИЛИ (|). Заметьте, что множественно число в имени AnchorStyles (в отличие от одного значения в DockStyle) предполагает использование нескольких членов. Каждый член AnchorStyles представлен одним битом. В перечисление входят следующие члены (в скобках указаны их значения): None (0), Тор (1), Bottom (2), Left (4) и Right (8).
116 ГЛАВА 3 По умолчанию свойство Anchor имеет значение AnchorStyles.Top \AnchorStylesleft (a HeAncborStyles.None, как можно было бы подумать). Поэтому когда форма становится больше, расположенные на ней элементы остаются на прежнем месте относительно левого верхнего угла формы. Если задать свойству Anchor значение Anchor- StylesBottom \AnchorStyles.Right, элемент управления будет сохранять определенную дистанцию от правого нижнего угла. Если задать значение AncborStyles.None, элемент управления остается в форме при изменении ее размеров. Если привязать элемент управления ко всем четырем сторонам, при изменении размера формы он будет оставаться на месте и изменять размер в соответствии с изменением формы. Наверно, при традиционном размещении самое значимое преимущество свойства Anchor — возможность изменять размеры элементов управления при изменении размеров формы, на которой они размещены. Приведем пример. В программе используется высота шрифта для размещения четырех меток и связанных с ними текстовых полей. AnchorFields.cs // // AnchorFields.cs (с) 2005 by Charles Petzold // using System; using System.Drawing; using System.Windows.Forms; class AnchorFields : Form { [STAThread] public static void Main() { Application. EnableVisualStylesO; Application.Run(new AnchorFieldsO); } public AnchorFieldsO { Text = "Anchor Fields"; int iSpace = Font.Height; int у = iSpace; for (int i = 0; i < 4; i++) { Label lbl = new Label(); lbl.Parent = this; lbl.AutoSize = true; lbl.Text = (new string[] { "Name:", "Address:", "Job:",
Панели и динамическое размещение 117 "Very personal information:" })[i]; lbl.Location = new Point(iSpace, y); TextBox txtbox = new TextBoxO; txtbox.Parent = this; txtbox.Location = new Point(lbl.Right + iSpace, y); txtbox.Size = new Size(ClientSize.Width - iSpace - txtbox.Left, txtbox.Height); txtbox.Anchor |= AnchorStyles.Right; у = txtbox.Bottom + iSpace; } В предпоследней строке кода свойству Anchor элемента управления TextBox задается значение AnchorStylesLeft\AncborStyles.Top \AnchorStylesRight. Текстовое поле будет сохранять дистанцию не только, как обычно, от левого и верхнего краев, но и от правого края. Если сделать форму шире, элемент TextBox также станет шире. If Anchor Fields tlHW Name: Address: Job: Г Very personal hrormabon: Нельзя смешивать свойства Dock и Anchor. Если задать свойству Dock элемента управления значение, отличное от DockStyleNone, свойство Anchor моментально примет значение по умолчанию. Точно также, при задании свойству A nchor значения, отличного от значения по умолчанию, свойство Dock принимает значение DockStyle.None. Как будет показано далее в этой главе, при использовании FlowLayoutPanel и TableLayoutPanel свойство Anchor становится почти таким же важным, как и Dock.
11 8 ГЛАВА 3 Разделители Разделитель (splitter) — это тонкая горизонтальная или вертикальная полоска, используемая для изменения относительных размеров двух областей экрана. Разделители используются в Проводнике Windows (Windows Explorer) для отделения дерева просмотра в левой части от списка в правой части. В Visual Studio разделители отделяют окно редактора от окон Solution Explorer и Error List. В Internet Explorer разделители применяются для отделения фреймов Web-страницы. В .NET Framework 1.x разделитель Windows Forms представлялся элементом управления класса Splitter, который размещался между двумя другими элементами управления (обычно панелями, списками или деревьями просмотра) с соблюдением строгого порядка стыковки. Если порядок был неверным, разделитель мог появиться у края окна, а не между элементами управления (см. главу 22 моей книги Programming Microsoft Windows with С*). Splitter остался в .NET Framework 2.0, но в новых программах лучше применять SplitContainer — он намного проще в управлении. SplitContainer создает и отображает на своей поверхности две панели типа SplitterPanel, отделяя их тонким (толщиной в 4 пиксела) горизонтальным или вертикальным разделителем. При перемещении мышью этого разделителя, изменяются относительные размеры двух элементов SplitterPanel Три наиболее важные свойства элемента управления SplitContainer — это Panell и Рапе12 (которые дают доступ к двум элементам управления SplitPanel, которые создает элемент SplitContainer), а также Orientation, которое является членом перечисления Orientation и определяет вид разделителя: вертикальная или горизонтальная полоска. Можно создать собственные элементы управления SplitterPanel, но возможности их применения ограничены. Конструктор SplitterPanel требует аргумента Split- Container, а свойства Panell и Рапе12 элемента SplitContainer доступны только для чтения. Вот пример: PrimevalExplorer.cs // // PrimevalExplorer.cs (с) 2005 by Charles Petzold // using System; using System.Drawing; using System.Windows.Forms; class PrimevalExplorer : Form { [STAThread] public static void Main()
Панели и динамическое размещение 119 Application.EnableVisualStyles(); Application.Run(new PrimevalExplorerO); } public PrimevalExplorerO < Text = "PrimevalExplorer"; SplitContainer split = new SplitContainerO; split.Parent = this; split.Dock = DockStyle.Fill; TreeView tree = new TreeViewO; tree. Parent = split. Panel"!; tree.Dock = DockStyle.Fill; tree.Nodes.Add("tree"); ListView list = new ListView(); list.Parent = split.Panel2; list.Dock = DockStyle.Fill; list.Items.Add("list"); } } Заметьте, что всем свойствам Dock присваивается значение DockStyle.Fill Родителем элемента TreeView является левая панель (Panel 1) элемента SplitContainer, a родителем элемента ListView — правая панель {Panel!). 1 PrimevalExplorer list - Р RC Эта программа могла бы включать меню, панель инструментов и строку состояния. Но нужно добавлять их в родительский элемент после элемента SplitContainer. Когда окно расширяется или сужается, размеры обеих панелей пропорционально увеличиваются или уменьшаются. Это следствие того, что свойство FixedPanel имеет
120 ГЛАВА 3 значение FixedPanelNone. Задайте свойству FixedPanel значение FixedPanelPanell 1, если размеры должны изменяться только у панели Рапе12 (возможно, пользователь ожидает этого исходя из своего предыдущего опыта), или задайте значение Fixed- Panel.Panel2, чтобы менялась только панель Panelll. Можно создать более сложные разделители, добавив дополнительные элементы управления SplitContainer и задать их родителям значения Panell и Рапе12 предыдущего элемента SplitContainer. Свойства Padding и Margin Два новых свойства элементов управления Windows Forms в .NET Framework 2.0 — Padding и Margin. Оба задают отступ: один внутри, а другой вне элемента управления. Я почти запомнил, какой из них за что отвечает, повторив около двухсот раз следующую мантру: «Padding — внутри, Margin — снаружи». Свойство Padding обеспечивает дополнительный отступ внутри элемента управления. Например, вот обычная кнопка, свойство AutoSize которой равно true: AUosized Button) Единственное ее отличие от следующей кнопки состоит в наличии отступа {Padding) в 20 пикселов по краям: Auto«2dd Button 1 При автоматическом изменении размера элемента управления учитывается и свойство Padding. Свойство Padding — это объект типа Padding, который является структурой, определяющий два параметрических конструктора. Общий случай таков: new Padding(iLeft, iTop, iRight, iBottom) Все аргументы определяются в пикселах. Когда нужно добавить дополнительный отступ по сторонам элемента, можно использовать следующее сокращение: new Padding(iAll); В структуре Padding пять доступных для чтения и записи свойств: Left, Top, Bottom и All (которые равны -1, если все четыре стороны не одинаковы). Свойства Horizontal и Vertical доступны только для чтения. Свойство Horizontal является суммой Left и Right, a Vertical суммой Тор и Bottom. Для контейнера, такого как форма или панель, дополнительный отступ добавляется вокруг краев контейнера. Пристыкованные элементы управления сдвинуты от края. Вот программа PrimevalNotepad, показанная выше:
Панели и динамическое размещение 121 I can type in the Text В ox and it v wrap the text for me.| Вот та же программа с 20 пикселами отступа на форме: I Primeval Notepad [-J П) | шшмтяш&^ ill 11 [11 |§1А#д8Йй*^ tree 1 can type in the TextBox and it will wrap the text for me. status ^ rii xj Окно выглядит весьма своеобразно, но дополнительное пространство может быть полезным при размещении, описанном ранее в этой главе. Вспомним программу AutoSizeDemo из начала этой главы: | Look back on time with kindly eye*. He doubtless did his best; How softly sinks his trembling sun in human nature's west! Ей просто жизненно необходимы дополнительные отступы. Однако, если мы добавим 20 пикселов, получится что-то странное:
122 ГЛАВА 3 Появились отступы, и размер формы изменился соответствующим образом. Но все 40 дополнительных пикселов по горизонтали и по вертикали добавились справа и снизу. Это произошло потому, что положение кнопки задано как (0, 0). Даже несмотря на то, что в коде это явно не указано, по умолчанию свойство Location имеет именно такое значение. Свойство Margin, которое также является структурой типа Padding, влияет на отступ между элементами управления. Чтобы увидеть, как оно работает, мы рассмотрим две новых сложных панели, используемых в .NET Framework 2.0 для размещения: FlowLayoutPanel и TableLayoutPanel. И запомните: Padding — внутри, Margin — снаружи. Размещение в панели FlowLayoutPanel Модель реализации панели FlowLayoutPanel напоминает размещение HTML, за тем единственным исключением, что вместо текста и изображений используются элементы управления. (Если нужен текст, используйте элемент управления Label, а если изображение — PictureBox.) Панель FlowLayoutPanel отвечает за последовательное размещение дочерних элементов управления слева направо (по умолчанию), а затем сверху вниз. Направление, в котором FlowLayoutPanel размещает элементы управления, определяется свойством FlowDirection. Оно принимает значения из перечисления FlowDirection и по умолчанию имеет значение FlowDirectionLeftToRigbt. Другие возможные значения — TopDown, RigbtToLeft и BottomUp. В режиме по умолчанию дочерние элементы управления размещаются на панели FlowLayoutPanel слева направо. Если очередной элемент управления не вмещается в доступное пространство, он переносится в следующую строку (или столбец, если свойство FlowDirection имеет значение TopDoivn или BottomUp). Это следствие того, что свойство WrapContents по умолчанию равно true. Если свойство WrapContents равно false, нужно самостоятельно обрабатывать переходы на новую строку (или в новый столбец), и даже если используется значение WrapContens по умолчанию, иногда требуется начать новую строку (или столбец) определенным элементом управления. Если floiv — объект типа FlowLayoutPanel, a Ctrl — его дочерний объект, перенос этого элемента на новую строку или столбец выполняется так: flow.SetFlowBreak(ctrl, true);
Панели и динамическое размещение 123 Метод GetFlowBreak возвращает логическое значение с аргументом элемента управления. Стыковка и привязка в панели FlowLayoutPanel Может показаться, что свойства Dock и Anchor не играют особой роли в управлении элементами управления в FlowLayoutPanel, но это не так. Свойства Dock и Anchor дочерних элементов управления на панели служат для их вертикального выравнивания (для горизонтального размещения) или горизонтального выравнивания (для вертикального размещения). Если направление размещения горизонтальное, высота любой горизонтальной строки элементов управления определяется высотой самого высокого элемента этой строки. Все элементы управления выравниваются по верхнему краю ряда. Для любого элемента управления доступны четыре варианта выравнивания: ■ по верхнему краю самого высокого элемента (по умолчанию); ■ по нижнему краю самого высокого элемента; ■ по центру самого высокого элемента; ■ растяжение по вертикали до достижения одинаковой высоты с самым высоким элементом. Выравниванием элемента управления по вертикали можно управлять через свойство Anchor. ■ AnchorStyles.Top — выравнивание по верхнему краю; ■ AnchorStylesBottom — выравнивание по нижнему краю; ■ AnchorStyles.Top \ AnchorStylesBottom — выравнивание по центру; ■ AnchorStylesNone — растяжение по вертикали до достижения той же высоты. Биты AnchorStylesLeft и AnchorStylesRighi игнорируются. Некоторых из этих эффектов можно достичь при помощи свойства Dock; в этом случае любые значения свойства Anchor, отличные от значения по умолчанию, игнорируются, точнее переопределяются: ■ DockStyle.Top — выравнивание по верхнему краю; ■ DockStyleBottom — выравнивание по нижнему краю; ■ DockStyleFill или DockStyleleft или DockStyleRight — растяжение по вертикали. Поскольку Anchor предоставляет больше возможностей, для выравнивания по вертикали я рекомендую использовать именно его. Когда направление размещения вертикальное (сверху вниз или снизу вверх), ширина каждого столбца элемента управления определяется самым широким элементом. Элементы управления обычно выравниваются по левому краю. Свойство Anchor позволяет изменить выравнивание относительно самого широкого элемента управления в столбце:
124 ГЛАВА 3 ■ AnchorStylesLeft — выравнивание по левому краю (по умолчанию); ■ AnchorStylesRight — выравнивание по правому краю; ■ AnchorStylesLeft \ AncborStylesRight — выравнивание по центру; ■ AnchorStylesNone — растяжение по горизонтали до достижения той же ширины. Биты AnchorStyles.Top и AnchorStyles.Bottom игнорируются. Если хотите поэкспериментировать с использованием свойств Anchor и Dock для управления выравниванием в элементе управления FlowLayoutPanel, я предлагаю следующую небольшую программу: FlowPanelAlignment.cs // // FlowPanelAlignment.cs (с) 2005 by Charles Petzold // using System; using System.Drawing; using System.Windows.Forms; class FlowPanelAlignment : Form { [STAThread] public static void Main() { Application. EnableVisualStylesO; Application.Run(new FlowPanelAlignmentO); > public FlowPanelAlignmentO { Text = "Flow Panel Alignment"; FlowLayoutPanel flow = new FlowLayoutPanelO; flow.Parent = this; flow.Dock = DockStyle.Fill; flow.Text = "Flow Panel"; flow.Click += ClickHandler; Random rand = new Random(DateTime.Now.Millisecond); for (int i = 0; i < 20; i++) { Button btn = new Button(); btn.Parent = flow; btn.Text = "Button " + (i + 1); btn.Click += ClickHandler;
Панели и динамическое размещение 125 } // Задаем случайный размер (но не выходя за рамки разумного). Size sz = btn.PreferredSize; sz.Width = (int)(sz.Width * (1 + 2 * rand.NextDouble())); sz.Height = (int)(sz.Height * (1 + 2 * rand.NextDouble())); btn.Size = sz; } } void ClickHandler(object objSrc, EventArgs args) { Control Ctrl = (Control)objSrc; Form frm = new Form(); frm.Text = Ctrl.Text; frm.Owner = this; PropertyGrid prop = new PropertyGridO; prop.SelectedObject = objSrc; prop.Parent = frm; prop.Dock = DockStyle.Fill; frm.Show(); В программе создается элемент управления FlowLayoutPanel, который заполняет клиентскую область формы. Затем в нем создается 20 кнопок-потомков, их ширина и высота могут случайно изменяться в диапазоне от одной до трех стандартных единиц размера. Вот пример окна программы: I Flow Panel Alignment Button 1 Button 2 |[ Button 3 | t Button 4 | 1 """" ' ■ 1 Button 5 Button 7 | Button 6 j | ( Button 8 J II Button 9 1 Button 10 Вот что замечательно: если щелкнуть кнопку или форму, появится диалоговое окно, содержащее элемент управления PropertyGrid с таблицей свойств кнопок или формы. Можно изменить свойства Dock или Anchor и проверить эффект. Можно
126 ГЛАВА 3 увидеть (при горизонтальном направлении размещения), что кнопки нельзя выровнять по горизонтали: они не перемещаются ни вправо, ни влево, а их центры не выстраиваются на одной линии. Но можно как угодно выравнивать кнопки по вертикали (вверх или вниз). При экспериментировании со свойствами элементов управления, попробуйте изменить значение свойства Margin одной из кнопок. По умолчанию это свойство равно трем пикселам с каждой стороны. Если увеличить значения, можно увидеть, что увеличилось расстояние между расположенными рядом кнопками, а также между кнопками и краями клиентской области. Можно также изменить значение свойства Padding формы, чтобы увеличить расстояние между краями клиентской области и массивом кнопок. Если изменить значение свойства AutoSize элемента FlowLayoutPanel на true, разбиения на строки и столбцы не будет, за исключением того, что явно определено свойством SetFlowBreak. Задействуем FlowLayoutPanel для создания простого диалогового окна About, используя в качестве модели диалоговое окно About из главы 16 книги Programming Microsoft Windows with С*. В том окне требовалось вычислить и явно задать много координат. Здесь же единственные явно заданные числа — это размеры шрифта и смещения символов. FlowPanel AboutBoxl .cs // // FlowPanelAboutBox1.cs (с) 2005 by Charles Petzold // using System; using System.Diagnostics; using System.Drawing; using System.Windows.Forms; class FlowPanelAboutBoxl : Form { [STAThread] public static void Main() { Application.EnableVisualStyles(); Application.Run(new FlowPanelAboutBoxl()); } public FlowPanelAboutBoxl() { Text = "Flow Panel About Box #1"; AutoSize = true; AutoSizeMode = AutoSizeMode.GrowAndShrink; FormBorderStyle = FormBorderStyle.FixedDialog;
Панели и динамическое размещение 127 FlowLayoutPanel flow = new FlowLayoutPanelQ; flow.Parent = this; flow.AutoSize = true; flow.FlowDirection = FlowDirection.TopDown; Label lbl = new Label(); lbl.Parent = flow; lbl.AutoSize = true; lbl.Anchor = AnchorStyles.None; lbl.Margin = new Padding(Font.Height); lbl.Text = "AboutBox Version 1.0"; lbl.Font = new Font(FontFamily.GenericSerif, 24, FontStyle.Italic); LinkLabel Ink = new LinkLabelO; Ink.Parent = flow; Ink.AutoSize = true; Ink.Anchor = AnchorStyles.None; Ink.Margin = new Padding(Font.Height); Ink.Text = "\x00A9 2005 by Charles Petzold"; Ink.Font = new Font(FontFamily.GenericSerif, 16); Ink.LinkArea = new LinkArea(10, 15); Ink.LinkClicked += delegate { Process.Start("http://www.charlespetzold.com"); }; Button btn = new ButtonO; btn.Parent = flow; btn.AutoSize = true; btn.Anchor = AnchorStyles.None; btn.Margin = new Padding(Font.Height); btn.Text = "OK"; > > Заметьте, что свойство AutoStze формы имеет значение true. Форма будет изменять размер в соответствии с изменением размера дочернего элемента, Flowlayout- Panel, свойство AutoSize которого тоже имеет значение true, что позволяет ему изменять размер так, чтобы он вмещал три дочерние элемента. Свойство FlowDirection имеет значение TopDown, поэтому три элемента управления будут выравниваться по вертикали. Свойство AutoStze всех трех дочерних элементов имеет значение true, a свойство Anchor значение AncborStyles.None, чтобы элементы управления выравнивались по центру по горизонтали. Свойство Margin всех трех элементов управления равно высоте шрифта по умолчанию, так что плотность не слишком высокая:
128 ГЛАВА 3 AboutBox Version 1.0 © 2005 by Charles Petzold Я не стал определять отдельный метод для обработчика события LinkClicked элемента управления LinkLabel, а использовал анонимный метод (это новая возможность Visual C# 2005) с ключевым словом delegate. Анонимные методы часто удобно использовать для обработчиков событий, если обработчик содержит только одну- две строки кода. Если кода больше, их вид, по-моему, будет сбивать с толку. Конечно, в большинстве традиционных диалогов About помимо прочего есть значки. Разместить значок ниже или выше первой строки текста несложно. Но что если значок нужно разместить слева от первой строки текста? Один из простых способов — использовать две панели FlowLayoutPanel Вместо первой строки текста будет панель с горизонтальной ориентацией, на которой будет располагаться значок и текстовая строка. В Visual Studio я создал значок как часть проекта FlowPanelAboutBox2 и отметил его как внедренный ресурс. FlowPanelAboutBox2.cs // // FlowPanelAboutBox2.cs (с) 2005 by Charles Petzold // using System; using System.Diagnostics; using System.Drawing; using System.Windows.Forms; class FlowPanelAboutBox2 : Form { [STAThread] public static void Main() { Application.EnableVisualStyles(); Application.Run(new FlowPanelAboutBox2()); > public FlowPanelAboutBox2() { Text = "Flow Panel About Box #2"; AutoSize = true;
Панели и динамическое размещение 129 AutoSizeMode = AutoSizeMode.GrowAndShrink; FormBorderStyle = FormBorderStyle.FixedDialog; Icon = new Icon(GetType(), "FlowPanelAboutBox2.AforAbout.ico"); FlowLayoutPanel flow = new FlowLayoutPaneK); flow.Parent = this; flow.AutoSize = true; flow.FlowDirection = FlowDirection.TopDown; FlowLayoutPanel flow2 = new FlowLayoutPaneK); flow2.Parent = flow; flow2.AutoSize = true; flow2.Margin = new Pad8ing(Font.Height); PictureBox picbox = new PictureBoxQ; picbox.Parent = flow2; picbox. Image = Icon.ToBitmapO; picbox.SizeMode = PictureBoxSizeMode.AutoSize; picbox.Anchor = AnchorStyles.None; Label lbl = new Label(); lbl.Parent = flow2; lbl.AutoSize = true; lbl.Anchor = AnchorStyles.None; lbl.Text = "AboutBox Version 2.0"; Ibl.Font = new FontCFontFamily.GenericSerif, 24, FontStyle.Italic); LinkLabel Ink = new LinkLabeK); Ink.Parent = flow; Ink.AutoSize = true; Ink.Anchor = AnchorStyles.None; Ink.Margin = new Padding(Font.Height); Ink.Text = "\x00A9 2005 by Charles Petzold"; Ink.Font = new FontCFontFamily.GenericSerif, 16); Ink.LinkArea = new LinkArea(10, 15); Ink.LinkClicked += delegate { Process.Startf'http://www.charlespetzold.com"); }; Button btn = new ButtonO; btn.Parent = flow; btn.AutoSize = true; btn.Anchor = AnchorStyles.None; btn.Margin = new Padding(Font.Height); btn.Text = "OK"; > }
130 ГЛАВА 3 Теперь значок размещается, как требовалось: ^ш^^^^^^^^м т:В ,ШШ1ШШ1ШШ Прощай, GroupBox GroupBox считается прежде всего основным элементом управления для объединения в группу взаимоисключающих переключателей. Однако существующий элемент GroupBox требует явно задавать позиции переключателей. А ведь было бы замечательно, если бы дочерние элементы в элементе управления GroupBox размещались так же, как на панели FlowLayoutPanel\ С GroupBox надо срочно что-то делать, и сейчас мы этим займемся. Мой Group- Panel является наследником FlowLayoutPanel и призван наилучшим образом имитировать внешний вид GroupBox. GroupPanel.cs // // GroupPanel.cs (с) 2005 by Charles Petzold // using System; using System.Drawing; using System.Windows.Forms; class GroupPanel : FlowLayoutPanel { int xDpi, yDpi; public GroupPanel() { FlowDirection = FlowDirection.TopDown; WrapContents = false; AutoSize = true; AutoSizeMode = AutoSizeMode.GrowAndShrink; Graphics grfx = CreateGraphicsO; xDpi = (int)grfx.DpiX;
Панели и динамическое размещение 131 yDpi = (int)grfx.DpiY; grfx.Dispose(); Padding = new Padding(xDpi / 10, yDpi / 10 + Font.Height, xDpi / 10, yDpi / 10); } public string Check { set { RadioButton radio = Controls[value] as RadioButton; if (radio != null) radio.Checked = true; } get { foreach (Control Ctrl in Controls) { RadioButton radio = Ctrl as RadioButton; if (radio != null && radio.Checked) return radio.Name; } return ""; } } protected override void OnFontChanged(EventArgs args) { base.OnFontChanged(args); Padding = new Padding(Padding.Left, yDpi / 10 + Font.Height, Padding.Right, Padding.Bottom); } protected override void OnPaint(PaintEventArgs args) { Graphics grfx = args.Graphics; int ylndent = yDpi / 25 + Font.Height / 2; int xlndentl = xDpi / 10, xlndent2; if (Text != null && Text.Length > 0) { grfx.DrawString(" " + Text + " ", Font, new SolidBrush(ForeColor), xlndentl, yDpi / 25); xlndent2 = xlndentl + (int) (grfx.MeasureStringC " + Text + " ", } Font).Width);
132 ГЛАВА 3 else { xlndent2 = xlndentl; } Pen pnLight = new Pen(ControlPaint.Light(BackColor)); Pen pnDark = new Pen(ControlPaint.Dark(BackColor)); grfx.DrawLine(pnDark, xlndentl, ylndent, 0, ylndent); grfx.DrawLine(pnDark. 0, ylndent, 0, Height - 2); grfx.DrawLine(pnDark, 0, Height - 2, Width - 2, Height - 2); grfx.DrawLine(pnDark, Width - 2, Height - 2, Width - 2, ylndent); grfx.DrawLine(pnDark, Width - 2, ylndent, xlndent2, ylndent); grfx.Drawl_ine(pnLight, xlndentl, ylndent + 1, 1, ylndent + 1); grfx.DrawLine(pnLight, 1, ylndent + 1, 1, Height - 3); grfx.DrawLine(pnLight, 0, Height - 1, Width - 1, Height - 1); grfx.DrawLine(pnLight, Width - 1, Height - 1, Width - 1, ylndent); grfx.DrawLine(pnLight, Width - 3, ylndent + 1, xlndent2, ylndent + 1); } } Конструктор получает горизонтальное и вертикальное разрешение (в точках на дюйм) и использует его, как и высоту шрифта, для определения значения свойства Padding. Поскольку в верхней части этого элемента управления отображается текст, для него нужно предусмотреть дополнительный отступ в верхней части. Значение свойства Padding должно меняться при изменении шрифта. Поскольку этот элемент управления предназначен главным образом для размещения переключателей, он содержит свойство, позволяющее задавать и получать текущий выбранный переключатель по его свойству Name. Остальная часть кода содержит логику рисования элемента управления. Вот пример диалогового окна с двумя элементами управления GroupPanel. ColorFillDialog.cs // // ColorFillDialog.cs (с) 2005 by Charles Petzold // using System; using System.Drawing; using System.Reflection; using System.Windows.Forms; class ColorFillDialog : Form {
Панели и динамическое размещение 133 protected GroupPanel grppnll, grppnl2; protected CheckBox chkbox; public ColorFillDialogO { Text = "Color/Fill Select"; FormBorderStyle = FormBorderStyle.FixedDialog; ControlBox = MinimizeBox = MaximizeBox = ShowInTaskbar = false; AutoSize = true; AutoSizeMode = AutoSizeMode.GrowAndShrink; FlowLayoutPanel flow = new FlowLayoutPanelO; flow.Parent = this; flow.AutoSize = true; flow.FlowDirection = FlowDirection.TopDown; FlowLayoutPanel flow2 = new FlowLayoutPanelO; flow2.Parent = flow; flow2.AutoSize = true; flow2.Anchor = AnchorStyles.None; grppnll = new GroupPanelO; grppnll.Parent = flow2; grppnll.AutoSize = true; grppnll.Text = "Color"; grppnl2 = new GroupPanelO; grppnl2.Parent = flow2; grppnl2.AutoSize = true; grppnl2.Text = "Background"; grppnll.SuspendLayout(); grppnl2.SuspendLayout(); // Получение сведений о свойствах класса Systemlnformation. Type type = typeof(Color); PropertyInfo[] apropinfo = type.GetPropertiesO; // Обработка в цикле сведений о свойствах, foreach (Propertylnfo pi in apropinfo) { if (pi.CanRead && pi.GetGetMethod().IsStatic) { // Получение имен и значений параметров, if (pi.Name[0] == 'S' || pi.Name[0] == 'P') {
134 ГЛАВА 3 RadioButton radio = new RadioButton(); radio.Parent = pi.Name[0] == ' S* ? grppnll : grppnl2; radio.AutoSize = true; radio.Text = radio.Name = pi.Name; } } > g rppnll.ResumeLayout(); g rppnl2.ResumeLayout(); chkbox = new CheckBoxO; chkbox.Parent = flow; chkbox.AutoSize = true; chkbox.Text = "Fill Ellipse"; chkbox.Anchor = AnchorStyles.None; FlowLayoutPanel flow3 = new FlowLayoutPanelQ; flow3.Parent = flow; flow3.AutoSize = true; flow3.Anchor = AnchorStyles.None; Button btn = new ButtonO; btn.Parent = flow3; btn.AutoSize = true; btn.Text = "OK"; btn.DialogResult = DialogResult.OK; AcceptButton = btn; btn = new ButtonO; btn.Parent = flow3; btn.AutoSize = true; btn.Text = "Cancel"; btn.DialogResult = DialogResult.Cancel; CancelButton = btn; > public Color Color { set { grppnll.Check = value.Name; } get { return Color.FromName(grppnll.Check); } } public Color Background { set { grppnl2.Check = value.Name; } get { return Color.FromName(grppnl2.Check); } } public bool Fill
Панели и динамическое размещение 135 { set { chkbox.Checked = value; } get { return chkbox.Checked; } } } В этом диалоговом окне три панели FlowLayoutPanel и две панели с группами элементов управления. При помощи отражения (reflection) конструктор получает члены класса Color, но игнорирует все цвета, которые не начинаются с букв S и Р. Эти цвета становятся группами переключателей на двух панелях. Эти два файла, как и следующий файл ColorFill.cs входят в проект ColorFill, который аналогичен программе из главы 16 книги Programming Microsoft Windows with С*. ColorFill.es // // ColorFill.es (с) 2005 by Charles Petzold // using System; using System.Drawing; using System.Windows.Forms; class ColorFill : Form { Color clrEllipse = Color.Salmon; bool bFillEllipse = false; [STAThread] public static void MainQ { Application.EnableVisualStyles(); Application. Run(new ColorFilK)); } public ColorFillO { Text = "olor Fill"; ResizeRedraw = true; BackColor = Color.PowderBlue; Menu = new MainMenuO; Menu.Menultems.Add("&0ptions"); Menu.MenuItems[0].MenuItems.Add("&Color...", MenuColorOnClick); } void MenuColorOnClick(object objSrc, EventArgs args) { ColorFillDialog dig = new ColorFillDialogO;
136 ГЛАВА 3 dig.Color = clrEllipse; dig.Fill = bFillEllipse; dig.Background = BackColor; if (dlg.ShowDialogO == DialogResult.OK) { clrEllipse = dig.Color; bFillEllipse = dig.Fill; BackColor ■ dig.Background; InvalidateO; } } protected override void OnPaint(PaintEventArgs args) { Graphics grfx = args.Graphics; Rectangle rect = new Rectangle(0, 0, ClientSize.Width - 1, ClientSize.Height - 1); if (bFillEllipse) grfx.FillEllipse(new SolidBrush(clrEllipse), rect); else grfx.DrawEllipse(new Pen(clrEllipse), rect); Вот диалоговое окно программы: * Сок» ■•'• л 0 Salmon О SandyBrown О SeaGreen QSeaShel О Sienna OS»vef QSkyBlue OSIateBlue О SlatoGray О Snow О SpringGreen OStedBlue Background О PateGoldenrod О PateGreen О PaleTurcruowe О PateVWetRed О PapayaWhip О PeachPutf Op«u OPWc I OPIum 0 PowderBkie | OPurple DriE«P«e j OK 1 [ Cancel
Панели и динамическое размещение 137 Другая программа из книги Programming Microsoft Windows with C#, остро нуждающаяся в переводе на рельсы FlowLayoutPanel, — программа ImageDirectory из главы 22. Класс ImagePanel служит для отображения и размещения нескольких элементов управления PictureBox. FlowLayoutPanel как нельзя лучше подходит для решения этой задачи, и в следующей главе я покажу, как это сделать. Однако FlowLayoutPanel не подходит для форм, содержащих элементы управления различных типов и размеров, которые нужно выстраивать как по горизонтали, так и по вертикали. Для этого лучше подойдет элемент управления Table- LayoutPanel. Панели TableLayoutPanel Панель TableLayoutPanel размещает дочерние элементы в сетке строк и столбцов. Обычно каждый дочерний элемент управления занимает одну ячейку сетки, но ничто не запрещает размещать отдельные элементы на нескольких смежных клетках. Конечно, другие панели, содержащие другие элементы TableLayoutPanel, также могут занимать эти ячейки. Однако поскольку поддержка и управление таблицами довольно ресурсоемкое занятие, лучше не увлекаться вложением таблиц. Во многих случаях элемент FlowLayoutPanel справляется с подобной задачей не хуже, чем дочерние элементы TableLayoutPanel Есть два основных способа использования TableLayoutPanel В большинстве случаев значением true инициируется свойство AutoSize элемента TableLayoutPanel и контейнера панели. Таким образом, размеры таблицы и ее контейнера подгоняются для размещения элементов управления. В качестве альтернативы можно предоставить пользователям возможность изменять размер контейнера таблицы либо в виде перемещаемой границы, либо с использованием элемента SplitContainer. В этом случае свойству Dock элемента TableLayoutPanel присваивается значение DockStyle.Fill Документация предупреждает, что это не самый эффективный способ использования TableLayoutPanel, но это идеальный вариант для выполнения некоторых нестандартных задач. Автоматическое расширение таблицы Допустим в конструкторе некоторого контейнера — Form или Panel — мы решили создать элемент TableLayoutPanel примерно так: TableLayoutPanel table = new TableLayoutPanelQ; table.Parent « this; table.AutoSize = true; Таблица обычно размещается в левом верхнем углу родительского элемента, но в качестве родителя может выступать FlowLayoutPanel или другой элемент Table-
138 ГЛАВА 3 LayoutPanel Если свойству A utoSize не присвоить значение true, размер таблицы не будет автоматически увеличиваться для нормального отображения всех ячеек и придется явно задавать размер в пикселах. Экспериментируя с TableLayoutPanel, лучше всего сделать границы (обрамление) ячеек видимыми: table.CellBorderStyle = TableLayoutPanelCellBorderStyle.Single; Доступны и другие стили обрамления (Inset, InsetDouble, Outset, OutsetDouble и OutsetPartial), но для большинства задач размещения обрамление вообще не понадобится. Следующий шаг — создание элемента управления (например, Button), который нужно разместить в таблице. Button btn = new Button(); btn.Text = "Button 1"; btn.AutoSize = true; Как обычно, свойство AutoSize позволяет кнопке динамически менять размер в соответствии с размером текста. Сделать кнопку потомком таблицы можно двумя способами: btn.Parent = table; или table.Controls.Add(btn); Если создать и добавить в таблицу еще несколько кнопок с текстом, получится примерно такая таблица с одним столбцом: При добавлении элементов в таблице появляются дополнительные ячейки. По умолчанию ячейки располагаются в один столбец. Такое поведение определяется тремя свойствами элемента TableLayoutPanel. RowCount, ColumnCount и GrowStyle. По умолчанию значения свойств RowCount и ColumnCount равны 0, a GrowStyle принимает значение из перечисления TableLayoutPanelGrowStyleAddRows. Свойства
Панели и динамическое размещение 139 RoivCount и ColumnCount HE говорят о том, сколько строк и столбцов имеет таблица. Элемент TableLayoutPanel HE изменяет начальные значения этих свойств или значения, назначенные программой. Так как свойство GrowStyle по умолчанию имеет значение TableLayoutPanelGrow- StyleAddRows, свойство RoivCount игнорируется. Свойству ColumnCount можно задать значение, определяющее нужное число столбцов. Значение 0 имеет тот же эффект, что и значение 1. Если свойству ColumnCount задать значение 2, таблица расширится следующим образом: Со значением 3 таблица будет иметь три столбца. Щ% И так далее. Значение свойства ColumnCount можно изменять в любое время при добавлении новых или реорганизации уже имеющихся элементов управления. Если свойство GrowStyle имеет значение TableLayoutPanelGroivStyleAddColumns, свойство ColumnCount игнорируется. Свойству RoivCount можно присвоить значение, соответствующее нужному числу строк. Если значение свойства RoivCount равно О, элементы управления выстраиваются так же, как и при значении по умолчанию: Мне это кажется немного нелогичным — я считаю, что ячейки должны размещаться так же, как и при свойстве RoivCount равном 1, то есть горизонтально:
140 ГЛАВА 3 При больших значениях RowCount размещение элементов управления изменяется при добавлении дополнительных элементов. Например, при значении 2 первые два элемента управления размещаются примерно так: Но при добавлении третьего элемента второй перемещается в новый столбец: Четвертый элемент добавляется туда, куда и ожидается-. Но пятый элемент управления снова вызывает реорганизацию размещения: Так значение свойства RowCount, равное 2, ограничивает число строк до двух. Следующая программа — простая имитация пользовательского интерфейса диалогового окна для определения параметров элементов матричного преобразования, используемого в графике Windows Forms. Функциональная версия этого диалогового окна (и программа, где оно используется) описана в главе 18 книги Programming Microsoft Windows with С*. В той версии есть примерно следующий код: label.Location = new Point(8, 8 + 16 * i); label.Size = new Size(64, 8) Эта новая версия позволяет элементу TableLayoutPanel со свойством AutoSize выполнять размещение и определение размеров. Свойству GrowStyle оставлено его значение по умолчанию (AddRows), а свойство ColumnCount равно 2. MatrixElements.cs // // MatrixElements.cs (с) 2005 by Charles Petzold
Панели и динамическое размещение 141 // using System; using System.Drawing; using System.Windows.Forms; class MatrixElements : Form { [STAThread] public static void Main() { Application.EnableVisualStyles(); Application.Run(new MatrixElementsO); } public MatrixElementsO { Text = "Matrix Elements"; FormBorderStyle = FormBorderStyle.FixedDialog; AutoSize = true; AutoSizeMode = AutoSizeMode.GrowAndShrink; TableLayoutPanel table = new TableLayoutPanelO; table.Parent = this; table.Padding = new Padding(Font.Height); table.AutoSize = true; table.ColumnCount = 2; table. SuspendLayoutO; for (int i = 0; i < 6; i++) { Label lbl = new Label(); lbl.Parent = table; lbl.AutoSize = true; lbl.Text = new string[] { "X Scale:", "Y Shear:", "X Shear:", "Y Scale:", "X Translate", "Y Translate:" }[i]; NumericUpDown updn = new NumericUpDownO; updn.Parent = table; updn.AutoSize = true; updn.DecimalPlaces = 2; } Button btn = new ButtonO; btn.Parent = table;
142 ГЛАВА 3 btn.AutoSize = true; btn.Text = "Update"; btn = new Button(); btn.Parent = table; btn.AutoSize = true; btn.Text = "Methods.. table. ResumeLayoutO; } } Обратите внимание на использование SuspendLayout и ResumeLayout. Их отсутствие приводит к значительным переменам в инициализации формы. Единственный случай явного задания размера — это значение FontHeight, используемое при определении свойства Padding элемента TableLayoutPanel для задания расстояния между элементами формы. Matrix Elements X Scale: Y Shear: XShean Y Scale; X Translate Y Translate: [ Update poo Ш I &J3 ^m feoo m ffJbo >.$ jblo~~ll ] | Methods... J | Окно не похоже на идеальную форму «ручной работы», за которую не приходится краснеть, тем не менее, качество очень даже приличное. Это диалоговое окно демонстрирует тот тип размещения, который как нельзя лучше подходит для организации средствами элемента TableLayoutPanel. Элемент FlowLayoutPanel не будет работать нужным образом. Если свойству FloivDirection задать значение LeftToRigbt и переходить на новую строку после каждого элемента- счетчика, счетчики не выстроятся в ровный столбец, поскольку каждый будет размещаться, ориентируясь на ширину предшествующей метки. Если свойству Flow- Direction задать значение TopDoivn и перейти на новую строку после первой кнопки, метки не будут визуально соответствовать счетчикам, так как у них разная высота по умолчанию. До сих пор мы обсуждали два параметра свойства GrowStyle —AddRows nAddCo- lumns. Третий и последний параметр — TableLayoutPanelGrowStyleFixedSize. Здесь число строк и столбцов фиксируется свойствами RowCount и ColumnCount. (Перед
Панели и динамическое размещение 143 заданием свойству GroivStyle значения FixedSize нужно присвоить этим двум свойствам ненулевые значения.) Если свойство GrowStyle имеет значение FixedSize, ячейки заполняются слева направо, а затем сверху вниз, как и при значении TableLayoutPanelGrowStyleAddRows. Ситуация коренным образом меняется, когда число элементов управления превышает фиксированное число ячеек (значение RowCount умноженное на значение ColumnCount), — в элементе TableLayoutPanel возникнет исключение. Ранее я показал, как сделать элемент управления потомком элемента TableLayoutPanel, задав значение свойству Controls элемента управления или вызвав метод Add свойства Controls элемента TableLayoutPanel. Свойство Controls является объектом типа TableLayoutControlCoollection, и определен дополнительный метод Add: table.Controls.Add(ctrl, col, row); Аргументы col и row — координаты столбца и строки, отсчитываемые от нуля и указывающие, где в таблице нужно разместить элемент управления. Эту версию метода Add можно использовать с любым GrowStyle. Если при вызове метода Add в этой ячейке уже есть другой элемент управления, он будет перемещен в соседнюю ячейку, а остальные ячейки перегруппируются в соответствии со значениями свойств GrowStyle, RowCount и ColumnCount. Позиции ячеек При определенных обстоятельствах — например, при добавлении элементов управления, свойство GrowStyle которых имеет значение AddColumns, или при помещении элемента управления в ячейку, где уже имеется другой элемент — TableLayoutPanel перегруппирует элементы управления в ячейках таблицы. Механизм динамического размещения элементов управления основан на шести методах TableLayoutPanel, отвечающих за размещение ячеек. В методах GetCellPosition и SetCellPosition используется структура TableLayout- PanelCellPosition, которая имеет два свойства — Column и Row. Можно использовать эти два метода по отношению к любому элементу управления, потомку TableLayoutPanel В следующем примере Istbox — элемент управления класса ListBox, a table — класса TableLayoutPanel. Допустим, элемент Istbox добавляется в набор элементов управления таблицы методом Add с координатами столбца и строки: table.Controls.Add(lstbox, col, row); Затем программа вызывает метод GetCellPosition-. TableLayoutPanelCellPosition cellpos = table.GetCellPosition(lstbox); Как и следовало ожидать, свойства Column и Row объекта cellpos будут указывать те же значения столбца и строки. Если объект Istbox добавлен в набор элементов управления таблицы простым методом Add
144 ГЛАВА 3 table.Controls.Add(lstbox); или заданием значения свойству Parent-. lstbox.Parent = table; то вызвав метод GetCellPosition, можно выяснить, что значения cellpos.Column и cellposRow равны -1, иначе говоря, у элемента управления нет явно заданных координат ячеек. Если нужно, таблица может перемещать элементы управления в другие ячейки, то есть элемент управления может «плавать». Методы GetColumn и GetRow полностью соответствуют GetCellPosisition, но они возвращают целочисленные значения. Поэтому, скорее всего, проще в использовании. Предположим, в ячейку последовательно добавляются два элемента управления: table.Controls.Add(lstbox, 3, 2); table.Controls.Add(btn, 3, 2); Второй метод Add «выталкивает» элемент LixtBox в другую ячейку. Но при передаче этих элементов методу GetCellPosition (или GetColumn, или GetRoiv) обнаруживается, что они оба находятся в столбце 3 и строке 2, поскольку программа задала именно такие значения. Эти два элемента имеют больший приоритет при размещении в этой ячейке, чем элементы со значениями -1, но все же нет никаких гарантий, что они будут размещены именно в этой конкретной ячейке. Область действия методов SetCellPosition, SetColumn и SetRow элемента управления TableLayoutPanel ограничена только имеющимися потомками таблицы. Можно использовать эти методы для перемещения элементов в другие ячейки. Если значения строки и столбца равны -1, элемент управления перемещается в первую пустую ячейку. Если нужно узнать, в какой ячейке действительно находится конкретный элемент управления, используйте метод GetPositionFromControl, который также возвращает объект типа TableLayoutCellPositiom TableLayoutPanelCellPosition cellpos = table.GetPositionFromControl(lstbox); Теперь значения cellpos.Column и cellpos.Row показывают ячейку, в которой находится элемент управления. Вот еще один метод элемента управления TableLayoutPanel. Control Ctrl = table.GetControlFromPosition(col, row); Если в ячейке нет элемента управления, метод вернет null
Панели и динамическое размещение 145 Стили строк и столбцов По умолчанию ширина ячейки равна ширине элемента управления, расположенного в ней. Но поскольку у всех ячеек в определенном столбце одна ширина, в действительности ширина ячейки равна ширине самого широкого элемента в этом столбце. Аналогично, высота ячейки равна высоте самого высокого элемента в строке. У таблицы есть также другие параметры, известные как стили строк и столбцов. Свойства ColumnStyles и RowStyles элемента управления TableLayoutPanel являются наборами типов TableLayoutColumnStyleCollection и TableLayoutRowStylesCollection. В эти наборы входят объекты ColumnStyle и RowStyle соответственно. Классы ColumnStyle и RowStyle производные от класса TableLayoutStyle. У последнего есть свойство SizeType. В класс ColumnStyle добавлено свойство Width, а в класс RowStyle — Height. Свойство SizeType принимает значения из перечисления SizeType с тремя членами: AutoSize (по умолчанию), Absolute и Percent. Я знаю, что все это довольно запутанно. Но в действительности дело обстоит довольно просто. Помните, что не следует смешивать RowStyle и ColumnStyle, если размер строк и столбцов должен изменяться автоматически. В противном случае в коде будет несколько выражений, выглядящих примерно так: table.RowStyles.Add(new RowStyle(SizeType.Absolute, 75)); Первый из этих операторов применяется к первой строке, второе — ко второй и т. д. Не нужно задавать значение RowStyle для всех строк таблицы — достаточно задать вплоть до последней строки, размер которой не нуждается в автоматическом изменении. Значение SizeTypeAutoSize указывает на то, что ширина столбца (или высота строки) должна определяться по самому большому элементу столбца (или строки). При этом неважно, равно или нет true свойство AutoSize элементов управления в этом столбце (или строке). Свойство Width свойства ColumnStyle (или свойство Height свойства RowStyle) игнорируется. Если задано значение SizeType Absolute, ширина столбца (или высота строки) указывается в пикселах и определяется значением свойства Width (или Height). Когда элемент TableLayoutPanel задает ширину и высоту столбцов и строк, в первую очередь обрабатываются столбцы и строки со стилями SizeType Absolute и SizeTypeAutoSize. Оставшееся пространство распределяется между столбцами и строками со стилем SizeType.Percent. Числовые значения Width и Heigth, связанные со стилем SizeType.Percent не имеют значения — важны только их относительные пропорции. Я сказал, что элемент TableLayoutPanel «распределяет оставшееся пространство» среди ячеек со стилем SizeType.Percent. Это предполагает, что стиль SizeType.Percent
146 ГЛАВА 3 не работает в таблицах, свойство AutoSize которых равно true — он годится для таблиц с явно определенными размерами или со свойством Dock, имеющим значение DockStyle.Fill. После определения размеров всех столбцов и строк в элементе TableLayoutPanel, в программе можно получить размеры в пикселах в виде массивов целых чисел: int[] aiWidths = table.GetColumnWidthsQ; int[] aiHeights = table.GetRowHeightsO; Когда нужно узнать номера столбцов и строк таблицы, наверно, проще всего воспользоваться следующими методами: iNumCols = table.GetColumnWidths().Length; iNumRows = table.GetRowHeightsO. Length; Свойства Dock и Anchor Для выравнивания элемента управления в ячейке можно воспользоваться свойствами Dock к Anchor элемента управления. Dock можно игнорировать, поскольку все, что это свойство позволяет делать, можно выполнить при помощи Anchor, и даже больше. Свойству Anchor можно задать значение одного или нескольких членов перечисления AnchorStyles, разделенных битовым оператором OR (|). Как я уже говорил, в AnchorStyles входят следующие члены: Left, Top, Right, Bottom и None. Вот все, что нужно запомнить: включение любого члена перечисления AnchorStyles в Anchor пристыковывает элемент управления к соответствующему краю ячейки, в противном случае элемент располагается в центре ячейки. Размер элемента управления не изменяется, если элемент не должен стыковаться к противоположным краями ячейки. Например, чтобы поместить элемент управления в верхний правый угол ячейки, задайте AnchorStyles.Top \AnchorStylesRight. Чтобы поместить в нижнюю часть ячейки и выровнять по центру по горизонтали, используйте AnchorStylesBottom. Чтобы выровнять по верхнему краю ячейки и растянуть от левого до правого края, используйте AnchorStylesLeft\AnchorStylesRight. Чтобы элемент управления располагался в центре ячейки, используйте AnchorStylesNone. Ветераны программирования в Windows наверняка узнают следующую программу. Color Scroll впервые была опубликована в мае 1987 года в журнале Microsoft Systems Journal, и ее версия появлялась в каждой книге по программированию в Windows, написанной мной с тех пор. Довольно любопытно, что, начиная с самых ранних версий, основной задачей программы Color Scroll всегда была демонстрация динамического размещения! (Тогда я просто не знал, как это называется.) Когда пользователь изменял размер окна программы, программа изменяла размер и расположение элементов управления,
Панели и динамическое размещение 147 выполняя определенные вычисления. С элементом управления TableLayoutPanel программа работает почти так же, но вычисления исчезли (во всяком случае, они исчезли из моего кода; вполне вероятно, что в самом элементе управления Table- LayoutPanel некоторые вычисления остались). ColorScrollTable.cs // // ColorScrollTable.es (с) 2005 by Charles Petzold // using System; using System.Drawing; using System.Windows.Forms; class ColorScrollTable : Form { Panel pnlColor; Label[] alblValue = new Label[3]; [STAThread] public static void Main() { Application.EnableVisualStyles(); Application.Run(new ColorScrollTableO); } public ColorScrollTableO { Text = "Color Scroll with TableLayoutPanel"; // Создание контейнера SplitContainer, заполняющего клиентскую область. SplitContainer spit = new SplitContainerQ; spit.Parent = this; spit.Dock = DockStyle.Fill; splt.SplitterDistance = ClientSize.Width / 2; // Элемент TableLayoutPanel располагается слева от разделителя. TableLayoutPanel table = new TableLayoutPanelO; table.Parent = splt.Panell; table.Dock = DockStyle.Fill; table.BackColor = Color.White; table.ColumnCount = 3; table.RowCount = 3; // Сохранение правой панели SplitterPanel как поля. pnlColor = splt.Panel2;
148 ГЛАВА 3 // Два массива для названий цветов и начальных значений. string[] astrColors = { "Red", "Green", "Blue" }; int[] aiPanelColor = new int[3] { pnlColor.BackColor.R, pnlColor.BackColor.G, pnlColor.BackColor.B }; // Обработка в цикле трех столбцов (red, green, blue). for (int col = 0; col < 3; col++) { // Метка в верхней части показывает значение red, green или blue. Label lbl = new Label(); lbl.AutoSize = true; lbl.Anchor = AnchorStyles.None; lbl.Text = astrColors[col]; lbl.ForeColor = Color.FromName(astrColors[col]); table.Controls.Add(lbl, col, 0); // Создание элемента Scrollbar для задания новых значений. VScrollBar vscrl = new VScrollBarO; vscrl.Parent = table; vscrl.Anchor = AnchorStyles.Top | AnchorStyles.Bottom; vscrl.TabStop = true; vscrl.LargeChange = 16; vscrl.Maximum = 255 + vscrl.LargeChange - 1; vscrl.Value = aiPanelColor[col]; vscrl.ValueChanged += OnScrollValueChanged; table.Controls.Add(vscrl, col, 1); // Метка показывает значение цвета, привязанного к полосе прокрутки. alblValue[col] = new Label(); alblValue[col].AutoSize = true; alblValue[col].Anchor = AnchorStyles.None; alblValue[col].ForeColor = Color.FromName(astrColors[col]); alblValue[col].DataBindings.Add("Text", vscrl, "Value"); table.Controls.Add(alblValue[col], col, 2); // ColumnStyles задает три столбца равного размера. table.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 33)); // С помощью RowStyles делаем среднюю строку максимально большой, table.RowStyles.Add(new RowStyle(SizeType.AutoSize)); table.RowStyles.Add(new RowStyle(SizeType.Percent, 100)); table.RowStyles.Add(new RowStyle(SizeType.AutoSize));
Панели и динамическое размещение 149 // Изменение цвета фона панели при изменении значений полосы прокрутки. void OnScrollValueChanged(object objSrc, EventArgs args) { pnlColor.BackColor = Color.FromArgb(int.Parse(alblValue[0].Text), int.Parse(alblValue[1].Text), int.Parse(alblValue[2].Text)); } } Вот программа в действии: Клиентская область формы заполнена элементом SplitContainer. Справа мы видим Рапе12, значение свойства BackColor которого определяется тремя полосами прокрутки. Слева располагается элемент TableLayoutPanel, свойство Dock которого имеет значение DockStyle.Fill Свойство AutoSize шести меток равно true, а их свойство Anchor — AncborStyles.None. Свойство AutoSize трех полос прокрутки не определено (полосы прокрутки все равно автоматически получают нужную ширину), а свойство Anchor равно AnchorStyles.Top \AnchorStyles.Bottom, чтобы полосы занимали всю высоту ячейки. Другой ключевой элемент, обеспечивающий корректную работу программы — набор RowStyles. В самом конце конструктора создаются три объекта RowStyle. В первой и третьей строках свойству SizeType задается значение AutoSize. В средней строке свойству SizeType задается значение Percent, что позволяет элементу TableLayoutPanel использовать все доступное пространство. В качестве эксперимента можно закомментировать эти три оператора, чтобы увидеть производимый ими эффект. (Размер полосы прокрутки по умолчанию сделает полосы короткими, а все оставшееся пространство заполнит третья строка.) Удаление только третьего оператора RowStyles.Add, никакого эффекта не даст. Если RowStyle не определить явно, эта строка все равно будет иметь значение AutoSize.
150 ГЛАВА 3 Диапазоны столбцов и строк До сих пор я приводил примеры программ, где элемент TableLayoutPanel представлял собой жестко фиксированную сетку строк и столбцов. Редко бывает так, чтобы форму можно было втиснуть в такие жесткие рамки. В большинстве случаев формы должны гибче работать с элементами управления, занимающими несколько ячеек таблицы. Для этого используются диапазоны (spans) строк и столбцов, например: table.SetColumnSpan(ctrl, 2); Элемент управления Ctrl должен быть потомком table. Второй аргумент задает диапазон в две ячейки. Если перед этим вызовом элемент Ctrl занимает ячейку (2, 4), после него он будет занимать ячейки (2, 4) и (3, 4). Если в ячейке (3, 4) расположен другой элемент управления, он будет перемещен в соседнюю ячейку, возможно, сдвигая другие элементы управления и вызывая исключение, если ячеек недостаточно, чтобы вместить все элементы. В общем случае, лучше не полагаться на логику сдвига ячеек, если элементы управления должны располагаться в определенном порядке. Если у ячеек есть обрамление, граница между ячейками (2, 4) и (3, 4) не будет удалена. Следующий элемент управления будет обрабатываться аналогично: table.SetRowSpan(ctrl, 3); Если перед этим вызовом элемент Ctrl занимает ячейку (2, 4), после вызова он будет занимать ячейки (2, 4), (2, 5) и (2, 6). Методы SetColumnSpan и SetRowSpan можно вызывать для одного и того же элемента управления. Методы GetColumnSpan и GetRowSpan принимают в качестве аргумента элемент управления и возвращают целочисленное значение диапазона. Для элемента управления, занимающего несколько строк и столбцов, метод GetPositionFromControl возвращает самую верхнюю и самую левую ячейку, которую занимает элемент управления. Метод GetControlFromPosition возвращает один и тот же элемент управления для всех ячеек, которые он занимает. Пример программы: диалоговое окно выбора шрифта Теперь давайте познакомимся с более близким к реальной жизни проектом и попробуем продублировать стандартный диалог выбора шрифта. Так выглядит экземпляр диалога FontDialog, где свойствам ShowEJJects и ShowColor присвоено значение true-.
Панели и динамическое размещение 151 Mistral О Modem No. 20 Ч? Monarch» О Monotype Corsiva О Monterey ВТ О MS Outlook Effects D Strikeout D Underline Color: "vl Sqipt: _ | Western Здесь видны четыре столбца, в первых трех расположены поля со списком, а в четвертом две кнопки. Есть также три сроки, первая из которых содержит метки, вторая — поля со списком, а третья — все остальное. Остальное размещение можно выполнить при помощи элементов FlowLayoutPanel и GroupPanel Я постарался в своем диалоговом окне реализовать максимум функциональности оригинала. Некоторые неудобства вызывает способ обработки стилей шрифтов этим диалоговым окном. В перечисление FontStyle входят члены Regular, Bold, Italic, Strikeout и Underline. Но стандартное диалоговое окно имеет поле со списком, в котором перечислены стили Regular, Bold, Italic и Bold Italic. Параметры Strikeout (зачеркивание) и Underline (подчеркивание) представлены флажками. Другая проблема заключается в том, что не все семейства шрифтов поддерживают все стили. Попытка создать шрифт с неподдерживаемым стилем вызовет исключение. Чтобы изолировать весь код, связанный со стилями шрифтов, я создал отдельный класс StyleComboBox. StyleComboBox.cs // // StyleComboBox.cs (с) 2005 by Charles Petzold // using System; using System.Drawing; using System.Windows.Forms; class StyleComboBox : ComboBox { public string FamilyName
152 ГЛАВА 3 { set { TryStyle(value, FontStyle.Regular, "Regular"); TryStyle(value, FontStyle.Italic, "Italic"); TryStyle(value, FontStyle.Bold, "Bold"); TryStyle(value, FontStyle.Bold | FontStyle.Italic, "Bold Italic"); Selectedlndex = 0; } } public FontStyle FontStyle { set { if ((value & FontStyle.Bold & FontStyle.Italic) != 0) Selectedltem = "Bold Italic"; else if ((value & FontStyle.Bold) != 0) Selectedltem = "Bold"; else if ((value & FontStyle.Italic) != 0) Selectedltem = "Italic"; else Selectedltem = "Regular"; } get { if (Selectedltem.ToStringO == "Bold Italic") return FontStyle.Bold | FontStyle.Italic; else if (Selectedltem.ToStringO == "Bold") return FontStyle.Bold; else if (Selectedltem.ToStringO == "Italic") return FontStyle.Italic; return FontStyle.Regular; } } void TryStyle(string strFamilyName, FontStyle fntstyle, string strStyle) { int index = FindStringExact(strStyle); try {
Панели и динамическое размещение 153 new Font(strFamilyName, 12, fntstyle); if (index == -1) Items.Add(strStyle); } catch { if (index != -1) Items.Remove(st rStyle); } } } Это поле со списком имеет доступное для записи свойство FamilyName, позволяющее добавлять или удалять любые стили, которые поддерживаются или не поддерживаются конкретным семейством шрифтов. Свойство FontStyle выполняет необходимые преобразования членов перечисления FontStyle и текста в поле со списком. Поскольку поле со списком, в котором отображается цвет шрифта, должно отображать действительный цвет, оно должно отрисовываться владельцем. Это поле тоже представлено собственным классом. ColorComboBox.cs // // ColorComboBox.cs (с) 2005 by Charles Petzold // using System; using System.Drawing; using System.Windows.Forms; class ColorComboBox : ComboBox { public ColorComboBoxO { DataSource = new string[] { "Black", "Maroon", "Green", "Olive", "Navy", "Purple", "Teal", "Gray", "Silver", "Red", "Lime", "Yellow", "Blue", "Fuchsia", "Aqua", "White" }; DropDownStyle = ComboBoxStyle.DropDownList; DrawMode = DrawMode.OwnerDrawFixed; ItemHeight = Font.Height; } public Color Color {
154 ГЛАВА 3 get { return Color.FromName((string) Selectedltem); } set { SelectedText = value.Name; } } protected override void OnDrawItem(DrawItemEventArgs args) { Graphics grfx = args.Graphics; Rectangle rectColor = new Rectangle(args.Bounds.Left, args.Bounds.Top, 2 * args.Bounds.Height, args.Bounds.Height); rectColor.Inflate(-1, -1); Rectangle rectText = new Rectangle(args.Bounds.Left + 2 * args.Bounds.Height, args.Bounds.Top, args.Bounds.Width - 2 * args.Bounds.Height, args.Bounds.Height); args.DrawBackground(); grfx.DrawRectangle(Pens.Black, rectColor); grfx.FillRectangle( new SolidBrush(Color.FromName(Items[args.Index].ToStringO)), rectColor); grfx.DrawString(Items[args.Index].ToStringO, Font, new SolidBrush(args.ForeColor), rectText); } } Теперь мы готовы разместить эти два и остальные элементы управления на форме. NewFontDialog.cs // // NewFontDialog.cs (с) 2005 by Charles Petzold // using System; using System.Drawing; using System.Windows.Forms; class NewFontDialog : Form { ComboBox comboFont, comboSize; StyleComboBox comboStyle; CheckBox chkboxStrikeout, chkboxUnderline; ColorComboBox comboColor; Label lblColor, lblSample;
Панели и динамическое размещение 155 public NewFontDialogO { FormBorderStyle = FormBorderStyle.FixedDialog; MinimizeBox = MaximizeBox = ShowInTaskbar = false; Text = "Font"; AutoSize = true; TableLayoutPanel table = new TableLayoutPanelO; table.Parent = this; table.AutoSize = true; table.ColumnCount = 4; table.RowCount = 3; table.Padding = new Padding(base.Font.Height); Label lbl = new Label(); lbl.Text = "&Font:"; lbl.AutoSize = true; table.Controls.Add(lbl, 0, 0); comboFont = new ComboBoxO; comboFont.DropDownStyle = ComboBoxStyle.Simple; comboFont.AutoSize = true; comboFont.TextChanged += OnComboBoxTextChanged; table.Controls.Add(comboFont, 0, 1); // Заполнение поля comboFont именами семейств шрифтов, foreach (FontFamily fntfam in FontFamily.Families) comboFont.Items.Add(fntfam.Name); lbl = new Label(); lbl.Text = "&Font st&yle:"; lbl.AutoSize = true; table.Controls.Add(lbl, 1, 0); comboStyle = new StyleComboBoxO; comboStyle.DropDownStyle = ComboBoxStyle.Simple; comboStyle.AutoSize = true; comboStyle.TextChanged += OnComboBoxTextChanged; table.Controls.Add(comboStyle, 1, 1); lbl = new Label(); lbl.Text = "&Size:"; lbl.AutoSize = true; table.Controls.Add(lbl, 2, 0);
156 ГЛАВА 3 comboSize = new ComboBox(); comboSize.DropDownStyle = ComboBoxStyle.Simple; comboSize.AutoSize = true; comboSize.TextChanged += OnComboBoxTextChanged; table.Controls.Add(comboSize, 2, 1); // Добавление размеров шрифтов в поле со списком, for (int i = 8; i < 12; i++) comboSize.Items.Add(i); for (int i = 12; i < 30; i += 2) comboSize.Items.Add(i); for (int i = 36; i <= 72; i += 12) comboSize.Items.Add(i); comboSize.Selectedlndex = 0; // Создание GroupPanel. GroupPanel grppnl = new GroupPanel(); grppnl.Text = "Effects"; table.Controls.Add(grppnl, 0, 2); // Флажок CheckBox, отвечающий за зачеркивание. chkboxStrikeout = new CheckBoxO; chkboxStrikeout.Text = "Stri&keout"; chkboxStrikeout.AutoSize = true; chkboxStrikeout.Click += delegate { ShowNewFontO; }; grppnl.Controls.Add(chkboxStrikeout); // Флажок CheckBox, отвечающий за подчеркивание. chkboxUnderline = new CheckBoxO; chkboxUnderline.Text = "&Underline"; chkboxUnderline.AutoSize = true; chkboxUnderline.Click += delegate { ShowNewFontO; }; grppnl.Controls.Add(chkboxUnderline); // Метка цвета. lblColor = new LabelO; lblColor.Text = "&Color:"; lblColor.AutoSize = true; lblColor.Visible = false; grppnl.Controls.Add(lblColor); // Поле со списком цветов. comboColor = new ColorComboBoxO;
Панели и динамическое размещение 157 comboColor.Visible = false; comboColor.AutoSize = true; comboColor.TextChanged += OnComboBoxTextChanged; grppnl.Controls.Add(comboColor); // Панель FlowLayoutPanel для образца и сценария. FlowLayoutPanel flow = new FlowLayoutPanelO; flow.AutoSize = true; flow.FlowDirection = FlowDirection.TopDown; table.Controls.Add(flow, 1, 2); table.SetColumnSpan(flow, 2); // Элемент GroupPanel для образца и кодировки (Script), grppnl = new GroupPaneK); grppnl.Text = "Sample"; flow.Controls.Add(grppnl); // Метка образца. lblSample = new Label(); lblSample.Text = "AaBbYyZz"; lblSample.Font = base.Font; IblSample.TextAlign = ContentAlignment.MiddleCenter; lblSample.BorderStyle = BorderStyle.Fixed3D; IblSample.Size = new Size(20 * base.Font.Height, 3 * base.Font.Height); grppnl.Controls.Add(lblSample); // Панель FlowLayoutPanel для кнопок, flow = new FlowLayoutPanelO; flow.AutoSize = true; flow.FlowDirection = FlowDirection.TopDown; table.Controls.Add(flow, 3, 1); // Кнопка ОК. Button btn = new ButtonQ; btn.Text = "OK"; btn.AutoSize = true; btn.DialogResult = DialogResult.OK; AcceptButton = btn; flow.Controls.Add(btn); // Кнопка Cancel. btn = new Button(); btn.Text = "Cancel"; btn.AutoSize = true; btn.DialogResult = DialogResult.Cancel;
158 ГЛАВА 3 CancelButton = btn; flow.Controls.Add(btn); // Инициирование события для заполнения поля стилей. comboFont.Selectedltem = base.Font.FontFamily.Name; public new Font Font { set { lblSample.Font = value; comboFont.Selectedltem = value.FontFamily.Name; comboStyle.FamilyName = value.FontFamily.Name; comboStyle.FontStyle = value.Style; chkboxStrikeout.Checked = (value.Style & FontStyle.Strikeout) != 0; chkboxUnderline.Checked = (value.Style & FontStyle.Underline) != 0; comboSize.Selectedltem = value.SizelnPoints; comboSize.Text = value.SizelnPoints.ToStringO; } get { return lblSample.Font; } } public Color Color { set { comboColor.Color = value; } get { return comboColor.Color; } } public bool ShowColor { set { lblColor.Visible = comboColor.Visible = value;
Панели и динамическое размещение 159 } get { return comboColor.Visible; } } void OnComboBoxTextChanged(object objSrc, EventArgs args) { ComboBox combo = (ComboBox)objSrc; if (combo == comboColor) { IblSample.ForeColor = comboColor.Color; return; if (combo == comboFont) { int index = comboFont.FindStringExact(comboFont.Text); if (index != -1) { comboFont.Selectedlndex = index; comboStyle.FamilyName = comboFont.Text; } } ShowNewFontO; } void ShowNewFontO { FontStyle fntstyle; try { fntstyle = comboStyle.FontStyle; } catch { return; if (chkboxStrikeout.Checked) fntstyle |= FontStyle.Strikeout; if (chkboxUnderline.Checked) fntstyle |= FontStyle.Underline;
160 ГЛАВА 3 try { Font fnt = new Font(comboFont.Text, float.Parse(comboSize.Text), fntstyle); lblSample.Font = fnt; } catch { } } } По существу, это элемент управления TableLayoutPanel с четырьмя столбцами и тремя строками. В разделе Effects используется элемент управления GroupPanel Разделы Sample и Script располагаются на элементе управления FlowPanel (поле со списком Script я не реализовал), а метка Sample находится внутри элемента Group- Panel. В нескольких местах размеры заданы явно: значение свойства Padding элемента управления TableLayoutPanel равно высоте шрифта по умолчанию, и размеры метки Sample также основаны на размерах шрифта по умолчанию. Размеры всех остальных элементов определяются автоматически. Эти файлы, как и приведенный ниже, являются частью проекта FontDialogMimic. FontDialogMimic.cs // // FontDialogMimic.cs (с) 2005 by Charles Petzold // using System; using System.Drawing; using System.Windows.Forms; class FontDialogMimic : Form { [STAThread] public static void Main() { Application. EnableVisualStylesO; Application.Run(new FontDialogMimicO); } public FontDialogMimicO { Text = "FontDialog Mimic"; ResizeRedraw = true; ForeColor = Color.Red;
Панели и динамическое размещение 161 Menu = new MainMenu(); Menu.MenuItems.Add("&Format"); Menu.MenuItems[0].MenuItems.Add("&01d Font Dialog...", OnOldFont); Menu.MenuItems[0].MenuItems.Add("&New Font Dialog...", OnNewFont); } void 0n01dFont(object objSrc, EventArgs args) { FontDialog fntdlg = new FontDialogO; fntdlg.Font = Font; fntdlg.Color = ForeColor; fntdlg.ShowColor = true; if (fntdlg.ShowDialogO == DialogResult.OK) { Font = fntdlg.Font; ForeColor = fntdlg.Color; InvalidateO; } } void OnNewFont(object objSrc, EventArgs args) { NewFontDialog fntdlg = new NewFontDialogO; fntdlg.Font = Font; fntdlg.Color = ForeColor; fntdlg.ShowColor = true; if (fntdlg.ShowDialogO == DialogResult.OK) { Font = fntdlg.Font; ForeColor = fntdlg.Color; InvalidateO; } } protected override void OnPaint(PaintEventArgs args) { Graphics grfx = args.Graphics; StringFormat strfmt = new StringFormat(); strfmt.LineAlignment = strfmt.Alignment = StringAlignment.Center; grfx.DrawString(Font.ToString(), Font, new SolidBrush(ForeColor), ClientRectangle, strfmt); } } Из меню этой программы можно запускать как стандартный, так и новый диалог выбора шрифта. Вот как выглядит новое диалоговое окно:
162 ГЛАВА 3 font: Mistral Modern No. 20 Monarchia Monotype Corsiva Monterey ВТ :.-■& MS Outlook MS Reference Sam MS Reference Spec M Effects- Fonts': Regular | щшшшшшшшшш.. Italic Bold Bold Italic £ee: \b H^^ 10 11 M2 |14 J16 18 [20 ■■Ha:: '$ #! Sample- OK Cancel AaBbYyZz Тестирование размещения Нужно тестировать не только функциональность каждой формы и диалогового окна, но и их поведение на самом разном оборудовании. Попробуйте поменять разрешение экрана [диалоговое окно Свойства: Экран (Display Properties) можно вызвать из Панели управления (Control Panel), щелкнув правой кнопкой мыши элемент Экран (Display) и выбрав команду Свойства (Properties)] и размер шрифта. Все элементы управления должны нормально воспринять эти изменения и соответствующим образом изменить свои размеры.
Глава 4 Пол ьзовател ьские элементы управления Некоторым людям, особенно программистам, всегда чего-то не хватает. При столь большом выборе элементов управления в Windows Forms им все равно хочется выйти за рамки привычного и создать что-то свое. С точки зрения программирования, нестандартный (пользовательский) элемент управления — это определенный программистом класс, который напрямую или косвенно наследует классу Control. Пользовательский элемент управления может быть как модификацией существующего, так и совершенно новым. Элемент управления можно существенно адаптировать, просто установив в нем дополнительные обработчики событий. Но если нужно полностью изменить стандартный механизм обработки событий, потребуется новый класс. Например, установив обработчик события Paint, можно добавить в элемент управления Button новые визуальные эффекты. Но лишь создание нового класса и переопределение метода OnPaint позволит изменить визуальное поведение, присущее самому объекту Button. Новый класс также необходим, если понадобится добавить в готовый элемент управления новые поля и свойства. Вместе с тем, для добавления произвольных данных вполне подойдет свойство Tag, предназначенное именно для этой цели. Его тип определяется как object, поэтому при обращении к нему потребуется приведение типов, тем не менее оно отлично подходит для простой идентификации элементов управления и присоединения произвольных данных. Какова реальная польза от нестандартных элементов управления? Как и другие фрагменты кода, их можно повторно использовать во многих приложениях, продавать за деньги или бесплатно распространять среди программистов, стяжая себе славу профессионала своего дела.
164 ГЛАВА 4 Совершенствование существующих элементов управления Элемент управления — это, по сути, фильтр, обрабатывающий поступающую от пользователя информацию и преобразующий ее в действия приложения, например в события. Чаще всего элемент управления выполняет три важные задачи. Во-первых, он отображается на экране в виде узнаваемого пользователем визуального объекта. Во- вторых, он обрабатывает поступающую от пользователя (обычно с клавиатуры и мыши) информацию. (Есть специальные элементы управления, поддерживающие обработку голосового ввода или ввода со стило в планшетных ПК.) И, в-третьих, он инициирует события, оповещая приложение о конкретных изменениях. Готовые элементы управления были разработаны и протестированы на предмет корректного выполнения всех трех функций. Поэтому гораздо проще усовершенствовать готовый элемент управления, создав, например, производный класс от Button, чем начинать с нуля, создавая подкласс Control. Переопределение методов Вот очень простой пример: при щелчке мышью кнопка подает звуковой сигнал. BeepButton.cs И // BeepButton.cs (с) 2005 by Charles Petzold // using System; using System.Drawing; using System.Media; using System.Windows.Forms; class BeepButton : Button { protected override void OnClick(EventArgs args) { SystemSounds.Exclamation.Play(); base.OnClick(args); } } BeepButton просто наследует классу Button и переопределяет защищенный метод OnClick. Переопределенный метод инициирует звуковой сигнал с применением двух из трех классов появившегося в .NET Framework 2.0 пространства имен SystemMedia. В классе SystemsSounds есть несколько статических свойств — Asterisk, Веер, Exclamation, Hand и Question — означающих разные звуки. Они возвращают
Пользовательские элементы управления 165 объекты типа SystemSound. Обратите внимание на различие в именах классов: статические методы класса SystemSounds (в английском множественное число) возвращают объект типа SystemSound (единственное число). В классе SystemSound есть один метод Play и он служит для воспроизведения звука. Метод OnClick из нового класса обязательно должен вызывать метод OnClick базового класса: base.OnClick(args); Без этого вызова программа, где используется элемент управления BeepButton, не получит доступа к событию Click. И вот почему: Button наследует событие Click и метод OnClick у класса Control. Код, определяющий событие Click в Control, выглядит примерно так: public event EventHandler Click; Слово EventHandler в этом операторе означает, что все обработчики, установленные для события Click, должны определяться в соответствии с делегатом Event- Handler. Класс Control не имеет отношения к механизму подключения и отключения обработчиков программой: это происходит автоматически. Но Control должен инициировать событие Click в своем методе OnClick, примерно так: protected virtual void OnClick(EventArgs args) { if (Click != null) Click(this, args); } Метод OnClick вызывается как минимум дважды в программе. Разумеется, метод OnMouseDown элемента управления вызывает OnClick, когда пользователь наводит указатель на элемент управления и щелкает кнопку мыши. У кнопок OnClick также вызывается, когда кнопка находится в фокусе ввода и пользователь нажимает клавишу пробела или Enter. Многоточия в OnClick означают, что обработчик может выполнять и другие действия, но без обязательной инициации события Click никак не обойтись. Этот код можно перевести так: «Если есть установленные обработчики события Click, вызвать их, передав им текущий объект в качестве первого аргумента, а объект Event- Args — как второй аргумент». Если определить класс, наследующий (прямо или косвенно) Control, и переопределить метод OnClick, не предусмотрев в нем вызова одноименного метода базового класса, код, вызывающий все обработчики события Click, просто никогда не будет выполняться. (И, конечно же, не стоит вызывать
166 ГЛАВА 4 метод OnClick в базовом классе, если цель создания нового элемента управления — в отключении события Click?) Обычно вызов метода OnClick базового класса помещают в самом начале метода: protected override void OnClick(EventArgs args) { base.OnClick(args); Иногда нужно, чтобы код в базовом методе исполнялся первым, но в данном примере из этого ничего не вышло (вскоре вы узнаете, почему). Поэтому вызов base.OnClick помещен в конце метода OnClick в BeepButton. Вот простая программа, в которой создается объект типа BeepButton и на кнопку устанавливается обработчик события Click, который выводит окно с сообщением. BeepButtonDemo.cs И // BeepButtonDemo.cs (с) 2005 by Charles Petzold // using System; using System.Drawing; using System.Windows.Forms; class BeepButtonDemo : Form { [STAThread] public static void Main() { Application. EnableVisualStylesO; Application.Run(new BeepButtonDemoO); } public BeepButtonDemoO { Text = "BeepButton Demonstration"; BeepButton btn = new BeepButtonO; btn.Parent = this; btn.Location = new Point(100, 100); btn.AutoSize = true; btn.Text = "Click the BeepButton"; btn.Click += ButtonOnClick; } void ButtonOnClick(object objSrc, EventArgs args) {
Пользовательские элементы управления 167 SilentMsgBox.Show("The BeepButton has been clicked", Text); } } Стандартный класс MessageBox здесь использовать нельзя, потому как он сам издает сигнал при отображении окна с сообщением. Поэтому часть его функций была скопирована в новом классе. SilentMsgBox.cs И // SilentMsgBox.cs (с) 2005 by Charles Petzold // using System; using System.Drawing; using System.Windows.Forms; class SilentMsgBox { public static DialogResult Show(string strMessage, string strCaption) { Form frm = new Form(); frm.StartPosition = FormStartPosition.CenterScreen; frm.FormBorderStyle = FormBorderStyle.FixedDialog; frm.MinimizeBox = frm.MaximizeBox = frm.ShowInTaskbar = false; frm.AutoSize = true; frm.AutoSizeMode = AutoSizeMode.GrowAndShrink; frm.Text = strCaption; FlowLayoutPanel pnl = new FlowLayoutPanelO; pnl.Parent = frm; pnl.AutoSize = true; pnl.FlowDirection = FlowDirection.TopDown; pnl.WrapContents = false; pnl.Padding = new Padding(pnl.Font.Height); Label lbl = new LabelQ; lbl.Parent = pnl; lbl.AutoSize = true; lbl.Anchor = AnchorStyles.None; lbl.Margin = new Padding(lbl.Font.Height); lbl.Text = strMessage; Button btn = new ButtonO; btn.Parent = pnl; btn.AutoSize = true;
168 ГЛАВА 4 btn.Anchor = AnchorStyles.None; btn.Margin = new Padding(btn.Font.Height); btn.Text = "OK"; btn.DialogResult = DialogResult.OK; return frm.ShowDialogO; } } Файлы BeepButton.cs, BeepButtonDemo.es и SilentMsgBox.es входят в проект Веер- ButtonDemo. Чтобы убедиться, что все сказанное об обработчике события Click — правда, закомментируйте вызов метода base.OnClick в BeepButton, перекомпилируйте программу и обратите внимание на то, что BeepButtonDemo больше не оповещается о событии Click. А теперь сделайте так: в методе OnClick в BeepButton поменяйте местами эти два оператора так, чтобы метод OnClick в базовом классе вызывался до воспроизведения звукового сигнала. protected override void OnClick(EventArgs args) { base.OnClick(args); SystemSounds.Exclamation.Play(); } Щелчок кнопки мыши вызывает этот метод OnClick (скорее всего, из метода OnMouseDown в Control). В этом измененном коде BeepButton сначала вызывает метод OnClick своего базового класса — Button. Но метод OnClick в Button вызывает метод OnClick своего базового класса и так далее вплоть до вызова метода OnClick в Control. А этот OnClick отвечает за исполнение кода, вызывающего все обработчики, установленные для события Click. В классе BeepButtonDemo такой обработчик установлен, поэтому вызывается его метод ButtonOnClick, который в свою очередь вызывает статический метод Show из SilentMsgBox, а тот (как и MessageBox) выводит модальное диалоговое окно и ждет его закрытия пользователем. Когда пользователь закрывает это окно, Show возвращает управление ButtonOnClick, который, в свою очередь, возвращает управление методу OnClick в BeepButton. В итоге BeepButton (в измененном коде) воспроизводит звук — к сожалению, с момента нажатия кнопки проходит много времени. Мораль такова: вы вправе выбрать, когда метод с префиксом On вызовет этот метод в базовом классе. Но выбирать нужно с умом.
Пользовательские элементы управления 169 Добавление новых свойств В классе BeepButton показано не только, как усовершенствовать готовые элементы управления, но и как применять два из трех классов из пространства имен System.- Media. В следующей программе третий класс из этого пространства имен, Sound- Player, послужил для создания нового элемента управления SoundButton. Он отличается от BeepButton тем, что воспроизводит WAV-файл, а не простой сигнал. Например, благодаря этой возможности при нажатии на кнопку зазвучит умиротворяющий голос Барта Симпсона (Bart Simpson). Этот класс наследует классу Button. Первым делом в нем создается и сохраняется объект SoundPlayer как поле. SoundButton.cs И // SoundButton.cs (с) 2005 by Charles Petzold // using System; using System.Drawing; using System.10; using System.Media; using System.Windows.Forms; class SoundButton : Button { SoundPlayer sndplay = new SoundPlayer(); public string WaveFile { set { sndplay.SoundLocation = value; sndplay.LoadAsync(); } get { return sndplay.SoundLocation; } } public Stream WaveStream { set { sndplay.Stream = value; sndplay.LoadAsync(); }
170 ГЛАВА 4 get { return sndplay.Stream; } } protected override void OnClick(EventArgs args) { if (sndplay.IsLoadCompleted) sndplay.PlayO; base.OnClick(args); } } В этом классе определено два новых свойства — WaveFile и WaveStream. Они соответствуют свойствам SoundLocation и Stream из класса SoundPlayer, позволяющим по-разному указать в классе SoundPlayer путь к WAV-файлу. Свойство Sound- Location класса SoundPlayer (и WaveFile — класса SoundButton) — это строка, содержащая имя локального файла или URL-адрес. Stream (и WaveStream) представляет собой объект типа Stream, абстрактного класса из пространства имен System.IO. К потомкам Stream относятся FileStream, обычно ссылающийся на открытый файл, и MemoryStream — блок памяти, к которому обращаются как к файлу. Параметр Stream особенно полезен для внедрения WAV-файлов в исполняемый файл и доступа к ним как к ресурсам (вскоре я покажу, как это делается). Для обращения к WAV-файлу SoundPlayer использует свойство, указанное последним. Обычно SoundPlayer загружает WAV-файл в память перед его воспроизведением (в данном случае в ходе выполнения метода OnClick). Чтобы процесс загрузки не замедлил работу OnClick, эти два свойства загружают WAV-файлы сразу в отдельном потоке, вызывая LoadAsync. (С другой стороны метод Load класса SoundPlayer загружает WAV-файл синхронно, в том же потоке, что может замедлить инициализацию программы.) Обычно команда Play в SoundPlayer выполняется асинхронно. Метод OnClick начинает воспроизводить сигнал, а затем выполняет другие задачи: вызов метода OnClick в базовом классе и все последующие действия. Его «заменитель», метод Play- Sync в SoundPlayer, возвращает управление программе только по завершении воспроизведения сигнала. А метод PlayLooping асинхронный: он повторяет сигнал, пока не вызван Stop. В следующей программе показаны три способа применения SoundButton: загрузка локального файла (в данном случае это сигнал «tada.wav» из папки Windows Media), файла из Интернета (рев льва с Web-сайта зоопарка в Окленде) и использование ресурса. В проект SoundButtonDemo также входит файл SoundButton.cs и WAV-файл MakeItSo.wav, где я говорю «Make it so» («Да будет так»).
Пользовательские элементы управления 171 SoundButtonDemo.cs И // SoundButtonDemo.cs (с) 2005 by Charles Petzold // using System; using System.Drawing; using System.10; using System.Windows.Forms; class SoundButtonDemo : Form { [STAThread] public static void Main() { Application.EnableVisualStyles(); Application.Run(new SoundButtonDemo()); } public SoundButtonDemoO { Text = "SoundButton Demonstration"; SoundButton btn = new SoundButtonO; btn.Parent = this; btn.Location = new Point(50, 25); btn.AutoSize = true; btn.Text = "SoundButton with File"; btn.Click += ButtonOnClick; btn.WaveFile = Path.Combine( Environment.GetEnvironmentVariableC'windir"), "MediaWtada.wav"); btn = new SoundButtonO; btn.Parent = this; btn.Location = new Point(50, 125); btn.AutoSize = true; btn.Text = "SoundButton with URI"; btn.Click += ButtonOnClick; btn.WaveFile = "http://www.oaklandzoo.org/atoz/azlinsnd.wav"; btn = new SoundButtonO; btn.Parent = this; btn.Location = new Point(50, 225); btn.AutoSize = true; btn.Text = "SoundButton with Resource"; btn.Click += ButtonOnClick;
172 ГЛАВА 4 btn.WaveStream = GetType().Assembly.GetManifestResourceStream( "SoundButtonDemo.MakeItSo.wav"); } void ButtonOnClick(object objSrc, EventArgs args) { Button btn = objSrc as Button; SilentMsgBox.Show("The SoundButton has been clicked", btn.Text); } } Все три способа обращения к WAV-файлу нетривиальны. Файл tada.wav находится в подкаталоге Media установочного каталога Windows, имя которого обычно WINDOWS или WINNT. Переменная GetEnvironmentVariable класса Environment служит для получения имени каталога, которое затем объединяется с именами Media и tada.wav. Конечно, гораздо проще указать URL. Но для этого нужна стопроцентная уверенность, что адрес не изменится за все время эксплуатации программы и у нее будет доступ к Интернету. Надежнее обращаться к двоичным файлам, вставляя их в исполняемый файл как ресурсы. Для этого сначала нужно добавить файл в проект. Щелкните файл в панели Solution Explorer в Microsoft Visual Studio. В правом нижнем углу откроется панель Properties, где нужно обязательно изменить значение свойства Build Action на Embedded Resource. Иначе программа не загрузит ресурс в процессе выполнения, а вы потратите уйму времени, пытаясь докопаться до причины неполадки. Последнее выражение конструктора в SoundButtonDemo показывает, как нужно загружать такой двоичный ресурс в программу как объект Stream. Перед именем файла обязательно указывается «пространство имен ресурса». В Visual Studio оно совпадает с именем проекта, но его можно изменить в группе Application диалогового окна свойств проекта в поле Default namespace. Прорисовка элементов управления Радикально изменить готовый элемент управления можно, придав ему совершенно новый вид. Для этого как минимум нужно переопределить метод OnPaint. А для изменения стандартного размера элемента управления можно переопределить еще и GetPreferredSize и (возможно) OnResize. Если повезет, эти изменения не повлияют на логику обработки ввода с клавиатуры и мыши. Windows Forms облегчает работу программиста, относящуюся к прорисовке элементов управления. Прежде чем «изобретать... кнопку», внимательно изучите класс ControlPaint. В нем есть несколько статических методов для преобразования системной палитры в более светлые и темные оттенки и решения различных задач по прорисовке. Между прочим, системная палитра, перья и кисти находятся в трех
Пользовательские элементы управления 173 классах пространства имен System.Drawing-. SystemColors, SystemPens и SystemBrushes соответственно. В классах ProfessionalColors и ProfessionalColorTable в System.Windows.- Forms есть цвета, примерно похожие на палитру Microsoft Office. ProfessionalColors — это набор статических свойств, a ProfessionalColorTable содержит экземплярные версии этих свойств. Я предпочел, по большей части, создать собственную логику прорисовки и выбрал свои цвета в классе RoundButton. Последний является подклассом Button, но создаваемая кнопка по форме круглая. В этом коде также показано, как создавать непрямоугольные элементы управления. RoundButton.cs И // RoundButton.cs (с) 2005 by Charles Petzold // using System; using System.Drawing; using System.Drawing.Drawing2D; using System.Windows.Forms; class RoundButton : Button { public RoundButton() { SetStyle(ControlStyles.UserPaint, true); SetStyle(ControlStyles.AllPaintingInWmPaint, true); } public override Size GetPreferredSize(Size szProposed) { //Отображается базовый размер текстовой строки. Graphics grfx = CreateGraphicsO; SizeF szf = grfx.MeasureString(Text, Font); int iRadius = (int)Math.Sqrt(Math.Pow(szf.Width /2, 2) + Math.Pow(szf.Height / 2, 2)); return new Size(2 * iRadius, 2 * iRadius); } protected override void OnResize(EventArgs args) { base.OnResize(args); //Круглый регион делает кнопку непрямоугольной. GraphicsPath path = new GraphicsPathQ; path.AddEllipse(ClientRectangle); Region = new Region(path); }
174 ГЛАВА 4 protected override void OnPaint(PaintEventArgs args) { Graphics grfx = args.Graphics; grfx.SmoothingMode = SmoothingMode.AntiAlias; Rectangle rect = ClientRectangle; //Отрисовка внутренней части кнопки (при нажатии она становится темнее), bool bPressed = Capture & ((MouseButtons & MouseButtons.Left) != 0) & ClientRectangle.Contains(PointToClient(MousePosition)); GraphicsPath path = new GraphicsPathO; path.AddEllipse(rect); PathGradientBrush pgbr = new PathGradientBrush(path); int k = bPressed ? 2 : 1; pgbr.CenterPoint = new PointF(k * (rect.Left + rect.Right) / 3, k * (rect.Top + rect.Bottom) / 3); pgbr.CenterColor = bPressed ? Color.Blue : Color.White; pgbr.SurroundColors = new Color[] { Color.SkyBlue }; grfx, FillRectangle(pgbr, rect); //Отображение границ кнопки (утолщенные для кнопки по умолчанию). Brush br = new LinearGradientBrush(rect, Color.FromArgb(0, 0, 255), Color.FromArgb(0, 0, 128), LinearGradientMode.ForwardDiagonal); Pen pn = new Pen(br, (IsDefault ? 4 : 2) * grfx.DpiX / 72); grfx.DrawEllipse(pn, rect); //Отображение текста в центре прямоугольника (если кнопка неактивна, // цвет должен быть бледно-серым). StringFormat strfmt = new StringFormatO; strfmt.Alignment = strfmt.LineAlignment = StringAlignment.Center; br = Enabled ? SystemBrushes.WindowText : SystemBrushes.GrayText; grfx.DrawString(Text, Font, br, rect, strfmt); //Отображение пунктира вокруг текста, если кнопка в фокусе ввода. if (Focused) { SizeF szf = grfx.MeasureString(Text, Font, PointF.Empty, StringFormat.GenericTypographic); pn = new Pen(ForeColor); pn.DashStyle = DashStyle.Dash; grfx.DrawRectangle(pn, rect.Left + rect.Width / 2 - szf.Width / 2, rect.Top + rect.Height / 2 - szf.Height / 2, szf.Width, szf.Height); }
Пользовательские элементы управления 175 В конструкторе устанавливаются два флага ControlStyles. По умолчанию они оба равны true в объекте Button, но это верно не для всех элементов управления. Когда этим флагам присвоено значение true, вся отрисовка выполняется в методе OnPaint. Поэтому чтобы изменить логику отрисовки, достаточно переопределить этот метод. (Если же флаг AllPaintinglnWmPaint имеет значение/я/se, фон кнопки задается путем переопределения OnPaintBackground.) Метод GetPreferredSize очень важен для автоматической регулировки размеров. Когда значение AutoSize — true, диспетчер размещения вызывает его для определения нужных размеров элемента управления. При любом действии, способном повлиять на размеры элемента управления (например, при изменении свойств Font или Text элемента управления), для получения новых размеров снова вызывается метод GetPreferredSize. Этот метод, определенный в RoundButton, получает размеры свойства Text в пикселах, вызывая MeasureString. Затем он вычисляет расстояние от центра этого прямоугольника до угла — это и есть радиус круглой кнопки. Метод OnResize вызывается при любом изменении размеров элемента управления — автоматическом или явном. В этом методе также задается круглая форма кнопки путем назначения свойства Region, определенного в классе Control. Свойство Region назначается объекту типа Region (это класс в System.Drawing). В графическом регионе область неправильной формы представляется как набор дискретных строк растра. После назначения свойства Region у элемента управления остаются размеры прямоугольника. Но все части элемента управления, выходящие за пределы заданной регионом области, будут невидимыми — как визуально, так и для мыши. Класс Region позволяет создавать регионы только из булевых комбинаций нескольких прямоугольников. Общий подход — сначала создать траекторию с помощью класса GraphicsPath. Траектория — это набор линий и кривых, необязательно соединенных между собой и необязательно образующих замкнутую кривую. Подробно о траекториях и регионах см. главу 15 моей книги «Programming Microsoft Windows with C#», Microsoft Press, 2001 [Программирование для MS Windows на С#. M.: том 2, Русская Редакция, 2002]. Траектория, создаваемая в RoundButton, состоит просто из эллипса размером с клиентскую область кнопки. Этот эллипс (или точнее, его внутренняя часть) преобразуется в регион, а затем это значение присваивается свойству Region кнопки. Второй (и последний) метод в RoundButton — OnPaint — не вызывает метод OnPaint базового класса, потому что это не нужно. Этот метод вызывается всякий раз, когда нужно перерисовать кнопку. Сначала метод отрисовывает внутреннюю область кнопки. Для придания кнопке вида полусферы использована кисть PathGradientBrush. (Кисти — градиент и другие — обсуждаются в главе 17 уже упомянутой книги «Programming Microsoft Windows with C#».) Но внешний вид кнопки должен меняться по щелчку мыши. Цвет кнопки становится темнее, когда свойство Capture равно true (то есть информа-
176 ГЛАВА 4 ция от мыши поступает в элемент управления), указатель находится на элементе управления и нажата левая кнопка мыши. Затем OnPaint отображает границы кнопки, используя перо на основе Linear- GradientBrush. Обычно у кнопки по умолчанию (она откликается на нажатие клавиши Enter) утолщенная граница. Для изменения толщины границы в RoundButton служит свойство IsDefault. Дальше OnPaint отображает текст. И это не так просто, как может показаться. На неактивной кнопке текст должен быть серым. Для отображения текста метод выбирает одну из двух кистей: SystemBrushesWindowText или System.Brushes.GrayText. Объект StringFormat помогает выровнять текст по центру кнопки. Если кнопка находится в фокусе ввода, вокруг текста должна появиться пунктирная линия. В конце OnPaint опять вызывается MeasureString для получения ширины и высоты текстовой строки, и по этим размерам создается пунктирный прямоугольник. Заметьте: вызовы MeasureString в GetPreferredSize и OnPaint чуть отличаются. В последнем методе аргумент StringFormat.GenericTypographic передавался методу вместе с текстовой строкой и шрифтом. По умолчанию MeasureString возвращает размеры, немного больше габаритов строки. Я думал, что это нужно для определения размера кнопки, чтобы вокруг текста было немного свободного места. Но при вызове того же MeasureString для создания пунктирного контура текста, получалось, что левый и правый края пунктирного прямоугольника перекрывались границами кнопки. Передача StringFormat.GenericTypographic методу MeasureString позволяет приблизить размеры к реальным границам текста, тем самым создавая компактный пунктирный прямоугольник. Подробнее о GenericTypographic см. главу 9 книги «Programming Microsoft Windows with C#», Microsoft Press, 2001 [Программирование для MS Windows на С#. М.: том 2, Русская Редакция, 2002]. Вот небольшая программа для тестирования кнопок. Этот файл наряду с Round- Button.cs входит в проект RoundButtonDemo. RoundButtonDemo.cs И // RoundButtonDemo.cs (с) 2005 by Charles Petzold // using System; using System.Drawing; using System.Windows.Forms; class RoundButtonDemo : Form { [STAThread] public static void Main()
Пользовательские элементы управления 177 Application. EnableVisualStylesO; Application.Run(new RoundButtonDemoO); } public RoundButtonDemoO { Text = "RoundButton Demonstration"; Font = new Font("Times New Roman", 18); AutoSize = true; AutoSizeMode = AutoSizeMode.GrowAndShrink; FlowLayoutPanel flow = new FlowLayoutPanelO; flow.Parent = this; flow.AutoSize = true; flow.FlowDirection = FlowDirection.TopDown; FlowLayoutPanel flowTop = new FlowLayoutPanelO; flowTop.Parent = flow; flowTop.AutoSize = true; flowTop.Anchor = AnchorStyles.None; Label lbl = new Label(); lbl.Parent = flowTop; lbl.AutoSize = true; lbl.Text = "Enter some text:"; lbl.Anchor = AnchorStyles.None; TextBox txtbox = new TextBoxO; txtbox.Parent = flowTop; txtbox.AutoSize = true; FlowLayoutPanel flowBottom = new FlowLayoutPanelO; flowBottom.Parent = flow; flowBottom.AutoSize = true; flowBottom.Anchor = AnchorStyles.None; RoundButton btnOk = new RoundButtonO; btnOk.Parent = flowBottom; btnOk.Text = "OK"; btnOk.Anchor = AnchorStyles.None; btnOk.DialogResult = DialogResult.OK; AcceptButton = btnOk; RoundButton btnCancel = new RoundButtonO; btnCancel.Parent = flowBottom; btnCancel.AutoSize = true;
178 ГЛАВА 4 btnCancel.Text = "Cancel"; btnCancel.Anchor = AnchorStyles.None; btnCancel.DialogResult = DialogResult.Cancel; CancelButton = btnCancel; btnOk.Size = btnCancel.Size; } } В этой программе моделируется диалоговое окно с меткой, текстовым полем (TextBox) и двумя элементами управления RoundButton в виде круглых кнопок ОК и Cancel. Три элемента управления FlowPanel занимают свое положение в форме по механизму динамического размещения (dynamic layout). Сначала свойству AutoSize обоих элементов управления RoundButton я присвоил значение true — как обычным кнопкам. Результат не выдерживает критики. I RoundButton Demonstration U iPOIHCI Enter some text: С этим нужно было что-то делать. Тогда я приравнял true только свойство AutoSize кнопки Cancel. А после создания обоих кнопок, уравнял их размеры. btnOk.Size = btnCancel.Size; Внешний вид диалогового окна заметно улучшился. Ш RoundButton Demonstration I fill Enter some text: И мне стало ясно, почему по умолчанию кнопки некруглые. Пусть в программе нужна круглая кнопка (RoundButton), внешний вид которой нужно к тому же изменить. Устанавливается обработчик события Paint, и... ничего не происходит. Дело в том, что RoundButton переопределяет OnPaint, но не вызывает одноименный обработчик базового класса, потому что от базового класса ничего не требуется. Но ведь именно базовый класс инициирует событие Paint.
Пользовательские элементы управления 179 A RoundButton его инициировать не может, потому как это разрешено только классу, где событие определено. Верное решение — определить в RoundButton новое событие Paint с помощью ключевого слова new и вызвать его в методе OnPaint в RoundButton. Комбинирование готовых элементов управления Очень часто новые элементы управления создают путем комбинирования готовых. Чтобы воспользоваться преимуществами логики готовых элементов управления, рекомендуется создавать такие элементы управления как наследники класса User- Control. Среди прочего, этот класс поддерживает переключение между несколькими элементами управления с клавиатуры. Первый пример — создание элемента управления, во многом похожего на популярные готовые элементы управления. Будь то числовой или иной наборный счетчик или класс NumericUpDown — есть одна общая проблема с элементами управления, которые для изменения числовых значений используют кнопки со стрелками вверх и вниз. С одной стороны, стрелка вверх означает «выше» и подразумевает увеличение значения, а стрелка вниз означает «ниже». А если список возможных чисел представить следующим образом: О 1 2 3 тогда стрелка вверх будет уменьшать значение (вплоть до 0), а стрелка вниз — увеличивать. Этого противоречия удалось бы избежать, если задействовать горизонтальные, а не вертикальные полосы прокрутки. Тогда стрелка влево указывала бы на меньшие значения, а стрелка вправо — на большие, хотя бы в тех странах, где принято читать слева направо. Я убежден, что я прав и что мой пример убедительно доказывает необходимость перейти на горизонтальные полосы прокрутки. Поэтому я поместил элемент управления, который назвал NumericScan, в динамически подключаемую библиотеку NumericScan.dll. Также, я доработал этот элемент управления, чтобы его можно было использовать в конструкторе форм Visual Studio. Параметр /target компилятора С# определяет, станет ли набор файлов с исходным текстом исполняемым файлом (с расширением .ехе) или динамически подключаемой библиотекой (с расширением .dll). Иначе говоря, параметр /target-.exe или /target.-winexe служит для создания ЕХЕ-файла, a /target-.library — DLL-файла. В Visual Studio нужный тип файла (исполняемый файл или DLL-библиотека) указывается в свойствах проекта.
180 ГЛАВА 4 DLL-проект создать проще, чем программный проект. Если вас устраивают готовые шаблоны проектов в Visual Studio, выберите шаблон Class Library или Windows Control Library. Если работа начата с пустого проекта (Empty Project), создайте проект, как обычно, а затем откройте окно свойств проекта и в поле Output type выберите Class Library, чтобы создать DLL-библиотеку, а не исполняемый файл. Добавьте в проект хотя бы один файл с исходным текстом, и полный вперед! DLL-файл нельзя исполнить напрямую, поэтому настанет момент, когда код скомпилирован, а работу элемента управления проверить невозможно. Для этого потребуется настоящая программа. Поэтому при создании DLL-библиотеки под рукой должна быть тестовая программа. Проще всего поместить DLL-проект и эту программу в одно решение Visual Studio. Как это сделать, показано на следующем примере. В Visual Studio в меню File выберите New/Project. В диалоговом окне New Project укажите имя проекта (в данном случае NumericScan) и установите флажок Create directory for solution. (Этот флажок означает, что будет создана папка решения NumericScan с одноименной подпапкой проекта.) Щелкните ОК. Затем снова выберите в меню File New/Project. В диалоговом окне New Project введите имя проекта TestProgram, в поле Solution выберите Add To Solution и щелкните ОК. Теперь в решении NumericScan два проекта — NumericScan и TestProgram. Обязательно в окне Project Properties проекта NumericScan в поле Output type выберите пункт Class Library. В панели Solution Explorer щелкните правой кнопкой TestProgram и выберите Set As Startup Project. Тогда после перекомпиляции всего решения Visual Studio запустит TestProgram. При создании и тестировании DLL- библиотеки потребуются дополнительные усилия, но об этом позже. Представленный здесь элемент управления NumericScan состоит из элемента управления TextBox и двух кнопок. Но это не совсем обычные кнопки. Щелкнув стрелку обычного элемента управления NumericUpDown и удерживая кнопку мыши, вы увидите, что эти кнопки работают почти как полоса прокрутки, последовательно перебирая значения (то же можно сделать и с помощью клавиатуры). Эту кнопку я назвал ClickmaticButton, a ClickmaticButton.cs — это первый файл в проекте NumericScan. ClickmaticButton.cs И // ClickmaticButton.es (с) 2005 by Charles Petzold // using System; using System.Drawing; using System.Windows.Forms; namespace Petzold.ProgrammingWindowsForms {
Пользовательские элементы управления 181 class ClickmaticButton : Button { Timer tmr = new TimerO; int iDelay = 250 * (1 + Systemlnformation.KeyboardDelay); int iSpeed = 405 - 12 * Systemlnformation.KeyboardSpeed; protected override void OnMouseDown(MouseEventArgs args) { base.OnMouseDown(args); if ((args.Button & MouseButtons.Left) != 0) { tmr.Interval = iDelay; tmr.Tick += TimerOnTick; tmr.Start(); } void TimerOnTick(object objSrc, EventArgs args) 0nClick(EventArgs.Empty); tmr.Interval = iSpeed; protected override void OnMouseMove(MouseEventArgs args) base.OnMouseMove(args); tmr.Enabled = Capture & ClientRectangle.Contains(args.Location); protected override void OnMouseUp(MouseEventArgs args) base.OnMouseUp(args); tmr.StopO; } } Обратите внимание, что этот класс определен в пространстве имен. При создании DLL-библиотеки важно определять классы в пространстве имен, чтобы избежать конфликта имен с другими программами, использующими эту же библиотеку. Имя пространства имен в данном случае создано по общему правилу: «название компании + точка + имя продукта». Независимо от пространства имен этот класс невидим извне DLL-библиотеки, потому что в его определении нет ключевого слова public. Открытый класс или закрытый не столь важно и имеет значение только при добавлении классов в DLL- библиотеку.
182 ГЛАВА 4 Наш класс довольно прост: он наследует классу Button и переопределяет метод OnMouseDown, который нужен ему для отслеживания щелчков мыши. Вызов метода в базовом классе приводит к обычному вызову OnClick, который затем инициирует событие Click. Далее в методе OnMouseDown запускается таймер. Обработчик событий (таймер) повторно вызывает OnClick, пытаясь обнаружить повторные события Click. Если указатель мыши сместится с кнопки при нажатой кнопке мыши, таймер временно остановится. Он также останавливается, когда пользователь отпускает кнопку мыши. Какое-то время я не мог подобрать правильное значение параметра Interval таймера. Между щелчком кнопки и началом перебора значений должна быть задержка, а дальше временной интервал должен быть короче. К счастью, я обнаружил, что в .NET Framework 2.0 у класса Systemlnformation появились новые возможности. SystemlnformationXeyboardDelay определяется как «величина задержки при повторном нажатии клавиши клавиатуры: от 0 (около 250 мс) до 3 (примерно 1 с)». A System- InformationKeyboardSpeed — это «скорость повторного нажатия клавиши клавиатуры: от 0 до 31 (от 2,5 до 30 повторов в секунду)». Эти значения отлично подошли. Класс ArrowButton наследует ClickmaticButton и переопределяет метод ОпРаШддя отрисовки стрелок методом ControlPaint.DrawScrollButton. Направление стрелки передается через открытое свойство. ArrowButton. cs И // ArrowButton.cs (с) 2005 by Charles Petzold // using System; using System.Drawing; using System.Windows.Forms; namespace Petzold.ProgrammingWindowsForms { class ArrowButton : ClickmaticButton { ScrollButton scrbtn = ScrollButton.Right; public ArrowButtonO { SetStyle(ControlStyles.Selectable, false); } public ScrollButton ScrollButton { set { scrbtn = value;
Пользовательские элементы управления 183 InvalidateO; } get { return scrbtn; } } protected override void OnPaint(PaintEventArgs args) { Graphics grfx = args.Graphics; ControlPaint.DrawScrollButton(grfx, ClientRectangle, scrbtn, '.Enabled ? ButtonState.Inactive : (Capture & ClientRectangle.Contains( PointToClient(MousePosition))) ? ButtonState.Pushed : ButtonState.Normal); } protected override void OnMouseCaptureChanged(EventArgs args) { base.OnMouseCaptureChanged(args); InvalidateO; } } } В классе ArrowButton есть конструктор, присваивающий флагу ControlStyles.- Selectable значение/я/se. Кнопки на элементе управления NumericScan должны быть недоступными пользователю, то есть не получать фокус ввода. Фокус должен быть только у поля ввода. Вот открытый класс NumericScan, потомок UserControl, создающий элемент управления из TextBox и два элемента управления ArrowButton. NumericScan. cs И // NumericScan.es (с) 2005 by Charles Petzold // using System; using System.ComponentModel; using System.Drawing; using System.Reflection; using System.Windows.Forms; [assembly: AssemblyTitle("NumericScan")] [assembly: AssemblyDescription("NumericScan Control")] [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("www.charlespetzold.com")] [assembly: AssemblyProduct("NumericScan")] [assembly: AssemblyCopyright("(c) Charles Petzold, 2005")]
184 ГЛАВА 4 [assembly: AssemblyTrademark("")] [assembly: AssemblyVersion("1.0.*"] namespace Petzold.ProgrammingWindowsForms { [DefaultEvent("ValueChanged")] public class NumericScan : UserControl { public event EventHandler ValueChanged; TextBox txtbox; ArrowButton btnl, btn2; //У этих закрытых полей есть соответствующие открытые свойства. int iDecimalPlaces = 0; decimal mValue = 0; decimal mlncrement = 1 decimal mMinimum = 0; decimal mMaximum = 100 public NumericScan() { txtbox = new TextBoxO; txtbox.Parent = this; txtbox.TextAlign = HorizontalAlignment.Right; txtbox.Text = ValueToText(mValue); txtbox.TextChanged += TextBoxOnTextChanged; txtbox.KeyDown += TextBoxOnKeyDown; btnl = new ArrowButtonO; btnl.Parent = this; btnl.Text = "btnl"; btnl.ScrollButton = ScrollButton.Left; btnl.Click += ButtonOnClick; btn2 = new ArrowButtonO; btn2.Parent = this; btn2.Text = "btn2"; btn2.ScrollButton = ScrollButton.Right; btn2.Click += ButtonOnClick; Width = 4 * Font.Height; Height = txtbox.PreferredHeight + Systemlnformation.HorizontalScrollBarHeight; } string ValueToText(decimal mValue)
Пользовательские элементы управления 185 return mValue.ToString("F" + DecimalPlaces); } [CategoryC'Data"), Description("Value displayed in the control")] public decimal Value { set { txtbox.Text = ValueToText(mValue = value); } get { return mValue; } [CategoryC'Data"), DescriptionC'The amount to increment or decrement on a button click")] public decimal Increment { set { mlncrement = value; } get { return mlncrement; } } [CategoryC'Data"), Description("Minimum allowed value")] public decimal Minimum { set { if ((mMinimum = value) > Value) Value = mMinimum; } get { return mMinimum; } } [CategoryC'Data"), Description("Maximum allowed value")] public decimal Maximum { set { if ((mMaximum = value) < Value) Value = mMaximum; } get { return mMaximum; } }
186 ГЛАВА 4 [CategoryC'Data"), Description("Number of decimal places to display")] public int DecimalPlaces { set { iDecimalPlaces = value; } get { return iDecimalPlaces; } } public override Size GetPreferredSize(Size szProposed) { return new Size(4 * Font.Height, txtbox.PreferredHeight + Systemlnformation.HorizontalScrollBarHeight); } protected override void OnResize(EventArgs args) { base.OnResize(args); txtbox.Height = txtbox.PreferredHeight; txtbox.Width = Width; btnl.Location = new Point(0, txtbox.Height); btn2. Location = new Point(Width / 2, txtbox.Height); btnl.Size = btn2.Size = new Size(Width / 2, Height - txtbox.Height); } void TextBoxOnTextChanged(object objSrc, EventArgs args) { if (txtbox.Text.Length == 0) return; try { mValue = Decimal.Parse(txtbox.Text); } catch { } txtbox.Text = ValueToText(mValue); } void TextBoxOnKeyDown(object objSrc, KeyEventArgs args) { switch (args.KeyCode) { case Keys.Enter: OnValueChanged(EventArgs.Empty); break; } } void ButtonOnClick(object objSrc, EventArgs args) {
Пользовательские элементы управления 187 ArrowButton btn = objSrc as ArrowButton; decimal mNewValue = Value; if (btn == btnl) if ((mNewValue -= Increment) < Minimum) return; if (btn == btn2) if ((mNewValue += Increment) > Maximum) return; Value = mNewValue; OnValueChanged(EventArgs.Empty); } protected override void OnLeave(EventArgs args) { base.OnLeave(args); OnValueChanged(EventArgs.Empty); } protected virtual void OnValueChanged(EventArgs args) { Value = Math.Max(Minimum, Value); Value = Math.Min(Maximum, Value); Value = Decimal.Round(Value, DecimalPlaces); if (ValueChanged != null) ValueChanged(this, args); } } } Внутри квадратных скобок в файле находятся атрибуты. Об атрибутах в начале файла с исходным кодом мы уже говорили в главе 1, а остальные определены в пространстве имен System.ComponentModel. Перед каждым открытым свойством стоят атрибуты Category и Description. Они нужны элементу управления PropertyGrid для группировки сходных свойств и их краткого описания. Стоящий непосредственно перед определением класса атрибут DefaultEvent нужен конструктору форм Visual Studio для определения, какое событие задействовать, когда разработчик пытается установить обработчик, дважды щелкая элемент управления. Обратите внимание, что этот класс определен как public (открытый), поэтому он виден извне DLL-библиотеки. Первый определенный в нем член — открытое событие ValueChanged. На форме NumericScan росположен один элемент управления TextBox и два ArrowButton. Как и в NumericUpDown, элемент управления предоставляет открытые
188 ГЛАВА 4 свойства Value, Minimum, Maximum, Increment и DecimalPlaces. Неплохо бы тщательнее поработать над проверкой корректности данных и поведения программы, чем это сделал я в данном примере. Например, элемент управления NumericUpDoivn платформы .NET генерирует несколько исключений, если программа присваивает Value значение, выходящее из диапазона, ограниченного Minimum и Maximum. Но вот чего уж точно не следует делать, так это реализовывать проверки так, чтобы программа сбоила, если присваивать значения свойствам в определенном порядке. Например, по умолчанию значения свойств Minimum и Maximum — 0 и 100 соответственно. А ведь в программе могут встретиться такие операторы: numscan.Minimum = 200; numscan.Maximum = 300; Если элемент управления генерирует исключение всякий раз, когда значение Minimum меньше Maximum, первое выражение вызовет исключение, а этого быть не должно. Практически весь оставшийся код класса посвящен обработке событий, поступающих от TextBox и ArrowButton. При изменении текста обработчик TextBoxOnText- Changed проверяет его и, если это не число, возвращает предыдущее значение. Он не отслеживает выхода за границы диапазона. (Такая же ситуация с элементом управления NumericUpDown — в него можно ввести любое число.) Как и в NumericUpDown, событие ValueChanged должно инициироваться при любом изменении значения с кнопок, а также когда пользователь нажимает Enter или элемент управления выходит из фокуса ввода. Именно здесь проверяется, не вышло ли число из диапазона. Отслеживание нажатия клавиши Enter обеспечивается установкой в элементе управления обработчика событий KeyDown поля TextBox. И здесь, признаюсь, я обнаружил, что чуть промахнулся, выбрав кнопки со стрелками вправо и влево: они должны управляться клавишами управления курсором со стрелками вправо и влево, а на самом деле эти клавиши нужны для перемещения внутри TextBox. Выходит, есть смысл в том, что для изменения значения в наборном счетчике используются именно стрелки вверх и вниз! Метод OnValueChanged в самом конце кода класса определяется как virtual (виртуальный), чтобы его легко было переопределить в программах, где нужно создавать подклассы NumericScan. Этот метод задает минимальное и максимальное значение диапазона, округляет его до нужного числа знаков после запятой и инициирует событие ValueChanged. Как уже упоминалось, в решении NumericScan есть два проекта: NumericScan, в котором создается файл NumericScan.dll, и TestProgram, в котором создается файл TestProgram.exe из следующего файла с исходным текстом.
Пользовательские элементы управления 189 TestProgram.cs И // TestProgram.cs (с) 2005 by Charles Petzold // using Petzold.ProgrammingWindowsForms; using System; using System.Drawing; using System.Windows.Forms; class TestProgram : Form { Label lbl; NumericScan numscanl, numscan2; [STAThread] public static void Main() { Application.EnableVisualStyles(); Application.Run(new TestProgramO); } public TestProgramO { Text = "Test Program"; FlowLayoutPanel pnl = new FlowLayoutPanelQ; pnl.Parent = this; pnl.Dock = DockStyle.Fill; numscanl = new NumericScanO; numscanl.Parent = pnl; numscanl.AutoSize = true; numscanl.ValueChanged += NumericScanOnValueChanged; numscan2 = new NumericScanO; numscan2.Parent = pnl; numscan2.AutoSize = true; numscan2.ValueChanged += NumericScanOnValueChanged; lbl = new LabelO; lbl.Parent = pnl; lbl.AutoSize = true; } void NumericScanOnValueChanged(object objSrc, EventArgs args) {
190 ГЛАВА 4 lbl.Text = "First: " + numscanl.Value + ", Second: " + numscan2.Value; } } Эта тестовая программа всего лишь содержит два элемента управления Numeric- Scan с установленными обработчиками событий и элемент Label, отображающий два значения. Задавая ссылки в проекте TestProgram, помимо стандартных DLL-библиотек System, System.Drawing и System.Windows.Forms нужно не забыть указать Numeric- Scan.dll. На вкладке Projects диалогового окна Add Reference выберите Numeric- Scan. Обратите внимание, что в файле TestProgram.cs также есть директива using для пространства имен PetzoldProgrammingWindowsForms. Чтобы добавить этот элемент управления в окно инструментария Visual Studio, щелкните в нем правой кнопкой одну из вкладок, выберите Add Tab и введите имя, например More Controls. Щелкните правой кнопкой новую вкладку и выберите Choose Items. С помощью кнопки Browse в диалоговом окне Choose Toolbox Items найдите файл NumericScan.dll. Следующая программа намного интенсивнее использует возможности элемента управления NumericScan. Ее первая часть — класс, производный от TableLayoutPanel Он отображает шесть элементов управления NumericScan, нужных для определения шести полей объекта матричного преобразования в платформе .NET Framework. Этот класс очень похож на программу MatrixElements из последней главы, но он более универсальный. Панели назначено открытое свойство Matrix, позволяющее задавать и получать значения элементов управления NumericScan в форме объекта Matrix. В панели также определено открытое событие Change, которое инициируется всякий раз, когда один из элементов NumericScan инициирует событие Value- Changed. Обратите внимание на первую директиву using со ссылкой на пространство имен элемента управления NumericScan. MatrixPanel.cs И // MatrixPanel.cs (с) 2005 by Charles Petzold // using Petzold.ProgrammingWindowsForms; using System; using System.Drawing; using System.Drawing.Drawing2D; using System.Windows.Forms; class MatrixPanel: TableLayoutPanel { public event EventHandler Change;
Пользовательские элементы управления 191 NumericScan[] numscan = new NumericScan[6]; public Matrix Matrix { set { for (int i = 0; i < 6; i++) numscan[i].Value = (decimal)value.Elements[i]; } get { return new Matrix((float)numscan[0].Value, (float)nurnscan[1].Value, (float)numscan[2].Value, (float)numscan[3].Value, (float)numscan[4].Value, (float)numscan[5].Value); } } public MatrixPaneK) { AutoSize = true; Padding = new Padding(Font.Height); ColumnCount = 2; SuspendLayoutQ; 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]; numscan[i] = new NumericScanO; 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 += NumericScanOnValueChanged; } ResumeLayoutO; Matrix = new MatrixO; }
192 ГЛАВА 4 void NumericScanOnValueChanged(object objSrc, EventArgs args) { OnChange(EventArgs.Empty); } protected virtual void OnChange(EventArgs args) { if (Change != null) Change(this, args); } } Поскольку TableLayoutPanel происходит от Control, а новый класс — от ТаЫе- LayoutPanel, значит ли это, что новый класс является пользовательским элементом управления? Конечно, да. Любой класс, прямо или косвенно наследующий Control, можно считать пользовательским элементом управления. Добавление новых свойств и событий делает элемент управления более полезным и пригодным для решения конкретных задач. А вершина «карьеры» пользовательского элемента управления — возможность повторного его использования в других приложениях. Вот еще один пример «элемента управления». Он является потомком Panel и отображает значение свойства Text после выполнения матричного преобразования, заданного через свойство Transform. Некоторые матричные преобразования вызывают исключения при попытке модификации объекта Graphics. При попытке выполнить неверное матричное преобразование объект Graphics генерирует исключение, а на экран выводится сообщение об исключительной ситуации. DisplayPanel, cs И // DisplayPanel.cs (с) 2005 by Charles Petzold // using System; using System.Drawing; using System.Drawing.Drawing2D; using System.Windows.Forms; class DisplayPanel : Panel { Matrix matx = new MatrixO; public DisplayPaneK) { ResizeRedraw = true; } public Matrix Transform {
Пользовательские элементы управления 193 set { matx = value; InvalidateO; } get { return matx; } } protected override void OnPaint(PaintEventArgs args) { Graphics grfx = args.Graphics; Brush brsh = new SolidBrush(ForeColor); try { grfx.Transform = matx; grfx.DrawString(Text, Font, brsh, Point.Empty); } catch (Exception exc) { StringFormat strfmt = new StringFormatO; strfmt.Alignment = strfmt.LineAlignment = StringAlignment.Center; grfx.DrawString(exc.Message, Font, brsh, ClientRectangle, strfmt); } brsh.Dispose(); } } Обычно панели не отображают свое свойство Text, поэтому никто не заботится о его определении. Правильное значение свойства Text и, при необходимости, Font присваивается в программе, где используется DisplayPanel Matrixinteractive — это не название нового блокбастера, а проект, включающий файлы MatrixPanel.cs, DisplayPanel.cs и Matrixlnteractive.cs. Matrixlnteractive.cs И // Matrixinteractive.cs (с) 2005 by Charles Petzold // using System; using System.Drawing; using System.Windows.Forms;
194 ГЛАВА 4 class Matrixlnteractive : Form { MatrixPanel matxpnl; DisplayPanel disppnl; [STAThread] public static void Main() { Application.EnableVisualStyles(); Application.Run(new MatrixInteractiveQ); } public MatrixInteractiveO { Text = "Matrix Interactive"; TableLayoutPanel pnl = new TableLayoutPanelO; pnl.Parent = this; pnl.Dock = DockStyle.Fill; pnl.ColumnCount = 2; matxpnl = new MatrixPanelO; matxpnl.Parent = pnl; matxpnl.Anchor = AnchorStyles.Left | AnchorStyles.Right; matxpnl.Change += MatrixPanelOnChange; disppnl = new DisplayPanelO; disppnl.Parent = pnl; disppnl.Dock = DockStyle.Fill; disppnl.BackColor = Color.White; disppnl.ForeColor = Color.Black; disppnl.Text = "Sample Text"; disppnl.Font = new Font(FontFamily.GenericSerif, 24); Width = 3 * matxpnl.Width; Height = 3 * matxpnl.Height / 2; } void MatrixPanelOnChange(object objSrc, EventArgs args) { disppnl.Transform = matxpnl.Matrix; } } Здесь клиентская область разделена на две части с помощью TableLayoutPanel Слева отцентрированный по вертикали располагается MatrixPanel, справа — объект DisplayPanel. цвет фона — белый, цвет текста — черный, значение свойства Text —
Пользовательские элементы управления 195 «Sample Text» и размер шрифта — 24 пт. Всякий раз, когда MatrixPanel инициирует событие Change, класс получает объект матричного преобразования от MatrixPanel и присваивает его DisplayPanel Вот снимок экрана программы. X Scale: YSheer X Shear: Y Scale: X Translate: V Trarwlate: 31 j 2.00] P 700~ "TO [ 191.00! * 1 * 1 Это сладкое слово — «автопрокрутка» У класса Form и многих классов Panel есть любопытная функция, «автопрокрутка», которая не нашла широкого применения, поскольку не получила всеобщего признания в качестве технологий обычного пользовательского интерфейса. Функция включается простым присваиванием свойствуAutoScroll этих классов значения true. Теперь, когда объект Form или Panel не вмещает все дочерние элементы управления, откуда ни возьмись, появляются полосы прокрутки, позволяющие просмотреть все недостающие элементы управления. Автопрокрутка реализована в ScrollableControl и доступна в любом элементе управления, восходящем к этому классу. Но не каждый элемент управления с полосами автопрокрутки — потомок ScrollableControl. Так, TextBox, ListBox и ScrollBar, в отличие от Form и Panel, не наследуют классу ScrollableControl. Не думаю, что автопрокрутка — наилучший способ разместить много элементов управления в маленьком диалоговом окне, но с ее помощью можно существенно упростить некоторые пользовательские элементы управления. Например, если на элементе управления предполагается отображать заведомо неизвестное, но большое число микрокартинок (thumbnails), представляющих графические файлы, вам достаточно разместить их на панели и включить автопрокрутку — она обеспечит правильное поведение интерфейса. Познакомимся еще с одним нестандартным элементом управления, ImageScan, который отображает микрокартинки всех графических файлов из указанной папки в виде прокручиваемого списка, где картинки расположены в один столбец.
196 ГЛАВА 4 Размер микрокартинки — дюйм на дюйм (1 дюйм = 2,54 см). ImageScan является потомком FlowLayoutControl, а микрокартинки реализованы с помощью элемента управления PictureBox. При создании этого элемента управления обнаружилась одна сложность. Хотелось, чтобы перемещение между микрокартинками выполнялось с помощью Tab или клавиш со стрелками. Но PictureBox в принципе неспособен получить фокус ввода и, следовательно, сообщить о таком событии. Поэтому сначала мне пришлось создать графическое окно с возможностью размещения его в фокусе и простым интерфейсом управления с клавиатуры. SelectablePictureBox.es И // SelectablePictureBox.cs (с) 2005 by Charles Petzold // using System; using System.Drawing; using System.Windows.Forms; class SelectablePictureBox : PictureBox { public SelectablePictureBox() { SetStyle(ControlStyles.Selectable, true); TabStop = true; } protected override void OnMouseDown(MouseEventArgs args) { base.OnMouseDown(args); Focus(); } protected override void OnKeyPress(KeyPressEventArgs args) { if (args.KeyChar == '\r') OnClick(EventArgs.Empty); else base.OnKeyPress(args); } protected override void OnEnter(EventArgs args) { base.OnEnter(args); Invalidate(); } protected override void OnLeave(EventArgs e) {
Пользовательские элементы управления 197 base.OnLeave(e); InvalidateO; } protected override void OnPaint(PaintEventArgs args) { base.OnPaint(args); if (Focused) { Graphics grfx = args.Graphics; grfx.DrawRectangle(new Pen(Brushes.Black, grfx.DpiX / 12), ClientRectangle); } } } Конструктор присваивает флагу ControlStylesSelectable и свойству TabStop значение true. Переопределенный метод OnPaint дает возможность методу OnPaint базового класса выполнить свою работу, а затем, если элемент управления в фокусе, обводит его черной рамкой. Также были расширены несколько других методов с префиксом On. По щелчку внутри области элемента управления он получает фокус ввода. При нажатии клавиши Enter элемент управления моделирует вызов OnClick. При попадании/выходе элемента управления из фокуса вызывается соответственно метод OnEnter или OnLeave. Эти переопределенные методы просто запрещают элементу управления вызывать OnPaint и обеспечивают его правильную отрисовку. Элемент управления ImageScan наследует классу FlowLayoutPanel. Обратите внимание на конструктор, присваивающий WrapContents значение false, zAutoScroll — true. ImageScan.cs // // ImageScan.es (с) 2005 by Charles Petzold // using System; using System.ComponentModel; // Для AsyncCompletedEventArgs using System.Drawing; using System.10; using System.Windows.Forms; class ImageScan : FlowLayoutPanel { Size szlmage; string strlmageLocation; ToolTip tips = new ToolTipO;
198 ГЛАВА 4 public ImageScanO { FlowDirection = FlowDirection.LeftToRight; WrapContents = false; AutoScroll = true; //Создание объекта Size размером "дюйм на дюйм". Graphics grfx = CreateGraphicsQ; szlmage = new Size((int)grfx.DpiX, (int)grfx.DpiY); // квадрат со стороной // 1 дюйм grfx.Dispose(); Height = szlmage.Height + Font.Height + Systemlnformation.HorizontalScrollBarHeight; } public string Directory { set { Controls.Clear(); tips.RemoveAHO; string[] astrFiles = System.10.Directory.GetFiles(value, "*.*"); foreach (string strFile in astrFiles) { PictureBox picbox = new SelectablePictureBoxO; picbox.Parent = this; picbox.Size = szlmage; picbox.SizeMode = PictureBoxSizeMode.Zoom; picbox.Click += PictureBoxOnClick; picbox.LoadCompleted += PictureBoxOnLoadCompleted; picbox.LoadAsync(strFile); } } } public string SelectedlmageFile { get { return strlmageLocation; } } void PictureBoxOnClickCobject objSrc, EventArgs args)
Пользовательские элементы управления 199 PictureBox picbox = objSrc as PictureBox; strlmageLocation = picbox.ImageLocation; OnClick(args); } //He инициировать событие Click, когда пользователь щелкает в области панели. protected override void OnMouseDown(MouseEventArgs args) { } void PictureBoxOnLoadCompleted(object objSrc, AsyncCompletedEventArgs args) { PictureBox picbox = objSrc as PictureBox; if (args.Error == null) tips.SetToolTip(picbox, Path.GetFileName(picbox.ImageLocation)); else Controls.Remove(picbox); } } На этом элементе управления, в сущности, размещается набор элементов управления SelectablePictureBox, каждый из которых соответствует одному графическому файлу в папке. Компонент ToolTips служит для отображения имен файлов в виде вплывающих подсказок. ImageScan реализует открытое свойство Directory, содержащее информацию о папке на диске. При задании этого свойства элемент управления сначала удаляет имеющийся набор дочерних элементов управления и все вплывающие подсказки. Затем он получает информацию обо всех файлах папки и создает для каждого объект SelectablePictureBox. Но постойте! Наш элемент управления должен отображать только графические файлы, a SelectablePictureBox создается для всех файлов, и не только графических. Обратите внимание на две особенности элементов управления SelectablePictureBox. Во-первых, устанавливается обработчик события LoadCompleted, и, во-вторых, для файла вызывается метод LoadAsync объекта PictureBox. Этот метод загружает файл в дополнительном потоке и по завершении загрузки инициирует событие Load- Completed. Это событие предоставляется с объектом типа AsyncCompletedEventArgs. При успешной загрузке файла свойству Error этого объекта присваивается значение null. Если же файл загрузить не удалось (для этой программы это не редкость, потому что в папке может быть много файлов других типов) элемент управления удаляет его из набора своих дочерних элементов управления. Вот программа, в которой используется ImageScan. В проект ImageDirectory входят файлы SelecablePictureBox.cs, ImageScan.cs и этот файл.
200 ГЛАВА 4 lmageDirectory.es И // ImageDirectory.es (с) 2005 by Charles Petzold // using System; using System.Drawing; using System.Windows.Forms; class ImageDirectory: Form { PictureBox picbox; ImageScan imgscan; Label lblDirectory; [STAThread] public static void Main() { Application.EnableVisualStyles(); Application.Run(new ImageDirectory()); } public ImageDirectoryO { Text = "Image Directory"; picbox = new PictureBoxO; picbox.Parent = this; picbox.Dock = DockStyle.Fill; picbox.SizeMode = PictureBoxSizeMode.Zoom; imgscan = new ImageScan(); imgscan.Parent = this; imgscan.Dock = DockStyle.Top; imgscan.Click += ImageScanOnClick; FlowLayoutPanel pnl = new FlowLayoutPanelO; pnl.Parent = this; pnl.AutoSize = true; pnl.Dock = DockStyle.Top; Button btn = new ButtonO; btn.Parent = pnl; btn.AutoSize = true; btn.Anchor = AnchorStyles.Left; btn.Text = "Directory..."; btn.Click += ButtonOnClick;
Пользовательские элементы управления 201 lblDirectory = new LabelQ; lblDirectory.Parent = pnl; lblDirectory.AutoSize = true; lblDirectory.Anchor = AnchorStyles.Right; // Инициализация. imgscan.Directory = lblDirectory.Text = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments); } void ButtonOnClick(object objSrc, EventArgs args) { FolderBrowserDialog dig = new FolderBrowserDialogQ; dlg.SelectedPath = lblDirectory.Text; dlg.ShowNewFolderButton = false; if (dlg.ShowDialogO == DialogResult.OK) imgscan.Directory = lblDirectory.Text = dlg.SelectedPath; } void ImageScanOnClick(object objSrc, EventArgs args) { picbox.ImageLocation = imgscan.SelectedlmageFile; } } Эта программа организует элементы управления в панели и объединяет их. Button вызывает FolderBrowserDialog, диалоговое окно, где пользователь выбирает нужную папку, a Label отображает путь к ней. ImageScan отображает графические файлы из этой папки, а выбранные изображения занимают остальную часть клиентской области программы. На этом снимке экрана показано изображение, выбранное в папке Windows.
202 ГЛАВА 4 При выборе папки, содержащей много неграфических файлов, практически сразу активизируется функция автопрокрутки объекта ImageScan для всех его дочерних элементов управления SelectablePictureBox. Неграфические файлы по мере их обнаружения удаляются, и полоса прокрутки может вовсе исчезнуть. Это немного необычное поведение, но оно не мешает программе решать поставленную задачу Создание элементов управления «с нуля» Есть элементы управления, сильно отличающиеся от стандартных, — для них приходится писать собственный код прорисовки и обработки пользовательского ввода. Их создание лучше начать с производного от Control класса и практически сразу переходить к кропотливому написанию кода. Я приведу два практических примера: линейку, похожую на ту, которая появляется над документом в текстовых редакторах (например, в WordPad), и простую сетку для выбора цвета. Интерактивная линейка Познакомимся поближе с программой Windows WordPad. Она собрана на основе элемента управления, который в API-интерфейсе Windows называется RicbEdit. В Windows Forms он доступен через класс RicbTextBox. Элемент управления RicbEdit сохраняет документы в формате RTF, что позволяет применять форматирование абзацев и символов к различным частям документа. На линейке в окне WordPad можно интерактивно задавать отступ слева, справа и для первой строки, а также табуляцию в выбранном абзаце. [Те же параметры форматирования можно изменить, выбрав команды Абзац (Paragraph) и Табуляция (Tabs) в меню Формат (Format)]. Посмотрим на линейку в более сложном текстовом редакторе, скажем, в Microsoft Office Word. На первый взгляд, она похожа на свою «сестру» из WordPad, но позволяет менять левое и правое поля. Эти поля применяются ко всему документу [их также можно задать, выбрав команду Параметры страницы (Page Setup) в меню Файл (File)]. Поля имеют смысл только в связи с шириной листа бумаги — это области справа и слева страницы (обычно шириной около дюйма), где текст не печатается. Таким образом, ширина текста на странице равна ширине страницы за вычетом левого и правого полей. В Word отступ абзаца слева и справа обычно равен нулю, то есть абзацы занимают всю ширину страницы от левого до правого поля. Отступ можно увеличить, чтобы сделать абзац уже, или сделать его отрицательным — в этом случае текст займет часть поля. Также можно задать отступ первой строки абзаца, чтобы она либо была смещена вправо относительно остальной части абзаца, либо «нависала» слева над абзацем. Левое поле принято отмерять от левого края страницы, отступ слева — от левого поля, а отступ первой строки — от отступа слева. Величина левого поля положительна для обычного отступа, отрицательна — для обратного отступа и нулевая,
Пользовательские элементы управления 203 если первая строка должна быть без отступа. Правое поле измеряется от правого края страницы, а отступ справа — от правого поля. Обычно это положительные величины. Сначала я хотел создать элемент управления DocumentRuler (так я его назвал) для задания полей и отступов, похожий на линейку в Microsoft Word. Но в RichTextBox, в отличие от Word, работа с полями поддерживается гораздо хуже. Нечто подобное можно сделать с помощью его свойства RigbtMargin, задающего ширину отображаемого текста в пикселах. Иначе говоря, значение этого свойства примерно равно ширине страницы за вычетом левого и правого полей. По умолчанию Right- Margin равно 0. Это значит, что ширина текста, отображаемого элементом управления, определяется шириной самого элемента. Какое-то время я, забавы ради, пытался применить традиционные концепции ширины и полей страницы к RichTextBox, но в итоге предпочел более простой подход. Моя линейка похожа на линейку WordPad, но дополнительно позволяет менять ширину текста. Хотя DocumentRuler не требует обязательного использования с RichTextBox, она проста и вполне соответствует небогатым возможностям этого элемента управления. Для настоящих текстовых редакторов, таких как Word, ее необходимо улучшить. Я подсчитал, что при ширине страницы 81/2 дюйма и ширине полей V/4 дюйма свойству RigbtMargin можно присвоить начальное стандартное значение в б дюймов. Обычно поля, отступы и табуляцию измеряют в дюймах, особенно если над документом есть линейка. Но в RichTextBox все не так: RigbtMargin и все задающие отступ свойства указываются в пикселах. Я решил в интерфейсе программы для линейки использовать дюймы. У линейки будут свойства Leftlndent, Rightlndent и FirstLinelndent типа float, измеряемые в дюймах. Из-за такого решения, мне потребуется многократно преобразовывать дюймы в пикселы и обратно как в самом элементе управления (линейке), так и программе. Разумеется, поскольку линейка отображается на экране, эти преобразования зависят от его разрешения, которое извлекается из свойств DpiX и DpiY объекта Graphics. Еще больше преобразований останутся невидимыми. В RTF-формате принята единица измерения «твип» (twip), равная 72опУнкта или Ум4оДюйма- Хотя разметка в дюймах на линейке DocumentRuler основана на разрешении экрана, мне не удалось сделать другие свойства линейки настолько независимыми от аппаратного обеспечения, как хотелось бы. В особенности, мелкие перемещаемые маркеры отступов слишком малы и созданы в точности с учетом различных размеров, вычисляемых в зависимости от разрешения экрана. Преобразование дюймов и пикселов — не единственное необходимое в этой программе. Три свойства RichTextBox, связанные с отступами, называются Selection- Indent, SelectionRightlndent и SelectionHanginglndent. К сожалению, поведение первых двух немного отличается из привычных методов форматирования абзацев. Selection- Indent — это отступ первой строки абзаца от левого края текстового поля, a Selection-
204 ГЛАВА 4 Hanginglndent — отступ остальной части абзаца по отношению к его первой строке. Если отступ абзаца от левого края текстового поля равен 100, а отступ первой строки — 50 пикселам, то значение Selectionlndent равно 150, a SelectionHanging- Indent —50. Я решил реализовать отступы в DocumentRuler в более привычном виде, а необходимые преобразования единиц измерения возьмет на себя программа, создающая RichTextBox и DocumentRuler. Теперь мы готовы познакомиться с кодом. DocumentRuler реализует лишь одно событие, которое я, не мудрствуя лукаво, назвал Change. Оно инициируется всякий раз, когда пользователь изменяет на линейке поле, отступ или табуляцию. Я хотел, чтобы использующая эту линейку программа, узнавала о внесенных изменениях (например, об изменении отступа слева), и чтобы эти сведения передавались через событие. Его нельзя создать на основе стандартного делегата EventHandler, поэтому потребовался пользовательский делегат. Сначала определяется перечисление с полями для всех параметров, определяемых с помощью линейки. RulerProperty.cs И // RulerProperty.cs (с) 2005 by Charles Petzold // public enum RulerProperty { TextWidth, Leftlndent, Rightlndent, FirstLinelndent, Tabs } С событием Change предоставляется объект типа RulerEventArgs. Этот класс восходит к EventArgs, но реализует дополнительное свойство типа RulerProperty. RulerEventArgs.cs // // RulerEventArgs.cs (с) 2005 by Charles Petzold // using System; public class RulerEventArgs : EventArgs { RulerProperty rlrprop; public RulerEventArgs(RulerProperty rlrprop) {
Пользовательские элементы управления 205 this.rlrprop = rlrprop; } public RulerProperty RulerChange { get { return rlrprop; } set { rlrprop = value; } } } В классе также определен конструктор создания нового объекта RulerEventArgs путем указания члена RulerProperty. При определении нового класса, передающего сведения обработчику событий, нужно также определить новый делегат для этого обработчика. Этот код прост. RulerEventHandler.es И // RulerEventHandler.cs (с) 2005 by Charles Petzold // public delegate void RulerEventHandler(object objSrc, RulerEventArgs args); На линейке есть четыре мелких графических объекта, обозначающих отступы слева и справа, отступ первой строки и табуляцию. Эти значки не только должны быть отрисованы — линейка также должна определять, когда их щелкают кнопкой мыши. Я решил реализовать эти объекты в отдельных классах, производных от одного абстрактного класса RulerSlider. RulerSlider.cs // // RulerSlider.cs (с) 2005 by Charles Petzold // using System; using System.Drawing; using System.Drawing.Drawing2D; using System.Windows.Forms; abstract class RulerSlider { // Закрытые поля. RulerProperty rlrprop; float fValue; int x, y; Bitmap bm; Region rgn;
206 ГЛАВА 4 // Открытые свойства. public RulerProperty RulerProperty { get { return rlrprop; } set { rlrprop = value; } } public float Value { get { return fValue; } set { fValue = value; } } public int X { get { return x; } set { x = value; } } public virtual Rectangle Rectangle { get { return new Rectangle(X - bm.Width / 2, Y, bm.Width, bm.Height); } } // Защищенное свойство. protected int Y { get { return y; } set { у = value; } } // Открытые методы. public virtual void Draw(Graphics grfx) { grfx.DrawImage(bm, X - bm.Width / 2, Y); } public virtual bool HitTest(Point pt) { return rgn.IsVisible(pt.X - X + bm.Width / 2, pt.Y - Y); } protected void CreateBitmap(int ex, int cy, Point[] apt) { bm = new Bitmap(cx, cy); GraphicsPath path = new GraphicsPathO; path.AddLines(apt); rgn = new Region(path);
Пользовательские элементы управления 207 Graphics grfx = Graphics.Fromlmage(bm); grfx.FillPolygon(Brushes.LightGray, apt); grfx.Clip = rgn; Shading(grfx, Pens.White, 1, apt); Shading(grfx, Pens.Gray, -1, apt); grfx.ResetClipO; grfx.DrawPolygon(Pens.Black, apt); grfx.Dispose(); } void Shading(Graphics grfx, Pen pn, int iOffset, Point[] apt) { grfx.TranslateTransform(iOffset, 0); grfx.DrawPolygon(pn, apt); grfx.TranslateTransform(-iOffset, iOffset); grfx.DrawPolygon(pn, apt); grfx.TranslateTransform(0, -iOffset); } } Как вы вскоре увидите, различные классы, потомки RulerSlider, сами приравнивают свойство RulerProperty соответствующему члену перечисления RulerProperty, а свойство У— фиксированному положению ползунка относительно верха элемента управления. Значение свойства X меняется по мере перемещения ползунка. Я также решил, что у этих ползунков должно быть свойство Value типа float, хранящее текущее значение в дюймах. Теоретически, преобразование между Value и X обратимо, но я хотел оставить значение с плавающей точкой нетронутым, чтобы избежать ошибок округления в ходе преобразования. Это позволит исключить ситуации, когда свойству Leftlndent присваивается одно значение, а затем оно возвращает практически такое же, но все-таки чуть отличное значение. Метод Draw объекта RulerSlider отрисовывает ползунок с координатами X и У. В RulerSlider метод Draw просто отрисовывает битовую карту, созданную в защищенном методе CreateBitmap. В этот метод передается ширина и высота рисунка и массив объектов Point. Этот массив определяет замкнутую область на рисунке, отрисованную заданным цветом и оттенением. Также создается объект Region, используемый в методе HitTest. Если HitTest передать координаты размещенного над объектом указателя мыши, он вернет значение true. Свойство Rectangle, доступное только для чтения, возвращает прямоугольник, ограничивающий растровое изображение на линейке. Оно используется для отключения областей линейки при перемещении ползунка. Вот класс Rightlndent, производный от RulerSlider.
208 ГЛАВА 4 Rightlndent.es И // Rightlndent.cs (с) 2005 by Charles Petzold // using System; using System.Drawing; using System.Windows.Forms; class Rightlndent : RulerSlider { public RightlndentO { RulerProperty = RulerProperty.Rightlndent; Y = 9; CreateBitmap(9, 8, new Point[] { new Point(0, 7), new Point(0, 4), new Point(4, 0), new Point(8, 4), new Point(8, 7), new Point(0, 7) }); } } Этот класс просто задает значения двух свойств RulerSlider и вызывает метод CreateBitmap для создания изображения, похожего на маленький домик. Leftlndent отличается от Rightlndent лишь тем, что его изображение немного сложнее. Leftlndent.cs II // Leftlndent.cs (с) 2005 by Charles Petzold // using System; using System.Drawing; using System.Windows.Forms; class Leftlndent : RulerSlider { public LeftlndentO { RulerProperty = RulerProperty.Leftlndent; Y = 9; CreateBitmap(9, 14, new Point[] { new Point(0, 7), new Point(0, 4), new Point(4, 0), new Point(8, 4), new Point(8, 7), new Point(0, 7), new Point(0, 13), new Point(8, 13),
Пользовательские элементы управления 209 new Point(8, 7), new Point(0, 7) }); } } FirstLinelndent похож на Rigbtlndent, но его ползунок перевернут и расположен вверху элемента управления. FirstLinelndent.cs И // FirstLinelndent.cs (с) 2005 by Charles Petzold // using System; using System.Drawing; using System.Windows.Forms; class FirstLinelndent : RulerSlider { public FirstLinelndentO { RulerProperty = RulerProperty.FirstLinelndent; Y = 1; CreateBitmap(9, 8, new Point[] { new Point(0, 0), new Point(8, 0), new Point(8, 3), new Point(4, 7), new Point(0, 3), new Point(0, 0) }); } } Символ Tab (L-образной формы) больше подходит для простой линии. В классе переопределены методы Draw и HitTest, а также свойство Rectangle. Tab.cs // // Tab.cs (с) 2005 by Charles Petzold // using System; using System.Drawing; using System.Windows.Forms; class Tab : RulerSlider { public Tab() {
210 ГЛАВА 4 RulerProperty = RulerProperty.Tabs; Y = 9; } public override void Draw(Graphics grfx) { Pen pn = new Pen(Color.Black, 2); grfx.DrawLine(pn, X, Y, X, Y + 4); grfx.DrawLine(pn, X, Y + 4, X + 4, Y + 4); } public override bool HitTest(Point pt) { return pt.X >= X - 1 && pt.X <= X + 1 && pt.Y >= Y - 1 && pt.Y <= Y + 6; } public override Rectangle Rectangle { get { return new Rectangle(X - 1, Y - 1, 6, 6); } } } Последний класс — TextWidth, он потомок RulerSlider. Изменение ширины текста немного отличается от перемещения маркеров. Отрисовка выполняется в главном методе OnPaint элемента управления, поэтому у TextWidth немного работы. TextWidth.cs И // TextWidth.cs (с) 2005 by Charles Petzold // using System; using System.Drawing; using System.Windows.Forms; class TextWidth : RulerSlider { public TextWidthO { RulerProperty = RulerProperty.TextWidth; } public override void Draw(Graphics grfx) { } public override bool HitTest(Point pt)
Пользовательские элементы управления 211 < return (pt.X >= X - 2) && (pt.X <= X + 2); } public override Rectangle Rectangle { get { return Rectangle.Empty; } } } Теперь можно перейти к созданию класса RulerDocument. Для удобства я разделил его на два файла с исходным текстом с помощью ключевого слова partial. В первый файл вошли конструктор и все свойства. DocumentRuler1.cs // // DocumentRuler1.cs (с) 2005 by Charles Petzold // using System; using System.Collections.Generic; using System.Drawing; using System.Windows.Forms; public partial class DocumentRuler : Control { // Закрытые поля. int iLeftMargin; float fDpi; Control ctrlDocument; // Объекты RulerSlider. Leftlndent rsLeftlndent = new LeftlndentQ; Rightlndent rsRightlndent = new Rightlndent(); FirstLinelndent rsFirstlndent = new FirstLineIndent(); TextWidth rsTextWidth = new TextWidthQ; List<RulerSlider> rsCollection = new List<RulerSlider>(); // Конструктор, public DocumentRulerO { Dock = DockStyle.Top; ResizeRedraw = true; TabStop = false; Height = 23; Font = new Font(Font.Name, 14, GraphicsUnit.Pixel);
212 ГЛАВА 4 Graphics grfx = CreateGraphicsQ; fDpi = grfx.DpiX; grfx.DisposeQ; rsCollection.Add(rsLeftlndent); rsCollection.Add(rsRightlndent); rsCollection.Add(rsFirstlndent); rsCollection.Add(rsTextWidth); // Открытые свойства, public float TextWidth { get { return rsTextWidth.Value; } set { rsTextWidth.Value = value; CalculateDisplayOffsets(); } } public float Leftlndent { get { return rsLeftlndent.Value; } set { rsLeftlndent.Value = value; CalculateDisplayOffsets(); } } public float Rightlndent { get { return rsRightlndent.Value; } set { rsRightlndent.Value = value; CalculateDisplayOffsets(); } } public float FirstLinelndent { get { return rsFirstlndent.Value; } set { rsFirstlndent.Value = value; CalculateDisplayOffsets(); }
Пользовательские элементы управления 213 } public float[] Tabs { get { List<float> fTabs = new List<float>(); foreach (RulerSlider rs in rsCollection) if (rs is Tab) fTabs.Add(rs.Value); // Для RichTextBox табуляторы должны перечисляться по порядку. float[] afTabs = fTabs.ToArrayO; Array.Sort(afTabs); return afTabs; } set { // Сначала удалить табуляторы, не входящие в массив значений. List<Tab> rsTabsDelete = new List<Tab>(); foreach (RulerSlider rs in rsCollection) if (rs is Tab && (Array.IndexOf(value, rs.Value) == -1)) rsTabsDelete.Add(rs as Tab); foreach (Tab tab in rsTabsDelete) { rsCollection.Remove(tab); Invalidate(tab.Rectangle); } // Затем добавить табуляторы, которых нет в rsCollection. foreach (float fTab in value) { bool bAdd = true; foreach (RulerSlider rs in rsCollection) if (rs is Tab && rs.Value == fTab) bAdd = false; if (bAdd) { Tab tab = new Tab(); tab.Value = fTab; tab.X = LeftMargin + InchesToPixels(fTab); rsCollection.Add(tab);
214 ГЛАВА 4 Invalidate(tab.Rectangle); } } } } public int LeftMargin { get { return iLeftMargin; } set { iLeftMargin = value; CalculateDisplayOffsets(); } // Для отображения линии при перемещении ползунков. public Control DocumentControl { get { return ctrlDocument; } set { ctrlDocument = value; } } // Эти два метода вычисляют значение X для четырех типов ползунков // (кроме табуляторов). При изменении X объявить недействительным прямоугольник // в прежнем и новом положении. void CalculateDisplayOffsets() { CalculateDisplayOffsets2(rsTextWidth, LeftMargin + InchesToPixels(rsTextWidth.Value)); CalculateDisplayOffsets2(rsLeftIndent, LeftMargin + InchesToPixels(rsLeftlndent.Value)); CalculateDisplayOffsets2(rsRightIndent, LeftMargin + InchesToPixels(TextWidth - rsRightlndent.Value)); CalculateDisplayOffsets2(rsFirstIndent, LeftMargin + InchesToPixels(Leftlndent + rsFirstlndent.Value)); } void CalculateDisplay0ffsets2(RulerSlider rs, int xNew) { if (rs.X != xNew) { Invalidate(rs.Rectangle); rs.X = xNew; Invalidate(rs.Rectangle); } } float PixelsToInches(int i)
Пользовательские элементы управления 215 { return i / fDpi; } int InchesToPixels(float f) { return (int)Math.Round(f * fDpi); } } Поля определяют различные объекты RulerSlider так, что rsLeftlndent — это объект типа Leftlndent, rsRightlndent — объект типа Rightlndent и так далее. Таюке создается набор List типа RulerSlider с именем rsCollection. В него конструктор добавляет все объекты RulerSlider. В итоге он также должен содержать все объекты типа Tab. Затем в файле определяются открытые свойства, дающие доступ к соответствующим объектам: TextWidth — к объекту rsTextWidth, Leftlndent — к rsLeftlndent и так далее. Когда свойству Value присваивается объект RulerSlider, CalculateDisplayOffset вычисляет значение X для каждого из этих свойств. Код свойства Tabs длиннее кода других свойств, просто потому что оно охватывает не один, а несколько объектов. У каждого абзаца может быть свой набор табуляторов. В общем случае, по мере отображения на линейке информации о различных частях документа старые табуляторы в rsCollection должны удаляться и заменяться новыми. Изначально я так и сделал, но из-за постоянного стирания и перерисовывания в процессе ввода в RichTextBox маркеры мерцали и дрожали. Представленный вашему вниманию код не удаляет и не воссоздает маркеры на том же месте. Обратите внимание на открытое свойство LeftMargin типа int, которое предоставляет доступ к полю iLeftMargin и используется почти при каждом вычислении. Его следовало назвать Kludge (клудж, то есть часть программы, которая теоретически не должна работать, но почему-то работает). Для удобства восприятия на линейке должно быть немного места слева от нулевого положения для отображения ползунка левого отступа. Поэтому в следующей программе, объединяющей элементы управления RichTextBox и DocumentRuler, я поступил как WordPad: присвоил свойству ShowSelectionMargin объекта RichTextBox значение true. Это позволяет оставить немного свободного места слева, чтобы пользователь мог выбирать целые строки текста. Но сколько? 10 пикселов вполне достаточно, и в следующей программе свойству LeftMargin объекта DocumentRuler присвоено это значение. Мне не нравится такой подход и боюсь, что это решение будет преследовать меня до конца жизни, но ничего лучшего мне в голову не приходит. Последнему открытому свойству в DocumentRuler, DocumentControl, присваивается элемент управления для обработки текста, связанный с DocumentRuler. Это свойство нужно DocumentRuler для отображения вертикальной линии под элементом управления, когда пользователь перемещает один из ползунков. Прочее взаи-
216 ГЛАВА 4 модействие между DocumentRuler и RichTextBox не затрагивает напрямую эти два элемента управления. Первая часть класса DocumentRuler посвящена вводу/выводу для программ, использующих этот элемент управления, а его вторая часть уделяется вводу и выводу информации об операциях пользователя. В ней также переопределены методы OnPaint, OnMouseDown, OnMouseMove и OnMouseUp. DocumentRuler2.cs И // DocumentRuler2.cs (с) 2005 by Charles Petzold // using System; using System.Collections.Generic; using System.Drawing; using System.Windows.Forms; public partial class DocumentRuler : Control { // Открытое событие. public event RulerEventHandler Change; // Закрытые поля, используемые при перемещении мыши. RulerSlider rsDragging; Point ptDown; int xOriginal; int xLineOverTextBox; // Метод OnPaint выполняет почти всю отрисовку. protected override void OnPaint(PaintEventArgs args) { Graphics grfx = args.Graphics; Rectangle rect = new Rectangle(LeftMargin, 0, rsTextWidth.X - LeftMargin, Height - 4); grfx.FillRectangle(Brushes.White, rect); ControlPaint.DrawBorder3D(grfx, rect); for (int i = 1; i < 8 * PixelsToInches(Width); i++) { int x = LeftMargin + InchesToPixels(i / 8f); if (i X 8 == 0) { StringFormat strfmt = new StringFormatQ; strfmt.Alignment = strfmt.LineAlignment = StringAlignment.Center;
Пользовательские элементы управления 217 grfx.DrawString((i / 8).ToString(), Font, Brushes.Black, x, 9, strfmt); else if (i X 4 == 0) grfx.Drawl_ine(Pens.Black, x, 7, x, 10); else grfx.Drawl_ine(Pens.Black, x, 8, x, 9); } // Отображение всех ползунков, foreach (RulerSlider rs in rsCollection) rs.Draw(grfx); return; } // OnMouseDown для перемещения ползунков и создания табуляторов. protected override void OnMouseDown(MouseEventArgs args) { // Игнорировать, если это не левая кнопка. if ((args.Button & MouseButtons.Left) == 0) return; // Перебор существующих ползунков для поиска ползунка, // который щелкнул пользователь, foreach (RulerSlider rs in rsCollection) if (rs.HitTest(args.Location)) { rsDragging = rs; ptDown = args.Location; xOriginal = rsDragging.X; if (rsDragging is TextWidth) Cursor.Current = Cursors.SizeWE; DrawReversibleLine(xLineOverTextBox = args.X); return; } // Если такого табулятора нет, создать новый. rsDragging = new Tab(); rsCollection.Add(rsDragging); ptDown = args.Location; xOriginal = rsDragging.X = ptDown.X;
218 ГЛАВА 4 Invalidate(rsDragging.Rectangle); DrawReversibleLine(xLineOverTextBox = args.X); return; } // OnMouseMove для перемещения ползунков. protected override void OnMouseMove(MouseEventArgs args) { if (!Capture) // то есть, когда кнопка мыши не нажата. { //Если превышено значение TextWidth, изменить вид указателя, if (!rsRightIndent.HitTest(args.Location) && rsTextWidth.HitTest(args.Location)) Cursor.Current = Cursors.SizeWE; return; } // Если rsDragging не равно null, выполняется перемещение курсора. if (rsDragging != null) { if (rsDragging is TextWidth) Cursor.Current = Cursors.SizeWE; int xNow = xOriginal - ptDown.X + args.X; // Ползунки не должны выходить за пределы текста, if (rsDragging is Tab && (xNow < LeftMargin || xNow > rsTextWidth.X)) return; if ((rsDragging == rsLeftlndent || rsDragging == rsFirstlndent) && (xNow < LeftMargin || xNow > rsRightlndent.X)) return; if (rsDragging == rsRightlndent && (xNow > rsTextWidth.X || xNow < rsLeftlndent.X || xNow < rsFirstlndent.X)) return; if (rsDragging == rsTextWidth && xNow < rsRightlndent.X) return; if (rsDragging == rsTextWidth) { Invalidate(new Rectangle(Math.Min(rsDragging.X, xOriginal) - 1, 0, Math.Abs(rsDragging.X - xOriginal) + 2, Height)); rsDragging.X = xNow; }
Пользовательские элементы управления 219 else { // Обновить свойство X ползунка и объявить недействительными // старое и новое значения. Invalidate(rsDragging.Rectangle); rsDragging.X = xNow; Invalidate(rsDragging.Rectangle); } // Переместить линию над текстовым полем. DrawReversibleLine(xLineOverTextBox); DrawReversibleLine(xLineOverTextBox = args.X); } // OnMouseUp - новое положение ползунка. protected override void OnMouseUp(MouseEventArgs args) { if (rsDragging != null) { // Вычисляем новые свойства Value и инициируем событие. if (rsDragging == rsLeftlndent || rsDragging == rsFirstlndent) { rsLeftlndent.Value = PixelsToInches(rsLeftlndent.X - LeftMargin); rsFirstlndent.Value = PixelsToInches(rsFirstlndent.X - rsLeftlndent.X); OnChange(new RulerEventArgs(rsDragging.RulerProperty)); } else if (rsDragging == rsRightlndent || rsDragging == rsTextWidth) { rsTextWidth.Value = PixelsToInches(rsTextWidth.X - LeftMargin); rsRightlndent.Value = PixelsToInches(rsTextWidth.X - rsRightlndent.X); OnChange(new RulerEventArgs(rsTextWidth.RulerProperty)); OnChange(new RulerEventArgs(rsRightlndent.RulerProperty)); } else if (rsDragging is Tab) { rsDragging.Value = PixelsToInches(rsDragging.X - LeftMargin); OnChange(new RulerEventArgs(rsDragging.RulerProperty)); } // Прекращение операции перемещения. rsDragging = null; DrawReversibleLine(xLineOverTextBox); }
220 ГЛАВА 4 // Отрисовка линии под текстовым полем в экранных координатах. void DrawReversibleLine(int x) { if (ctrlDocument != null) { Point pt1 = ctrlDocument.PointToScreen(new Point(x, 0)); Point pt2 = ctrlDocument.PointToScreen( new Point(x, ctrlDocument.Height)); ControlPaint.DrawReversibleLine(pt1, pt2, ctrlDocument.BackColor); } } // Метод OnChange инициирует событие Change, protected virtual void OnChange(RulerEventArgs args) { if (Change != null) Change(this, args); } } Метод OnPaint проще, чем могло бы быть, благодаря двум строкам кода ближе к КОНЦу: foreach (RulerSlider rs in rsCollection) rs.Draw(grfx); Объекты RulerSlider отрисовываются сами, поэтому метод OnPaint здесь не нужен. Они также сами выполняют проверку на щелчок кнопкой мыши, а метод OnMouseDotvn использует их, чтобы определить, щелкнул ли пользователь имеющийся ползунок. Если нет, пользователю нужен новый табулятор. В любом случае, OnMouseDotvn создает несколько закрытых полей, служащих для перемещения ползунков: rsDragging (перемещаемый в настоящий момент ползунок),ptDown (исходная точка, где пользователь щелкнул мышью) и xOriginal (исходное положение ползунка). Метод OnMouse- Move в основном предназначен для контроля, чтобы ползунки не выходили за пределы допустимого диапазона положений. В частности, свойство SelectionRightlndent объекта RichTextBox не принимает отрицательное значение. Это значит, что правое поле нельзя переместить левее отступа справа. Операцию перемещения завершает OnMouseUp. Класс RichTextWitbRuler позволяет воочию познакомиться с линейкой и опробовать ее в действии. Он является потомком Form и создает элементы управления RichTextBox и DocumentRuler. В проект RichTextWitbRuler входит этот файл и все остальные файлы, следующие за RulerProperty.cs.
Пользовательские элементы управления 221 RichTextWithRuler.cs И // RichTextWithRuler.cs (с) 2005 by Charles Petzold // using System; using System.Drawing; using System.Windows.Forms; class RichTextWithRuler : Form { DocumentRuler ruler; RichTextBox txtbox; float fDpi; [STAThread] public static void Main() { Application.EnableVisualStyles(); Application.Run(new RichTextWithRulerO); } public RichTextWithRulerO { Text = "RichText with Ruler"; Graphics grfx = CreateGraphicsO; fDpi = grfx.DpiX; grfx.Dispose(); txtbox = new RichTextBox(); txtbox.Parent = this; txtbox.AcceptsTab = true; txtbox.Dock = DockStyle.Fill; txtbox.RightMargin = InchesToPixels(6); txtbox.ShowSelectionMargin = true; txtbox.SelectionChanged += TextBoxOnSelectionChanged; ruler = new DocumentRulerQ; ruler.Parent = this; ruler.LeftMargin = 10; ruler.TextWidth = PixelsToInches(txtbox.RightMargin); ruler.DocumentControl = txtbox; ruler.Change += RulerOnChange; // Инициализация линейки со значениями текстового поля. TextBoxOnSelectionChanged(txtbox, EventArgs.Empty);
222 ГЛАВА 4 } void TextBoxOnSelectionChanged(object objSrc, EventArgs args) { ruler. Leftlndent = PixelsToInches(txtbox.SelectionIndent + txtbox.SelectionHanginglndent); ruler.Rightlndent = PixelsToInches(txtbox.SelectionRightlndent); ruler.FirstLinelndent = PixelsToInches(-txtbox.SelectionHanginglndent); float[] fTabs = new float[txtbox.SelectionTabs.Length]; for (int i = 0; i < txtbox.SelectionTabs.Length; i++) fTabs[i] = PixelsToInches(txtbox.SelectionTabs[i]); ruler.Tabs = fTabs; } void RulerOnChange(object objSrc, RulerEventArgs args) { switch (args.RulerChange) { case RulerProperty.TextWidth: txtbox.RightMargin = InchesToPixels(ruler.TextWidth); break; case RulerProperty.Leftlndent: case RulerProperty.FirstLinelndent: txtbox.Selectionlndent = InchesToPixels(ruler.Leftlndent + ruler.FirstLinelndent); txtbox.SelectionHanginglndent = InchesToPixels(-ruler.FirstLinelndent); break; case RulerProperty.Rightlndent: txtbox.SelectionRightlndent = InchesToPixels(ruler.Rightlndent); break; case RulerProperty.Tabs: int[] iTabs = new int[ruler.Tabs.Length]; for (int i = 0; i < ruler.Tabs.Length; i++) iTabs[i] = InchesToPixels(ruler.Tabs[i]); txtbox.SelectionTabs = iTabs; break; } PixelsToInches(int i)
Пользовательские элементы управления 223 { return i / fDpi; } int InchesToPixels(float f) { return (int)Math.Round(f * fDpi); } } Как я и обещал, и в этом файле есть много преобразований дюймов и пикселов. Программа должна обеспечить интерфейс между RicbTextBox и DocumentRuler, что в основном достигается в обработчиках события SelectionChanged текстового поля и события Change линейки. В следующем примере отступы слева и справа равны нулю, а отступ первой строки — 1 дюйм. When on board the H.M.S. 'Beagle,' as naturalist, I was much struck with certain facts in the distribution of the inhabitants of South America, and in the geological relations of the past inhabitants of that continent. These facts seemed to me to throw some light on the origin of species - that mystery of mysteries, as it has been called by one of our greatest philosophers.! Сетка выбора цветов В .NET есть элементы управления для выбора из шести миллионов цветов. Один из них, ColorGrid, отображает массив из 40 цветов в виде сетки. Цвета можно выбирать с помощью мыши или клавиш со стрелками на клавиатуре. У этого элемента управления самый развитой в этой главе код обработки информации поступающей с клавиатуры. ColorGrid.cs И // ColorGrid.cs (с) 2005 by Charles Petzold // using System; using System.Drawing; using System.Windows.Forms; class ColorGrid : Control { // Число цветов по горизонтали и по вертикали. const int xNum = 8; const int yNum = 5;
224 ГЛАВА 4 // Цвета. Color[,] aclr = new Color[yNum, xNum] { { Color.Black, Color.Brown, Color.DarkGreen, Color.MidnightBlue, Color.Navy, Color.DarkBlue, Color.Indigo, Color.DimGray }, { Color.DarkRed, Color.OrangeRed, Color.Olive, Color.Green, Color.Teal, Color.Blue, Color.SlateGray, Color.Gray }, { Color.Red, Color.Orange, Color.YellowGreen, Color.SeaGreen, Color.Aqua, Color.LightBlue, Color.Violet, Color.DarkGray }, { Color.Pink, Color.Gold, Color.Yellow, Color.Lime, Color.Turquoise, Color.SkyBlue, Color.Plum, Color.LightGray }, { Color.LightPink, Color.Tan, Color.LightYellow, Color.LightGreen, Color.LightCyan, Color.LightSkyBlue, Color.Lavender, Color.White } }; // Выбранный цвет как закрытое поле. Color clrSelected = Color.Black; // Прямоугольники для отображения цветов и границ. Rectangle rectTotal, rectGray, rectBorder, rectColor; // Координата цвета, подсвеченного с помощью клавиатуры или мыши, int xHighlight = -1; int yHighlight = -1; // Конструктор, public ColorGridO { AutoSize = true; // Получение разрешения экрана. Graphics grfx = CreateGraphicsQ; int xDpi = (int)grfx.DpiX; int yDpi = (int)grfx.DpiY; grfx.DisposeO; // Вычисление прямоугольников для отображения цвета. rectTotal = new Rectangle(0, 0, xDpi / 5, yDpi / 5); rectGray = Rectangle.Inflate(rectTotal, -xDpi / 72, -yDpi / 72); rectBorder = Rectangle.Inflate(rectGray, -xDpi / 48, -yDpi / 48); rectColor = Rectangle.Inflate(rectBorder, -xDpi / 72, -yDpi / 72);
Пользовательские элементы управления 225 // Свойство SelectedColor - доступ к полю clrSelected. public Color SelectedColor { get { return clrSelected; } set { clrSelected = value; Invalidate(); } // Требуется для автоматической корректировки размеров. public override Size GetPreferredSize(Size sz) { return new Size(xNum * rectTotal.Width, yNum * rectTotal.Height); } // Отрисовка всех цветов в сетке. protected override void OnPaint(PaintEventArgs args) { Graphics grfx = args.Graphics; for (int у = 0; у < yNum; y++) for (int x = 0; x < xNum; x++) DrawColor(grfx, x, y, false); } // Отрисовка отдельного цвета, (grfx может равняться null) void DrawColor(Graphics grfx, int x, int y, bool bHighlight) { bool bDisposeGraphics = false; if (x < 0 || у < 0 || x >= xNum || у >= yNum) return; if (grfx == null) { grfx = CreateGraphicsO; bDisposeGraphics = true; } // Определить, выбран ли в данный момент указанный цвет, bool bSelect = aclr[y, x].ToArgb() == SelectedColor.ToArgbO;
226 ГЛАВА 4 Brush br = (bHighlight | bSelect) ? SystemBrushes.HotTrack : SystemBrushes.Menu; // Начать отрисовку прямоугольников. Rectangle rect = rectTotal; rect.Offset(x * rectTotal.Width, у * rectTotal.Height); grfx.FillRectangle(br, rect); if (bHighlight || bSelect) { br = bHighlight ? SystemBrushes.ControlDark : SystemBrushes.ControlLight; rect = rectGray; rect.Offset(x * rectTotal.Width, у * rectTotal.Height); grfx.FillRectangle(br, rect); } rect = rectBorder; rect.Offset(x * rectTotal.Width, у * rectTotal.Height); grfx.FillRectangle(SystemBrushes.ControlDark, rect); rect = rectColor; rect.Offset(x * rectTotal.Width, у * rectTotal.Height); grfx.FillRectangle(new SolidBrush(aclr[y, x]), rect); if (bDisposeGraphics) grfx.Dispose(); // Методы для обработки перемещения и щелчков мышью, protected override void OnMouseEnter(EventArgs args) { xHighlight = -1; yHighlight = -1; } protected override void OnMouseMove(MouseEventArgs args) { int x = args.X / rectTotal.Width; int у = args.Y / rectTotal.Height; if (x != xHighlight || у != yHighlight) { DrawColor(null, xHighlight, yHighlight, false); DrawColor(null, x, y, true);
Пользовательские элементы управления 227 xHighlight = х; yHighlight = у; } } protected override void OnMousel_eave(EventArgs args) { DrawColor(null, xHighlight, yHighlight, false); xHighlight = -1; yHighlight = -1; } protected override void OnMouseDown(MouseEventArgs args) { int x = args.X / rectTotal.Width; int у = args.Y / rectTotal.Height; SelectedColor = aclr[y, x]; base.OnMouseDown(args); // Инициирует событие Click. Focus(); } // Методы для управления с клавиатуры, protected override void OnEnter(EventArgs args) { if (xHighlight < 0 || yHighlight < 0) for (yHighlight = 0; yHighlight < yNum; yHighlight++) { for (xHighlight = 0; xHighlight < xNum; xHighlight++) { if (aclr[yHighlight, xHighlight].ToArgbQ == lor.ToArgbQ) break; } if (xHighlight < xNum) break; } if (xHighlight == xNum && yHighlight == yNum) xHighlight = yHighlight = 0; DrawColor(null, xHighlight, yHighlight, true); } protected override void Onl_eave(EventArgs args) { DrawColor(null, xHighlight, yHighlight, false); xHighlight = yHighlight = -1; }
228 ГЛАВА 4 protected override bool IsInputKey(Keys keyData) { return keyData == Keys.Home || keyData == Keys.End || keyData == Keys.Up || keyData == Keys.Down || keyData == Keys.Left || keyData == Keys.Right; } protected override void OnKeyDown(KeyEventArgs args) { DrawColor(null, xHighlight, yHighlight, false); int x = xHighlight, у = yHighlight; switch (args.KeyCode) { case Keys.Home: x = у = 0; break; case Keys.End: x = xNum - 1; у = yNum - 1; break; case Keys.Right: if (++x == xNum) { x = 0; if (++y == yNum) { Parent.GetNextControl(this, true).Focus(); } } break; case Keys.Left: if (-x == -1) { x = xNum - 1; if (-y == -1) { Parent.GetNextControl(this, false).Focus(); } } break; case Keys.Down: if (++y == yNum)
Пользовательские элементы управления 229 { У = 0; if (++x == xNum) { Parent.GetNextControl(this, true).Focus(); } } break; case Keys.Up: if (-y == -1) { У = 0; if (-x == -1) { Parent.GetNextControl(this, false).Focus(); } } break; case Keys.Enter: case Keys.Space: SelectedColor = aclr[y, x]; OnClick(EventArgs.Empty); break; default: base.OnKeyDown(args); return; } DrawColor(null, x, y, true); xHighlight = x; yHighlight = y; } } В этом элементе управления определено одно новое открытое свойство, Selected- Color. Программа, использующая этот элемент управления, оповещается об изменении SelectedColor с помощью обычного события Click. Конструктор определяет четыре прямоугольника, служащие для отображения каждого цвета в сетке. Их размеры всецело определяются вертикальным и горизонтальным разрешением монитора. ColorGrid — один из немногих элементов управления, которые не пришлось перекодировать при переходе на разрешение экрана в 300 точек на дюйм. Метод OnPaint просто вызывает DrawColor для каждого
230 ГЛАВА 4 из 40 цветов сетки, который и выполняет основную работу Логика отрисовки различает выбранный (доступный из свойства SelectedColor) и подсвеченный цвет. Подсвеченный элемент управления изменяется, когда над ним проходит курсор мыши или когда он находится в фокусе ввода и пользователь нажимает клавишу со стрелкой. Цвет изменяется с подсвеченного на выделенный по щелчку кнопки мыши либо по нажатию клавиши Enter или пробела. Изменение подсветки в зависимости от положения курсора мыши — задача переопределенных методов OnMouseEnter, OnMouseMove и OnMouseLeave. Метод OnMouseDown назначает новый выбранный цвет и вызывает базовый метод для инициирования события Click. Обработка ввода с клавиатуры сложнее. Прежде всего, элемент управления должен «узнавать», когда получает или теряет фокус ввода. Когда над элементом нет курсора мыши, цвет подсвечивается, только когда элемент управления в фокусе. Для этого в ColorGrid используются методы OnEnter и OnLeave. Есть и еще одна трудность: многие клавиши, используемые ColorGrid, особенно клавиши со стрелками, нужны и родительскому элементу управления — для перемещения фокуса ввода между потомками. ColorGrid должен переопределить IslnputKey и вернуть true для клавиш, которые будет использовать только он. Затем эти клавиши обрабатываются в методе OnKeyDown. Клавиши со стрелками перемещаются по строкам и столбцам сетки до достижения цвета в верхнем левом или нижнем правом углу. В этот момент фокус ввода переходит к предыдущему или следующему элементу управления этого же уровня. Обратите внимание, что обработка клавиш Enter и пробела изменяет выбор цвета. В проект ColorGridDemo входит ColorGrid.cs и следующий файл. ColorGridDemo.es // // ColorGridDemo.cs (с) 2005 by Charles Petzold // using System; using System.Drawing; using System.Windows.Forms; class ColorGridDemo : Form { Label lbl; [STAThread] public static void Main() { Application.EnableVisualStyles(); Application.Run(new ColorGridDemoO);
Пользовательские элементы управления 231 } } public ColorGridDemo() { Text = "Custom Color Control"; AutoSize = true; TableLayoutPanel table = new TableLayoutPanelO; table.Parent = this; table.AutoSize = true; table.ColumnCount = 3; Button btn = new Button(); btn.Parent = table; btn.AutoSize = true; btn.Text = "Button One"; ColorGrid clrgrid = new ColorGridO; clrgrid.Parent = table; clrgrid.Click += ColorGridOnClick; btn = new Button(); btn.Parent = table; btn.AutoSize = true; btn.Text = "Button Two"; lbl = new LabelO; lbl.Parent = table; lbl.AutoSize = true; lbl.Font = new Font("Times New Roman", 24); lbl.Text = "Sample Text"; table.SetColumnSpan(lbl, 3); clrgrid.SelectedColor = lbl.ForeColor; } void ColorGridOnClick(object objSrc, EventArgs args) { ColorGrid clrgrid = (ColorGrid) objSrc; lbl.ForeColor = clrgrid.SelectedColor; } Класс ColorGridDemo создает элемент управления ColorGrid и размещает его между двумя элементами управления Button — они нужны лишь для проверки правильности работы механизма перемещения фокуса. При выборе нового цвета он используется как основной цвет элемента управления Label.
232 ГЛАВА 4 В О С: 11Н D зшс:й::ш:.;..:: Sample Text Наверняка вы заметите, что цветовая сетка напоминает сетку в меню Формат/ Фон (Format/Background) приложения Microsoft Office, — вынужден сознаться, что именно она вдохновила меня на создание этого элемента управления. В следующей главе нам предстоит добавить этот элемент управления в меню и панель инструментов.
Глава 5 Меню и панели инструментов Было время, когда меню и панели инструментов легко различались. Меню представляли собой иерархические наборы текстовых элементов, а панели инструментов состояли из обычных кнопок с изображениями-растрами. Но когда панели инструментов стали поддерживать выпадающие меню, а меню стали содержать графику, различать их стало сложнее. Возможности меню и панелей инструментов стали практически одинаковыми, что наглядно видно в приведенной ниже части иерархии классов Microsoft .NET Framework 2.0: Object MarshalByRefObject Component Control ScrollableControl ToolStrip MenuStrip StatusStrip ToolStrip, MenuStrip и StatusStrip — новые классы .NET Framework 2.0 для панелей инструментов, меню и строк состояния, соответственно. Конечно, классы Toolbar, MainMenu и StatusBar из .NET Framework 1.x все еще поддерживаются; я рассказывал о них в главах 14 и 20 моей книги Programming Microsoft Windows with С* [Программирование для MS Windows на С#. М.: Русская Редакция, 2002]. Однако новые классы так же просты (а может и проще) в использовании, поэтому ничто не мешает пользоваться ими при разработке новых приложений, которые будут работать в .NET Framework 2.0. Элемент управления ToolStrip предоставляет большинство гибких современных функций панелей инструментов, присутствующих в сложных приложениях, таких как Microsoft Office. ToolStrip поддерживает не только кнопки с растрами, но и поля редактирования текста, поля со списками и выпадающие меню. Можно разместить в окне несколько таких элементов управления и позволить пользователям переме-
234 ГЛАВА 5 щать их и прикреплять к любому краю окна, или перемещать элементы с одной панели ToolStrip на другую. (Однако, .NET Framework 2.0 напрямую не поддерживает плавающие панели инструментов, которые являются, по сути, панелями инструментов, превращенными в немодальные диалоговые окна.) Новый класс MenuStirp кардинально отличается от класса MainMenu из .NET Framework 1.x. Последний предоставляет программный доступ к обычным меню, являющимся частью стандартного окна. Это то же старое меню, которое доступно через Windows API, и если надо разместить изображение рядом с элементом меню, придется использовать нестандартные «картинки», что едва ли можно назвать приятной процедурой. С другой стороны, новый элемент управления MenuStrip предоставляет все возможности, доступные в ToolStrip, включая комбинирование текста и графики. Единственное существенное различие между ToolStrip и MenuStrip в том, что MenuStrip активизируется по нажатию клавиши Alt. Тот факт, что MenuStrip (в отличие от MainMenu) элемент управления, а не часть окна, имеет еще одно важное следствие: если в программе используется класс MainMenu, клиентская область уменьшается, так как часть пространства занимает меню. A MenuStrip является элементом управления, расположенным в клиентской области (обычно в ее верхней части), и если надо разместить там другие элементы управления, нужно учитывать пространство, занятое им (а также элементами управления ToolStrip и StatusStrip). В большинстве случаев панель размещается в центре клиентской области путем присваивания свойству Dock значения DockStyleFill Но несмотря на все внешние украшения, самый важный тип меню — это меню, отображающее все параметры программы в виде иерархического набора текстовых строк. Так что в первую очередь рассмотрим именно его. Меню и его команды Меню, как панели инструментов и строки состояния, обычно содержат много «элементов», или команд. Каждый элемент обычно обозначается кратким текстом, например, File (файл) или Save As (сохранить как). Некоторые элементы — часто это File, Edit (редактирование), View (вид) и некоторые другие — отображаются всегда. Они называются элементами меню верхнего уровня, их можно выбирать при помощи клавиатуры или мыши. Обычно выбор элемента меню верхнего уровня приводит к появлению подменю или выпадающего меню, содержащего дополнительные элементы, часто представленные текстовыми строками, которые иногда дополняются изображениями и быстрыми клавишами (например, Ctrl+V для команды Вставить [Paste]). Элементы меню могут вызывать команды или открывать другие подменю. Подменю можно вкладывать друг в друга, причем глубина ограничена лишь чувством целесообразности программиста. Меню часто содержат горизонтальные линии, называемые разделителями (separator), которые помогают объединять связанные команды одного подменю.
Меню и панели инструментов 235 Класс MenuStrip — это элемент управления, значение свойства Dock которого равно DockStyle.Top, и где отображаются элементы меню верхнего уровня, выстроенные в окне по горизонтали. Каждый элемент меню является отдельным объектом. В простых меню чаще всего используются элементы меню, основанные на классах с (не очень удачными) именами ToolStripMenuItem и ToolStripSeparator. Это не элементы управления, а экземпляры класса Component и абстрактного класса ToolStripItem, как показано в следующей иерархии: Object MarshalByRefObject Component ToolStripItem (абстрактный) ToolStripDropDownltem ToolStripMenuItem ToolStripSeparator Класс MenuStrip наследует от ToolStrip свойство Items типа ToolStripItemCollection, которое (как понятно из его имени) является набором объектов ToolStripItem. Набор Items содержит элементы меню верхнего уровня. Это обычно объекты ToolStripMenuItem, и в традиционных приложениях они были бы связаны с текстовыми строками File (файл), Edit (правка), View (вид) и Help (справка). Класс ToolStripMenuItem происходит от класса ToolStnpDropDownltem. В этом классе реализовано свойство DropDownltems, также типа ToolStripItemCollection. Таким образом, у каждого из элементов верхнего уровня есть свойство DropDownltems, содержащее элементы подменю. Дальнейшее вложение выполняется за счет использования свойств DropDoivnltems этих элементов. Пока мы не углубились в детали, вкратце рассмотрим программу с простым меню. Это вариант традиционной «Hello, world». SimpleMenu.es И // SimpleMenu.cs (с) 2005 by Charles Petzold // using System; using System.Drawing; using System.Windows.Forms; class SimpleMenu: Form { [STAThread] public static void Main() { Application.EnableVisualStyles(); Application.Run(new SimpleMenuQ);
236 ГЛАВА 5 } public SimpleMenuO { Text = "Simple Menu"; // Главное меню. MenuStrip menu = new MenuStripO; menu.Parent = this; // Команда File. ToolStripMenuItem itemFile = new ToolStripMenuItemO; itemFile.Text = "&File"; menu.Items.Add(itemFile); // Команда Open. ToolStripMenuItem itemOpen = new ToolStripMenuItemO; itemOpen.Text = "&0pen..."; itemOpen.ShortcutKeys = Keys.Control | Keys.O; itemOpen.Click += OpenOnClick; itemFile.DropDownltems.Add(itemOpen); // Разделитель. ToolStripSeparator itemSep = new ToolStripSeparatorO; itemFile.DropDownltems.Add(itemSep); // Команда Exit. ToolStripMenuItem itemExit = new ToolStripMenuItemO; itemExit.Text = "E&xit"; itemExit.Click += ExitOnClick; itemFile.DropDownltems.Add(itemExit); // Команда Help. ToolStripMenuItem itemHelp = new ToolStripMenuItemO; itemHelp.Text = "&Help"; menu.Items.Add(itemHelp); // Команда About ToolStripMenuItem itemAbout = new ToolStripMenuItemO; itemAbout.Text = "&About..."; itemAbout.Click += AboutOnClick; itemHelp.DropDownltems.Add(itemAbout); } void OpenOnClick(object objSrc, EventArgs args) { MessageBox.Show("\"Open\" feature not yet implemented", Text); }
Меню и панели инструментов 237 void ExitOnClick(object objSrc, EventArgs args) { CloseO; } void AboutOnClick(object objSrc, EventArgs args) { MessageBox.ShowC(c) 2005 by Charles Petzold ", Text); } } Здесь создается объект типа MenuStrip, который назначается потомком формы. Каждый элемент, добавляемый в меню, представляет собой экземпляр класса ToolStrip- Menultem или ToolStripSeparator. Если это экземпляр ToolStripSeparator, программа задает значение свойству Text и определяет событие Click, которое инициируется по щелчку элемента. Меню создается при помощи метода Add набора Items (для элементов верхнего уровня) или DropDownltems. Элементы меню: общая картина Класс ToolStripItem (от которого происходит класс ToolStripMenuItem) не является производным от класса Control, но в нем реализованы некоторые относящиеся к элементам управления свойства и события. Например, в этом классе есть свойства Size, Width и Height {Location нет, но имеется доступное только для чтения свойство Bounds). Как и элементы управления, у объектов класса ToolStripItem есть свойства Margin, Paddling, Dock и Anchor. Класс ToolStripItem также содержит свойства Font, ForeColor и BackColor. Например, если нужно назначить определенному элементу меню (или панели инструментов) шрифт Times New Roman размером 24 пункта, это можно сделать так: item.Font = new Font(*Times New Roman", 24); He менее часто применяются логические (булевы) свойства Enabled и Visible. Если свойство Enabled равно false, элемент становится блеклым и недоступным для пользователя. Если значение false присвоить свойству Visible, элемент станет невидимым. Класс ToolStripItem позволяет отображать текстовую строку (определенную в свойстве Text) и/или изображение (из свойства Image). При использовании класса ToolStripMenuItem изображение всегда отображается слева от текста в отдельном столбце раскрывающегося меню (панели инструментов предоставляют более широкие возможности использования изображений). Кроме задания значения свойству Image напрямую, программа может сопоставлять свойство ImageList набору изображений объекта ImageList, а затем ссылаться на эти изображения по свойствам Imagelndex и ImageKey класса ToolStripItem.
238 ГЛАВА 5 Не забывайте, что класс Image в .NET Framework является родителем классов Bitmap и Metafile. Поместить метафайл в меню (или на панель инструментов) так же просто, как и изображение. Изображения можно загружать из файлов и ресурсов или создавать их непосредственно в программе. Как и ожидалось, класс ToolStripMenuItem расширяет класс ToolStripItem новыми возможностями. Быстрые клавиши определяются в свойстве ShortCutKeys с использованием одного или нескольких членов перечисления Keys, соединенных побитовым оператором «или». Например, таю item.ShortCutKeys = Key.Control | Keys.O; Обычно создается соответствующая текстовая строка (в данном случае — Ctrl+O), но поведение можно изменить, определив в свойстве ShortcutKeyDisplayString другую строку или вообще запретив отображение быстрых клавиш путем присвоения свойству ShowShortaitKeys значения false. Элементы меню могут также отмечаться флажками-галочками в зависимости от значений свойств Checked и CheckState. Свойство Checked булево, а свойству CheckState значения присваиваются из перечисления CheckState, состоящего из членов Checked, Unchecked и Indeterminate. Если свойство Checked элемента меню, с которым связано изображение, равно true, это изображение выделяется рамкой, говорящей о том, что элемент отмечен. Свойство CheckOnClick автоматически меняет состояние (установлен/сброшен) флажка по щелчку элемента меню. Как и в Control, в классе ToolStripItem определено событие Paint и соответствующий метод OnPaint. Можно менять внешний вид элементов панелей инструментов или меню по собственному усмотрению, определяя производный от ToolStripItem класс с переопределенными методами GetPreferredSize и OnPaint. В классе ToolStripItem реализованы события мыши, но не клавиатуры. (Интерфейс клавиатуры для элементов панели инструментов или меню определяется классами ToolStrip или MenuStrip.) В частности, в классе ToolStripItem реализовано событие Click, являющееся основным способом оповещения программы о том, что пользователь инициировал выполнение команды из класса ToolStrip или MenuStrip. Событие Click является одним из двух событий, которое часто встречается при работе с меню. Второе — DropDownOpening, реализованное в классе ToolStripDrop- Downltem и унаследованное от класса ToolStripMenuItem. Это событие инициируется перед отображением раскрывающегося меню и прекрасно подходит для включения или отключения элементов меню. Например, команды Cut (вырезать), Сору (копировать) и Delete (удалить) из меню Edit (правка) должны быть отключены, если нет текущего выделения, а команда Paste (вставить) должна отключаться, если в буфере обмена нет данных.
Меню и панели инструментов 239 Сборка меню Меню состоит из одного объекта MemiStrip и нескольких объектов ToolStripMeriuItem, которые могут разделяться объектами ToolStripSeparator. Элементы верхнего уровня собираются в наборы Items объектов MenuStrip. Для многократного вложения в классе ToolStripMeriuItem имеется свойство DropDownltems. Создать объект MenuStrip и сделать его дочерним по отношению к форме можно так: MenuStrip menu = new MenuStripO; menu.Parent = this; Значение свойства Dock равно DockStyle.Top. Параметров сборки меню из отдельных элементов до нелепости много. Для каждого элемента меню нужно создать (явно или неявно) объект типа TooIStrip- Menultem. Приведем, возможно, самый прямолинейный и ясный пример кода: ToolStripMenuItem itemOpen = new ToolStripMenuItemO; itemOpen.Text = "&0pen..."; itemOpen.Image = bmOpen; itemOpen.ShortcutKeys = Keys.Control | Keys.O; itemOpen.Click += OpenOnClick; Обратите внимание на знак амперсанда (&) в текстовой строке — он задает символ, который будет подчеркнут, когда пользователь нажмет клавишу Alt, позволяя ему выбрать команду меню с клавиатуры. Многоточие (...) общепринято для обозначения команды меню, вызывающей диалоговое окно. OpenOnClick — обработчик события. У класса ToolStripMenuItem есть 5 других конструкторов, позволяющих определять текстовые строки, изображения, пары «текстовая строка + изображение», тройки «текстовая строка + изображение + обработчик события Click» или все перечисленное и еще быстрая клавиша: ToolStripMenuItem itemOpen = new ToolStripMenuItem("&Open...", bmOpen, OpenOnClick, Keys.Control | Keys.O); Кода меньше, но он не столь понятен. Я предпочитаю конструктор, требующий только текстовую строку: ToolStripMenuItem itemFile = new ToolStripMenuItem("&File"); Для элементов меню верхнего уровня можно также установить обработчик события DropDownOpening: itemFile.DropDownOpening += FileOnDropDown;
240 ГЛАВА 5 Следующий код добляет команду Open (Открыть) в раскрывающееся меню File (Файл): itemFile.DropDownltems.Add(itemOpen); А так команда File вводится в основное меню MenuStrip: menu.Items.Add(itemFile); Эти два метода Add не обязательно вызывать в определенном порядке. Задавать значения свойств объекта ToolStripMenuItem можно и после его добавления в набор. Набор Items используется только для добавления в меню элементов верхнего уровня. Для добавления новых команд в меню верхнего уровня используют набор DropDownltems. Элементы меню отображаются в том порядке, в каком они добавлялись в набор. Наборы Items и DropDownltems имеют тип ToolStripItemCollection. В этом классе реализованы дополнительные методы Add, позволяющие неявно создавать объекты ToolStripMenuItem. Вместо явного создания элемента File и добавления его в меню следующим образом: ToolStripMenuItem itemFile = new ToolStripMenuItem("&File"); menu.Items.Add(itemFile); можно выполнить это одним оператором: ToolStripMenuItem itemFile = menu.Items.Add("&File"); Метод Add возвращает созданный объект, что удобно, если с ним нужно сделать что-то еще, например добавить обработчик события DropDownOpening. Хотя в документации по методу Add сказано, что при добавлении элемента в MenuStrip возвращается объект типа ToolStripItem, на самом деле создается и возвращается объект ToolStripMenuItem. При всем желании невозможно создать объект ToolStripItem при помощи этого метода, так как это абстрактный класс. Класс ToolStripItemCollection тоже включает метод Add, который позволяет определить изображение, «текст + изображение» или «текст + изображение + обработчик события Click». Более того, этот класс содержит метод AddRange, позволяющий добавлять несколько элементов за раз. Есть две версии этого метода: одна принимает аргумент типа ToolStripItemCollection, другая — массив объектов ToolStripItem. Последнюю версию метода можно использовать следующим образом: menu.Items.AddRange(new ToolStripMenuItem[] { itemFile, itemEdit, itemView, itemHelp }); Есть еще один конструктор ToolsStripMenuItem, который я еще не упоминал. Он требует строку, изображение и массив объектов ToolStripItem и позволяет создавать и собирать раскрывающееся меню следующим образом:
Меню и панели инструментов 241 itemFile = new ToolStripMenuItem("&File", null, new ToolStripMenuItem[] { itemOpen, itemSave, itemSaveAs, itemClose }); При желании можно создать целое меню при помощи одного оператора, используя вложение, как показано в следующей программе. Внимательно следите за тем, чтобы все круглые и фигурные скобки имели пару CrazyMenuAssemblage.es И // CrazyMenuAssemblage.cs (с) 2005 by Charles Petzold // using System; using System.Drawing; using System.Windows.Forms; class CrazyMenuAssemblage : Form { [STAThread] public static void Main() { Application. EnableVisualStylesO; Application.Run(new CrazyMenuAssemblageQ); } public CrazyMenuAssemblageQ { Text = "Crazy Menu Assemblage"; MenuStrip menu = new MenuStripO; menu.Parent = this; menu.Items.AddRange(new ToolStripMenuItem[] { new ToolStripMenuItemC'&File", null, new ToolStripMenuItem[] { new ToolStripMenuItem("&Open...", null, DefaultOnClick, Keys.Control | Keys.O), new ToolStripMenuItem("&Save", null, DefaultOnClick, Keys.Control | Keys.S), new ToolStripMenuItem("&Close", null, DefaultOnClick) }), new ToolStripMenuItem("&Edit", null, new ToolStripMenuItem[] { new ToolStripMenuItem("Cu&t", null, DefaultOnClick, Keys.Control | Keys.X), new ToolStripMenuItemC'&Copy", null, DefaultOnClick, Keys.Control | Keys.C),
242 ГЛАВА 5 new ToolStripMenuItem("&Paste", null, DefaultOnClick, Keys.Control | Keys.V) }), new ToolStripMenuItem("&Help", null, new ToolStripMenuItem[] { new ToolStripMenuItem("&Help", null, DefaultOnClick, Keys.F1), new ToolStripMenuItem("&About...", null, DefaultOnClick) }) }); } void DefaultOnClick(object obj, EventArgs args) { MessageBox.Show("Menu item not yet implemented", Text); } } Хотя ни в одном из этих элементов меню нет изображений (соответствующий аргумент конструктора ToolStripMenuItem в этом коде равен null), ситуация была бы иной, если бы изображения загружались до выполнения этого кода (чуть позже вы узнаете, как это сделать). У каждого элемента может быть собственный обработчик события Click. Этот метод сборки меню не предоставляет немедленного доступа к отдельным объектам ToolStripMenuItem. Если нужно было бы отключить некоторые элементы меню, пометить их или установить обработчики события DropDownOpening, пришлось бы обращаться ко всем элементам через наборы объектов ToolStripMenuItem, хранимых в свойствах Items и DropDownltems. Более ясный и безопасный способ сборки меню — создавать каждый объект явно. Именно этот способ применяется в Microsoft Visual Studio для генерации кода меню в конструкторе форм (Designer). Однако, как и в случае с элементами управления, здесь проявляется уродливая особенность Visual Studio: каждый элемент ToolStripMenuItem представляется полем. В Visual Studio для сборки элементов в раскрывающееся меню и меню верхнего уровня используется метод AddRange, а не Add. Вот мой, более разумный вариант сборки того же меню. SaneMenuAssemblage.cs И // SaneMenuAssemblage.cs (с) 2005 by Charles Petzold // using System; using System.Drawing; using System.Windows.Forms; class SaneMenuAssemblage : Form
Меню и панели инструментов 243 [STAThread] public static void Main() { Application. EnableVisualStylesO; Application.Run(new SaneMenuAssemblageO); } public SaneMenuAssemblageO { Text = "Sane Menu Assemblage"; MenuStrip menu = new MenuStripO; menu.Parent = this; // Сборка меню File. ToolStripMenuItem itemFile = new ToolStripMenuItem("&File"); menu.Items.Add(itemFile); ToolStripMenuItem item = new ToolStripMenuItem("&Open..."); item.ShortcutKeys = Keys.Control | Keys.O; item.Click += DefaultOnClick; itemFile.DropDownltems.Add(item); item = new ToolStripMenuItem("&Save"); item.ShortcutKeys = Keys.Control | Keys.S; item.Click += DefaultOnClick; itemFile.DropDownltems.Add(item); item = new ToolStripMenuItem("&Close"); item.Click += DefaultOnClick; itemFile.DropDownltems.Add(item); // Сборка меню Edit. ToolStripMenuItem itemEdit = new ToolStripMenuItem("&Edit"); menu.Items.Add(itemEdit); item = new ToolStripMenuItem("Cu&t"); item.ShortcutKeys = Keys.Control | Keys.X; item.Click += DefaultOnClick; itemEdit.DropDownltems.Add(item); item = new ToolStripMenuItem("&Copy"); item.ShortcutKeys = Keys.Control | Keys.C; item.Click += DefaultOnClick; itemEdit.DropDownltems.Add(item);
244 ГЛАВА 5 item = new ToolStripMenuItem("&Paste"); item.ShortcutKeys = Keys.Control | Keys.V; item.Click += DefaultOnClick; itemEdit.DropDownltems.Add(item); // Сборка меню Help. ToolStripMenuItem itemHelp = new ToolStripMenuItem("&Help"); menu.Items.Add(itemHelp); item = new ToolStripMenuItem("&Help"); item.ShortcutKeys = Keys.F1; item.Click += DefaultOnClick; itemHelp.DropDownltems.Add(item); item = new ToolStripMenuItem("&About..."); item.Click += DefaultOnClick; itemHelp.DropDownltems.Add(item); } void DefaultOnClick(object obj, EventArgs args) { MessageBox.Show("Menu item not yet implemented", Text); } Поля и обращение к элементам меню В программе SaneMenuAssemblage одна и та же переменная item используется для всех элементов всех раскрывающихся меню. В реальных программах такой прием подходит для многих элементов меню, но вряд ли для всех. В программе может потребоваться ссылаться на некоторые элементы меню, и самый простой способ сделать это — хранить элементы как поля. Например, команды Cut (вырезать), Сору (копировать) и Delete (удалить) из меню Edit (правка) должны быть доступными только, если выбран какой-то текст, который можно поместить в буфер обмена. Аналогично, команда Paste (вставить) должна становиться доступной, только когда буфер обмена не пустой. Команды, которые должны становиться доступными только при определенных условиях, могут храниться как поля: ToolStripMenuItem itemCut, itemCopy, itemPaste, itemDelete; Кроме этого, нужно установить обработчик события DropDoivnOpening для раскрывающегося меню, в котором размещены эти команды (в нашем случае это меню Edit): itemEdit.DropDownOpening += EditOnDropDownOpening;
Меню и панели инструментов 245 Метод EditOnDropDownOpening делает доступными или отключает элементы itemCut, itemCopy, itemPaste и itemDelete (пример будет чуть ниже). Даже если элементы меню не хранятся как поля, к ним можно обратиться через наборы Items и DropDoivnItems. Можно индексировать эти наборы и обращаться к ним за нужными элементами, как к массивами. Это может быть непросто, поскольку потребуется приведение типов. Наборы Items и DropDoivnItems — это объекты типа ToolStripItem, у которого нет свойства DropDoivnItems — оно реализовано в классе ToolStripDropDownltem, стоящего в иерархии над классом ToolStripMenuItem. Допустим, объект menu типа MenuStrip сохранен как поле. В любой из программ CrazyMenuAssemblage или SaneMenuAssemblage можно найти объект ToolStripMenuItem команды Paste таким образом: ToolStripMenuItem item = (ToolStripMenuItem) ((ToolStripMenuItem) menu.Items[1]).DropDownItems[2]; Из набора Items в объекте menu можно извлечь элемент Edit с индексом 1 (конечно же, индексирование начинается с нуля; индекс 0 соответствует меню File). Затем этот элемент можно привести к объекту ToolStripMenuItem, чтобы получить доступ к набору DropDoivnItems. По индексу 2 можно обратиться к элементу Paste, и таким же образом привести его к объекту типа ToolStripMenuItem. Хотя приведение не назовешь удобным, но использование явных числовых индексов чревато большими неудобствами. Если добавить новые элементы меню или изменить их порядок, придется менять индексы. Есть лучший способ: в документации по ToolStripItemCollection сказано, что есть альтернативный способ индексации элементов — по текстовым строкам, а именно по свойству Name. Для этого нужно определять свойство Name при создании каждого элемента меню: ToolStripMenuItem itemEdit = new ToolStripMenuItem("&Edit"); itemEdit.Name = "Edit"; item = new ToolStripMenuItem("&Paste"); item.Name = "Paste"; Я рекомендую использовать свойство Name, которое совпадает или практически идентично тексту элемента меню, но без амперсандов и многоточий. В нашем примере можно обратиться к команде Paste примерно так.- ToolStripMenuItem item = (ToolStripMenuItem) ((ToolStripMenuItem)menu.Items["Edit"]).DropDownItems["Paste"]; Элементы управления, элементы меню и владельцы На экране отображаются только элементы управления, т. е. экземпляры подклассов класса Control. Экземпляры классов MenuStrip, ToolStrip и StatusStrip являются элементами управления и обычно занимают прямоугольную область на экране.
246 ГЛАВА 5 Элемент управления MenuStrip отображает элементы-команды меню верхнего уровня, содержащиеся в наборе Items, но они не элементы управления. Пространство, занимаемое ими на экране, в действительности управляется объектом MenuStrip, к которому эти элементы принадлежат. Когда происходит перерисовка объекта MenuStrip (возможно, в результате восстановления окна программы после сворачивания), эти элементы перерисовывают сами себя, но на поверхности элемента управления MenuStrip. При щелчке элемента меню верхнего уровня появляется прямоугольное раскрывающееся меню, где отображаются содержимое набора DropDoivnltems этого элемента. Поскольку это меню занимает пространство на экране, оно должно быть элементом управления. Однако элемент, который мы щелкаем — это объект типа ToolStripMenuItem, который не является элементом управления. Элементы, отображаемые в раскрывающемся меню, также относятся к типу ToolStripMenuItem. Но как же создается элемент управления, содержащий элементы меню? Класс ToolStripDropDownltem (от которого происходит класс ToolStripMenuItem) включает свойство DropDoivn типа ToolStripDropDown, восходящее к классу Control. Именно этот элемент управления служит для отображения раскрывающихся меню. Каждый раз при создании объекта типа ToolStripMenuItem, тот создает элемент управления типа ToolStripDropDownMenu (который наследует классу ToolStripDrop- Down). Свойство Visible этого объекта ToolStripDropDownMenu изначально имеет значение/я/52; этот объект хранится в свойстве DropDown объекта ToolStripMenuItem. Если щелкнуть этот элемент, он проверит набор DropDownltems. Если последний непустой, элемент использует для отображения этих элементов на экране элемент управления, хранящийся в свойстве DropDown. Приведем иерархию, показывающую все производные от ToolStrip классы: Object MarshalByRefObject Component Control ScrollableControl ToolStrip BindingNavigator MenuStrip StatusStrip ToolStripDropDown ToolStripDropDownMenu ContextMenuStrip ToolStripOverflow Обратите внимание на ToolStripDropDown и ToolStripDropDownMenu. Они используются для отображения «временных» (непостоянных) элементов в элементах управления ToolStirp и MenuStrip. Элемент управления BindingNavigator — это специ-
Меню и панели инструментов 247 альная панель инструментов для навигации по базам данных; с ней мы познакомимся в следующей главе. Все сказанное выше необходимо для понимания концепции «владельца» элементов меню. У класса ToolStripItem есть свойство Owner типа ToolStrip. Оно определяет элемент управления, на котором отображается данный элемент. У элементов меню верхнего уровня свойство Owner указывает на объект MenuStrip, на котором они отображаются. У любого другого объекта ToolStripMenultem это свойство указывает на объект ToolStripDropDownMenu. У ToolStripItem также есть защищенное свойство Parent. Обычно оно указывает на тот же объект, что и свойство Owner. Однако если на ToolStrip недостаточно места для отображения всех элементов, оставшиеся группируются в дополнительное раскрывающееся меню, на которое будет ссылаться свойство Parent. Однако свойство Owner не меняет своего значения. У ToolStripDropDown есть еще свойство Ownerltem типа ToolStripItem, указывающее на элемент, вызвавший раскрывающееся меню. Если item — экземпляр класса ToolStripMenultem, следующее выражение всегда равно true: item == item.DropDown.Ownerltem Вызываемое элементом свойство Ownerltem раскрывающегося меню всегда ссылается на этот элемент. Если itemFile и itemOpen — экземпляры класса ToolStripMenultem и элемент itemOpen добавлен в набор DropDoivnItems объекта itemFile (как обычно это делается), тогда следующее выражение всегда будет равно true-. itemFile.DropDown == itemOpen.Owner Оба выражения ссылаются на объект ToolStripDropDownMenu, который вызывается меню File и содержит команду Open. Это выражение требует некоторых вычислений, но также имеет значение true: itemFile == ((ToolStripDropDownMenu) itemOpen.Owner).Ownerltem Владельцем элемента Open является объект ToolStripDropDownMenu, на котором этот элемент отображается. Свойство Ownerltem при этом ссылается на элемент File, который вызывает раскрывающееся меню. Следующее выражение также равно true: itemFile == (ToolStripDropDownMenu) itemFile.DropDownItems[0].Owner).Ownerltem Установка и снятие флажков Класс ToolStripDropDownMenu важен для работы с меню, хотя напрямую взаимодействовать с ним приходится редко. Он отвечает за отображение поля слева от эле-
248 ГЛАВА 5 ментов меню, которое (по умолчанию) используется для размещения изображений и флажков. Для отображения флажка (галочки) рядом с командой меню, нужно задать свойству Checked значение true или свойству CheckState — одно из значений перечисления CheckState.Checked. Флажок снимается присвоением свойству Checked значения false или свойству CheckState — значения CheckState.Unchecked. Единственное преимущество CheckState перед Checked заключается в наличии параметра CheckState- Indeterminate, который позволяет вместо флажка отображать точку, которая информирует, что состояние флажка не определенно. Свойства Checked и CheckState можно изменять в обработчике события Click главного меню. Если свойству CheckOnClick задать значение true, состояние флажка меняется по щелчку автоматически. Но если программа должна как-то реагировать на установку/сброс флажка, придется установить обработчик события для этой команды меню. Свойство CheckOnClick не очень подходит для выбора одного из нескольких взаимоисключающих элементов. В таких случаях почти всегда приходится использовать один обработчик события Click для всех элементов. Обычно этот обработчик снимает флажок с отмеченного элемента и устанавливает его на элементе, инициировавшем событие Click. Можно выполнить и другие действия, но они определяются логикой конкретных элементов меню. Приведем пример программы с меню Font (шрифт), которое используется для вывода списка всех доступных шрифтов. В большинстве систем полное меню не поместится на экране. Объект ToolStripDropDownMenu отвечает за отображение стрелок сверху и снизу от списка, позволяющих перемещаться по раскрывающемуся меню. Панель с выбранным шрифтом выделяется цветом за счет присвоения свойству Dock значения DockStyleFill. FontMenu.cs И // FontMenu.cs (с) 2005 by Charles Petzold // using System; using System.Drawing; using System.Windows.Forms; class FontMenu : Form { Panel pnl; ToolStripMenuItem itemSelectedFont; [STAThread] public static void Main() {
Меню и панели инструментов 249 Application.EnableVisualStyles(); Application.Run(new FontMenuO); } public FontMenuO { Text = "Font Menu"; pnl = new Panel(); pnl.Parent = this; pnl.Dock = DockStyle.Fill; pnl.Paint += PanelOnPaint; MenuStrip menu = new MenuStripO; menu.Parent = this; ToolStripMenuItem itemFormat = new ToolStripMenuItem("&Format"); menu.Items.Add(itemFormat); ToolStripMenuItem itemFont = new ToolStripMenuItem("&Font"); itemFormat.DropDownItems.Add(itemFont); Graphics grfx = CreateGraphics(); foreach (FontFamily fntfam in FontFamily.GetFamilies(grfx)) { if (fntfam.IsStyleAvailable(FontStyle.Regular)) { ToolStripMenuItem item = new ToolStripMenuItem(fntfam.Name); item.Click += FontOnClick; itemFont.DropDownltems.Add(item); if (fntfam.Name == Font.Name) { itemSelectedFont = item; itemSelectedFont.Checked = true; } } } grfx.Dispose(); } void PanelOnPaint(object objSrc, PaintEventArgs args) { Graphics grfx = args.Graphics; Font fnt = new Font(itemSelectedFont.Text, 48); grfx.DrawString(fnt.Name, fnt, new SolidBrush(ForeColor), 0, 0); }
250 ГЛАВА 5 void FontOnClick(object objSrc, EventArgs args) { ToolStripMenuItem item = (ToolStripMenuItem)objSrc; itemSelectedFont.Checked = false; itemSelectedFont = item; itemSelectedFont.Checked = true; pnl.Invalidate(); } } В этой программе продемонстрирован довольно традиционный способ установки/снятия флажков на элементах меню. Отмеченный элемент сохраняется в поле itemSelectedFont. Обработчик события Click просто снимает флажок с элемента itemSelectedFont, сопоставляет itemSelectedFont элементу, инициирующему событие Click, помечает этот элемент флажком, а затем вызывает метод Invalidate, объявляя панель недействительной и инициируя событие Paint. Обработчик события Paint получает имя шрифта из свойства Text выбранного элемента и создает панель для этого шрифта. Если нужно, чтобы текст каждого элемента меню Font отображался соответствующим шрифтом, просто добавьте следующую строку после выражения new, которое создает очередной элемент этого меню: item.Font = new Font(fntfam.Name, item.Font.SizelnPoints); Однако, есть одна загвоздка: возникает задержка — как при инициализации программы, так и при первом отображении раскрывающегося меню. Также нет гарантии, что у всех шрифтов есть определения всех символов, необходимых для отображения имени шрифта. Эту программу можно написать и без явного сохранения каких-либо сведений об отмеченном элементе меню. Программа неявно «знает», какой элемент помечен, поскольку отметка хранится в объекте Font, ассоциированном с панелью. Приведем другой пример отметки флажками элементов меню. FontMenu2.cs И // FontMenu2.cs (с) 2005 by Charles Petzold // using System; using System.Drawing; using System.Windows.Forms; class FontMenu2 : Form { Panel pnl;
Меню и панели инструментов 251 [STAThread] public static void Main() { Application. EnableVisualStylesO; Application.Run(new FontMenu2()); } public FontMenu2() { Text = "Font Menu #2"; pnl = new Panel(); pnl.Parent = this; pnl.Dock = DockStyle.Fill; pnl.Font = new Font(pnl.Font.Name, 24); pnl.Paint += PanelOnPaint; MenuStrip menu = new MenuStripO; menu.Parent = this; ToolStripMenuItem itemFormat = new ToolStripMenuItem("&Format"); menu.Items.Add(itemFormat); ToolStripMenuItem itemFont = new ToolStripMenuItem("&Font"); itemFormat.DropDownItems.Add(itemFont); Graphics grfx = CreateGraphicsO; foreach (FontFamily fntfam in FontFamily.GetFamilies(grfx)) { if (fntfam.IsStyleAvailable(FontStyle.Regular)) { ToolStripMenuItem item = new ToolStripMenuItem(fntfam.Name); item.Click += FontOnClick; itemFont.DropDownltems.Add(item); if (fntfam.Name == Font.Name) item.Checked = true; } } grfx.Dispose(); } void PanelOnPaint(object objSrc, PaintEventArgs args) { Graphics grfx = args.Graphics; grfx.DrawString(pnl.Font.Name, pnl.Font, new SolidBrush(ForeColor), 0, 0); }
252 ГЛАВА 5 void FontOnClick(object objSrc, EventArgs args) { ToolStripMenuItem itemClick = (ToolStripMenuItem)objSrc; ToolStripMenuItem itemFont = (ToolStripMenuItem) ((ToolStripDropDownMenu) itemClick.Owner).Ownerltem; foreach (ToolStripMenuItem item in itemFont.DropDownltems) item.Checked = false; itemClick.Checked = true; pnl.Font = new Font(itemClick.Text, 24); } } Эта программа получает элемент меню Font из элемента, который щелкнул пользователь. После этого снимается отметка флажком со всех элементов набора DropDoivnItems и отмечается выбранный пользователем элемент. Снятие флажка со всех элементов занимает столько же времени, сколько и поиск текущего отмеченного элемента и снятия с него флажка. В программах FontMenu и FontMenu2 используется свойство Text, имеющее именно то значение, что требуется для новой панели шрифта. Конечно, это не всегда возможно. В другом способе (он представлен в следующем примере) используется свойство Tag, где хранятся сведения о конкретном элементе меню. Также в следующей программе-примере для установки/снятия флажка не используется обработчик события Click; вместо этого программа ожидает, когда отобразится раскрывающееся меню. Добавление изображений Может показаться странным, что в меню приложений Microsoft Office рядом с командой, к примеру, Paste (Вставить), видно изображение. Ведь слово Paste гораздо информативнее, чем непонятное изображение. Но изображения предназначены не только для меню. Они полезны, когда аналогичное изображение располагается на панели инструментов. Пользуясь меню, пользователь привыкает к характерной «картинке» той или иной команды и, в конце концов, начинает пользоваться панелью инструментов, за счет чего его работа становится эффективнее. В Visual Studio 2005 содержится большой набор стандартных панелей инструментов и меню. В каталоге \Program Files\Microsoft Visual Studio 8\Common7\VS2005- ImageLibrary хранится ZIP-файл VS2005ImageLibrary.zip (к сожалению, он не входит в состав Visual C# 2005 Express Edition). Распакуйте его в эту же или другую папку, сохранив структуру каталогов. Наиболее часто используемые в меню изображения хранятся в папке bitmaps\commands} которая содержит подпапки с изображениями с различной глубиной цвета. Если создаваемые программы не предназначены
Меню и панели инструментов 253 для компьютеров, видеосистема которых поддерживает только 16 цветов, лучше использовать 24- или 32-разрядные изображения. Изображения в программе проще всего использовать как ресурсы. Ресурсы — это двоичные данные, внедренные в файл .ЕХЕ и легко доступные программе. Если проект открыт в Visual Studio, выберите в меню Project команду Add Existing Item или щелкните правой кнопкой мыши имя проекта и выберите Add\Existing Item. Найдите нужное изображение и щелкните кнопку Add. Изображение станет частью проекта и будет указано в списке исходных файлов в окне Solution Explorer в верхней правой части экрана. При щелчке любого изображения в нижней правой части экрана Visual Studio отображается окно Properties. Один очень важный нюанс: в окне Properties измените Build Action на Embedded Resource, таким образом отметив, что это внедренный ресурс. Например, если проект содержит файл Paste.bmp, и это изображение отмечено как внедренный ресурс (Embedded Resource), можно загрузить его в программу как объект Bitmap, используя следующий код: Bitmap bmPaste = new Bitmap(GetType(), "namespace.Paste.bmp"); Первый передаваемый конструктору аргумент, Bitmap, должен ссылаться на какой-либо класс, определенный в программе. В пределах любого экземплярного метода или конструктора в любом определенном в программе классе можно просто использовать метод GetType. Второй аргумент — это имя файла изображения, которому предшествует пространство имен ресурсов программы (выделенное курсивом слово namespace). He путайте пространство имен ресурсов с пространствами имен .NET Framework. Пространство имен ресурсов используется исключительно по отношению к ресурсам, и по умолчанию в Visual Studio оно совпадает с именем проекта. Чтобы изменить это значение надо открыть свойства проекта и указать значение в поле Default Namespace на вкладке Application. Значение свойства Image элемента меню можно задать следующим образом: itemPaste.Image = bmPaste; Это все, что нужно для отображения изображения в меню. Изображение будет отображаться слева от текста меню, в том же столбце, который используется для отображения флажков. Приведем пример, в котором встретится многое из того, что мы успели изучить в этой главе. Эта программа позволяет загружать и сохранять файлы изображений, копировать и вставлять их из буфера обмена и отображать их четырьмя разными способами.
254 ГЛАВА 5 lmageFilter.es // // ImageFiler.es (с) 2005 by Charles Petzold // using System; using System.Drawing; using System.Drawing.Imaging; using System.10; using System.Windows.Forms; class ImageFiler : Form { PictureBox picbox; ToolStripMenuItem itemSaveAs, itemCut, itemCopy, itemPaste, itemDelete; [STAThread] public static void Main() { Application.EnableVisualStyles(); Application. Run (new ImageFilerO); } public ImageFilerO { Text = "Image Filer"; // Создание рамки рисунка, picbox = new PictureBoxO; picbox.Parent = this; picbox.Dock = DockStyle.Fill; // Загрузка изображений. Bitmap bmOpen = new Bitmap(GetType(), "ImageFiler.Open.bmp"); Bitmap bmCut = new Bitmap(GetType(), "ImageFiler.Cut.bmp"); Bitmap bmCopy = new Bitmap(GetType(), "ImageFiler.Copy.bmp"); Bitmap bmPaste = new Bitmap(GetType(), "ImageFiler.Paste.bmp"); Bitmap bmDelete = new Bitmap(GetType(), "ImageFiler.Delete.bmp"); // Создание меню. MenuStrip menu = new MenuStripQ; menu.Parent = this; // Сборка меню File. ToolStripMenuItem itemFile = new ToolStripMenuItem("&File"); itemFile.DropDownOpening += FileOnDropDown; menu.Items.Add(itemFile);
Меню и панели инструментов 255 ToolStripMenuItem item = new ToolStripMenuItem("&Open..."); item.Image = bmOpen; item.ImageTransparentColor = Color.Magenta; item.ShortcutKeys = Keys.Control | Keys.O; item.Click += OpenOnClick; itemFile.DropDownltems.Add(item); itemSaveAs = new ToolStripMenuItemf'Save &As..."); itemSaveAs.Click += SaveAsOnClick; itemFile.DropDownltems.Add(itemSaveAs); itemFile.DropDownItems.Add(new ToolStripSeparatorO); item = new ToolStripMenuItem("E&xit"); item.Click += ExitOnClick; itemFile.DropDownltems.Add(item); // Сборка меню Edit. ToolStripMenuItem itemEdit = new ToolStripMenuItem("&Edit"); itemEdit.DropDownOpening += EditOnDropDown; menu.Items.Add(itemEdit); itemCut = new ToolStripMenuItem("Cu&t"); itemCut.Image = bmCut; itemCut.ImageTransparentColor = Color.Magenta; itemCut.ShortcutKeys = Keys.Control | Keys.X; itemCut.Click += CutOnClick; itemEdit.DropDownItems.Add(itemCut); itemCopy = new ToolStripMenuItem("&Copy"); itemCopy.Image = bmCopy; itemCopy.ImageTransparentColor = Color.Magenta; itemCopy.ShortcutKeys = Keys.Control | Keys.C; itemCopy.Click += CopyOnClick; itemEdit.DropDownItems.Add(itemCopy); itemPaste = new ToolStripMenuItem("&Paste"); itemPaste.Image = bmPaste; itemPaste.ImageTransparentColor = Color.Magenta; itemPaste.ShortcutKeys = Keys.Control | Keys.V; itemPaste.Click += PasteOnClick; itemEdit.DropDownItems.Add(itemPaste); itemDelete = new ToolStripMenuItem("&Delete"); itemDelete.Image = bmDelete;
256 ГЛАВА 5 itemDelete.ImageTransparentColor = Color.Magenta; itemDelete.ShortcutKeys = Keys.Delete; itemDelete.Click += DeleteOnClick; itemEdit.DropDownItems.Add(itemDelete); // Создание и сборка команд меню View. ToolStripMenuItem itemView = new ToolStripMenuItem("&View"); itemView.DropDownOpening += ViewOnDropDown; menu.Items.Add(itemView); item = new ToolStripMenuItemC&Normal"); item.Tag = PictureBoxSizeMode.Normal; item.Click += ViewItemOnClick; itemView.DropDownltems.Add(item); item = new ToolStripMenuItem("&Center"); item.Tag = PictureBoxSizeMode.Centerlmage; item.Click += ViewItemOnClick; itemView.DropDownltems.Add(item); item = new ToolStripMenuItem("&Stretch"); item.Tag = PictureBoxSizeMode.Stretchlmage; item.Click += ViewItemOnClick; itemView.DropDownltems.Add(item); item = new ToolStripMenuItem("&Zoom"); item.Tag = PictureBoxSizeMode.Zoom; item.Click += ViewItemOnClick; itemView.DropDownltems.Add(item); } void FileOnDropDown(object obj, EventArgs args) { itemSaveAs.Enabled = picbox.Image != null; } void EditOnDropDown(object obj, EventArgs args) { itemCut.Enabled = itemCopy.Enabled = itemDelete.Enabled = picbox.Image != null; IDataObject data = Clipboard. GetDataObjectO; itemPaste.Enabled = data.GetDataPresent(typeof(Bitmap)) || data.GetDataPresent(typeof(Metafile)); } void ViewOnDropDown(object obj, EventArgs args)
Меню и панели инструментов 257 { ToolStripMenuItem itemView = (ToolStripMenuItem)obj; foreach (ToolStripItem item in itemView.DropDownltems) { ToolStripMenuItem mitem = (ToolStripMenuItem)item; mitem.Checked = (PictureBoxSizeMode)mitem.Tag == picbox.SizeMode; } } void OpenOnClick(object obj, EventArgs args) { OpenFileDialog dig = new OpenFileDialogO; dig.Filter = "All Image Files|*.bmp;*.ico;*.gif;*.jpeg;*.jpg;" + "*.jfif;*.png;*.tif;*.tiff;*.wmf; *.emf|" + "Windows Bitmap (*.bmp)|*.bmp|" + "Windows Icon (*.ico)|*.ico|M + "Graphics Interchange Format (*.gif)|*.gif|" + "JPEG File Interchange Format (*.jpg)|" + "*.jpg;*.jpeg;*.jfif|" + "Portable Network Graphics (*.png)|*.png|" + "Tag Image File Format (*.tif)|*.tif;*.tiff|" + "Windows Metafile (*.wmf)|*.wmf|" + "Enhanced Metafile (*.emf)|*.emf|" + "All Files (*.*)!*.*"; if (dlg.ShowDialogO == DialogResult.OK) { try { picbox.Image = Image.FromFile(dlg.FileName); } catch (Exception exc) { MessageBox.Show(exc.Message, Text); return; } } } void SaveAsOnClick(object obj, EventArgs args) { SaveFileDialog savedlg = new SaveFileDialogO; savedlg.AddExtension = true; savedlg.Filter = "Windows Bitmap (*.bmp)|*.bmp|" + "Graphics Interchange Format (*.gif)|*.gif|" + "JPEG File Interchange Format (*.jpg)|" + "*.jpg;*.jpeg;*.jfifl" +
258 ГЛАВА 5 "Portable Network Graphics (*.png)|*.png|" + "Tagged Imaged File Format (*.tif)|*.tif;*.tiff"; ImageFormat[] aif = { ImageFormat.Bmp, ImageFormat.Gif, ImageFormat.Jpeg, ImageFormat.Png, ImageFormat.Tiff }; if (savedlg.ShowDialogO == DialogResult.OK) { try { picbox.Image.Save(savedlg.FileName, aif[savedlg.Filterlndex - 1]); } catch (Exception exc) { MessageBox.Show(exc.Message, Text); return; } } } void CutOnClick(object obj, EventArgs args) { CopyOnClick(obj, args); DeleteOnClick(obj, args); } void CopyOnClick(object obj, EventArgs args) { Clipboard.SetDataObject(picbox.Image, true); } void PasteOnClick(object obj, EventArgs args) { IDataObject data = Clipboard.GetDataObject(); if (data.GetDataPresent(typeof(Metafile))) picbox.Image = (Image)data.GetData(typeof(Metafile)); else if (data.GetDataPresent(typeof(Bitmap))) picbox.Image = (Image)data.GetData(typeof(Bitmap)); } void DeleteOnClick(object obj, EventArgs args) { picbox.Image = null; } void ViewItemOnClick(object obj, EventArgs args) { ToolStripMenuItem item = (ToolStripMenuItem)obj; picbox.SizeMode = (PictureBoxSizeMode) item.Tag;
Меню и панели инструментов 259 } void ExitOnClick(object obj, EventArgs args) { Close(); } } Помимо MenuStrip, клиентская область программы содержит элемент управления PictureBox, свойство Dock которого равно DockStyleJrill. Программе не нужно явно обращаться к объекту Image, так как PictureBox содержит изображение и отображает его. В программе установлены обработчики события DropDownOpening для всех трех раскрывающихся меню. В меню File команда Save As доступна только, если открыто изображение. Так же ведут себя команды Cut, Copy и Delete в меню Edit, а команда Paste доступна, только когда буфер обмена содержит объект Bitmap или Metafile. Свойство SizeMode объекта PictureBox определяет, как элемент управления выводит на экран изображение — в натуральную величину, по центру, по размерам элемента управления или растянутым без искажения. Для каждого из четырех элементов раскрывающегося меню View свойству Tag назначается соответствующее значение из перечисления PictureBoxSizeMode. В обработчике VieivOnDropDown свойство Checked каждого элемента вычисляется путем логического сравнения свойства Tag элемента со свойством SizeMode объекта PictureBox, Таким образом, обработчику события Click этих четырех элементов (ViewItemOnClick) не нужно сбрасывать или отмечать флажком элемент — он просто записывает SizeMode в свойство Tag щелкнутого пользователем элемента. В программе ImageFiler свойству Image нескольких элементов присваиваются объекты Bitmap, загружаемые из ресурсов. Для передачи прозрачности в этих изображениях используется пурпурный цвет, и элементы меню должны «знать» об этом. Другой способ использования растров сложнее, но лучше подходит для управления большим числом изображений. Сначала создается объект ImagelisP. ImageList imglst = new ImageListO; Каждое загружаемое изображение добавляется в список изображений по его ключевому имени: imglst.Images.Add("Paste", new Bitmap(GetType(), "Paste.bmp")); Сам список изображений привязывается к объекту MenuStrip: menu.ImageList = imglst; А ссылка на изображение из элемента меню выполняется по ранее назначенному ключевому имени:
260 ГЛАВА 5 itemPaste.ImageKey = "Paste"; Если планируется использовать много изображений, можно даже определить класс, производный от ImageList, и применить конструктор этого класса для загрузки всех нужных программе ресурсов-изображений. Независимо от используемого способа, нужно также указать, какой цвет в изображениях должен имитировать прозрачность. Для изображений, поставляемых с Visual Studio 2005, это пурпурный (magenta). Если используется объект ImageList, достаточно указать этот цвет только раз: imglst.TransparentColor = Color.Magenta; В противном случае вам придется указывать цвет для каждого элемента меню, у которого есть изображение: itemPaste.ImageTransparentColor = Color.Magenta; Именно так сделано в программе ImageFiler. Я же считаю, что проще указывать прозрачный цвет только один раз, поэтому нужно использовать ImageList. Столбец в левой части раскрывающегося меню обычно используется для отображения как изображений, так и флажков. Поведение и внешний вид этого столбца управляется двумя свойствами класса ToolStripDropDownMenu — ShowImageMargin, которое по умолчанию равно true и ShowCheckMargin (значение по умолчанию — false). В такой конфигурации по умолчанию для вывода изображений и флажков используется один столбец. Если у элемента меню есть и изображение, и флажок, последний не отображается, но изображение заключается в рамку. Если свойству ShowImageMargin присвоить false, a ShowCheckMargin — true, будут видны только флажки, но не изображения. Если оба свойства равны/я&е, скрыты будут и изображения, и флажки. Если же обоим свойствам присвоить значение true, будет отображаться два столбца — для изображений и для флажков. Нестандартные элементы меню Если принять Microsoft Office за образец для подражания при создании меню и панелей инструментов, нам нужно будет научиться делать нечто большее, чем простое меню, содержащее текст и изображения. Например, в Word команда Background (Фон) в меню Format (Формат) содержит палитру цветов. Давайте выясним, как повторить это. Можно создать нестандартный (пользовательский) элемент меню как потомка класса ToolStripItem и переопределить, как минимум, методы GetPreferredSize и OnPaint. Однако есть способ получше. Можно поместить любой элемент управления на MenuStrip или ToolStrip, «инкапсулировав» его в класс ToolStnpControlHost. Это производный от ToolStripItem класс, имеющий один конструктор, получающий на вход
Меню и панели инструментов 261 объект типа Control. Этот элемент управления волшебным образом ведет себя как элемент меню или панели инструментов. К счастью, у нас уже есть нужный элемент управления для создания такой же палитры, как в Word. Это элемент управления ColorGrid из проекта ColorGridDemo главы 4. Вот класс ToolStripColorGrid, производный от ToolStripControlHost, инкапсулирующий элемент управления ColorGrid. ToolStripColorGrid.cs И // ToolStripColorGrid.es (с) 2005 by Charles Petzold // using System; using System.Drawing; using System.Windows.Forms; class ToolStripColorGrid : ToolStripControlHost { public ToolStripColorGridO : base(new ColorGridO) { } public Color SelectedColor { get { return ((ColorGrid)Control).SelectedColor; } set { ((ColorGrid)Control).SelectedColor = value; } } protected override void OnClick(EventArgs args) { base.OnClick(args); ((ToolStripDropDown)Owner).Close( ToolStripDropDownCloseReason.ItemClicked); } } Конструктор класса, производного от ToolStripControlHost, способен на большее, но в нашем случае просто вызывается конструктор базового класса, которому передается объект типа Control. Дальнейший доступ к этому элементу управления прост — он хранится в доступном только для чтения свойстве Control класса ToolStripControlHost. Класс ColorGrid, как вы помните, реализует открытое свойство Selected-
262 ГЛАВА 5 Color, которое предоставляет доступ к выбранному цвету. Из программы, где используется ToolStripColorGrid, можно получить доступ к этому свойству путем приведения свойства Control к типу ColorGrid, но правильнее продублировать это свойство в самом классе ToolStripControlGrid. (Обычно также создается свойство только для чтения, напрямую ссылающееся на элемент управления. Чаще его называют ColorGrid и оно возвращает объект типа ColorGrid?) Когда нужно, чтобы производный от ToolStripControlHost класс вызывал некоторые события родителя, определяют открытое событие (возможно, с тем же именем, что и у родительского элемента управления) и его обработчик. Последний должен вызывать событие, определенное в родительском классе. В производном классе также переопределяют методы OnSubscribeControlEvents и OnUnsubscribeControlEvents, которые устанавливают и удаляют обработчик события. В этих методах также нужно вызывать методы OnSubscribeControlEvents и OnUnsubcribeControlEvents базового класса, чтобы обеспечить правильную установку других событий в классе ToolStripControlHost. Немного поэкспериментировав с ToolStripColorGrid, я обнаружил, что необходимо переопределить обработчик OnClick, чтобы явно закрывать меню. Без этого раскрывающееся меню продолжает отображаться после выбора элемента щелчком мыши. Вот пример программы, где ToolStripColorGrid используется в меню. Меню позволяет только изменять цвет фона формы, но оно также содержит команду вызывающую стандартный диалог ColorDialog для более точного выбора цвета. CustomColorMenu.cs // // CustomColorMenu.cs (с) 2005 by Charles Petzold // using System; using System.Drawing; using System.Windows.Forms; class CustomColorMenu : Form { ToolStripColorGrid clrgrd; ColorDialog clrdlg = new ColorDialogO; [STAThread] public static void Main() { Application. EnableVisualStylesO; Application.Run(new CustomColorMenuO); } public CustomColorMenuO
Меню и панели инструментов 263 } { Text = "Custom Color Menu"; MenuStrip menu = new MenuStripO; menu.Parent = this; ToolStripMenuItem itemFormat = new ToolStripMenuItem("&Format"); itemFormat.DropDownOpening += FormatOnDropDownOpening; menu.Items.Add(itemFormat); ToolStripMenuItem itemBackground = new ToolStripMenuItem( "&Background Color"); itemFormat.DropDownItems.Add(itemBackground); clrgrd = new ToolStripColorGrid(); clrgrd.Click += ColorGridOnClick; itemBackground.DropDownltems.Add(clrgrd); itemBackground.DropDownItems.Add(new ToolStripSeparatorO); ToolStripMenuItem item = new ToolStripMenuItem("&More Colors..."); item.Click += MoreColorsOnClick; itemBackground.DropDownltems.Add(item); } void FormatOnDropDownOpening(object objSrc, EventArgs args) { clrgrd.SelectedColor = BackColor; } void ColorGridOnClick(object objSrc, EventArgs args) { BackColor = clrgrd.SelectedColor; } void MoreColorsOnClick(object objSrc, EventArgs args) { clrdlg.Color = BackColor; if (clrdlg.ShowDialogO == DialogResult.OK) BackColor = clrdlg.Color; } Заметьте, что объект ColorDialog создается только один раз и хранится в поле. Это гарантирует, что любые выбранные пользователем цвета сохранятся при следующем отображении этого диалогового окна. (Конечно, выбранные цвета не сохранятся после закрытия программы. Обсуждение возможных решений см. в моей
264 ГЛАВА 5 книге Programming Microsoft Windows with С* [Программирование для MS Windows на C#. М.: Русская Редакция, 2002].) Контекстное меню В последней из представленных в этой главе иерархий приведен класс ContextMenuStrip, производный от ToolStripDropDownMenu. Этот элемент управления создается внутри программы и служит для отображения раскрывающихся меню. Контекстные меню обычно отображаются по щелчку правой кнопки мыши. Элементы меню ContextMenuStrip это обычно объекты ToolStripMenuItem, но вы вправе задействовать для этой цели любые объекты любого производного от ToolStripItem класса. В сущности, есть два способа использования контекстных меню. В классе Control определено свойство ContextMenuStrip, которое можно инициализировать конкретным объектом ContextMenuStrip. По щелчку элемента управления правой кнопкой открывается контекстное меню. Если инициировано ContextMenuStrip, не нужно инициировать свойство ContextMenu — последнее используется для меню .NET Framework 1.x. Приведем пример использования ContextMenuStrip, демонстрирующий еще один способ выбора цвета. Здесь в семи подменю перечисляются все цвета, имеющиеся в .NET Framework, и предоставляется возможность выбрать из них цвет для фона формы. ContextMenuStripDemo.cs И // ContextMenuStripDemo.cs (с) 2005 by Charles Petzold // using System; using System.Drawing; using System.Reflection; using System.Windows.Forms; class ContextMenuStripDemo : Form { ToolStripMenuItem itemChecked; [STAThread] public static void Main() { Application.EnableVisualStyles(); Application.Run(new ContextMenuStripDemo()); } public ContextMenuStripDemoO {
Меню и панели инструментов 265 Text = "ContextMenuStrip Demo"; BackColor = Color.White; // Упорядочение цветов по алфавиту. int[] iMenu = { 0,0,0,1,2,2,2,2,2,2,2,3,4,5,5,5,5,5,6,6,6,6,6,6,6,6 }; // Создание ContextMenuStrip и присоединение к форме // через свойство ContextMenuStrip класса Control. ContextMenuStrip menu = new ContextMenuStripO; ContextMenuStrip = menu; // Элементы верхнего уровня показывают диапазон букв. for (int i = 0; i <= 6; i++) { ToolStripMenuItem item = new ToolStripMenuItemO; char chFirst = Convert.ToChar(Array.IndexOf(iMenu, i) + 'A'); char chLast = Convert.ToChar(Array.LastlndexOf(iMenu, i) + 'A'); item.Text = String.Format("Colors {0} to {1}", chFirst, chLast); ((ToolStripDropDownMenu)item.DropDown).ShowCheckMargin = true; menu.Items.Add(item); } // Получение массива объектов Propertylnfo со свойствами Color. PropertyInfo[] api = typeof(Color).GetPropertiesO; // Включение отдельных цветов в ToolStripMenuItem. foreach (Propertylnfo pi in api) { if (pi.CanRead && pi.PropertyType == typeof(Color)) { Color clr = (Color)pi.GetValue(null, null); int i = iMenu[clr.Name[0] - 'A']; ToolStripMenuItem item = new ToolStripMenuItemO; item.Text = CamelSpaceOut(clr.Name); item.Name = clr.Name; item.Image = CreateBitmap(clr); item.Click += ColorOnClick; ((ToolStripMenuItem)menu.Items[i]).DropDownltems.Add(item); if (clr.Equals(BackColor)) (itemChecked = item).Checked = true; } } } void Color0nClick(object objSrc, EventArgs args)
266 ГЛАВА 5 { ToolStripMenuItem item = (ToolStripMenuItem) objSrc; itemChecked.Checked = false; (itemChecked = item).Checked = true; BackColor = Color.FromName(itemChecked.Name); } Bitmap CreateBitmap(Color clr) { Bitmap bm = new Bitmap(16, 16); Graphics grfx = Graphics.Fromlmage(bm); grfx.FillRectangle(new SolidBrush(clr), 0, 0, 16, 16); grfx.Dispose(); return bm; } string CamelSpaceOut(string str) { for (int i = 1; i < str.Length; i++) if (Char.IsUpper(str[i])) str = str.Insert(i++, " "); return str; } } Массив iMenu содержит 26 целочисленных значений, соответствующих 26 буквам английского алфавита для разделения 128 стандартных цветов на 7 подменю. Цикл for создает элементы меню верхнего уровня, отображающие диапазон цветов каждого подменю. Обратите внимание, что свойству ShoivCheckMargin присвоено значение true, чтобы создать отдельные столбцы для изображений и флажков. Цикл foreach проходит по всем цветам, полученным как массив Prepertylnfo. В свойство Name каждого элемента ToolStripMenuItem записывается название цвета, а в свойство Text — текстовая строка, обработанная методом CamelSpaceOtit. Этот метод берет название цвета в виде строки (например, «LightGoldenrodYellow») и вставляет пробелы перед всеми заглавными (текст превращается в «Light Goldenrod Yellow»). Метод CreateBitmap создает простое изображение, иллюстрирующее цвет. В нашем примере контекстное меню появляется по щелчку правой кнопкой любой части клиентской области формы. Если нужно получить больше контроля над отображением контекстного меню, можно установить обработчики события «щелчок правой кнопки мыши» и отображать контекстное меню методом Show, который ContextMenuStrip наследует от ToolStripDropDown. Аргументы метода Show позволяют указать место отображения контекстного меню.
Меню и панели инструментов 267 Панели инструментов и их компоненты Одно из огромных преимуществ объединения функциональности меню и панелей инструментов в .NET Framework 2.0 заключается в возможности активно использовать уже имеющиеся знания. После освоения MenuStrip можно сразу переходить к использованию класса ToolStrip. Обычно на панелях инструментов элементы разнообразнее, чем в меню, но принципы остаются теми же. Вот полная иерархия классов, производных от ToolStripItem-. Object MarshalByRefObject Component ToolStripItem (абстрактный) ToolStripButton ToolStripControlHost ToolStripComboBox ToolStripProgressBar ToolStripTextBox ToolStripDropDownltem ToolStripDropDownButton ToolSt ripOverflowButton ToolStripMenuItem ToolStripSplitButton ToolSt ripLabel StatusStripPanel ToolStripStatusLabel ToolSt rlpSeparator Мы видели, что элементы меню с текстом могут также содержать изображения, вместе с тем, изображения расположены всегда слева от текста. Панели инструментов гибче в этом отношении. Если нужно отобразить текст и изображение, их взаимное расположение определяется свойством TextlmageRelation, которому можно задать значение из одноименного перечисления, содержащего члены TextAbovelmage (текст над изображением), ImageAboveText (изображением над текстом), TextBefore- Image (текст поверх изображения), ImageBeforeText (изображение поверх текста, это значение по умолчанию) или Overlay (накладка). Часто на панелях инструментов тексты очень короткие, а изображения — непонятны, в этом случае очень кстати оказываются всплывающие подсказки. Это короткие пояснения, появляющиеся во всплывающей рамке, когда курсор мыши задерживается над элементом. Обычно отображается текст из свойства Text, но это поведение можно изменить, задав значение свойству ToolTipText. Чтобы запретить появление всплывающих подсказок, задайте свойству AutoToolTip значение false.
268 ГЛАВА 5 Кнопки на панели инструментов В иерархии показаны три типа классов кнопок, производных от ToolStripItem. Наверно, чаще всего используется класс ToolStripButton, который обычно применяется для выполнения определенных команд, таких как Cut, Copy или Paste. Как обычно, нужно установить обработчик события Click, чтобы отслеживать щелчки по кнопке. Нужно ли использовать одни и те же обработчики событий Click для элементов меню и панелей инструментов? Да, конечно, без сомнения. Программа должна иметь один обработчик PasteOnClick, и он должен вызываться и командами меню, и элементами панели инструментов. Активизация и отключение объектов ToolStripButton немного отличается от включения и отключения элементов меню. Элементы раскрывающегося меню не нужно делать доступными для пользователя, пока меню не отображается. Однако, многие элементы панели инструментов видимы постоянно. Например, для команд Cut, Copy и Delete может понадобиться обработчик событий, который будет определять, есть ли у программы данные, которые можно скопировать в буфер обмена. С кнопкой Paste дело обстоит еще сложнее. Возможно, придется запускать таймер, по которому каждую десятую долю секунды содержимое буфера обмена будет проверяться на предмет возможности вставки его содержимого в программу. Элемент ToolStripButton может также функционировать в качестве выключателя, как кнопки Bold (полужирный) и Italic (курсив) на панелях инструментов для форматирования текста. Как и у ToolStripMenuItem, у элемента ToolStripButton есть логическое свойство Checked и свойство CheckState, принимающее значения из перечисления CheckState. Если задать свойству CheckOnClick значение true, кнопка будет менять свое состояние при каждом ее щелчке. Несколько элементов ToolStripButton можно использовать как группу переключателей, позволяющую выбрать один из нескольких возможных вариантов. Если кнопка должна вызывать раскрывающийся список элементов, нужно использовать класс ToolStripDropDoivnButton. Как и ToolStripMenultem, этот класс наследует свойства от ToolStripDropDownltem и имеет свойство DropDoivnltems, которое является набором объектов ToolStripItem. Возможно, устанавливать обработчик события Click не придется, но обычно устанавливают обработчик события DropDoivn- Opening, если нужно включать или отключать определенные элементы раскрывающегося меню. Пользователи Microsoft Office, наверняка, привыкли как к кнопкам, выполняющим определенное действие, так и к кнопкам с маленьким треугольником, щелчок которых отображает раскрывающееся меню. Такой стиль кнопки реализован в классе ToolStripSplitButton, который также производный от ToolStripDropDownltem. Добавляют элементы в раскрывающееся меню такой кнопки так же, как и в ToolStripDropDoivnButton. Однако, устанавливать обработчик события Click нужно не всегда, поскольку это событие инициируется по щелчку любой части кнопки. Чтобы по-
Меню и панели инструментов 269 лучать уведомления о щелчке части кнопки, не вызывающей раскрывающееся меню, нужно установить обработчик события ButtonClick. Элементы управления как элементы ToolStrip Помимо кнопок (и весьма тривиального элемента ToolStripLabet), есть другая крупная категория элементов, где можно размещать панели инструментов; она включает элементы ToolStripComboBox, ToolStripProgressBar и ToolStripTextBox. Все эти три класса происходят от класса ToolStripControlHost, и мы уже видели, как этот класс работает оболочкой элементов управления так, что они становятся пригодными для использования в меню и панелях инструментов. Как можно догадаться, классы ToolStripComboBox, ToolStripProgressBar и ToolStripTextBox позволяют использовать на панелях инструментов обычные элементы ComboBox, ProgressBar и TextBox. (Элемент ToolStripProgressBar чаще всего размещают в элементах управления StatusStrip, расположенных в нижней части окна.) Вместо того чтобы демонстрировать применение каждого из этих элементов по отдельности, рассмотрим пример, приближенный к «реальной» программе, имеющей элемент ToolStrip, который включает элементы ToolStripComboBox, ToolStripButton, ToolStripSplitButton, ToolStripLabel, ToolStripTextBox и наш собственный нестандартный элемент ToolStripColorGrid. Панель инструментов для форматирования текста В текстовых редакторах, таких как Microsoft Word или WordPad, обычно есть панель инструментов, содержащая элементы для форматирования текста. Приложение WordPad построено на основе элемента управления RichEdit с использованием Windows API. Этот элемент доступен программистам .NET Framework в виде класса RichTextBox. В отличие от более простого элемента управления TextBox, RichTextBox позволяет иметь в одном документе разные шрифты и абзацы с разным форматированием. Как я уже говорил в главе 4, у RichTextBox есть несколько свойств, которые позволяют получать текущие параметры форматирования и устанавливать новые. Все эти свойства начинаются со слова Selection и относятся к выделенному в текущий момент тексту. Выбор текста — обычная выполняемая пользователем операция (либо мышью, либо при нажатой клавише Shift стрелками на клавиатуре), так что форматирование на основе текущего выделения выглядит вполне логичным. Но в действительности все не совсем так. Например, если txbox — это элемент управления RichTextBox, программа может задать шрифт текущего выделения при помощи свойства SelectionFont: txtbox.SelectionFont = new Font("Times New Roman", 24, FontStyle.Italic); Если нет выделенного текста, новый шрифт будет применяться ко всему вводимому тексту, начиная с места, где стоит текстовый курсор (эта позиция называется «курсором ввода»).
270 ГЛАВА 5 Узнать шрифт текущего выделения можно аналогичным образом: Font fnt = txtbox.SelectionFont; Это единственный способ узнать шрифт определенной части документа. Если нужно узнать шрифт части документа, которая не входит в текущее выделение, нужно выделить эту часть методом Select. Перед этим логично сохранить информацию о текущем выделении, чтобы потом восстановить его (пример будет чуть ниже). Допустим, пользователь выбрал какой-то текст и хочет выделить его полужирным начертанием. После получения информации о текущем шрифте при помощи свойства SelectionFont (например, только что приведенной строкой кода), нужно создать и установить новый шрифт с требуемым стилем. Перечисление FontStyle — это сочетание битовых флагов, определяющих полужирное начертание, курсив, подчеркнутый и зачеркнутый текст. Чтобы добавить новый стиль оформления шрифта, нужно присоединить значение FontStyle Bold к текущему стилю побитовым оператором «или» (|): txtbox.SelectionFont = new Font(fnt, fnt.Style | FontStyle.Bold); Аналогично отменяется полужирное начертание. Текущий стиль нужно объединить с побитовым дополнением значения FontStyleBold побитовым оператором «и» (&): txtbox.SelectionFont = new Font(fnt, fnt.Style & "FontStyle.Bold); Но что если в текущем выделении более одного стиля? Например, часть выделенного текста выделена курсивом, а остальной текст — обычным начертанием. А нам нужно чтобы первая часть получила полужирное курсивное начертание, а вторая — полужирное, но не курсивное. <* В документации сказано, что если в текущем выделении текста определено более одного шрифта, это свойство SelectionFont равно null Элементарная проверка показывает, что это неверно. Я обнаружил, что SelectionFont равно null только, если выделение содержит два или более шрифтов с разными именами (например, Times New Roman и Anal). Если в выделенном тексте только один шрифт, но лишь часть текста выделена полужирным, свойство SelectionFont вернет шрифт без полужирного начертания. Если выделение содержит один шрифт, но различного размера, то SelectionFont вернет размер в 13 пунктов. Как видите, корректное изменение выделения, содержащего различные шрифты — непростая задача. Программа должна сохранять два целочисленных значения, помечая начало и длину текущего выделения, а затем проверять выделенный текст символ за символом, каждый раз создавая выделения, состоящие из одного символа. После выполнения поставленной задачи нужно восстановить исходное выделение.
Меню и панели инструментов 271 Класс RichTextBox позволяет изменять не только шрифты. Свойства SelectionColor и SelectionBackColor содержат цвет шрифта и фона выбранного текста. Если эти свойства не задействованы, текст pi фон определяются обычными свойствами Fore- Color и BackColor, реализованными в классе Control. Свойство SelectionCharOffset определяет вертикальный сдвиг в пикселах, что используется для создания надстрочных или подстрочных индексов. Рекомендуется одновременно со сдвигом уменьшать размера шрифта. Все остальные свойства относятся к форматированию абзацев. Свойство Selection- Alignment принимает значения из перечисления HorizontalAlignment (Center, Left или Right) и определяет, выравнивание абзаца. Логическое свойство SelectionBullet определяет наличие маркера перед абзацем. Свойство SelectionTabs — это массив целочисленных значений, задающих позиции табуляции в пикселах. Selectionldent, SelectionRightlndent и SelectionHanginglndent — целочисленные переменные, определяющие в пикселах отступы справа и слева, а также отступ первой строки абзаца. В главе 4 мы видели, что «поведение» этих свойств чуть отличается от привычного понимания о форматировании абзаца. Приведенный ниже пример программы содержит элементы RichTextBox и ToolStrip, позволяющие изменять все параметры форматирования, за исключением горизонтального сдвига символов и табуляции. По сути, возможности программы ограничены форматированием символов и абзацев. Чтобы максимально прозрачно продемонстрировать использование элемента ToolStrip, я tie стал реализовывать загрузку или сохранение файлов. Вместе с тем, элемент управления RichTextBox поддерживает обычные сочетания клавиш, позволяющие вырезать, копировать и вставлять текст и отменять действия — Ctrl+X, Ctrl+C, Ctrl+V и Ctrl+Z. Между параметрами форматирования RichTextBox и элементами панели ToolStrip нет точного однозначного соответствия. Например, свойство SelectionFont соответствует шести элементам ToolStrip — двум полям со списком и четырем кнопкам. Опасность такой ситуации в том, что в результате можно получить массу обработчиков событий, выполняющих одни и те же операции лишь с небольшими различиями. Несколько особых приемов (в основном, с использованием свойства Tag) позволяют мне избежать подобных повторений. Например, одни и те же обработчики событий могут управлять цветами фона и шрифта. В этой программе для выбора цвета используется класс ToolStripColorGrid, так что в проект нужно включить файл ToolStripColorGnd.cs из этой главы и файл ColorGrid.cs из главы 4. FormattingToolStrip.cs И // FormattingToolStrip.cs (с) 2005 by Charles Petzold //
272 ГЛАВА 5 using System; using System.Drawing; using System.Reflection; using System.Windows.Forms; class FormattingToolStrip : Form { protected RichTextBox txtbox; ToolStripComboBox comboName, comboSize; ToolStripButton btnBold, btnltalic, btnUnderline, btnStrikeout; ToolStripButton btnLeft, btnRight, btnCenter, btnBullets; ToolStripTextBox txtLeftlndent, txtRightlndent, txtFirstLine; ColorDialog clrdlg = new ColorDialogO; float xDpi; bool bSuspendSelectionChanged = false; [STAThread] public static void Main() { Application.EnableVisualStyles(); Application.Run(new FormattingToolStripO); } public FormattingToolStripO { Text = "Formatting ToolStrip"; Width = 800; // Получение горизонтального разрешения экрана в точках на дюйм. Graphics grfx = CreateGraphicsO; xDpi = grfx.DpiX; grfx.DisposeO; // Загрузка изображений для панели инструментов. ImageList imglst = new ImageListO; imglst.TransparentColor = Color.Magenta; imglst.Images.AddC'BackColor", new Bitmap(GetType(), "ChooseColor.bmp")); imglst.Images.Add("ForeColor", new Bitmap(GetType(), "Forecolor.bmp")); imglst.Images.Add("Left", new Bitmap(GetType(), "AlignTableCellMiddleLeftJustHS.bmp")); imglst.Images.Add("Right", new Bitmap(GetType(), "AlignTableCellMiddleRight.bmp")); imglst.Images.Add("Cente r", new Bitmap(GetType(), "AlignTableCellMiddleCenter.bmp")); imglst.Images.Add("Bullets", new Bitmap(GetType(), "List_Bullets.bmp"));
Меню и панели инструментов 273 // Создание еще нескольких изображений. imglst.Images.Add("Bold", FontStyleBitmap("В", FontStyle.Bold)); imglst.Images.Add("Italic", FontStyleBitmap("I", FontStyle.Italic)); imglst.Images.Add("Underline", FontStyleBitmap("U", FontStyle.Underline)); imglst.Images.Add("Strikeout", FontStyleBitmap("S", FontStyle.Strikeout)); // Создание элемента управления RichTextBox, // занимающего основную часть клиентской области. txtbox = new RichTextBoxO; txtbox.Parent = this; txtbox.Dock = DockStyle.Fill; txtbox.SelectionChanged += TextBoxOnSelectionChanged; // Создание элемента управления ToolStrip. ToolStrip tool = new ToolStripO; tool.Parent = this; tool.ImageList = imglst; // Создание и заполнение поля со списком шрифтов. comboName = new ToolStripComboBoxO; comboName.ToolTipText = "Font Name"; comboName.SelectedlndexChanged += NameOnSelectionChanged; tool.Items.Add(comboName); foreach (FontFamily fntfam in FontFamily.Families) comboName.Items.Add(fntfam.Name); // Создание и заполнение поля со списком размеров шрифта. comboSize = new ToolStripComboBoxO; comboSize.ToolTipText = "Font Size"; comboSize.SelectedlndexChanged += SizeOnSelectionChanged; tool.Items.Add(comboSize); for (int i = 8; i <= 10; i++) comboSize. Items. Add(i.ToStringO); for (int i = 12; i <= 28; i += 2) comboSize. Items. Add(i.ToStringO); for (int i = 36; i <= 72; i += 12) comboSize. Items. Add(i.ToStringO); // Создание кнопок для применения полужирного, курсивного, // подчеркнутого и зачеркнутого шрифта. btnBold = new ToolStripButton(); btnBold.ImageKey = "Bold"; btnBold.ToolTipText = "Bold"; btnBold.Tag = FontStyle.Bold;
274 ГЛАВА 5 btnBold.CheckOnClick = true; btnBold.Click += FontStyleOnClick; tool.Items.Add(btnBold); btnltalic = new ToolStripButtonO; btnltalic.ImageKey = "Italic"; btnltalic.ToolTipText = "Italic"; btnltalic.Tag = FontStyle.Italic; btnltalic.CheckOnClick = true; btnltalic.Click += FontStyleOnClick; tool.Items.Add(btnltalic); btnUnderline = new ToolStripButtonO; btnUnderline.ImageKey = "Underline"; btnUnderline.ToolTipText = "Underline"; btnUnderline.Tag = FontStyle.Underline; btnUnderline.CheckOnClick = true; btnUnderline.Click += FontStyleOnClick; tool.Items.Add(btnUnderline); btnStrikeout = new ToolStripButtonO; btnStrikeout.ImageKey = "Strikeout"; btnStrikeout.ToolTipText = "Strikeout"; btnStrikeout.CheckOnClick = true; btnStrikeout.Tag = FontStyle.Strikeout; btnStrikeout.Click += FontStyleOnClick; tool.Items.Add(btnStrikeout); tool.Items.Add(new ToolStripSeparatorO); // Создание кнопки с раскрывающимся меню для выбора цвета фона. ToolStripSplitButton spltbtn = new ToolStripSplitButtonO; spltbtn.ImageKey = "BackColor"; spltbtn.ToolTipText = "Background Color"; spltbtn.ButtonClick += delegate { txtbox.SelectionBackColor = txtbox.BackColor; }; spltbtn.DropDownOpening += ColorOnDropDownOpening; tool.Items.Add(spltbtn); // Создание объекта Propertylnfo для цвета фона. Propertylnfo pi = typeof(RichTextBox).GetProperty("SelectionBackColor"); // Добавление палитры цветов в раскрывающийся список. ToolStripColorGrid clrgrid = new ToolStripColorGridO; clrgrid.Name = "ColorGrid";
Меню и панели инструментов 275 clrgrid.Tag = pi; clrgrid.Click += ColorGridOnClick; spltbtn.DropDownltems.Add(clrgrid); spltbtn.DropDownItems.Add(new ToolStripSeparatorO); // Добавление команды More Colors в раскрывающийся список. ToolStripMenuItem item = new ToolStripMenuItem("More colors..."); item.Tag = pi; item.Click += MoreColorsOnClick; spltbtn.DropDownltems.Add(item); // Создание кнопки для выбора цвета шрифта, spltbtn = new ToolStripSplitButtonO; spltbtn.ImageKey = "ForeColor"; spltbtn.ToolTipText = "Font Color"; spltbtn.ButtonClick += delegate { txtbox.SelectionColor = txtbox.ForeColor; }; spltbtn.DropDownOpening += ColorOnDropDownOpening; tool.Items.Add(spltbtn); // Создание объекта Propertylnfo для цвета шрифта. pi = typeof(RichTextBox).GetProperty("SelectionColor"); // Добавление палитры цветов и команды More Colors в раскрывающийся список. clrgrid = new ToolStripColorGridO; clrgrid.Name = "ColorGrid"; clrgrid.Tag = pi; clrgrid.Click += ColorGridOnClick; spltbtn.DropDownItems.Add(clrgrid); spltbtn.DropDownItems.Add(new ToolStripSeparatorO); item = new ToolStripMenuItem("More colors..."); item.Tag = pi; item.Click += MoreColorsOnClick; spltbtn.DropDownltems.Add(item); tool.Items.Add(new ToolStripSeparatorO); // Создание кнопок для выравнивания по центру, левому и правому краям. btnLeft = new ToolStripButtonO; btnLeft.ImageKey = "Left"; btnLeft.ToolTipText = "Align Left"; btnLeft.Tag = HorizontalAlignment.Left; btnLeft.Checked = true;
276 ГЛАВА 5 btnLeft.Click += AlignOnClick; tool.Items.Add(btnLeft); btnRight = new ToolStripButtonO; btnRight.ImageKey = "Right"; btnLeft.ToolTipText = "Align Right"; btnRight.Tag = HorizontalAlignment.Right; btnRight.Click += AlignOnClick; tool.Items.Add(btnRight); btnCenter = new ToolStripButtonO; btnCenter.ImageKey = "Center"; btnLeft.ToolTipText = "Align Center"; btnCenter.Tag = HorizontalAlignment.Center; btnCenter.Click += AlignOnClick; tool.Items.Add(btnCenter); // Создание кнопки маркеров. btnBullets = new ToolStripButtonO; btnBullets.ImageKey = "Bullets"; btnBullets.ToolTipText = "Bullets"; btnBullets.CheckOnClick = true; btnBullets.Click += BulletsOnClick; tool.Items.Add(btnBullets); tool.Items.Add(new ToolStripSeparatorO); // Создание меток и текстовых полей для отступов. ToolStripLabel lbl = new ToolStripLabel("Left:"); tool.Items.Add(lbl); txtLeftlndent = new ToolStripTextBoxO; txtLeftlndent.ToolTipText = "Left Indentation in Inches"; txtLeftlndent.TextChanged += IndentOnTextChanged; tool.Items.Add(txtLeftlndent); lbl = new ToolStripLabel("Right:"); tool.Items.Add(lbl); txtRightlndent = new ToolStripTextBoxO; txtRightlndent.ToolTipText = "Right Indentation in Inches"; txtRightlndent.TextChanged += IndentOnTextChanged; tool.Items.Add(txtRightlndent); lbl = new ToolStripLabel("First line:"); tool.Items.Add(lbl);
Меню и панели инструментов 277 txtFirstLine = new ToolStripTextBoxO; txtFirstLine.ToolTipText = "First Line Indentation in Inches"; txtFirstLine.TextChanged += IndentOnTextChanged; tool.Items.Add(txtFirstLine); // Инициализация ToolStrip. TextBoxOnSelectionChanged(txtbox, EventArgs.Empty); // Создание кнопок для применения полужирного, курсивного, // подчеркнутого и зачеркнутого шрифта. Bitmap FontStyleBitmap(string str, FontStyle fntstyle) { Bitmap bm = new Bitmap(16, 16); Font fnt = new Font("Times New Roman", 14, fntstyle, GraphicsUnit.Pixel); StringFormat strfmt = new StringFormatO; strfmt.Alignment = StringAlignment.Center; Graphics grfx = Graphics.Fromlmage(bm); grfx.DrawString(str, fnt, Brushes.Black, 8, 0, strfmt); grfx.Dispose(); return bm; // При каждом изменении выделения текста в RichTextBox // обновляем элементы ToolStrip. void TextBoxOnSelectionChanged(object objSrc, EventArgs args) { if (bSuspendSelectionChanged) return; Font fnt = txtbox.SelectionFont; if (fnt != null) { comboName.Selectedltem = fnt.Name; comboSize.Text = fnt.Size.ToStringO; btnBold.Checked = (fnt.Style & FontStyle.Bold) != 0; btnltalic.Checked = (fnt.Style & FontStyle.Italic) != 0; btnUnderline.Checked = (fnt.Style & FontStyle.Underline) != 0; btnStrikeout.Checked = (fnt.Style & FontStyle.Strikeout) != 0; } else {
278 ГЛАВА 5 comboName.Selectedltem = null; comboSize.Selectedltem = null; btnBold.CheckState = CheckState.Unchecked; btnltalic.CheckState = CheckState.Unchecked; btnUnderline.CheckState = CheckState.Unchecked; btnStrikeout.CheckState = CheckState.Unchecked; } HorizontalAlignment hAlign = txtbox.SelectionAlignment; btnLeft.Checked = hAlign == HorizontalAlignment.Left; btnRight.Checked = hAlign == HorizontalAlignment.Right; btnCenter.Checked = hAlign == HorizontalAlignment.Center; btnBullets.Checked = txtbox.SelectionBullet; txtLeftlndent.Text = ((txtbox.Selectionlndent + txtbox.SelectionHanginglndent) / xDpi).ToString(); txtRightlndent.Text = (txtbox.SelectionRightlndent / xDpi).ToString(); txtFirstLine.Text = (-txtbox.SelectionHanginglndent / xDpi).ToString(); // Изменение названия семейства шрифтов. void NameOnSelectionChanged(object objSrc, EventArgs args) { ChangeFont(comboName.Text, 0, 0, false); } // Изменение размера шрифта. void SizeOnSelectionChanged(object objSrc, EventArgs args) { float fSize = float.Parse(comboSize.Text); ChangeFont(null, fSize, 0, false); } // Изменение стиля шрифта. void FontStyleOnClick(object objSrc, EventArgs args) { ToolStripButton btn = (ToolStripButton)objSrc; FontStyle fntstyle = (FontStyle)btn.Tag; ChangeFont(null, 0, (FontStyle)btn.Tag, btn.Checked); // Основной способ изменения шрифта. void ChangeFont(string strName, float fSize, FontStyle fntsty, bool bAdd) { bSuspendSelectionChanged = true;
Меню и панели инструментов 279 } int iSelStart = txtbox.SelectionStart; int iSelLength = txtbox.SelectionLength; for (int iStartl = iSelStart; iStartl < iSelStart + iSelLength; ) { txtbox.Select(iStart1, 1); Font fnt = txtbox.SelectionFont; for (int iStart2 = iStartl + 1; iStart2 <= iSelStart + iSelLength; iStart2++) { txtbox.Select(iStart2, 1); Font fntNext = txtbox.SelectionFont; if (iStart2 == iSelStart + iSelLength || !fnt.Equals(fntNext)) { txtbox.Select(iStart1, iStart2 - iStartl); if (strName != null) txtbox.SelectionFont = new Font(strName, fnt.Size, fnt.Style); else if (fSize != 0) txtbox.SelectionFont = new Font(fnt.Name, fSize, fnt.Style); else if (bAdd) txtbox.SelectionFont = new Font(fnt, fnt.Style | fntsty); else txtbox.SelectionFont = new Font(fnt, fnt.Style & "fntsty); iStartl = iStart2; break; } } } bSuspendSelectionChanged = false; txtbox.Select(iSelStart, iSelLength); // Инициализация палитры цветов при открытии раскрывающегося списка. void ColorOnDropDownOpening(object objSrc, EventArgs args) { ToolStripSplitButton btn = (ToolStripSplitButton)objSrc; ToolStripColorGrid clrgrid = (ToolStripColorGrid)btn.DropDownItems["ColorGrid"]; Propertylnfo pi = (Propertylnfo) clrgrid.Tag;
280 ГЛАВА 5 clrgrid.SelectedColor = (Color)pi.GetValue(txtbox, null); } // Получение нового цвета из палитры цветов. void ColorGridOnClick(object objSrc, EventArgs args) { ToolStripColorGrid clrgrid = (ToolStripColorGrid)objSrc; Propertylnfo pi = (Propertylnfo)clrgrid.Tag; pi.SetValue(txtbox, clrgrid.SelectedColor, null); } // Отображение стандартного диалога выбора цвета, void MoreColorsOnClick(object objSrc, EventArgs args) { ToolStripMenuItem item = (ToolStripMenuItem)objSrc; Propertylnfo pi = (Propertylnfo)item.Tag; clrdlg.Color = (Color)pi.GetValue(txtbox, null); if (clrdlg.ShowDialogO == DialogResult.OK) pi.SetValue(txtbox, clrdlg.Color, null); } // Изменение выравнивания в зависимости от нажатой кнопки. void AlignOnClick(object objSrc, EventArgs args) { ToolStripButton btn = (ToolStripButton)objSrc; btnLeft.Checked = btnRight.Checked = btnCenter.Checked = false; btn.Checked = true; txtbox.SelectionAlignment = (HorizontalAlignment) btn.Tag; } // Включение или отключение маркера. void BulletsOnClick(object objSrc, EventArgs args) { ToolStripButton btn = (ToolStripButton)objSrc; txtbox.SelectionBullet = btn.Checked; } // Изменение отступа в зависимости от числа в текстовых полях. void IndentOnTextChanged(object objSrc, EventArgs args) { try { int iLeftlndent = (int)(xDpi * float.Parse(txtLeftIndent.Text)); int iRightlndent = (int)(xDpi * float.Parse(txtRightIndent.Text)); int iFirstLine = (int)(xDpi * float.Parse(txtFirstLine.Text));
Меню и панели инструментов 281 txtbox.Selectionlndent = iLeftlndent + iFirstLine; txtbox.SelectionHanginglndent = -iFirstLine; txtbox.SelectionRightlndent = iRightlndent; } catch { } } } Конструктор начинается с загрузки нескольких изображений в объект ImageList. (Я задал пространству имен по умолчанию значение пустой строки, поскольку последняя программа этой главы наследует свойства от этой формы, и, в противном случае, получила бы другое пространство имен по умолчанию и изображения не загружались бы корректно.) Эти изображения взяты из стандартного комплекта Visual Studio и отмечены как внедренные ресурсы. Хотя набор изображений из Visual Studio включает файлы Bold.bmp, Italic.bmp и Underline.bmp, файла Strikeout.bmp нет. Я решил создать все четыре изображения самостоятельно, используя метод FontStyleBitmap, код которого приводится после конструктора. В оставшейся части конструктора создаются элементы управления RicbTextBox и ToolStrip. Остальной код программы — это обработчики событий элементов управления и элементов панели ToolStrip. Исследовать логику создания и обработки событий нужно совместно. Программа устанавливает обработчик только одного события элемента RicbTextBox — это SelectionCbanged, обрабатываемое в программе методом TextBoxOn- SelectionCbanged. Обработчик события получает текущие значения SelectionFont, SelectionAlignment, Selectionlndent и другие, а затем на их основе задает значения элементам ToolStrip. Затем конструктор создает сам элемент ToolStrip и два элемента ToolStripComboBox, используемых для выбора семейства и размера шрифта. Для двух полей со списком устанавливаются обработчики события SelectedlndexCbanged. Эти обработчики (NameOnSelectionCbanged и SizeOnSelectionCbanged) довольно короткие и просто вызывают основной метод изменения шрифта CbangeFont. Затем идут четыре кнопки для изменения начертания шрифта — Bold, Italic, Underline и Strikeout. В каждом случае свойству Tag задается соответствующее значение из перечисления FontStyle: FontStyle Bold, FontStyleItalic и т. д. Задание значения свойству Tag позволяет этим кнопкам использовать один обработчик FontStyle- OnClick события Click. Этот обработчик также весьма короткий, и тоже вызывает метод CbangeFont. Метод CbangeFont должен изменять шрифт в текущем выделении, но шрифт может не отличаться у разных участков выделенного текста. Поэтому нужно изменять и анализировать выделенный текст — только так удастся корректно обновить шрифт.
282 ГЛАВА 5 Метод начинается с присвоения полю bSuspendSelectionChanged значения true. В методе TextBoxOnSelectionChanced эта переменная используется для предотвращения изменения элементов ToolStrip, когда метод ChangeFont изменяет выделенный текст. Код метода ChangeFont начинается с сохранения начала и длины текущего выделения в переменных iSelStart и iSelLength. Затем он последовательно выделяет по одному символу за раз и сравнивает шрифты. Найдя строку символов с нужным шрифтом, метод меняет его с учетом текущего шрифта и аргументов метода ChangeFont. Вернемся к конструктору: цвет фона и шрифта представлены кнопками Tool- StripSplitButton. Действие по умолчанию выполняют кнопки, у которых нет раскрывающегося меню. В данном случае я посчитал, что цвет выделения должен восстанавливаться, то есть задаваться тот, что задан в свойствах BackColor и ForeColor элемента RichTextBox. Это небольшая задача, которая выполняется прямо в конструкторе: spltbtn.ButtonClick += delegate { txtbox.SelectionBackColor = txtbox.BackColor; }; Раскрывающаяся часть кнопки отображает элементы ToolStripColorGridw ToolStrip- Menultem с текстом More colors. Оба эти элемента имеют обработчики события Click (они называются ColorGridOnClick и MoreColorsOnClick) и оба элемента числятся в наборе DropDownltems кнопки ToolStripSplitButton. Я хотел использовать для выбора обоих цветов один обработчик события Click. Один из возможных вариантов решения этой задачи — сопоставление свойству Name двух элементов TollStripColorGrid — «Foreground» и «Background», и добавление в обработчик ColorGridOnClick примерно следующего кода: if (clrgrid.Name == "Background") txtbox.SelectionBackColor = clrgrid.SelectedColor; else txtbox.SelectionColor = clrgrid.SelectedColor; Это корректный код, и он определенно выполняет задачу использования одного обработчика для выбора цветов фона и шрифта. Но я решил сделать что-то неординарное. Я создал объекты Propertylnfo, которые ссылаются на два интересующих меня свойства элемента RichTextBox. Для определения цвета фона я использовал следующее: Propertylnfo pi = typeof(RichTextBox).GetProperty("SelectionBackColor"); А для цвета шрифта: pi = typeof(RichTextBox).GetProperty("SelectionColor");
Меню и панели инструментов 283 Затем я присвоил свойствам Tag элементов ToolStripColorGrid и ToolStripMenuItem (одно из которых вызывает стандартный диалог выбора цвета) значение одноименного свойства из объекта Propertylnfo. Класс Propertylnfo чаще всего используется в рамках отображения для получения различных свойств класса, которые могут понадобиться программе. После создания объекта Propertylnfo программа может обращаться к конкретному свойству конкретного класса как к объекту. В обработчике ColorGridOnClick объект Propertylnfo служит для присвоения свойству объекта txtbox актуального значения: pi.SetValue(txtbox, clrgrid.SelectedColor, null); Если бы объект pi соответствовал свойству SelectionColror, этот код был бы эквивалентен следующему: txtbox.SelectionBackColor = clrgrid.SelectedColor; Оставшаяся часть программы относительно проста. Три кнопки, отвечающие за выравнивание текста, используют один обработчик события (AlignOnClick) и идентифицируют себя присвоением свойству Tag соответствующего члена перечисления (HorizontalAlignmentleft и т. д.). С кнопкой Bullets связано одно логическое свойство элемента RicbTextBox и у нее есть собственный обработчик события Click. Наконец, три элемента ToolStripLabel идентифицируют три элемента ToolStrip- TextBox для правого и левого отступов, а также отступа первой строки. Поскольку два из этих элементов (правый и левый отступы) связаны со свойствами элемента RicbTextBox, я снова решил использовать один обработчик для события TextCbanged. Он получает значения у всех трех элементов, преобразует их в пикселы и задает соответствующие свойства. Создание нескольких панелей инструментов К чему в конечном итоге стремится программист? Может быть, к тому, чтобы создать приложение с таким количеством панелей инструментов, что пользователь просто вынужден будет их перемещать с места на место? Вполне возможно. Если с вашим приложением так и происходит, есть несколько вариантов. Если нужно, чтобы все панели инструментов оставались в верхней части окна (или в любой другой его части), можно создать панель ToolStripPanel как потомка формы. Панель ToolStripPanel должна явно стыковаться к одному краю окна. Все объекты ToolStrip становятся дочерними для этой панели. Используйте метод Join класса ToolStripPanel, чтобы разместить объекты ToolStrip в нужных местах на панели. Пользователь сможет перемещать отдельные объекты ToolStrip в пределах общей панели. На эту панель можно поместить и объекты MenuStrip (как в приложениях Microsoft Office), тогда пользователь сможет перемещать меню под панелью. Свойству GripStyle объекта MenuStrip можно задать значение ToolStripGripStyleVisible — в крайней ле-
284 ГЛАВА 5 вой части объекта MenuStrip появится небольшая «ручка», за которую объект можно перетаскивать мышью. Программа может быть еще гибче, предоставляя пользователю возможность перемещать элементы ToolStrip и MenuStrip прикреплять их к любому из четырех краев окна или, как минимум, к верхнему и нижнему. Для этого элемент управления ToolStripContainer должен быть потомком формы, а его свойство Dock равняться DockStyle.Fill ToolStripContainer разделяет поверхность формы на пять частей. Сверху, снизу, слева и справа располагаются четыре объекта ToolStripPanel, идентифицируемые свойствами TopToolStripPanel, LeftToolStripPanel, BottomToolStripPanel и RightToolStripPanel. Объекты ToolStrip и MenuStrip размещаются на этих панелях путем назначения их потомками объекта ToolStripPanel или использования метода Join этого объекта. Можно запретить использование любой из этих панелей при помощи свойства TopToolStripPanelVisible и аналогичных. В центре этих четырех элементов управления ToolStripPanel располагается элемент управления ToolStripContentPanel, доступный через свойство ContentPanel Здесь можно размещать другие элементы управления, которые должны присутствовать на форме. Кроме этого, можно позволить пользователю изменять расположение отдельных элементов объекта ToolStrip, задав свойству AllowItemReorder значение true. Пользователь может перетаскивать мышью (удерживая кнопку Alt) отдельные элементы в пределах панели ToolStrip. В случае с несколькими панелями инструментов, пользователь может перетаскивать элементы с одной панели на другую, но только в том случае, если свойство AllowItemReorder обеих панелей равно true. Программа должна сохранять пользовательскую настройку панели инструментов до следующего запуска программы. Для этого организуют обработку событий Item- Removed и ItemAdded в каждой панели инструментов, где разрешено перемещение отдельных элементов. При перемещении элемента эти два события инициируются последовательно. (Эти события также инициируются, если сама программа добавляет или удаляет элемент панели инструментов.) Предоставляемый с событием объект представляет собой панель, с которой элемент удаляется или на которую размещается. Эти события предоставляются с объектами ToolStripItemEventArgs, у которых есть свойство Item, идентифицирующее перемещаемый элемент. Строка состояния После меню и панелей инструментов класс StatusStrip кажется простым. Строки состояния обычно располагаются в самом низу окна, поэтому свойство Dock по умолчанию равно DockStyle.Bottom. Если к нижней части окна пристыкованы другие элементы управления (например, ToolStripPanel) строку состояния нужно добавлять на форму после них.
Меню и панели инструментов 285 Обычно в форме присутствует одна строка состояния, но ничто не мешает разместить несколько таких элементов управления. Обычно правый нижний угол объекта StatusStrip содержит треугольничек, «взявшись» за который пользователь может изменить размер формы. Эту возможность можно отключить, задав свойству Sizing- Grip значение false. На панели ToolStripPanel можно также разместить один или несколько объектов StatusStrip. Если нужно разрешить пользователю перемещать эти объекты, задайте свойству GripStyle значение ToolStripGripStyleVisible. Набор элементов, отображающихся в строке состояния, обычно ограничиваются объектами ToolStripStatusLabel (которые просто отображают текст) и ToolStripProgress- Ваг. Это единственные компоненты, которые Visual Studio позволяет разместить на строке состояния, но если вас это не устраивает, при желании можно разместить там и другие элементы. Метки состояния Свойство AutoSize объекта ToolStripStatusLabel по умолчанию равно true, так что ширина метки зависит от ее содержимого. По умолчанию у меток нет обрамления. Если есть несколько элементов ToolStripStatusLabel, отображаемый в них текст будет повторяться. Можно инициировать AutoSize значением false — это позволит явно изменить свойство Width (ширина) метки. По умолчанию текст будет выравниваться по центру метки, но можно задать иное выравнивание, задав соответствующее значение свойству TextAlign. Однако лучше использовать свойство Margin. Размер метки будет подгоняться под объем содержимого, но с некоторым запасом экранного пространства. Хотя поля можно задать для всех четырех сторон, лучше ограничиться левой и правой. Еще одна популярная возможность — обрамление меток. Задайте свойству Border- Sides одно или несколько значений из перечисления ToolStripStatusLabelBorderSides — None, Left, Top, Right, Bottom и All. Внешний вид обрамления задается свойством BorderStyle, которое принимает значение из перечисления Border3DStyle. Значение по умолчанию — Border3DStyleFlat. Часто приходится располагать одни метки слева, другие — справа от строки состояния (для этого существует свойство Alignment), а третьи — посередине. Если задать свойству Spring метки, расположенной посередине, значение true, то метка будет использовать все оставшееся пространство, что даст искомый результат. Если же для всех элементов места недостаточно, текст, выходящий за пределы метки справа, виден не будет. Приведем пример программы (она наследует классу FormattingToolStrip) с объектом StatusStrip в нижней части формы. Метка слева отображает текущее выделение, а справа — показывает текущую дату и время.
286 ГЛАВА 5 Rich TextWithStatus.cs И // RichTextWithStatus.es (с) 2005 by Charles Petzold // using System; using System.Drawing; using System.Windows.Forms; class RichTextWithStatus: FormattingToolStrip { ToolStripStatusLabel lblSelection, lblDateTime; [STAThread] public static new void Main() { Application.EnableVisualStyles(); Application.Run(new RichTextWithStatus()); } public RichTextWithStatusO { Text = "Rich Text with Status"; txtbox.SelectionChanged += TextBoxOnSelectionChanged; StatusStrip stat = new StatusStripO; stat.Parent = this; lblSelection = new ToolStripStatusLabelO; stat.Items.Add(lblSelection); lblDateTime = new ToolStripStatusLabelO; lblDateTime.Alignment = ToolStripItemAlignment.Right; stat.Items.Add(lblDateTime); Timer tmr = new Timer(); tmr.Interval = 1000; tmr.Enabled = true; tmr.Tick += TimerOnTick; // Инициализация меток. TextBoxOnSelectionChanged(txtbox, EventArgs.Empty); Timer0nTick(tmr, EventArgs.Empty); } void TextBoxOnSelectionChanged(object objSrc, EventArgs args) { RichTextBox txtbox = (RichTextBox) objSrc;
Меню и панели инструментов 287 int iSelStart = txtbox.SelectionStart; int iSelLength = txtbox.SelectionLength; int iSelEnd = iSelStart + iSelLength; int iLine = txtbox.GetLineFromCharlndex(iSelStart); int iChar = iSelStart - txtbox.GetFirstCharlndexFromLine(iLine); IblSelection.Text = String.Format("Line {0} Character {1}", iLine + 1, iChar + 1); if (iSelLength > 0) { iLine = txtbox.GetLineFromCharlndex(iSelEnd); iChar = iSelEnd - txtbox.GetFirstCharlndexFromLine(iLine); IblSelection.Text += String.Format(" - Line {0} Character {1}", iLine + 1, iChar + 1); } } void TimerOnTick(object objSrc, EventArgs args) { lblDateTime.Text = DateTime.Now.ToString("G"); } }
Глава 6 Привязка и представление данных Программы, показанные в этой книге, как и следующая программа, обычно небольшие, понятные и самодостаточные. Настоящие программы большие, запутанные и используют внешние данные. Очень часто в элементах управления нужно отображать данные и изменять их по результатам операций пользователя в этих элементах управления. В большинстве случаев изменения элементов управления пользователями должны выявляться, а сами элементы управления — обновляться в соответствии с изменениями. Для создания связи между элементами управления и данными служит привязка данных (data binding) — процесс связывания свойства элемента управления со свойством другого объекта (его еще называют «источник данных»), при котором изменения в элементе управления отражаются в источнике данных и наоборот. Привязка данных всегда выполняется между свойствами двух объектов. Связывание элементов управления и данных Класс Binding из пространства имен SystemWindows.Forms — основной инструмент создания привязки данных между элементом управления и источником данных. Приложение Windows Forms обычно взаимодействует с объектами Binding через свойство DataBindings класса Control DataBindings — это экземпляр класса Control- BindingsCollection, наследующего BindingsCollection и (что следует из его названия) представляющего набор объектов Binding. Следующая программа содержит элемент управления Label, свойство Text которого привязано к свойству ClientSize окна. При изменении размера окна текст в элементе управления Label обновляется и отображает новый размер клиентской области окна.
290 ГЛАВА 6 WhatClientSize.es // // WhatClientSize.cs (с) 2005 by Charles Petzold // using System; using System.Drawing; using System.Windows.Forms; class WhatClientSize: Form { [STAThread] public static void Main() { Application. EnableVisualStylesO; Application.Run(new WhatClientSizeO); } public WhatClientSizeO { Text = "What Client Size?"; Label lbl = new Label(); lbl.Parent = this; lbl.AutoSize = true; lbl.DataBindings.Add("Text", this, "ClientSize"); } } В последнем операторе показана привязка, созданная в наборе DataBindings элемента управления Label. Это означает, что свойству Text элемента управления Label присваивается свойство ClientSize объекта this. В данном случае ясно, что ключевое слово this относится к форме, которая рассматривается как источник данных. У одного элемента управления может быть несколько привязок. Добавьте следующий оператор: lbl.DataBindings.Add("Location", this, "Location"); Перекомпилируйте программу. Теперь после ее запуска метка, скорее всего, будет не видна. Но, переместив окно в верхний левый угол экрана, вы увидите, что элемент управления Label перемещается относительно верхнего левого угла клиентской области. Свойство Location элемента управления Label (первый аргумент Add) отслеживает свойство Location формы (второй и третий аргументы Add). Метод Add в этой программе — один из семи методов Add, реализованных в классе ControlBindingsCollection. Он эквивалентен следующему коду, в котором использован другой метод Add:
Привязка и представление данных 291 Binding bind = new BindingC'Text", this, "ClientSize"); lbl.DataBindings.Add(bind); Как и метод Add в программе WhatClientSize, остальные методы Add служат для создания нового объекта Binding, а их дополнительные аргументы задают его различные свойства. В классе Binding есть шесть конструкторов, соответствующих этим методам Add. Как это работает Конструктор Binding и соответствующий метод Лй&/ класса ControlBindingsCollection требует, чтобы два связанных свойства указывались как строки. Использование строк может показаться странным. Но нет другого, более удобного способа обращения к конкретному свойству, чем по его значению (что обычно бывает при работе с объектами). Эти строки негласно преобразуются в объекты типа Propertylnfo (класс из пространства имен System.Reflectiori) с применением кода примерно следующего вида (здесь obj — объект, a str — имя свойства, определенного этим объектом): Propertylnfo propinfo = obj.GetType().GetProperty(str); GetType получает тип объекта и возвращает объект типа Туре, реализующий много свойств и методов для получения информации об этом типе, в числе которых и GetProperty. Экземпляр Propertylnfo ссылается на конкретное свойство конкретного класса, а не на свойство конкретного объекта. Если вам нужно свойство класса Form, для получения типа вместо obj.GetTypeQ можно использовать typeof(Form). Затем значение этого свойства для конкретного объекта (в данном случае obj) можно получить методом GetValue, определенным в Propertylnfo. object objValue = propinfo.GetValue(obj, null); Например, в классе, производном от Form, текущее значение свойства ClientSize можно получить с помощью такого «обычного» кода: Size sz = ClientSize; А еще его можно получить, используя отражение, вот таю Size sz = (Size) GetType().GetProperty("ClientSize").GetValue(this, null); Или так: Size sz = (Size) typeof(Form).GetProperty("ClientSize").GetValue(thisl null); Похожий метод, SetValue, позволяет задать значение свойства.
292 ГЛАВА 6 Поэтому когда программа WhatClientSize создает следующую привязку к элементу управления Label, совсем несложно получить текущее свойство ClientSize формы, преобразовать его в текст и присвоить свойству Text метки. 1Ы.DataBindings.Add("Text"', this, "ClientSize"); Это далеко не все, но значительная часть работы выполняется в самом классе PropertyManager. При создании привязки к данным элемент управления Label не просто отображает свойство ClientSize формы. Изображение меняется при изменении размера окна формы, когда PropertyManager к имени нужного свойства («ClientSize») добавляет слово «Changed», чтобы получилось «ClientSizeChanged». И неслучайно — это имя события, которое определено в классе Control и наследуется классом Form. Затем PropertyManager устанавливает обработчик этого события, чтобы получать оповещение при любом изменении свойства ClientSize и соответственно изменять метку. Что если в Form не реализовать событие ClientSizeChanged? Тогда нельзя было бы установить обработчик события, PropertyManager не узнал бы об изменении свойства ClientSize и элемент управления Label не обновился бы. Попробуйте в WhatClientSize.cs в вызове метода Add изменить «ClientSize» на «ClientRectangle». Затем перекомпилируйте и запустите программу. Теперь элемент управления Label отображает начальное значение свойства ClientRectangle формы, но при изменении размера окна он не будет обновляться, потому что события ClientRectangleCbanged не существует. Элемент управления меняет данные А можно ли сделать наоборот — чтобы источник данных откликался на изменения элемента управления? Разумеется, и, в сущности, это наиболее распространенный способ применения привязки. При создании привязки элемент управления инициализируется данными из источника, а в дальнейшем сам изменяет их. При наличии двусторонней связи элемент управления также может откликаться на изменения в источнике данных, но тогда источник данных должен поддерживать события Changed для связанных свойств. В следующей программе есть три элемента управления Checkbox, использующих форму программы как источник данных. В данном случае они изменяют данные источника за счет привязки к свойствам MinimizeBox, MaximizeBox и ControlBox формы. BooleanToggle.cs // // BooleanToggle.cs (с) 2005 by Charles Petzold // using System;
Привязка и представление данных 293 using System.Drawing; using System.Windows.Forms; class BooleanToggle : Form { [STAThread] public static void MainQ { Application.EnableVisualStyles(); Application.Run(new BooleanToggleO); } public BooleanToggleO { Text = "Boolean Toggle"; FlowLayoutPanel flow = new FlowLayoutPanel(); flow.Parent = this; flow.Dock = DockStyle.Fill; flow.FlowDirection = FlowDirection.TopDown; CheckBox chkbox = new CheckBoxO; chkbox.Parent = flow; chkbox.Text = "Minimize Box"; chkbox.AutoSize = true; chkbox.DataBindings.Add("Checked", this, "MinimizeBox"); chkbox.DataBindings[0].DataSourceUpdateMode = DataSourceUpdateMode.OnPropertyChanged; chkbox = new CheckBoxO; chkbox.Parent = flow; chkbox.Text = "Maximize Box"; chkbox.AutoSize = true; chkbox.DataBindings.Add("Checked", this, "MaximizeBox"); chkbox.DataBindings[0].DataSourceUpdateMode = DataSourceUpdateMode.OnPropertyChanged; chkbox = new CheckBoxO; chkbox.Parent = flow; chkbox.Text = "Control Box"; chkbox.AutoSize = true; chkbox.DataBindings.Add("Checked", this, "ControlBox"); chkbox.DataBindings[0].DataSourceUpdateMode = DataSourceUpdateMode.OnPropertyChanged; } }
294 ГЛАВА 6 При запуске программы свойства Checked всех элементов управления Checkbox инициализируется булевым значение свойства формы, с которым они связаны, то есть все флажки устанавливаются. Затем, вручную снимая флажки, можно отключить кнопки минимизации и максимизации или вообще убрать все кнопки с заголовка формы, сняв флажок Control Box. Обратите внимание, что за каждой привязкой следует выражение: chkbox.DataBindings[0].DataSourceUpdateMode = DataSourceUpdateMode. OnPropertyChanged; За счет индексации набора DataBindings этот оператор может обращаться к объекту Binding, созданному в предыдущем операторе. Два свойства класса Binding, ControlUpdateMode и DataSourceUpdateMode, управляют взаимным обновлением (синхронизацией) элементов управления и источников данных. Свойство ControlUpdateMode управляет обновлением элемента управления данными из источника. Оно приравнивается члену перечисления ControlUpdateMode — по умолчанию это OnPropertyChanged. To есть элемент управления будет обновляться всякий раз при изменении этого свойства в источнике данных. Поэтому программа WhatClientSize могла работать без определения свойства ControlUpdateMode. (Второй член перечисления ControlUpdateMode, Never, означает, что элемент управления не должен обновляться данными из источника.) Свойство DataSourceUpdateMode класса Binding определяет, как элемент управления обновляет источник данных. В качестве значения ему присваивается один из членов перечисления DataSourceUpdateMode-. OnPropertyChanged, OnValidation или Never. Значение по умолчанию, OnValidation, ссылается на события Validating и Validated, реализованные в классе Control. Они инициируются, когда элемент управления теряет фокус ввода. В программе они служат для проверки правильности введенных в элемент управления данных. При обработке события Validate элемента управления программа может присваивать свойству Cancel в предоставляемых событием аргументах CancelEventArgs значение true, чтобы предотвратить потерю фокуса элементом управления. (Хорошо бы, чтобы программа также сообщала пользователю, почему фокус ввода нельзя перенести на следующий элемент управления). Если свойство Cancel равно false, за событием Validating последует событие Validated, и элемент управления потеряет фокус ввода. Свойству DataSourceUpdateMode по умолчанию назначено значение DataSourceUpdateMode.OnValidation — этим создатели Windows Forms подсказывают программисту, что желательно проверить корректность значения элемента управления до того, как оно попадет в источник данных. Если проверка не нужна, приравняйте свойство DataSourceUpdateMode к DataSourceUpdateMode.OnPropertyChanged, как в программе BooleanToggle. Если убрать эти три оператора из программы, источник данных (форму) можно будет изменять с помощью флажков, но любое такое изменение вступит в силу, только после выхода соответствующего флажка из фокуса.
Привязка и представление данных 295 Поскольку Form также происходит от Control, привязку данных можно создавать как к самой форме, так и к любым элементам управления в форме. В следующей программе демонстрируются две односторонние привязки между полосой прокрутки и формой. При изменении значения полосы прокрутки меняется ширина формы, а при изменении ширины формы ползунок полосы прокрутки соответственно перемещается вверх или вниз. ChangeWidth.cs // // ChangeWidth.cs (с) 2005 by Charles Petzold // using System; using System.Drawing; using System.Windows.Forms; class ChangeWidth: Form { [STAThread] public static void Main() { Application.EnableVisualStyles(); Application.Run(new ChangeWidthQ); } public ChangeWidthQ { Text = "Change Width"; VScrollBar scrl = new VScrollBarQ; scrl.Parent = this; scrl.Dock = DockStyle.Left; scrl.Minimum = Systemlnformation.MinimumWindowSize.Width; scrl.Maximum = Systemlnformation.MaxWindowTrackSize.Width; scrl.Value = Width; // При изменении свойства Size.Width меняется свойство Value полосы прокрутки. scrl.DataBindings.Add("Value", this, "Size.Width"); scrl.DataBindings[0].DataSourceUpdateMode = DataSourcelJpdateMode. Never; // При изменении свойства Value меняется свойство Width полосы прокрутки. DataBindings.Add("Width", scrl, "Value"); DataBindings[0]. DataSourcelJpdateMode = DataSourcelJpdateMode. Never; }
296 ГЛАВА 6 В обеих привязках свойству DataSourceUpdateMode присваивается значение Never, то есть явно запрещается обновление источника данными из элемента управления. Такого же эффекта можно добиться, приравнивая эти свойства OnPropertyChanged. Так создается первая привязка в наборе полосы прокрутки. scrl.DataBindings.Adcl("Value", this, "Size.Width"); Может показаться, что свойство Value полосы прокрутки способно изменить свойство Size.Widtb формы, но это не так. Если добавить в форму оператор: Size.Width = 800; Тогда при компиляции программы появится сообщение об ошибке: «Cannot modify the return value of 'System.Windows.Forms.Form.Size' because it is not a variable» («невозможно изменить возвращаемое значение System.Windows\FormsJFormSize, потому что это не переменная»). Но хотя привязка данных не может обеспечить изменение Size.Widtb, она устанавливает обработчик события Size формы и соответственно изменяет свойство Value полосы прокрутки. В наборе формы создается вторая привязка: DataBindings.Add("Width", scrl, "Value"); Здесь полоса прокрутки рассматривается как источник данных. Именно так свойство Value полосы прокрутки изменяет свойство Widtb формы. В Form не реализовано событие WidtbCbanged, поэтому эта привязка не может изменять полосу прокрутки при изменении ширины формы. И снова о ColorScroll Элемент управления TableLayoutPanel в программе ColorScrollTable из главы 3 содержит три полосы прокрутки и несколько меток. Три полосы прокрутки служили для изменения интенсивности красного, зеленого и синего компонентов цвета фона панели. Я использовал привязку данных для обновления трех элементов управления Label с учетом текущих значений полос прокрутки. alblValue[col].DataBindings.Add("Text", vscrl, "Value"); Однако характер структуры Color не позволил мне в полной мере использовать привязку данных. Создав Color, изменить ее уже нельзя. Поля R,GwB доступны только для чтения, и в Color не реализованы никакие события. Для полной реализации привязки данных в этой программе сначала нужно создать новый класс цвета с необходимыми функциями. В приведенном ниже классе Rgb цвет представлен свойствами Red, Green и Blue, значения которых можно и считывать, и задавать через открытые свойства. Свойство Color служит для преобразования между обычной структурой Color и этими тремя компонентами цвета.
Привязка и представление данных 297 Rgb.cs // // Rgb.cs (с) 2005 by Charles Petzold // using System; using System.Drawing; using System.Windows.Forms; class Rgb { public event EventHandler ColorChanged; int r, g, b; public int Red { get { return r; } set { r = value; OnColorChanged(this, EventArgs.Empty); } } public int Green { get { return g; } set { g = value; 0nColorChanged(this, EventArgs.Empty); } } public int Blue { get { return b; } set { b = value; 0nColorChanged(this, EventArgs.Empty); } } public Color Color { get { return Color.FromArgb(Red, Green, Blue); } set {
298 ГЛАВА 6 г = value.R; g = value.G; b = value.B; OnColorChanged(this, EventArgs.Empty); } } protected virtual void OnColorChanged(object objSrc, EventArgs args) { if (ColorChanged != null) ColorChanged(objSrc, args); } } Класс Rgb также включает событие ColorChanged, инициируемое при каждом изменении цвета, хранимого в классе. Класс RgbScroll использует Rgb для реализации прокрутки цвета без явной обработки событий. RgbScroll.cs // // RgbScroll.cs (с) 2005 by Charles Petzold // using System; using System.Drawing; using System.Windows.Forms; class ColorScrollTable : Form { [STAThread] public static void Main() { Application.EnableVisualStyles(); Application.Run(new ColorScrollTableQ); } public ColorScrollTableO { Text = "Rgb Scroll with TableLayoutPanel"; // Создание SplitContainer, заполняющего собой клиентскую область. SplitContainer spit = new SplitContainerO; spit.Parent = this; spit.Dock = DockStyle.Fill; splt.SplitterDistance = ClientSize.Width / 2;
Привязка и представление данных 299 // TableLayoutPanel находится слева от разделителя. TableLayoutPanel table = new TableLayoutPanelQ; table. Parent = spit. Panel"!; table.Dock = DockStyle.Fill; table.BackColor = Color.White; table.ColumnCount = 3; table.RowCount = 3; // Создание правой панели SplitterPanel для отображения цвета. Panel pnlColor = splt.Panel2; // Создание объекта Rgb и назначение ему цвета pnlColor. Rgb rgb = new Rgb(); rgb.Color = pnlColor.BackColor; // Привязка цвета фона к rgb. pnlColor.DataBindings.Add("BackColor", rgb, "Color"); // Массив для названий цветов. string[] astrColors = { "Red", "Green", "Blue" }; // Перечисление в цикле трех столбцов (красного, зеленого, синего). for (int col = 0; col < 3; col++) { // Метка вверху указывает на цвета: красный, зеленый или синий. Label 1Ы = new LabelO; lbl.AutoSize = true; lbl.Anchor = AnchorStyles.None; lbl.Text = astrColors[col]; lbl.ForeColor = Color.FromName(astrColors[col]); table.Controls.Add(lbl, col, 0); // Полоса прокрутки, присваивающая новые значения, привязана к rgb. VScrollBar vscrl = new VScrollBarO; vscrl.Parent = table; vscrl.Anchor = AnchorStyles.Top | AnchorStyles.Bottom; vscrl.TabStop = true; vscrl.LargeChange = 16; vscrl.Maximum = 255 + vscrl.LargeChange - 1; vscrl.DataBindings.Add("Value", rgb, astrColors[col]); vscrl.DataBindings[0].DataSourceUpdateMode = DataSourceUpdateMode.OnPropertyChanged; table.Controls.Add(vscrl, col, 1); // Метка, отображающая цвет, привязана к полосе прокрутки. Label lblValue = new Label();
300 ГЛАВА 6 lblValue.AutoSize = true; lblValue.Anchor = AnchorStyles.None; lblValue.ForeColor = Color.FromName(astrColors[col]); lblValue.DataBindings.Add("Text", vscrl, "Value"); table.Controls.Add(lblValue, col, 2); // В ColumnStyles создаются столбцы одинакового размера, table.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 33)); } // В RowStyles задается максимально возможный размер средней строки, table.RowStyles.Add(new RowStyle(SizeType.AutoSize)); table.RowStyles.Add(new RowStyle(SizeType.Percent, 100)); table.RowStyles.Add(new RowStyle(SizeType.AutoSize)); } } В этой программе есть несколько вариантов привязки данных. В первом свойство BackColor панели привязывается к свойству Color объекта Rgb. pnlColor.DataBindings.Add("BackColor", rgb, "Color"); Цвет фона панели обновляется значением из источника данных (объекта rgb) при появлении события ColorCbanged. Свойство Value каждой полосы прокрутки связано с одним из свойств — Red, Green или Blue — объекта Rgb. vscrl.DataBindings.Add("Value", rgb, astrColors[col]); В этом случае элемент управления изменяет источник данных (объект rgb), a свойство DataSourceUpdateMode объекта Binding нужно приравнять OnProperty- Changed. В конце программы элементы управления Label привязываются к значениям полос прокрутки. lblValue.DataBindings.Add("Text", vscrl, "Value"); Именно эта привязка была реализована в предыдущей версии программы. Избавившись от всякой явной обработки событий, мне удалось создать программу, у которой нет «механических сочленений», а это значит, что она работает четче и требует меньших усилий на поддержку и сопровождение. Отличительные особенности СотЬоВох Свойства элементов управления ListBox и СотЬоВох также можно привязывать к источникам данных, но обычно это делается чуть иначе. Эти элементы управления
Привязка и представление данных 301 происходят от абстрактного класса ListControl, в котором определены нужные свойства. Следующие рассуждения относятся к СотЬоВох, но в равной степени справедливы для ListBox. Начнем с повторения пройденного. Обычно СотЬоВох заполняется несколькими вызовами метода Add свойства Items, причем заполнить его можно объектами любого типа. Для отображения объекта в списке используется метод ToString этого объекта. Как правило, устанавливается обработчик события SelectedlndexChanged или SelectedValueCbanged элемента управления. При инициации события выбранный пользователем элемент в списке можно определить, используя свойство Selectedlndex для получения искомого объекта по индексу из набора Items или напрямую получить свойство Selectedltem. Обратите внимание на различие имен события и свойства: обработчик устанавливается для события SelectedValueCbanged, а доступ выполняется к свойству Selectedltem. Для получения уведомления об изменении свойства Selectedltem логичнее установить обработчик события SelectedltemCbanged, но события с таким именем нет. Есть свойство SelectedValue, но если для заполнения списка задействовать свойство Items, значение SelectedValue останется равным null. При таком традиционном подходе к заполнению списка событие SelectedValueCbanged инициируется, даже когда свойство SelectedValue вообще не изменялось. Это значит, что попытка привязки показанным ниже оператором к свойству SelectedValue элемента управления СотЬоВох провалится, так как SelectedValue всегда возвращает значение null. combo.DataBindings.Add("SelectedValue", obj, "SomeProperty"); В классе ListControl (от которого происходят ListBox и СотЬоВох) реализован альтернативный метод заполнения элемента управления — с помощью свойства DataSource. Это свойство можно инициализировать экземпляром любого класса, который реализует интерфейс IList или IListSource и включает массивы, объекты ArrayList и List. Но, задав свойство DataSource, вы уже не сможете добавить в СотЬоВох другие элементы с помощью свойства Items. Например, СотЬоВох можно заполнить так: combo.DataSource = Enum.GetValues(typeof(KnownColor)); Метод Enum.GetValues возвращает массив из всех значений перечисления, поэтому этот код заполняет СотЬоВох всеми членами перечисления KnoivnColor. Теперь свойство SelectedValue возвращает то же значение, что и свойство Selectedltem — в данном случае это один из членов перечисления KnownColor. И все же, привязку данных для свойства SelectedValue создать нельзя. Иначе будет сгенерировано исключение, указывающее на проблему со свойством ValueMember.
302 ГЛАВА 6 Согласно документации свойство DataSource — это объект, реализующий интерфейсы Hist или IListSource и включающий массивы. Но для этого свойства необходим массив объектов, у которого есть хотя бы одно, лучше два открытых свойства. Одно из них рассматривается как реальное «значение» объекта, а второе (если оно есть) — это, скорее всего, текстовая строка для отображения объекта в элементе управления. Присвоив свойству DataSource элемента управления СотЬоВох массив (или что-то другое), нужно также присвоить свойству ValueMember элемента управления имя первого из этих свойств объектов-массивов, a DisplayMember — второго из этих свойств. Например, пусть у класса Doodle есть свойство DoodleValue и свойство DoodleName типа string. Имеется массив объектов Doodle с именем adoodle. Тогда СотЬоВох создается так: combo.DataSource = adoodle; combo.ValueMember = "DoodleValue"; combo.DisplayMember = "DoodleName". СотЬоВох использует свойства DoodleName для отображения элементов списка. А его свойство SelectedVdlue — это объект того же типа, что и свойство DoodleValue. Определим простой класс, который можно использовать со свойством DataSource элемента управления СотЬоВох. Он основан на перечислении KnownColor, содержащем имена всех цветов в структуре Color (например, AliceBlue и PapayaWhip), a также имена системных цветов (таких как HighlightText и InactiveCaption). KnownColorClass.cs // // KnownColorClass.cs (с) 2005 by Charles Petzold // using System; using System.Drawing; class KnownColorClass { KnownColor kc; public KnownColorClass(KnownColor kc) { this.kc = kc; } public Color Color { get { return Color.FromKnownColor(kc); } } public string Name
Привязка и представление данных 303 { get { string str = Enum.GetName(typeof(KnownColor), kc); for (int i = 1; i < str.Length; i++) if (Char.IsUpper(str[i])) str = str.Insert(i++, " "); return str; } } public static KnownColorClass[] KnownColorArray { get { // Создание массива объектов KnownColorClass. KnownColor[] akc = (KnownColor[])Enum.GetValues(typeof(KnownColor)); KnownColorClass[] akcc = new KnownColorClass[akc.Length]; for (int i = 0; i < akc.Length; i++) akcc[i] = new KnownColorClass(akc[i]); return akcc; } } } Этот класс хранит члены перечисления KnownColor в виде закрытых полей. Открытое свойство Color возвращает объект Color на основе члена этого перечисления, а свойство Name возвращает строку с именем цвета, разбитую на отдельные слова, начинающиеся с заглавной буквы. Для удобства статическое свойство KnownColorArray возвращает массив объектов KnownColorClass, созданный на основе всех членов перечисления KnownColor. Проект ComboBoxBind включает KnownColorClass.cs и следующий файл. ComboBoxBind.cs // // ComboBoxBind.cs (с) 2005 by Charles Petzold // using System; using System.Drawing; using System.Windows.Forms;
304 ГЛАВА 6 class ComboBoxBind : Form { [STAThread] public static void Main() { Application.EnableVisualStyles(); Application.Run(new ComboBoxBindO); } public ComboBoxBindO { Text = "ComboBox Bind"; // Создание элемента управления ComboBox. ComboBox combo = new ComboBox(); combo.Parent = this; combo.DropDownStyle = ComboBoxStyle.DropDownList; combo.AutoSize = true; combo.Width = 12 * Font.Height; // Инициация свойств DataSource, ValueMember и DisplayMember. combo.DataSource = KnownColorClass.KnownColorArray; combo.ValueMember = "Color"; combo.DisplayMember = "Name"; // Создание привязки ComboBox с цветом фона формы. combo.DataBindings.Add("SelectedValue", this, "BackColor"); combo.DataBindings[0].DataSourceUpdateMode = DataSourceUpdateMode.OnPropertyChanged; } } После создания ComboBox его свойству DataSource присваивается массив объектов KnownColorClass, созданный статическим свойством KnownColorArray в этом классе. Свойству ValueMember присваивается значение «Color» — имя свойства ъ KnownColorClass, возвращающего значение Color, a DisplayMember присваивается значение «Name» — имя свойства в KnownColorClass, возвращающего строку, которая обозначает цвет. И, наконец, конструктор форм завершает работу, создавая привязку между свойством SelectedValue элемента управления ComboBox и свойством BackColor формы. По умолчанию, свойство BackColor формы — это значение Color, полученное из статического свойства SystemColors.Control Это значение Color не соответствует ни одному из статических, доступных только для чтения свойств в структуре Color, но оно соответствует члену перечисления KnownColor с именем Control При запуске этой программы она и в самом деле показывает правильный исходный цвет в ComboBox.
Привязка и представление данных 305 Щ ComboBox Bind мшй И, конечно, при изменении значения в ComboBox цвет фона формы также меняется. Основы ввода данных Ввод данных — одна из наиболее распространенных задач, с которой сталкиваются программисты. Благодаря привязке данных он претерпел революционные изменения. Дополнения к Windows Forms в .NET 2.0 — особенно BindingNavigator и важный элемент управления DataGridView — еще больше упрощают ввод данных. Тема исключительно обширна, поэтому здесь я дам лишь поверхностный ее обзор. Я немного остановлюсь на пространстве имен System.Data, но лучше всего оно описано в книгах, посвященных работе с базами данных. Традиционный подход Допустим, нужно создать базу данных конкретной группы лиц, включив в нее имя, фамилию и дату рождения каждого человека. Для начала лучше определить класс, содержащий открытые свойства для этих трех элементов. Эти свойства просто обеспечивают открытый интерфейс к закрытым полям. Person.cs // // Person.cs (с) 2005 by Charles Petzold // using System; public class Person { // Закрытые поля. string strFirstName, strLastName; DateTime dtBirth = new DateTime(1800, 1, 1);
306 ГЛАВА 6 // Открытые свойства, public string FirstName get { return strFirstName; } set { strFirstName = value; } public string LastName get { return strLastName; } set { strLastName = value; } public DateTime BirthDate get { return dtBirth; } set { dtBirth = value; } } Затем нужно создать форму с элементами управления для ввода этой информации. Разместим все элементы управления не на объекте типа Form, а на панели, что позволит использовать ее повторно в дальнейшем. Чтобы упростить задачу, создадим класс, производный от FlowLayoutPanel Он содержит три метки, DataTimePicker и два элемента управления TextBox. Я назвал этот класс PersonPanelNoBinding, потому как в нем не используются привязки данных, о которых говорилось ранее в этой главе. PersonPanelNoBinding.cs // // PersonPanelNoBinding.cs (с) 2005 by Charles Petzold // using System; using System.Drawing; using System.Windows.Forms; class PersonPanelNoBinding : FlowLayoutPanel { TextBox txtboxFirstName, txtboxLastName; DateTimePicker dtPicker; // Открытое свойство, public Person Person { set {
Привязка и представление данных 307 txtboxFirstName.Text = value.FirstName; txtboxLastName.Text = value.LastName; dtPicker.Value = value.BirthDate; } get { Person pers = new Person(); pers.FirstName = txtboxFirstName.Text; pers.LastName = txtboxLastName.Text; pers.BirthDate = dtPicker.Value; return pers; } } // Конструктор. public PersonPanelNoBindingO { Label lbl = new Label(); lbl.Parent = this; lbl.Text = "First Name: "; lbl.AutoSize = true; lbl.Anchor = AnchorStyles.Left; txtboxFirstName = new TextBox(); txtboxFirstName.Parent = this; txtboxFirstName.AutoSize = true; this.SetFlowBreak(txtboxFirstName, true); lbl = new LabelQ; lbl.Parent = this; lbl.Text = "Last Name: "; lbl.AutoSize = true; lbl.Anchor = AnchorStyles.Left; txtboxLastName = new TextBoxO; txtboxLastName.Parent = this; txtboxLastName.AutoSize = true; this.SetFlowBreak(txtboxLastName, true); lbl = new Label(); lbl.Parent = this; lbl.Text = "Birth Date: "; lbl.AutoSize = true; lbl.Anchor = AnchorStyles.Left;
308 ГЛАВА 6 dtPicker = new DateTimePicker(); dtPicker.Parent = this; dtPicker.Format = DateTimePickerFormat.Long; dtPicker.AutoSize = true; } } В классе определено открытое свойство Person одноименного типа. Обратите внимание, как оно реализовано: аксессор set присваивает свойствам Text двух элементов управления TextBox значения свойств FirstName и LastName объекта Person, а свойству Value объекта DataTimePicker — значение свойства BirtbDate. Аксессор get создает новый объект типа Person, задает значения его свойств, полученные от трех элементов управления, и возвращает этот объект. Программа, в которой используется эта панель, может инициализировать все элементы управления от объекта Person, просто задав свойство Person, и получить запись пользователя из свойства Person. Легко продолжить аналогию, представив себе программу, в которой есть массив объектов Person и отдельные от этой панели дополнительные элементы управления, позволяющие просматривать массив, а также добавлять и удалять объекты Person. В каждый момент времени пользователю виден лишь один из этих объектов Person — тот, что отображается в панели. Конечно, со временем мы решим и эту задачу, но пока посмотрим, как успешно загрузить и сохранить один объект Person. Сериализация в XML Не так давно в подобной ситуации я стал бы рассказывать, как записать свойства Person в простой текстовый файл в формате с разделением полей запятыми (comma- separated values, CSV), используя классы файлового ввода-вывода платформы .NET. Но теперь если для этих целей вы используете что-либо отличное от XML, вас попросту не поймут или даже подвергнут остракизму. Для хранения одного объекта Person (именно это нам сейчас нужно) XML-файл будет примерно таким: <Person> <FirstName> Johannes </FirstName> <LastName> Brahms </LastName> <BirthDate> 1833-05-07T00:00:00 </BirthDate> </Person>
Привязка и представление данных 309 Такой файл можно записать и прочитать несколькими способами. Поскольку XML- файлы — это по сути просто текстовые файлы, при желании можно перейти к низкоуровневой записи и чтению угловых скобок и слешей с помощью классов StreamWriter и StreamReader из пространства имен System.lO. Однако гораздо лучше воспользоваться возможностями классов из пространства имен SystemXml Класс XmlTextWriter позволяет записывать XML-файл с помощью таких методов, как WriteStartElement, WriteString и WriteEndElement. Класс XmlTextReader немного сложнее: обычно для чтения следующего узла (будь то элемент, атрибут, данные и так далее) используют метод Read, а затем по свойствам NodeType, Name и Value определяют, что это за узел. Хотя классы XmlTextWriter и XmlTextReader намного проще и безопаснее, чем StreamWriter и StreamReader, проще всего считывать и записывать XML-файлы методами класса XmlSerializer из пространства имен SystemXmlSerialization. Сериали- зация (serialization) — это процесс преобразования объекта в форму, удобную для передачи, в данном случае — в XML-поток. Приведенный выше XML-файл в точности соответствует классу Person. Его элементы FirstName, LastName и BirthDate вложены в элемент Person — ведь свойства FirstName, LastName и BirthDate являются членами класса Person. Должен быть код, преобразующий объект типа Person в XML-файл и обратно — это класс XmlSerializer. Иногда он работает не совсем так, как нужно, но все равно его стоит опробовать. Имейте в виду, что в случае затруднений, ситуацию всегда можно взять под полный контроль, вернувшись к классам XmlTextWriter и XmlTextReader. Сначала создают объект типа XmlSeiializer, указав тип класса, с которым он должен работать, например: XmlSerializer xmlser = new XmlSerializer(typeof(Person)); Теперь можно сохранить объект типа Person в XML, вызвав метод Serialize. Этот метод нельзя перегружать, поэтому ему передается лишь имя файла. Необходим хотя бы один объект Stream, XmlWriter или TextWriter (от которого происходит Stream- Writer). Здесь программисту предоставляется определенная свобода выбора. При создании объекта типа StringWriter (который также наследует классу TextWriter) можно записывать XML в строковую переменную (типа string). Создав экземпляр Network- Stream или MemoryStream (оба наследуют классу Stream), можно записывать XML в указанное место в сети или в блок памяти. А так объект Person с именем pers записывается в обычный локальный файл. StreamWriter sw = new StreamWriter(strFileName); xmlser.Serialize(sw, pers); sw.Close();
310 ГЛАВА 6 Метод Deserialize класса XmlSerializer преобразует XML в объект, тип которого указан в конструкторе XmlSerializer. Здесь Deserialize создает объект типа Person. Deserialize по определению должен вернуть объект, поэтому возвращаемое значение нужно привести к соответствующему типу, если объект предполагается хранить в объекте типа Person. Например: StreamReader sr = new StreamReader(strFileName); pers = xmlser.Deserialize(sr) as Person; sr.Close(); Легко догадаться, что для обращения к членам класса, указанного в конструкторе, XmlSerializer использует отображение. Согласно документации он на самом деле генерирует код, исполняемый вне приложения. Поэтому любой класс, который нужно сериализовать, должен определяться как public (открытый). Вы, наверное, заметили, что класс Person определен именно так. XmlSerializer сериализует и десериализует только поля и свойства, определенные как открытые, и игнорирует все члены, доступные только для чтения или записи. Выполняя десериализацию, метод Deserialize создает объект нужного типа с помощью непараметризованного конструктора класса и задает свойства объекта, извлекая значения из элементов XML. Поэтому у класса должен быть ^параметризованный конструктор (как у класса Person). Если сериализуемые члены класса представляют собой сложные типы данных (например, другие классы или структуры), эти классы и структуры должны также определяться как открытые и иметь непараметризованные конструкторы. А их открытые свойства и поля записи-считывания XmlSerializer будет интерпретировать как вложенные элементы. XmlSerializer также работает со свойствами, которые являются массивами и объектами List. В пространстве имен SystemXml.Serialization также есть ряд атрибутов, позволяющих управлять процессом сериализации и десериализации. Например, такой атрибут, стоящий перед любым открытым полем или свойством записи-считывания, запрещает его сериализацию. [Xmllgnore] Теперь мы готовы перейти к классу DataEntryNoBinding, завершающему одноименный проект. В этом проекте помимо ссылок, обычно используемых в приложениях Windows Forms, нужна ссылка на динамически подключаемую библиотеку System. Xml В программе создается объект типа PersonPanelNoBinding и меню File с командами New, Open и Save. XmlSerializer определяется как поле, потому что он используется в обработчиках событий открытия (File Open) и сохранения (File Save) файла.
Привязка и представление данных 311 DataEntryNoBinding.cs // // DataEntryNoBinding.cs (с) 2005 by Charles Petzold // using System; using System.Drawing; using System.10; using System.Windows.Forms; using System.Xml.Serialization; class DataEntryNoBinding : Form { const string strFilter = "Person XML files (*.PersonXml)|" + "*.PersonXml|All files (*.*)|*.*"; PersonPanelNoBinding personpnl; XmlSerializer xmlser = new XmlSerializer(typeof(Person)); [STAThread] public static void Main() { Application.EnableVisualStyles(); Application.Run(new DataEntryNoBindingO); } public DataEntryNoBindingO { Text = "Simple Data Entry (No Binding)"; // Создание панели. personpnl = new PersonPanelNoBindingO; personpnl.Parent = this; personpnl.Dock = DockStyle.Fill; // Создание меню. MenuStrip menu = new MenuStripO; menu.Parent = this; ToolStripMenuItem item = (ToolStripMenuItem)menu.Items.Add("&File"); item.DropDownItems.Add("&New", null, FileNewOnClick); item.DropDownItems.Add("&0pen...", null, FileOpenOnClick); item.DropDownItems.Add("Save &As...", null, FileSaveAsOnClick); } void FileNewOnClick(object objSrc, EventArgs args) { personpnl.Person = new PersonO; } void File0pen0nClick(object objSrc, EventArgs args)
312 ГЛАВА 6 OpenFileDialog dig = new OpenFileDialogO; dig.Filter = strFilter; if (dlg.ShowDialogO == DialogResult.OK) { StreamReader sr = new StreamReader(dlg.FileName); personpnl.Person = xmlser.Deserialize(sr) as Person; sr.Close(); } } void FileSaveAsOnClick(object objSrc, EventArgs args) { SaveFileDialog dig = new SaveFileDialogO; dig.Filter = strFilter; if (dlg.ShowDialogO == DialogResult.OK) { StreamWriter sw = new StreamWriter(dlg.FileName); xmlser.Serialize(sw, personpnl.Person); sw.Close(); } А вот окно программы с введенными данными: Simple Data Entry (No Binding} 0НЮ First Name: j Johannes Last Name: j Brahms Bkth Date: Tuesday . May 07,J Можно поэкспериментировать с заполнением полей, сохранением файлов, их перезагрузкой, а также просмотреть файлы в блокноте, Internet Explorer или других программах редактирования и просмотра XML-файлов. Файлы сохраняются с расширением PersonXml.
Привязка и представление данных 313 Обработчики событий File New и File Open создают новый объект типа Person (первый делает это напрямую, а второй — неявно) и инициализируют его свойством Person класса PersonPanelNoBinding. Обработчик события File Save получает свойство Person от PersonPanelNoBinding и сохраняет его в файле. Программа создает объект типа Person лишь при выполнении обработчиков событий File New или File Open. Это объясняет одну особенность программы: при первом запуске Data Time- Picker отображает текущую дату. При выборе команды New в меню File отображается дата 01 января 1800 г. DataTimePicker отображает текущую дату, когда его свойство Value не инициализировано. А вторая дата отражает то, чем инициализируется поле BirtbDate класса Person при создании нового объекта Person. Во избежание этого разногласия в конструктор DataEntryNoBinding нужно добавить код для инициализации объекта PersonPanelNoBinding новым объектом типа Person. personpnl.Person = new Person(); Или смоделировать вызов обработчика события File New из конструктора: FileNewOnClick(null, EventArgs.Empty); Последний подход отлично подходит в случае, когда код обработчика занимает несколько строк. Когда привязка не работает Программа DataEntryNoBinding хорошо справляется со своей задачей, но она мне не нравится. Что если понадобится добавить новое свойство? Разумеется, придется менять класс Person. С этим еще можно смириться. Также может потребоваться добавить в панель дополнительные элементы управления. И это можно перетерпеть. Но при этом надо не забыть добавить несколько строк кода в аксессоры set и get свойства Person панели. Казалось бы, применение привязки позволит избежать подобных излишеств. Добавим привязку данных в PersonPanelNoBinding и посмотрим, что получится. Код привязки данных в двух элементах управления TextBox и DateTimePicker. txtboxFirstName.DataBindings.Add("Text", pers, "FirstName"); txtboxLastName.DataBindings.Add("Text", pers, "LastName"); dtPicker.DataBindings.Add("Value", pers, "BirthDate"); Второй аргумент метода Add, объект типа Person, должен существовать (то есть pers не должен быть равным null), когда метод Add создает объект Binding. Его лучше определить как поле: Person pers = new PersonO; Теперь свойство Person упрощается до нужного нам вида.
314 ГЛАВА 6 public Person Person { set { pers = value; } get { return pers; } } Чтобы проверить его в действии, вводим в поля произвольные данные, выбираем команду Save в меню File и смотрим полученный XML-файл, — он в порядке. На первый взгляд, все отлично, но... команды New и Open вообще не работают! Все дело в том, что привязка не позволяет получить уведомление об изменении источника данных (объекта Person), поэтому и не обновляются элементы управления. Можно было бы добавить в класс Person события FirstNameCbanged, LastName- Cbanged и BirtbDateCbanged, но это уже чересчур. Другой вариант — вручную обновить элементы управления с помощью следующего аксессора set в свойстве Person. set { pers = value; foreach (Control Ctrl in Controls) foreach (Binding bind in ctrl.DataBindings) bind.ReadValue(); } Этот код в цикле перечисляет все элементы управления набора Controls панели, а затем — все объекты Binding в наборе DataBindings каждого элемента управления и вызывает ReadValue для внесения новых данных в элемент управления. Но есть еще одна сложность: привязки, определенные для каждого из трех элементов управления, относятся не к переменной pers, а к объекту Person, который изначально был создан как поле и затем сохранен как pers. Person pers = new PersonQ; В первом выражении аксессора set этот объект заменяется на другой, созданный в обработчиках событий File New или File Open класса DataEntryNoBinding. A нам нужно скопировать все поля переданного аксессору значения в исходный объект Person, вот так: set { pers.FirstName = value.FirstName; pers.LastName = value.LastName; pers.BirthDate = value.BirthDate;
Привязка и представление данных 315 foreach (Control Ctrl in Controls) foreach (Binding bind in ctrl.DataBindings) bind.ReadValue(); } Этот код, наконец-то, работает, но мы не добились нужного упрощения программы. Есть другой подход: использовать во всей программе один объект Person, совместно используемый как в PersonPanelNoBinding, так и в DataEntryNoBinding. Но тогда обработчик события File New должен явно присваивать всем свойствам Person значения по умолчанию, а обработчику события File Open придется выполнять дополнительное копирование. На самом деле нужен посредник между элементами управления и объектом Person. Нужно привязать элементы управления к одному объекту, который не меняется на всем протяжении программы, и уже к нему подключать различные объекты Person. Нужный нам волшебный класс называется BindingSource, и впервые он появился в .NET 2.O. Он решает эту задачу с большим запасом гибкости. Посредничество BindingSource Сначала посмотрим, как можно использовать BindingSource на панели с элементами управления. Следующий класс PersonPanel содержит конструктор с параметром, в котором в класс передается объект BindingSource. Разумеется, объект BindingSource создается программой, которая также создает объект PersonPanel. PersonPanel.cs // // PersonPanel.cs (с) 2005 by Charles Petzold // using System; using System.Drawing; using System.Windows.Forms; class PersonPanel : FlowLayoutPanel { // Конструктор. public PersonPanel(BindingSource bindsrc) { Label lbl = new Label(); lbl.Parent = this; lbl.Text = "First Name: "; lbl.AutoSize = true; lbl.Anchor = AnchorStyles.Left;
316 ГЛАВА 6 TextBox txtboxFirstName = new TextBoxO; txtboxFirstName.Parent = this; txtboxFirstName.AutoSize = true; txtboxFi rstName.DataBindings.Add("Text", binds re, "Fi rstName"); txtboxFirstName.DataBindings[0].DataSourceUpdateMode = DataSourceUpdateMode.OnPropertyChanged; this.SetFlowBreak(txtboxFirstName, true); lbl = new Label(); lbl.Parent = this; lbl.Text = "Last Name: "; lbl.AutoSize = true; lbl.Anchor a AnchorStyles.Left; TextBox txtboxLastName = new TextBoxO; txtboxLastName.Parent = this; txtboxLastName.AutoSize = true; txtboxLastName.DataBindings.Add("Text", bindsrc, "LastName"); txtboxLastName.DataBindings[0].DataSourceUpdateMode = DataSourceUpdateMode.OnPropertyChanged; this.SetFlowBreak(txtboxLastName, true); lbl = new Label(); lbl.Parent = this; lbl.Text = "Birth Date: "; lbl.AutoSize = true; lbl.Anchor = AnchorStyles.Left; DateTimePicker dtPicker = new DateTimePickerO; dtPicker.Parent = this; dtPicker.Format = DateTimePickerFormat.Long; dtPicker.AutoSize = true; dtPicker.DataBindings.Add("Value", bindsrc, "BirthDate"); dtPicker.DataBindings[0].DataSourceUpdateMode = DataSourceUpdateMode.OnPropertyChanged; } } Этот код содержит привязки двух элементов управления TextBox и DataTimePicker. Вот первая из них: txtboxFirstName.DataBindings.Add("Text", bindsrc, "FirstName"); Первый аргумент метода Add ничем не выделяется: данные привязываются к свойству Text объекта TextBox. Второй аргумент — это переданный конструктору
Привязка и представление данных 317 объект BindingSource, а третий аргумент, как ни странно, — это строка «FirstName». Хотя о классе BindingSource пока известно немного, в нем точно нет свойства FirstName. Но, как мы увидим далее, можно создать привязку BindingSource к объекту (например, типа Person), у которого есть свойства FirstName, LastName и BirthDate. Именно так BindingSource предоставляет эту информацию. Но об этом чуть ниже. Между тем, у класса PersonPanel есть масса достоинств. В нем нет обработчиков событий и свойства Person, да и с классом Person у него нет ничего общего — он лишь получает данные от объекта BindingSource. В проекте DataEntryWithBinding есть ссылка на Person.cs, он также включает PersonPanel.cs и следующий файл. DataEntryWithBinding.cs // // DataEntryWithBinding.cs (с) 2005 by Charles Petzold // using System; using System.Drawing; using System.10; using System.Windows.Forms; using System.Xml.Serialization; class DataEntryWithBinding : Form { const string strFilter = "Person XML files (*.PersonXml)|" + "*.PersonXml|All files (*.*)!*■*"; XmlSerializer xmlser = new XmlSerializer(typeof(Person)); BindingSource bindsrc = new BindingSourceO; [STAThread] public static void Main() { Application. EnableVisualStylesO; Application.Run(new DataEntryWithBindingO); } public DataEntryWithBindingO { Text = "Simple Data Entry with Binding"; // Инициализация объекта BindingSource. bindsrc.Add(new PersonO); // Создание панели. PersonPanel personpnl = new PersonPanel(bindsrc); personpnl.Parent = this; personpnl.Dock = DockStyle.Fill;
318 ГЛАВА 6 // Создание меню. MenuStrip menu = new MenuStripQ; menu.Parent = this; ToolStripMenuItem item = (ToolStripMenuItem) menu.Items.Add("&File"); item.DropDownItems.Add("&New", null, FileNewOnClick); item.DropDownItems.Add("&Open...", null, FileOpenOnClick); item.DropDownltems.AddCSave &As...", null, FileSaveAsOnClick); } void FileNewOnClick(object objSrc, EventArgs args) { bindsrc[0] = new PersonO; } void FileOpenOnClick(object objSrc, EventArgs args) { OpenFileDialog dig = new OpenFileDialogO; dig.Filter = strFilter; if (dlg.ShowDialogO == DialogResult.OK) { StreamReader sr = new StreamReader(dlg.FileName); bindsrc[0] = xmlser.Deserialize(sr); sr.Close(); } } void FileSaveAsOnClick(object objSrc, EventArgs args) { SaveFileDialog dig = new SaveFileDialogO; dig.Filter = strFilter; if (dlg.ShowDialogO == DialogResult.OK) { StreamWriter sw = new StreamWriter(dlg.FileName); xmlser.Serialize(sw, bindsrc[0]); sw.Close(); } } } Объект BindingSource определен как поле, потому что он используется в нескольких методах этого класса. BindingSource bindsrc = new BindingSourceO; Конструктор инициализирует BindingSource, просто вызывая метод Add с объектом типа Person-. bindsrc.Add(new PersonO);
Привязка и представление данных 319 На самом деле BindingSource хранит набор объектов одного типа, а вызов метода Add определяет этот тип как Person. А вот альтернатива методу Add в этой программе: binds re. DataSou rce = new PersonO; В любом случае BindingSource теперь хранит набор из одного объекта типа Person. (Для этой программы не нужно, чтобы он хранил более одного объекта, поэтому здесь этого и нет.) Теперь объект BindingSource можно передать конструктору Person- Panel personpnl = new PersonPanel(bindsrc); Привязка данных в PersonPanel выполняется успешно, потому что BindingSource хранит объект типа Person со свойствами FirstName, LastName и BirthDate. Вместо вызова метода Add объекта BindingSource или приравнивания DataSource экземпляру Person, можно инициализировать объект BindingSource до передачи его конструктору PersonPanel. bindsre.DataSource = typeof(Person); Привязка данных в PersonPanel выполниться успешно, хотя в BindingSource еще нет ни одного объекта типа Person. BindingSource лишь «знает», что должен хранить объекты типа Person. Благодаря этому изменению в DataTimePicker будет отображаться текущая дата, а не 1 января 1800 г. Но ни одна из команд меню работать не будет, потому что все они считают, что BindingSource хранит как минимум один объект. Это легко уладить, добавив следующий вызов после инициации свойства DataSource: bindsre.AddNew(); С помощью отражения BindingSource создаст новый объект типа Person и добавит его в свой набор. BindingSource реализует индексатор, позволяющий обращаться к элементам набора как к членам массива. Поэтому команду File New можно реализовать, просто приравняв первый элемент набора (единственный используемый элемент) новому объекту типа Person-. bindsrc[0] = new Person(); Точно так же, обработчик File Open приравнивает этот элемент возвращаемому значению вызова Deserialize: bindsrc[0] = xmlser.Deserialize(sr); Обратите внимание, что в этой программе, в отличие от предыдущей, приведения к типу не требуется. При выполнении команд File New и File Open элементы
320 ГЛАВА 6 управления обновляются для отображения новых значений. Обработчик File Save ссылается на первый элемент набора. xmlser.Serialize(sw, bindsrc[0]); Ясно, что, храня только один объект типа Person, мы задействуем далеко не все преимущества Bindings оигсе, который скорее предназначен для поддержки набора объектов. У этого класса есть методы Add и Remove для добавления и удаления элементов, метод Find для поиска и сортировки элементов по значению их свойств. BindingSource также отслеживает текущий элемент набора, на который указывает свойство Position (целочисленный индекс) и свойство Current (объект). Методы MoveNext и MovePrevious в BindingSource служат для выбора другого объекта. Обычно в приложении пользователь может просматривать, редактировать, добавлять и удалять объекты — свойства и методы BindingSource это позволяют. Эту логику можно реализовать самостоятельно или с помощью элемента управления BindingNavigator. Он наследует ToolStrip и является единственной пользовательской панелью инструментов в библиотеке Windows Forms. Просмотр данных Сначала нужно решить, как будут храниться данные о нескольких лицах. Можно таю <PersonFile> <CreationDate>2005-08-13T14:44:32.7528768-04:00</CreationDate> <Persons> <Person> <Fi rstName>Johannes</Fi rstName> <LastName>Brahms</LastName> <BirthDate>1833-05-07T00:00:00</BirthDate> </Person> <Person> <FirstName>Franz</FirstName> <LastName>Schubert</LastName> <BirthDate>1797-01-31T00:00:00</BirthDate> </Person> </Persons> </PersonFile> Корневой элемент, PersonFile, содержит два подэлемента CreationDate и Persons (во множественном числе). В Persons содержится несколько элементов Person (в единственном числе). Этот формат позволяет использовать класс Person. Нам также потребуется новый класс PersonFile.
Привязка и представление данных 321 PersonFile.cs // // PersonFile.cs (с) 2005 by Charles Petzold // using System; using System.Collections. Generic; public class PersonFile { DateTime dtCreation = DateTime.Now; List<Person> persons = new List<Person>(); public DateTime CreationDate { get { return dtCreation; } . set { dtCreation = value; } } public List<Person> Persons { get { return persons; } set { persons = value; } } } Свойство Persons определяется как список (List) объектов Person. Этот класс се- риализуется точно в таком же формате, как показано выше. Если вам больше нравится XML-формат, в котором несколько элементов Person являются подэлемента- ми PersonFile, а элемента Persons нет вообще, нужно добавить перед определением свойства Persons атрибут [XmlElement("Person")]. Проект DataEntryWithNavigation включает файлы Person.cs, PersonFile.es, Person- Panel.cs и следующий файл. DataEntryWithNavigation.cs // // DataEntryWithNavigation.cs (с) 2005 by Charles Petzold // using System; using System.Drawing; using System.10; using System.Windows.Forms; using System.Xml.Serialization; class DataEntryWithNavigation : Form {
322 ГЛАВА 6 const string strFilter = "Person File files (*.PersonFileXml)|" + "*.PersonFileXml|All files (*.*)!*.*"; XmlSerializer xmlser = new XmlSerializer(typeof(PersonFile)); BindingSource bindsrc = new BindingSourceO; [STAThread] public static void Main() { Application. EnableVisualStylesQ; Application.Run(new DataEntryWithNavigationO); } public DataEntryWithNavigationO { Text = "Simple Data Entry with Navigation"; // Инициализация BindingSource. FileNewOnClick(null, EventArgs.Empty); // Создание панели. PersonPanel personpnl = new PersonPanel(bindsrc); personpnl.Parent = this; personpnl.Dock = DockStyle.Fill; // Создание меню. MenuStrip menu = new MenuStripO; menu.Parent = this; ToolStripMenuItem item = (ToolStripMenuItem) menu.Items.Add("&File"); item.DropDownItems.Add("&New", null, FileNewOnClick); item.DropDownItems.Add("&Open...", null, FileOpenOnClick); item.DropDownItems.Add("Save &As...", null, FileSaveAsOnClick); // Создание BindingNavigator. BindingNavigator bindnav = new BindingNavigator(true); bindnav.Parent = this; bindnav.Dock = DockStyle.Bottom; bindnav.BindingSource = bindsrc; } void FileNewOnClick(object objSrc, EventArgs args) { PersonFile persfile = new PersonFileO; persfile.Persons.Add(new Person()); bindsrc.DataSource = persfile; bindsrc.DataMember = "Persons"; } void FileOpenOnClick(object objSrc, EventArgs args) {
Привязка и представление данных 323 OpenFileDialog dig = new OpenFileDialogO; dig.Filter = strFilter; if (dlg.ShowDialogO == DialogResult.OK) { StreamReader sr = new StreamReader(dlg.FileName); bindsrc.DataSource = xmlser.Deserialize(sr); sr.Close(); } } void FileSaveAsOnClick(object objSrc, EventArgs args) { SaveFileDialog dig = new SaveFileDialogO; dig.Filter = strFilter; if (dlg.ShowDialogO == DialogResult.OK) { StreamWriter sw = new StreamWriter(dlg.FileName); xmlser.Serialize(sw, bindsrc.DataSource); sw.Close(); } } } Как и в предыдущей программе, объект BindingSource создан как поле, но теперь конструктор этого класса вызывает обработчик события File New, чтобы инициализировать объект BindingSource. Этот метод сначала создает объект типа PersonFile с одним объектом Person в наборе Persons. PersonFile persfile = new PersonFileO; persfile.Persons.Add(new PersonO); Затем объекту PersonFile присваивается свойство DataSource объекта BindingSource. bindsrc.DataSource = persfile; Однако, нам нужно лишь, чтобы BindingSource поддерживал часть этого объекта, а именно набор объектов Person, определенный свойством Persons. Этим свойством Persons (в форме текстовой строки) инициируется свойство DataMember объекта BindingSource: bindsrc.DataMember = "Persons"; Обработчик File Open просто переназначает свойство DataSource десериали- зованному XML-файлу: bindsrc.DataSource = xmlser.Deserialize(sr);
324 ГЛАВА 6 Точно так же обработчик File Save сериализует объект PersonFile, сохраненный в свойстве DataSource-. xmlser.Serialize(sw, bindsrc.DataSource); Обратите внимание, что эта программа сохраняет файлы с расширением Person- FileXml, а не PersonXml. Попытка загрузить в эту программу PersonXml-файл вызовет исключение. В «реальной» программе вызов Deserialize скорее всего был бы заключен в блок try, чтобы выявлять проблемы, связанные с недействительным форматом файлов. Последние операторы конструктора DataEntryWithNavigation создают объект типа BindingNavigator и размещают его в нижней части клиентской области: BindingNavigator bindnav = new BindingNavigator(true); bindnav.Parent = this; bindnav.Dock = DockStyle.Bottom; bindnav.BindingSource = bindsrc; Последний оператор присваивает свойству BindingSource объекта BindingNavigator объект BindingSource. (Как будто они созданы друг для друга.) А выглядит все это вот так: RstName: jFran^ Last Name: : Schubert Birth Date: fluesday , January 31,1797 v"! Кнопки на BindingNavigator позволяют перемещаться в начало или в конец списка, прокручивать список вперед и назад, добавлять новый элемент или удалять текущий. Разумеется, эти кнопки можно настроить, и поскольку BindingNavigator восходит к ToolStrip, можно добавлять другие кнопки и позволить пользователю перемещать элемент управления в другую область окна. Ясно, что вы вправе реализовать собственный порядок навигации. Две левые кнопки на BindingNavigator просто вызывают методы MoveFirst и MovePrevious объекта BindingSource. В поле ввода отображается свойство Position, а следующее текстовое поле — свойство Count. Остальные четыре кнопки на BindingNavigator вызывают методы BindingSource-. MoveNext, MoveLast, AddNew и RemoveCurrent.
Привязка и представление данных 325 BindingNavigator обеспечивает доступ к объектам ToolStripItem, которые он создает на своей поверхности. Например, если вам кажется, что кнопка Delete расположена слишком близко к кнопке New, предотвратить ее случайное нажатие можно, переместив ее вот так: bindnav.Deleteltem.Alignment = ToolStripItemAlignment.Right; Работаем с данными Программа DataEntryWithNavigation, на первый взгляд, исключительно компактна и демонстрирует основы ввода и редактирования данных, но перспективы работы с данными намного шире. Подумайте: ведь при наличии XML-файла нужного формата классы Person и PersonFile просто не нужны. Программа должна получать всю информацию о данных из XML-файла и именно на этом строить свою работу Ну, если честно, то не совсем так. В XML-файле все данные хранятся в виде строк, поэтому контроль типов не столь строг, как хотелось бы: из XML-файла нельзя понять, что элемент BirthDate — это объект DateTime. Поэтому для задания типов потребуется схема XML. На данном этапе можно воспользоваться классом DataSet из пространства имен System.Data. В DataSet есть два метода: ReadXml и WriteXml, и, что не менее важно, объекту DataSet можно присвоить свойство DataSource объекта BindingSource. Но изучение класса DataSet выходит за рамки данной книги. Элемент управления DataGridView DataGridView просто огромен — он реализует 118 собственных событий в дополнение к 69 событиям, которые наследует от Control, и, похоже, это далеко не все. Сообщество программистов активно работает над новыми расширениями, и я уверен, что вскоре появится отдельная книга о программировании DataGridView. Ясно, что я смогу затронуть лишь основные моменты. DataGridView часто служит законченным решением для просмотра и ввода данных. Этот элемент управления организует ячейки в виде сетки из строк и столбцов. (В целях сохранения иерархии классов строки и столбцы имеют тип bands) Заголовок есть над каждым столбцом и слева в начале каждой строки. В терминологии баз данных столбцы — это поля, а строки — записи. DataGridView обладает мощными возможностями настройки, но я сосредоточусь на нескольких простых способах применения этого элемента управления. DataGridView и текст Рассмотрим простую реализацию DataGridView, позволяющую вводить имена и адреса электронной почты.
326 ГЛАВА 6 SimpleDataGridView.es // // SimpleDataGridView.es (с) 2005 by Charles Petzold // using System; using System.Drawing; using System.Windows.Forms; class SimpleDataGridView : Form { [STAThread] public static void Main() { Application.EnableVisualStyles(); Application.Run(new SimpleDataGridViewO); } public SimpleDataGridViewO { Text = "Simple DataGridView"; DataGridView grid = new DataGridView(); grid.Parent = this; grid.AutoSize = true; grid.Dock = DockStyle.Fill; grid.ColumnCount = 3; grid.Columns[0].HeaderText = "First Name"; grid.Columns[1].HeaderText = "Last Name"; grid.Columns[2].HeaderText = "Email Address"; } } После назначения нескольких стандартных свойств (Parent, AutoSize и Dock) программа инициализирует элемент управления DataGridView, задавая число столбцов и текст заголовков. У DataGridView есть свойства ColumnCount и RowCount, а также Columns и Rows. Columns — это объект типа DataGridVieivColumnCollection, представляющий набор объектов DataGridViewColumn. Обычно ColumnCount задается в процессе инициализации элемента управления, явно (как в этой программе) или в результате добавления столбцов в элемент управления. Код, инициализирующий ColumnCount и HeaderText, можно заменить следующим: grid.Columns.Add("", "First Name"); grid.Columns.Add("", "Last Name"); grid.Columns.Add("", "Email Address");
Привязка и представление данных 327 Оба блока кода создают три объекта DataGridViewColumn. Первый аргумент метода Add становится свойством Name объекта DataGridViewColumn. Я присвою этому свойству реальные значения и вскоре воспользуюсь им. После этой инициализации DataGridView выглядит так. Изначально у DataGridView есть одна строка, над которой расположены заголовки столбцов. Перед каждой строкой слева стоит «заголовок строки» и небольшая картинка. Треугольная стрелка указывает на текущий ряд, активный для ввода с клавиатуры. Звездочка означает новый ряд, в котором еще ничего не введено. Когда пользователь начинает ввод в первой ячейке, создается новый ряд. Небольшой карандаш слева указывает, что ячейка находится в режиме редактирования. После нажатия клавиши Tab (или клавиш со стрелками) для перехода в следующую ячейку содержимое первой ячейки проверяется на корректность (при необходимости) и сохраняется в наборе ячеек элемента управления DataGridView. Говорят, что содержимое ячейки фиксируется (commit). Фокус ввода переходит во второй столбец, и пользователь может приступать к вводу в нем:
328 ГЛАВА 6 В этот момент свойство RowCount элемента управления отображает два, а набор Row хранит два объекта DataGridViewRow. Но свойство IsNewRow объекта Data- GridViewRow указывает, есть ли среди них новый ряд (он отмечен звездочкой), в котором ничего не введено. После ввода текста в последнюю строку и нажатия клавиши Tab фокус ввода переходит на следующую строку и так далее. Программа может присвоить свойству RowCount элемента управления DataGrid- View конкретное значение, означающее начальное количество строк. Метод Add набора Rows служит для задания явных значений ячеек, но обычно так элемент управления не инициализируют. DataGridView реализует двумерный индексатор. Если grid — это объект DataGridView, тогда следующее выражение — это объект типа DataGridViewCell на пересечении конкретного столбца и строки. grid[col][row] Индекс col должен быть меньше ColumnCount, a row — меньше RowCount. Свойство Value объекта DataGridViewCell — это объект типа object, указывающий на значение ячейки. В простой программе, приведенной выше, значение Value всех ячеек равно null, пока в нее не введен и не зафиксирован текст — тогда Value становится объектом string, отображаемым в ячейке. DataGridView можно также проиндексировать по названиям столбцов, например: grid["first"][row] Свойству Name столбца можно в любой момент присвоить значение, например так: grid.Columns[0].Name = "first"; Или сделать это методом Add: grid.Columns.Add("first", "First Name");
Привязка и представление данных 329 DataGridView вполне достаточно для ввода текстовых строк, однако большинство требований к данным намного сложнее. Важен не только текст, но даты и другие типы данных. Иерархия классов В документации для пространства имен SystemWindowsForms можно заметить, что многие классы начинаются с префикса DataGridView и используются в связи с элементами управления DataGridView. DataGridView напрямую наследует классу Control Много других важных классов наследуют DataGridViewElement, как показано в этой части иерархии классов: object DataGridViewElement DataGridViewBand DataGridViewColumn DataGridViewRow DataGridViewCell (абстрактный класс) Элемент управления DataGridView с тремя столбцами и четырьмя строками данных содержит 3 экземпляра DataGridViewColumn, 4 экземпляра DataGridViewRow и 20 экземпляров DataGridViewCell Последние включают 12 ячеек с данными, 3 заголовка столбцов и 4 заголовка строк (отображаемых слева в каждой строке) и заголовок в верхнем левом углу. Эти заголовки являются объектами классов, производных от DataGridViewCell DataGridViewCell (абстрактный класс) DataGridViewHeaderCell DataGridViewColumnHeaderCell DataGridViewTopLeftHeaderCell DataGridViewRowHeaderCell У DataGridViewCell есть еще шесть потомков для ячеек с различными типами данных: DataGRidViewCell (абстрактный класс) DataGridViewButtonCell DataGridViewCheckBoxCell DataGridViewComboBoxCell DataGridViewItnageCell DataGridViewLinkCell Da taGri dVi ewTextBoxCel 1 По умолчанию ячейки данных имеют тип DataGridViewTextBoxCell и предназначены для ввода текста. Чтобы получить ячейки других типов, над ними придется немного потрудиться.
330 ГЛАВА 6 Очень часто все ячейки одного столбца содержат данные одного типа. Поэтому вместо того, чтобы задавать тип данных для каждой ячейки, задают тип данных целого столбца с помощью следующих потомков DataGridViewColumm DataGridViewButtonColumn DataGridViewCheckBoxColumn DataGridViewComboBoxColumn DataGridViewImageColumn DataGridViewLinkColumn DataGridViewTextBoxColumn Последний класс из этого списка используется по умолчанию. Расширяем возможности по работе с данными Приступим к конструированию более крупной базы данных. Класс School состоит из набора объектов Student. School.cs // // School.cs (с) 2005 by Charles Petzold // using System.Collections.Generic; public class School { List<Student> studentlist = new List<Student>(); public List<Student> Students { set { studentlist = value; } get { return studentlist; } } } Каждый ученик — это экземпляр класса Student. Student.cs // // Student.cs (с) 2005 by Charles Petzold // using System; public class Student {
Привязка и представление данных 331 CourtesyTitle court = CourtesyTitle.None; string strFirstName = "<first name>"; string strLastName = "<last name>"; DateTime dtBirth = new DateTime(1985, 1,1); bool bEnrolled = false; public CourtesyTitle Courtesy set { court = value; } get { return court; } public string FirstName set { strFirstName = value; } get { return strFirstName; } public string LastName set { strLastName = value; } get { return strLastName; } public DateTime BirthDate set { dtBirth = value; } get { return dtBirth; } public bool Enrolled set { bEnrolled = value; } get { return bEnrolled; } Свойство Courtesy является членом перечисления CourtesyTitle. CourtesyTitle.es // // CourtesyTitle.es (с) 2005 by Charles Petzold // public enum CourtesyTitle { None, Mr, Ms,
332 ГЛАВА 6 Mrs, Miss } Таким образом, в классе Student использованы объекты типа string, boot, Courtesy- Title и DateTime. Мы уже умеем создавать элемент управления DataGridView для ввода строковых значений. Класс DataGridViewCheckBoxColumn предназначен для булевых значений (pool), а класс DataGridViewComboBoxColumn — для выбора значений из перечисления CourtesyTitle. Для BirthDate, скорее всего, подошел бы класс Data- GridViewDateTimePickerColumn, но, к сожалению, такого класса нет. Так что пока даты придется вводить в текстовой форме. План таков: сначала создать элемент управления DataGridView, а затем явно создать объекты типа DataGridViewComboBoxColumn, DataGridViewTextBoxColumn и DataGridViewCheckBoxColumn и добавить их в набор Columns элемента управления. Эта программа называется UnboundDataGridView, и в ней элемент управления не связан с источником данных. В следующей разновидности этой программы данные помещаются и извлекаются из элемента управления «вручную». UnboundDataGridView.cs // // UnboundDataGridView.cs (с) 2005 by Charles Petzold // using System; using System.Drawing; using System.Windows.Forms; class UnboundDataGridView : Form { protected DataGridView grid; [STAThread] public static void Main() { Application. EnableVisualStylesO; Application.Run(new UnboundDataGridViewO); } public UnboundDataGridViewO { Text = "Unbound DataGridView"; Width *= 2; grid = new DataGridViewQ; grid.Parent = this;
Привязка и представление данных 333 grid.AutoSize = true; grid.Dock = DockStyle.Fill; DataGridViewComboBoxColumn colCombo = new DataGridViewComboBoxColumnQ; colCombo.Name = "Courtesy"; colCombo.HeaderText = "Courtesy"; colCombo.DataSource = Enum.GetValues(typeof(CourtesyTitle)); colCombo.ValueType = typeof(CourtesyTitle); grid.Columns.Add(colCombo); DataGridViewTextBoxColumn colText = new DataGridViewTextBoxColumnQ; colText.Name = "FirstName"; colText.HeaderText = "First Name"; grid.Columns.Add(colText); colText = new DataGridViewTextBoxColumnQ; colText.Name = "LastName"; colText.HeaderText = "Last Name"; grid.Columns.Add(colText); colText = new DataGridViewTextBoxColumnO; colText.Name = "BirthDate"; colText.HeaderText = "Birth Date"; grid.Columns.Add(colText); DataGridViewCheckBoxColumn colCheck = new DataGridViewCheckBoxColumnQ; colCheck.Name = "Enrolled"; colCheck.HeaderText = "Enrolled?"; grid.Columns.Add(colCheck); } } Хотя в проект UnboundDataGridView входят файлы School.cs и Student.cs, они не нужны в этой программе и служат лишь в качестве образцов для определения столбцов. Как вы заметите, каждому столбцу назначается свойство Name, такое же, как и соответствующее свойство в классе Student, и чуть более дружественное свойство HeaderText, значение которого отображается на экране. Также заметьте, что свойство DataSource класса DataGridViewComboBoxColumn приравнивается к массиву членов перечисления CourtesyTitle. При использовании членов перечисления в поле со списком свойству ValueType также должен быть присвоен тип перечисления. Так выглядит окно программы с одной заполненной строкой и открытым полем со списком в начале второй строки:
334 ГЛАВА 6 Сохранение в XML-формате Следующие очевидные шаги — реализация меню File, сохранение введенных данных в XML-файле с применением сериализации класса School, а затем обратная загрузка данных. Сложнее всего с четвертым столбцом — строковым значением, которое нужно преобразовать в объект DateTime. Задача решается статическим методом DateTime.Parse, который принимает дату в любом из нескольких форматов и при недопустимой дате генерирует исключение. Поэтому при вводе даты нужно всякий раз ее проверять. (Ну, ничего, в конце мы справимся и с этим.) Класс UnboundDataGridViewWithFilelO наследует классу UnboundDataGridView и реализует команды File New, File Open и File Save As. Файлы сохраняются с расширением StudentXml. Проект UnboundDataGridViewWithFilelO включает все файлы проекта UnboundDataGridView, а также следующий файл. UnboundDataGridViewWithFilelO.cs // // UnboundDataGridViewWithFilelO.cs (с) 2005 by Charles Petzold // using System; using System.Drawing; using System.10; using System.Windows.Forms; using System.Xml.Serialization; class UnboundDataGridViewWithFilelO : UnboundDataGridView { const string strFilter = "School files (*.SchoolXml)|" + "*.SchoolXml|All files (*.*)!*■*"; XmlSerializer xmlser = new XmlSerializer(typeof(School));
Привязка и представление данных 335 [STAThread] public new static void Main() { Application.EnableVisualStyles(); Application.Run(new UnboundDataGridViewWithFileIO()); } public UnboundDataGridViewWithFileIO() { Text = "Unbound DataGridView with File 10"; // Создание меню. MenuStrip menu = new MenuStripO; menu.Parent = this; ToolStripMenuItem item = (ToolStripMenuItem) menu.Items.Add("&File"); item.DropDownItems.Add("&New", null, FileNewOnClick); item.DropDownItems.Add("&Open...", null, FileOpenOnClick); item.DropDownItems.Add("Save &As...", null, FileSaveAsOnClick); } void FileNewOnClick(object objSrc, EventArgs args) { grid.Rows.Clear(); } void FileOpenOnClick(object objSrc, EventArgs args) { OpenFileDialog dig = new OpenFileDialogO; dig.Filter = strFilter; if (dlg.ShowDialogO == DialogResult.OK) { // Чтение объекта School как XML. StreamReader sr = new StreamReader(dlg.FileName); School sch = (School) xmlser.Deserialize(sr); sr.Close(); // Очистка имеющихся строк в DataGridView. grid.Rows.Clear(); // Добавление строк в DataGridView: по одной на каждого ученика. foreach (Student sdt in sch.Students) { int index = grid.Rows.Add(); grid["Courtesy", index].Value = sdt.Courtesy; grid["FirstName", index].Value = sdt.FirstName; grid["LastName", index].Value = sdt.LastName;
336 ГЛАВА 6 grid["BirthDate", index].Value = sdt.BirthDate.ToShortDateStringO; grid["Enrolled", index].Value = sdt.Enrolled; } } } void FileSaveAsOnClick(object objSrc, EventArgs args) { SaveFileDialog dig = new SaveFileDialogO; dig.Filter = strFilter; if (dlg.ShowDialogO == DialogResult.OK) { // Завершение любого текущего редактирования. grid.EndEdit(); // Создание нового объекта School. School sch = new SchoolO; // Добавление объектов Student из строк в DataGridView. foreach (DataGridViewRow row in grid.Rows) { if (row.IsNewRow) continue; Student sdt = new Student(); sdt.Courtesy = (CourtesyTitle) row.Cells["Courtesy"].Value; sdt.FirstName = (string) row.Cells["FirstName"].Value; sdt.LastName = (string)row.Cells["LastName"].Value; sdt.BirthDate = DateTime.Parse((string) row.Cells["BirthDate"].Value); sdt.Enrolled = (bool)row.Cells["Enrolled"].Value; sch.Students.Add(sdt); } // Запись объекта School как XML. StreamWriter sw = new StreamWriter(dlg.FileName); xmlser.Serialize(sw, sch); sw.Close(); } } } А теперь заполним хотя бы две строки. Выберите тип обращения в поле со списком, введите имя, фамилию и дату в формате, показанном на приведенном ранее
Привязка и представление данных 337 снимке экрана. Чтобы снять флажок, щелкните поле с флажком мышью или нажмите пробел и щелкните его вновь, чтобы отключить. Если его не щелкнуть хотя бы раз, ячейка считается неинициализированной, и в следующую строку перейти нельзя. В конце второй или третьей записи оставьте выделенной ячейку с флажком. Выберите Save as в меню File и укажите имя. Если данные были указаны верно, программа не закроется по исключению. Считайте, что вам повезло. Сначала рассмотрим логику команды Save As. Получив имя файла, метод завершает любое текущее редактирование ячеек в элементе управления: grid.EndEdit(); В этой программе я порекомендовал оставить выделенной ячейку с флажком. Если перейти нажатием Tab к этой ячейке в первый раз, у нее не будет значения. Свойство Value объекта DataGridViewCell будет равно null. Даже если установить флажок, ячейка все еще будет в режиме редактирования. (Видите слева карандаш?) Затем при выполнении команды Save As свойство Value приводится к типу bool, что невозможно, если Value равно null. Вызов EndEdit фиксирует изменения, сделанные в ячейке. Затем логика Save As создает новый объект типа School и перечисляет все строки в элементе управления DataGriaView. Игнорируя новую строку, метод создает объект типа Student для каждой строки и назначает свойства из свойств ячейки Value. (Он обращается к ячейкам путем обращения по индексу свойства Cells в объекте DataGridViewRow.) Посложнее дела обстоят со столбцом BirthDate, строкой, которую нужно передать методу DateTime.Parse. Далее все объекты Student добавляются в набор Students объекта School, который затем сериализуется как XML. Команда File Open действует в обратном порядке. Новый объект School создается путем десериализации XML Для каждого объекта Student в списке Students создается новая строка в DataGriaView, а столбцы этой строки заполняются свойствами объекта Student. Проверка корректности данных и инициализация Понятно, что рассмотренная программа не должна давать так много сбоев. Сложнее всего дела обстоят с датой. Если ее формат неправильный, метод DateTime.Parse генерирует исключение. Хорошо бы в программе перехватывать и обрабатывать эти исключения. Но о неверном формате даты пользователь должен узнавать не при сохранении файла, а гораздо раньше, когда курсор еще находится в ячейке. Это и есть проверка корректности (validation) данных. Пусть дата указана верно, но в момент вызова команды Save As курсор все еще находится в поле BirthDate. Значение ячейки с флажком будет null и приведение его к типу bool вызовет исключение. Если в поле со списком не указать тип обращения, значение этой ячейки также будет равно null, что тоже приведет к исключению. Это проблемы инициализации (initialization).
338 ГЛАВА 6 Проверку корректности и инициализацию можно обеспечить, установив обработчики двух из огромного множества событий, реализованных в DataGridView, — OnDefaultValuesNeeded (для решения проблем инициализации) и OnValidating. Следующий файл входит в проект UnboundDataGridViewWithValidation, также включающий все предыдущие файлы, относящиеся к этой теме. UnboundDataGridViewWithValidation.cs // // UnboundDataGridViewWithValidation.cs (с) 2005 by Charles Petzold // using System; using System.Drawing; using System.Windows.Forms; class UnboundDataGridViewWithValidation: UnboundDataGridViewWithFilelO { [STAThread] public new static void Main() { Application. EnableVisualStylesO; Application.Run(new UnboundDataGridViewWithValidation()); } public UnboundDataGridViewWithValidation() { Text = "Unbound DataGridView with Validation"; grid.DefaultValuesNeeded += OnDefaultValuesNeeded; grid.CellValidating += OnValidating; } void OnDefaultValuesNeeded(object objSrc, DataGridViewRowEventArgs args) { // Создание объекта Student со значениями по умолчанию. Student sdt = new Student(); args.Row.Cells["Courtesy"].Value = sdt.Courtesy; args.Row.Cells["FirstName"].Value = sdt.FirstName; args.Row.Cells["LastName"].Value = sdt.LastName; args. Row. Cells["BirthDate"]. Value = sdt.BirthDate.ToShortDateStringO; args.Row.Cells["Enrolled"].Value = sdt.Enrolled; } void OnValidating(object objSrc, DataGridViewCellValidatingEventArgs args) { DataGridView grid = objSrc as DataGridView; DateTime dtResult;
Привязка и представление данных 339 grid.Rows[args.RowIndex].ErrorText = ""; if (args.Columnlndex != grid.Columns["BirthDate"].Index) return; // Проверка формата значения BirthDate. if (!DateTime.TryParse(args.FormattedValue.ToString(), out dtResult)) { args.Cancel = true; grid.Rows[args.RowIndex].ErrorText = "Enter the date like: September 4, 1985\r\n" + "or: 9/4/1985"; } } } Обработчик событий DefaultValuesNeeded предоставляет новый объект DataGrid- ViewRow с именем Row и запрашивает его инициализацию. Обработчик просто создает новый объект Student (который сам инициализирует свойства объекта) и создает из них строку. Обработчик события Validating интересует только столбец на пересечении свойства Name и BirthDate. Если метод TryParse объекта DateTime не сможет преобразовать дату, обработчик приравняет свойство Cancel аргументов события к true, а в свойство ErrorText этой строки добавится подсказка пользователю. Теперь при запуске программы столбцы Courtesy и BirthDate инициализируются корректными значениями. При вводе даты в неправильном формате вы не сможете переместить фокус ввода на другую ячейку, а в заголовке строки появится красный значок с восклицательным знаком.
340 ГЛАВА 6 Щелчок этого значка открывает строку-подсказку содержащую значение свойства ErrorText. Это вполне жизнеспособное решение. Но все же в DataGridView нам просто необходим DateTimePicker, элемент выбора даты и времени. Реализация столбца Calendar Я уже говорил, что класса DataGridViewDateTimePickerColumn не существует. Но в документации для платформы .NET Framework 2.0 есть раздел, посвященный размещению элементов управления в ячейках DataGridView в Windows Forms («How to: Host Controls in Windows Forms DataGridView Cells»). Это именно то, что нам нужно. Я бессовестно позаимствовал исходный текст из этого раздела и разбил его на три файла для классов CalendarEditingControl (производный от DateTimePicker и реализующий интерфейс IDataGridVieivEditingControl), CalendarCell (производный от DataGridViewTextBoxCell) и CalendarColumn (производный от DataGridViewColumn). Эти три файла есть в материалах к этой книге, который можно скачать с моего сайта. Они, а также CourtesyTitle.cs и следующий файл являются частью проекта Unbound- DataGridViewWithCalendar. UnboundDataGridViewWithCalendar.es // // UnboundDataGridViewWithCalendar.es (с) 2005 by Charles Petzold // using System; using System.Drawing; using System.Windows.Forms; class UnboundDataGridViewWithCalendar: Form { [STAThread] public static void Main() { Application.EnableVisualStyles(); Application.Run(new UnboundDataGridViewWithCalendar()); } public UnboundDataGridViewWithCalendarO { Text = "Unbound DataGridView with Calendar"; Width *= 2; DataGridView grid = new DataGridViewO; grid.Parent = this; grid.AutoSize = true; grid.Dock = DockStyle.Fill;
Привязка и представление данных 341 DataGridViewComboBoxColumn colCombo = new DataGridViewComboBoxColumn(); colCombo.Name = "Courtesy"; colCombo.HeaderText = "Courtesy"; colCombo.DataSource = Enum.GetValues(typeof(CourtesyTitle)); colCombo.ValueType = typeof(CourtesyTitle); grid.Columns.Add(colCombo); DataGridViewTextBoxColumn colText = new DataGridViewTextBoxColumnO; colText.Name = "FirstName"; colText.HeaderText = "First Name"; grid.Columns.Add(colText); colText = new DataGridViewTextBoxColumnO; colText.Name = "LastName"; colText.HeaderText = "Last Name"; g rid.Columns.Add(colText); CalendarColumn colBirth = new CalendarColumnO; colBirth.Name = "BirthDate"; colBirth.HeaderText = "Birth Date"; grid.Columns.Add(colBirth); DataGridViewCheckBoxColumn colCheck = new DataGridViewCheckBoxColumnO; colCheck.Name = "Enrolled"; colCheck.HeaderText = "Enrolled?"; grid.Columns.Add(colCheck); } } Для простоты в этой программе не реализован файловый ввод-вывод или проверка корректности. Здесь лишь показано, как использовать CalendarColumn при создании DataGridView. При появлении DataGridView на экране ячейка выглядит как предназначенная для ввода текста, но по щелчку в ней появляется стрелка DataTime- Picker. Поскольку свойство Value ячейки приравнивается к свойству Value базового элемента управления и является экземпляром DateTime, при сохранении или загрузке файла не нужны никакие преобразования, а также проверка корректности значений. DataGridView и привязка данных Привязать DataGridView к источнику данных довольно просто. Сначала нужно определить BindingSource, примерно так: BindingSource bindsrc = new BindingSource(); bindsrc.DataSource = typeof(School); bindsrc.DataMember = "Students";
342 ГЛАВА 6 Затем нужно создать DataGridView в такой же форме, как раньше: DataGridView grid = new DataGridView(); grid.Parent = this; grid.AutoSize = true; grid.Dock = DockStyle.Fill; И, наконец, нужно привязать DataGridView к Bindingsource: grid.DataSource = bindsrc; Этот код работает, но не совсем так, как нужно. Элемент управления DataGridView должен получать заголовки столбцов из свойств класса Student, а затем предоставлять пользователю возможность вводить в сетку данные. Но, как показывает опыт, этот код создает элемент управления DataGridView следующего вида: Здесь видно, что столбцы расположены не в том порядке. Более того, столбец Courtesy реализован как текстовое поле, а в столбце BirthDate не использован новый объект CalendarColumn (чего мы, впрочем, и не ждали). Это значит, что DataGridView нужно инициализировать немного иначе. В заключительной программе этой главы реализован другой подход. Прежде чем приравнивать свойство DataSource класса DataGridView к BindingSource, мы приравниваем свойство AutoGenerateColumns к false. Тогда столбцы не будут создаваться для каждого свойства BindingSource. Такие столбцы нам не нужны. Затем явно создаются все столбцы, как и в программе UnboundDataGridView, и свойство Data- PropertyName каждого столбца приравнивается свойству объекта Student, который связан с этим столбцом. Например, так выглядит код поля со списком: DataGridViewComboBoxColumn colCombo = new DataGridViewComboBoxColumn(); colCombo.DataPropertyName = "Courtesy"; colCombo.DataSource = Enum.GetValues(typeof(CourtesyTitle)); colCombo.HeaderText = "Courtesy"; grid.Columns.Add(colCombo); Также можно назначить свойство Name, но в этой программе оно не нужно, потому что она не ссылается на столбцы при вставке и удалении данных. Ей нужен лишь объект BindingSource. Проект DataGridViewWithBinding включает файлы School.cs, Student.cs и CourtesyTitle.cs, связанные с классом CalendarColumn, и следующий файл:
Привязка и представление данных 343 DataGridViewWithBinding.cs // // DataGridViewWithBinding.cs (с) 2005 by Charles Petzold // using System; using System.Drawing; using System.10; using System.Windows.Forms; using System.Xml.Serialization; class DataGridViewWithBinding : Form { const string strFilter = "School files (*.SchoolXml)|" + "*.SchoolXml|All files (*.*)!*.*"; XmlSerializer xmlser = new XmlSerializer(typeof(School)); BindingSource bindsrc = new BindingSource(); [STAThread] public static void Main() { Application.EnableVisualStyles(); Application.Run(new DataGridViewWithBindingO); } public DataGridViewWithBindingO { Text = "DateGridView with Binding"; Width *= 2; // Инициализация BindingSource. FileNewOnClick(null, EventArgs.Empty); // Создание элемента управления DataGridView. DataGridView grid = new DataGridViewQ; grid.Parent = this; grid.AutoSize = true; grid.Dock = DockStyle.Fill; // Создание привязки элемента управления к источнику данных. grid.AutoGenerateColumns = false; grid.DataSource = bindsrc; // Создание нужных столбцов для DataGridView. DataGridViewComboBoxColumn colCombo = new DataGridViewComboBoxColumnO; colCombo.DataPropertyName = "Courtesy"; colCombo.DataSource = Enum.GetValues(typeof(CourtesyTitle));
344 ГЛАВА 6 colCombo.HeaderText = "Courtesy"; g rid.Columns.Add(colCombo); DataGridViewTextBoxColumn colText = new DataGridViewTextBoxColumnO; colText.DataPropertyName = "FirstName"; colText.HeaderText = "First Name"; grid.Columns.Add(colText); colText = new DataGridViewTextBoxColumnO; colText.DataPropertyName = "LastName"; colText.HeaderText = "Last Name"; grid.Columns.Add(colText); CalendarColumn colBirth = new CalendarColumnO; colBirth.DataPropertyName = "BirthDate"; colBirth.HeaderText = "Birth Date"; grid.Columns.Add(colBirth); DataGridViewCheckBoxColumn colCheck = new DataGridViewCheckBoxColumnO; colCheck.DataPropertyName = "Enrolled"; colCheck.HeaderText = "Enrolled?"; grid.Columns.Add(colCheck); // Создание меню. MenuStrip menu = new MenuStripO; menu.Parent = this; ToolStripMenuItem item = (ToolStripMenuItem)menu.Items.Add("&File"); item.DropDownItems.Add("&New", null, FileNewOnClick); item.DropDownItems.Add("&Open...", null, FileOpenOnClick); item.DropDownItems.Add("Save &As...", null, FileSaveAsOnClick); } void FileNewOnClick(object objSrc, EventArgs args) { bindsrc.DataSource = new SchoolO; bindsrc.DataMember = "Students"; } void FileOpenOnClick(object objSrc, EventArgs args) { OpenFileDialog dig = new OpenFileDialogO; dig.Filter = strFilter; if (dlg.ShowDialogO == DialogResult.OK) { // Чтение объекта School как XML. StreamReader sr = new StreamReader(dlg.FileName);
Привязка и представление данных 345 bindsre.DataSource = xmlser.Deserialize(sr); sr.Close(); } } void FileSaveAsOnClick(object objSrc, EventArgs args) { SaveFileDialog dig = new SaveFileDialogO; dig.Filter = strFilter; if (dlg.ShowDialogO == DialogResult.OK) { // Запись объекта School как XML. StreamWriter sw = new StreamWriter(dlg.FileName); xmlser.Serialize(sw, bindsrc.DataSource); sw.Close(); } } } Хотя создавать все столбцы «вручную» крайне неудобно, у привязки данных все же есть ряд преимуществ. Во-первых, программе не нужно устанавливать никаких обработчиков событий элемента DataGridView, чтобы инициализировать данные в каждой новой строке. Во-вторых, процедурам File Open и File Save достаточно ссылаться только на BindingSource, а не на объект School или свойства класса Student. И, пожалуй, не стоит ждать от DataGridView, что он правильно создаст каждый столбец с помощью объекта DataGridViewColumn, идеально подходящего для этого типа данных. По мере появления новых потомков DataGridViewColumn ими можно будет воспользоваться при решении задач, для которых они предназначены.
Глава 7 Два настоящих приложения Очень часто в книгах-пособиях по программированию в качестве примеров приводится масса мелких программ, но практически никогда авторы не демонстрируют настоящие приложения. Я имею в виду не просто усовершенствованные программы-поделки. В современном мире объектно-ориентированного программирования настоящие приложения состоят из множества классов, которые оптимизированы и подогнаны друг к другу как винтики сложного механизма. Обсуждение серьезного приложения может потребовать отдельной книги, но я приведу в качестве примера две небольшие (но «настоящие») прикладные программы: ■ инструмент разработчика ControlExplorer, который обсуждался в главе 2. Он позволяет создать любой элемент управления из комплекта Microsoft Windows Forms и поэкспериментировать с его свойствами и событиями; ■ Web-браузер MdiBroivser с MD1-интерфейсом, в котором используется элемент управления WebBrowser, обеспечивающий большинство функциональности Internet Explorer. Эти приложения не только крупнее других примеров этой книги, они дооснаще- ны атрибутами обычно отсутствующими в мелких демонстрационных программах: ■ у обеих программ есть значки-логотипы и окна с информацией о программе; ■ все вспомогательные консольные окна удалены и обе программы скомпилированы в среде Microsoft Visual Studio в конфигурации по умолчанию Release; ■ в MdiBrowser создан небольшой файл справочной системы; ■ подготовлены установочные пакеты обоих программ. Я использовал новую процедуру установки ClickOnce, поэтому программы можно загружать и устанавливать с моего Web-сайта (см. Web-страницу «Programming Microsoft Windows Forms» no адресу www.cbarlespetzold.com/winforms).
348 ГЛАВА 7 Пример 1: ControlExplorer Основная форма ControlExplorer содержит элемент управления MenuStrip и Panel Элемент верхнего уровня меню называется Control От него наследует меню свою иерархию, которая дублирует иерархию классов всех элементов управления Windows Forms, производных от Control Когда пользователь выбирает один из элементов управления меню, ControlExplorer создает элемент управления соответствующего типа как потомок панели и отображает большое немодальное диалоговое окно: RightToLeft UseWartCutsor AUowDfop ContextMenuStnp Enabled LafgeChange Maximum Minimum SmaSChange Tablndex TabStop II J Fake Fake (nonej Tiue 10 100 0 1 Value The value (hat the scroS be< poskion repmsenfs N.^TT.... '..... * ....... Q AutoS«eChanged Q BackCofoiChanged Q BackgtoundlmageChanged Q BackgfoundmagelayoutChanged ""* BmdingCofrtextChariged CausesValklafionClwiged __ ChangeUICues Q Click Q CkerrtSireChanged Q ContexIMenuChanged Q CorrtextMenuS(npChangi?d Q Contio&dded Q ConttolRemoved Q CutroiChanged Q Disposed Q DockChanged Q DoubteChck ,D D'«9D'°P Pioperty AcoessibAtyQbiect LavoutEngine ; Bottom : Canfocui i CanScleet - OwntRectangle I CompanyNume ContamsFocus Controls ' Oeated DataBindmgs > DisplayFteetantite : 1 «Disposed Disposing Focused < Bottom The bottom of (he control. Value ContfolAccessibteObifccr Ownei« System.Wir System Wmdows Forms layout DefaultLayout 1? True True C<«0 Y*0,Width=80.H stghtxl 7} Microsoft Corporation False System Windows FoirrnXontrol+ContioCoHect Tiue System Windows Foims ConttolBmdingsColted {X«0.Y«0.Wnith»80^eiglii«17} False False False > in contain» coordinates. 'S.881 &aolSc»c^iientat(on*Hoii2or^iScroni^e^SfnaierKieJwrtNeviA/alue>r1 QldValue-G ' 8.891 VakteCbangedP^alue-l] ,7.032 Sciol ScfcC(rientation»Ho»«onfalScio8 Type*Eitd$cro« NewValue«1 OloYaiue»»1 '8.313 Scfol SciolOrientation»HofKon»ar5crol Tjpe-SmaBlncfement Nw\Value«2 OWVaJiie*! • 8 323VatueChe»nged^alue-2J 8.504 Serol 5«оЮп«Ча(юп«Нопгог*а!5сю8 Type»E«d$crall NevMsk*s»2 0ИУа!ие*2 2.489 Sciol ScJcJOtBnfation«HofKontaScfo» 7ype»SmaiSnciement N*wValue»3 OlaVafue»? . 2.439 ValueCbano^ {Valued] : 2.670 Sciol 6ооЮ«еп(а110п«Носйог1»а13сго1 Type»EmJScioS He*Mahe*3 QloVaJue*3 Это окно содержит три элемента управления. В верхнем левом углу — PropertyGrid, стандартный элемент управления Windows Forms, который позволяет просматривать и изменять свойства элемента управления для чтения/записи. Правее расположен аналогичный элемент управления, ReadOnlyPropertyGrid, показывающий значения всех свойств «только для чтения». В нижней части находится пользовательский элемент управления EventLogger, позволяющий увидеть определенные в элементе события. Этот элемент управления состоит из нескольких частей. В левой части элемента перечислены все его события, а в правой расположено текстовое поле TextBox, куда выводится отправляемая на консоль информация. Для каждого события указывается время (только в секундах и миллисекундах), имя и значения всех предоставленных событием открытых свойств. Если имя события заканчивается словом Changed (то есть «изменен»), в скобках указывается значение измененного свойства. При закрытии этого диалогового окна элемент управления
Два настоящих приложения 349 уничтожается. Вы вправе создать несколько дочерних элементов управления — каждое с собственным немодальным диалоговым окном. Понятно, что все нюансы работы этой программы основаны на отражении (reflection) — процессе получения информации о классах и объектах во время исполнения и использования этой информации для создания или управления объектами. Отражение использовано даже в диалоговом окне About. Я проанализирую исходный текст этой программы в направлении снизу вверх, то есть начну с простейших элементов управления, продолжу рассказом о сборке элементов управления в диалоговые окна и, наконец, расскажу о главной форме приложения. Иерархия классов Control Элемент меню верхнего уровня Control повторяет иерархию классов, производных от класса Control и находящихся в сборке System.Windows.Forms. Элемент Control вместе со своими потомками создается в следующем подклассе класса ToolStrip- Menultem. ControlMenultem.es // // ControlMenuItem.es (с) 2005 by Charles Petzold // Часть программы ControlExplorer // using System; using System.Collections.Generic; using System.Drawing; using System.Reflection; using System.Windows.Forms; class ControlMenuItem : ToolStripMenuItem { public ControlMenuItem(EventHandler evtClick) { // Узнаем сборку, в которой определен класс. Assembly asbly = Assembly.GetAssembly(typeof(Control)); // Это массив всех типов этого класса. Туре[] atype = asbly.GetTypesO; // Мы будем хранить наследников Control в сортированном списке. SortedList<string, ToolStripMenuItem> sortlst = new SortedList<string, ToolStripMenuItem>();
350 ГЛАВА 7 Text = "Control"; Tag = typeof(Control); sortlst.Add("Control", this); // Перечисляем все типы в массиве. // Для Control и его наследников создаем команды меню и // добавляем их в объект SortedList. // Обратите внимание: свойство Tag команды меню связано с объектом Туре. fоreach (Type typ in atype) { if (typ.IsPublic && (typ.IsSubclassOf(typeof(Control)))) { ToolStripMenuItem item = new ToolStripMenuItem(typ.Name); item.Click += evtClick; item.Tag = typ; sortlst.Add(typ.Name, item); } } // Перечисляем сортированный список и задаем родителей команд меню, fоreach (KeyValuePair<string, ToolStripMenuItem> kvp in sortlst) { if (kvp.Key != "Control") { string strParent = ((Type)kvp.Value.Tag).BaseType.Name; ToolStripMenuItem itemParent = sortlst[strParent]; itemParent.DropDownItems.Add(kvp.Value); // У itemParent не должно быть обработчика событий! itemParent.Click -= evtClick; } } // Еще раз перечисляем список: // Если элемент абстрактный и доступен для выбора, отключаем его. // Если элемент не абстрактный и не доступен для выбора, добавляем новый элемент, foreach (KeyValuePair<string, ToolStripMenuItem> kvp in sortlst) { Type typ = (Type) kvp.Value.Tag; if (typ.IsAbstract && kvp.Value.DropDownltems.Count == 0) kvp.Value.Enabled = false; if (!typ.IsAbstract && kvp.Value.DropDownltems.Count > 0) { ToolStripMenuItem item = new ToolStripMenuItem(kvp.Value.Text); item.Click += evtClick; item.Tag = typ;
Два настоящих приложения 351 kvp.Value.DropDownItems.Insert(0, item); } } } } Работа класса начинается с получения всех типов (то есть классов, структур, делегатов, интерфейсов и перечислений) сборки, в которой расположен элемент управления. (Естественно, что это сборка System.Windoivs.Forms.dll) Первый цикл foreach собирает информацию обо всех открытых производных от Control классах в объекте SortedList. В SortedList данные хранятся парами «запись-значение», отсортированными по-записям. Для хранимых в объекте SortedList классов название записи представляет собой текстовое имя класса (например «Button»), а значение — это свойство ToolStripMenuItem. Свойство Tag объекта ToolStripMenuItem представляет собой объект Туре класса конкретного элемента управления. После создания всех этих объектов ToolStripMenuItem необходимо объединить их в иерархию. Это делает второй цикл/oreacb. Код получает базовые классы из свойств Tag всех объектов ToolStripMenuItem, а затем использует SortedList для нахождения ToolStripMenuItem, в набор DropDownltems которого надо добавить элемент. Далеко не все родительские классы (такие как ButtonBase) абстрактны (например, Label — не абстрактный класс). Хорошо, что ButtonBase вызывает подменю с Button, CheckBox и RadioButton, потому что при всем желании мы не в состоянии создать объект типа ButtonBase. Но есть и недостаток: Label вызывает подменю, в котором есть только LinkLabel. Как же нам создать элемент Label? Для каждого неабстрактного класса, который является родителем других элементов управления, третий цикл foreach создает в подменю новый ToolStripMenuItem с тем же текстом, что и у родителя. Например, элемент Label вызывает подменю, которое содержит как Label, так и LinkLabel. Изначально эта программа была написана с использованием MainMenu и классов Menultem из .NET Framework 1.0, и когда я обновлял ее с применением MenuStrip и ToolStripMenuItem, обнаружил интересную странность: у Menultem, который отображает раскрывающееся меню, по идее, должен быть обработчик события Click, но оно не задействовано. А вот ToolStripMenuItem вызывает свой обработчик Click независимо от того, является ли он родителем подменю или нет. Мне не требовалось добавлять второй элемент Label в подменю Label. Но я решил, что элементы, родители подменю, не должны иметь обработчиков события Click, поэтому добавил код, удаляющий ранее установленные обработчики события. Свойства «только для чтения» Большая часть элемента управления ReadOnlyPropertyGrid — это элемент управления ListView в режиме View.Details с двумя столбцами, где отображаются свойства «только для чтения» и их значения. Но ReadOnlyPropertyGrid содержит и другие
352 ГЛАВА 7 элементы управления. Помимо ListView, занимающего верхнюю часть SplitterContainer, внизу расположены две метки, предоставляющие информацию о выбранном ListView свойстве и его краткое описание. Оно берется из атрибута Description свойства. Общепринятого способа уведомления об изменении свойства не существует, поэтому я создаю объект Timer, обновляющий элемент управления десять раз в секунду. ReadOnlyPropertyGrid.cs // // ReadOnlyPropertyGrid.cs (с) 2005 by Charles Petzold // Часть программы ControlExplorer // using System; using System.ComponentModel; // Для DescriptionAttribute using System.Drawing; using System.Reflection; using System.Windows.Forms; class ReadOnlyPropertyGrid: Control { object objSelected; ListView lvProps; Label labProp, labDesc; Timer tmr; // Открытое свойство, public object SelectedObject { get { return objSelected; > set { objSelected = value; FullRefreshO; tmr.Enabled = objSelected != null; > > // Конструктор, public ReadOnlyPropertyGrid() { ClientSize = new Size(30 * Font.Height, 30* Font.Height);
Два настоящих приложения 353 SplitContainer sc = new SplitContainerO; sc.Parent = this; sc.Dock = DockStyle.Fill; sc.Orientation = Orientation.Horizontal; sc.FixedPanel = FixedPanel.Panel2; sc.SplitterDistance = Height - 4 * Font.Height; // ListView отображает свойства и значения. lvProps = new ListViewO; lvProps.Parent = sc.Panell; lvProps.Dock = DockStyle.Fill; lvProps.View = View.Details; lvProps.HeaderStyle = ColumnHeaderStyle.Nonclickable; lvProps.GridLines = true; lvProps.FullRowSelect = true; lvProps.MultiSelect = false; lvProps.HideSelection = false; lvProps.Activation = ItemActivation.OneClick; IvProps.SelectedlndexChanged += ListViewOnSelectedlndexChanged; lvProps.Columns.Add("Property", 12 * Font.Height, HorizontalAlignment.Left); lvProps.Columns.AddC'Value", 18 * Font.Height, HorizontalAlignment.Left); // Метки отображают выбранное свойство и его значение. labDesc = new Label(); labDesc.Parent = sc.Panel2; labDesc.Dock = DockStyle.Fill; labProp = new LabelO; labProp.Parent = sc.Panel2; labProp.Dock = DockStyle.Top; labProp.Height = Font.Height; labProp.Font = new Font(labProp.Font, FontStyle.Bold); tmr = new Timer(); tmr.Interval = 100; tmr.Tick += TimerOnTick; > // Заполняем ListView свойствами и значениями, void FullRefresh() { lvProps.Items.Clear();
354 ГЛАВА 7 if (S9l9ct9dObj9ct == null) r9turn; Prop9rtyInfo[] api = Sel9Ct9dObject.G9tType().G9tProp9rti9s(); foreach (System.Reflection.Propertylnfo pi in api) { if (pi.CanRead && !pi.CanWrit9) { ListViewItem lvi = n9W ListViewItem(pi.Name); lvi.Tag = pi; obJ9Ct objValU9 = pi.GetValue(SelectedObject, null); lvi.SubItems.Add(objValue == null ? "" : objValu9.ToString()); lvP rops.It9ms.Add(lvi); lvi.S9l9Ct9d = pi.Name == "Bottom"; > > > // Обновляем значения всех изменившихся свойств, void ValueRefreshO { for9ach (ListVi9WItem lvi in lvProps.It9ms) { Prop9rtyInfo pi = (Prop9rtyInfo) lvi.Tag; object objValue = pi.GetValue(SelectedObject, null); string strNew = objValue == null ? "" : objValue.ToStringO; if (strNew != lvi.Subltems[1].Text) lvi.Subltems[1].Text = strNew; > > // Изменяем метки в соответствии с выбором в ListView. void ListViewOnSelectedIndexChanged(object objSrc, EventArgs args) { ListView lv = (ListView)objSrc; if (lv.Selectedltems.Count == 0) { labProp.Text = labDesc.Text = ""; return; > ListViewItem lvi = lv.SelectedItems[0]; Propertylnfo pi = (Propertylnfo)lvi.Tag; DescriptionAttribute dattr = (DescriptionAttribute) Att ribute.GetCustomAtt ribute(pi, typeof(Desc riptionAtt ribute));
Два настоящих приложения 355 labProp.Text = pi.Name; labDesc.Text = dattr == null ? "" : dattr.Description; > // По сигналу таймера обновляем все значения свойств, void TimerOnTick(object objSrc, EventArgs args) { ValueRefreshO; > > В элементе управления определено открытое свойство SelectedObject (я использовал то же имя, что и у соответствующего свойства PropertyGrid), и вызывается FullRefresh, когда оно меняется и требуется обновить ListView с учетом нового выбора. Метод GetProperties класса Туре получает массив объектов PropertyInfo\ для отображения в ListView отбираются только свойства, доступные исключительно для чтения. Метод GetValue объекта Propertylnfo получает значение нужного свойства. Обработчик события ListViewOnSelectedlndexChanged поддерживает в актуальном состоянии два элемента управления Label А атрибут Description программа получает статическим методом Attribute.GetCustomAttribute. Динамический перехват события Третий элемент управления, отображаемый программой в немодальном диалоговом окне, позволяет просматривать события элемента управления. Правая часть этого элемента управления представляет собой другой элемент управления, который имитирует окно консоли. ConsoleControl.es ,1 // ConsoleControl.cs (с) 2005 by Charles Petzold // From ControlExplorer program // using System; using System.Drawing; using System.Windows.Forms; class ConsoleControl: Control { TextBox txtbox; public ConsoleControlO { txtbox = new TextBoxO; txtbox.Parent = this;
356 ГЛАВА 7 txtbox.Multiline = true; txtbox.Wordwrap = false; txtbox.ScrollBars = ScrollBars.Both; txtbox.Readonly = true; txtbox.Dock = DockStyle.Fill; txtbox.TabStop = false; txtbox.HideSelection = false; public void ClearO txtbox.Clear(); public void WriteLine() Output("\r\n"); public void Write(object obj) Output(obj.ToStringO); public void WriteLine(object obj) Output(obj + "\r\n"); public void Write(string strFormat, params object[] aobj) Output(String.Format(strFormat, aobj)); public void WriteLine(string strFormat, params object[] aobj) Output(String.Format(strFormat, aobj) + "\r\n"); void Output(string str) txtbox.SelectionStart = txtbox.TextLength; txtbox.AppendText(str); > Это, несомненно, одна из самых простых частей всей программы. Элемент управления представляет собой простое поле TextBox, в которое добавляется текст, определяемый параметрами, что передаются методам Write и WriteLine. А вот класс EventLogger, который использует имитирующий консоль элемент управления, не так прост. Получить все определенные для класса события просто: нужно вызвать метод GetEvents относящегося к классу объекта Туре и получить массив
Два настоящих приложения 357 объектов Eventlnfo. Однако, если нужно что-либо делать по событию, придется установить обработчик. Для многих событий такой обработчик должен определяться в соответствии с делегатом EventHandler, определенным в пространстве имен System-. public delegate void EventHandler(object objSrc, EventArgs args); Можно определить подходящий метод в коде программы и привязать его к событию вызовом метода AddEventHandler нужного объекта Eventlnfo. Но некоторые события (такие как MouseMove) связаны с другими делегатами: public delegate void MouseEventHandler(object objSrc, MouseEventArgs args); В предыдущих версиях С# обработчик события MouseMove нужно было определять по аналогии с этим делегатом. В С# 2.0 для обработки MouseMove допускается использовать метод, определенный в соответствии с EventHandler. Метод может получать фактический тип args и использовать отражение для получения других свойств этого аргумента. Однако, если бы в EventLogger метод определялся в соответствии с делегатом EventHandler, в качестве обработчика всех событий можно было бы использовать только один метод. Чтобы корректно отображать информацию о событии, обработчик должен уметь различать события (такие как MouseMove). А как этот одинокий обработчик будет идентифицировать событие, которое обрабатывает? Да никак! Я пришел к выводу, что для каждого события, перехват которого задан вызовом EventLogger, элемент управления должен динамически создавать в памяти метод, обрабатывающий это событие. Похоже на те опасные ухищрения, к которым приходилось прибегать в С и ассемблере, тем не менее .NET Framework позволяет генерировать методы в памяти, оставаясь в рамках управляемого кода. Два использованных мной класса определены в пространстве имен System.ReJlection.Emit и называются DynamicMethod и ILGenerator. Имя последнего расшифровывается как «Intermediate Language Generator», то есть «генератор кода на промежуточном языке». Теперь вы готовы к анализу кода EventLogger. EventLogger.cs // // EventLogger.cs (с) 2005 by Charles Petzold // From ControlExplorer program // using System; using System.Collections.Generic; using System.Drawing; using System.Reflection; using System.Reflection.Emit; using System.Windows.Forms;
358 ГЛАВА 7 class EventLogger: SplitContainer { object objSelected; CheckBox chkbox; CheckedLlstBox lstbox; ConsoleControl cons; Dictionary<string, Delegate> deledict = new Dictionary<string, Delegate>(); static Dictionary<object, ConsoleControl> consdict = new Dictionary<object, ConsoleControl>(); // Открытое свойство для SelectedObject. public object SelectedObject { get { return objSelected; > set { if (objSelected != null) consdict.Remove(objSelected); objSelected = value; if (objSelected != null) consdict.Add(objSelected, cons); FullRefresh(); > > // Конструктор, public EventLoggerO { // В CheckedLlstBox отображаются все поддерживаемые // элементом управления события. lstbox = new CheckedListBox(); lstbox. Parent = Panel"!; lstbox.Dock = DockStyle.Fill; lstbox.CheckOnClick = true; lstbox.Sorted = true; lstbox.ItemCheck += ListBoxOnltemCheck; // Флажок для выбора или сброса всех событий, chkbox = new CheckBoxQ; chkbox. Parent = Panel"!; chkbox.AutoSize = true;
Два настоящих приложения 359 chkbox.AutoCheck = false; chkbox.ThreeState = true; chkbox.Text = "All"; chkbox.Dock = DockStyle.Top; chkbox.Click += CheckBoxOnClick; // ConsoleControl для регистрации событий. cons = new ConsoleControlO; cons.Parent = Panel2; cons.Dock = DockStyle.Fill; > // ListBoxOnltemCheck: подключение и отключение обработчиков событий, void ListBoxOnItemCheck(object objSrc, ItemCheckEventArgs args) { if (args.CurrentValue == args.NewValue) return; CheckedListBox lstbox = (CheckedListBox) objSrc; string strEvent = lstbox.Items[args.Index].ToStringO; // Подключить обработчик, если элемент отмечен флажком... if (args.NewValue == CheckState.Checked) { AttachHandler(strEvent); if (lstbox.Checkedlndices.Count + 1 == lstbox.Items.Count) chkbox.CheckState = CheckState.Checked; else chkbox.CheckState = CheckState.Indeterminate; > // ... в противном случае отключить обработчик, else { RemoveHandle r(st rEvent); if (lstbox.Checkedlndices.Count == 1) chkbox.CheckState = CheckState.Unchecked; else chkbox.CheckState = CheckState.Indeterminate; > > // Когда установлен или сброшен флажок, // отразить изменения в списке, void CheckBoxOnClick(object objSrc, EventArgs args) { CheckBox chkbox = (CheckBox)objSrc;
360 ГЛАВА 7 // Предоставляем ListBoxOnltemChecked возможность изменения состояния флажка. if (chkbox.CheckState == CheckState.Unchecked) { for (int i = 0; i < lstbox.Items.Count; i++) lstbox.SetItemChecked(i, true); > else { for (int i = 0; i < lstbox.Items.Count; i++) lstbox.SetItemChecked(i, false); > > // FullRefresh вызывается при изменениях SelectedProperty. void FullRefreshO { cons.Clear(); lstbox.Items.Clear(); if (SelectedObject == null) return; // Заполняем массив всеми событиями. EventInfo[] aevtinfo = SelectedObject.GetType().GetEvents(); lstbox.BeginUpdate(); // В цикле перечисляем все события, foreach (Eventlnfo evtinfo in aevtinfo) { bool bChecked = false; // Проверяем событие на предмет того, // наследует ли SelectedObject классу Control... if (SelectedObject.GetTypeO == typeof(Control)) bChecked = true; // ...в противном случае проверяем, реализовано ли событие в Control, else if (SelectedObject is Control) bChecked = typeof(Control).GetEvent(evtinfo.Name) == null; lstbox.Items.Add(evtinfo.Name, bChecked); > lstbox. EndUpdateO; > // Подключение обработчика, void AttachHandler(string strEvent)
Два настоящих приложения 361 // Информация о конкретном событии. Eventlnfo evtinfo = SelectedObject.GetType().GetEvent(strEvent); // Тип обработчика события. Type htype = evtinfo.EventHandlerType; // Информация о методе, обработчике события. MethodInfo[] methinfo = htype.GetMethods(); // Аргументы обработчика события. ParameterInfo[] parminfo = methinfo[0].GetParameters(); // Создаем метод с окончанием "ClickEventHandler" // и корректным типом и аргументами. DynamicMethod dynameth = new DynamicMethod(strEvent + "EventHandler", typeof(void), new Type[] { typeof(object), parminfo[1].ParameterType >, GetTypeO); // ILGenerator обеспечивает генерацию кода метода. ILGenerator ilg = dynameth.GetlLGeneratorO; // Обработчик события вызывает статический метод EventDump. Methodlnfo miEventDump = GetType().GetMethod("EventDump"); // Генерация кода, заталкивающего имя события и два аргумента в стек, // а затем вызывающего метод EventDump. ilg.Emit(OpCodes.Ldstr, strEvent); ilg.Emit(OpCodes.LdargJ)); ilg.Emit(0pCodes.Ldarg_1); ilg.EmitCalKOpCodes.Call, miEventDump, null); ilg.Emit(OpCodes.Ret); // Создание делегата, по типу соответствующего обработчику события. Delegate dynadele = dynameth.CreateDelegate(htype); // Наконец, устанавливаем обработчик события, evtinfo.AddEventHandler(SelectedObject, dynadele); // Добавление делегата в словарь для удаления позднее. deledict.Add(strEvent, dynadele); > // Удаление обработчика события, void RemoveHandler(string strEvent)
362 ГЛАВА 7 { Eventlnfo evtinfo = SelectedObject.GetType().GetEvent(strEvent); evtinfo.RemoveEventHandler(SelectedObject, deledict[strEvent]); deledict.Remove(strEvent); } // Этот статический метод отображает информацию о событии. // Поскольку метод статический, он должен получать корректный объект ConsoleControl // из словаря ConsoleControl (consdict). // Хотя последний аргумент определен как EventArgs, // для большинства событий он будет потомком EventArgs. public static void EventDump(string strEvent, object objSrc, EventArgs args) { // Выбираем корректный объект ConsoleControl из статического словаря. ConsoleControl cons = consdict[objSrc]; // Отображаем информацию о событии вместе с секундами и миллисекундами. DateTime dt = DateTime.Now; cons.Write("{0}.{1:D3} {2}", dt.Second X 10, dt.Millisecond, strEvent); // Отображаем информацию о всех свойствах EventArgs (или его потомках). PropertyInfo[] api = args.GetType().GetProperties(); foreach (Propertylnfo pi in api) cons.Write(" {0}={1}", pi.Name, pi.GetValue(args, null)); // Если имя события заканчивается на "Changed", отображаем новое свойство, if (strEvent.EndsWith("Changed")) { string strProperty = strEvent.Substring(0, strEvent.Length - 7); Propertylnfo pi = objSrc.GetType().GetProperty(strProperty); cons.Write(" [{0}={1}]", strProperty, pi.GetValue(objSrc, null)); } // Новая строка. cons.WriteLine(); } } В методе AttacbHandler методы DynamicMethod и ILGenerator, по существу, создают статический метод в памяти. Например, для события MouseMove сгенерированный метод можно было бы изобразить на С* таю static void MouseMoveEventHandler(object obj, MouseEventArgs args) { EventLogger.EventDump("MouseMove", obj, args); }
Два настоящих приложения 363 Он просто передает собственные аргументы наряду с именем события статическому методу EventDump в EventLogger, который затем отображает информацию о событии в ConsoleControL Но поскольку EventDump статический (потому как статический сгенерированный метод обработчика события) он не может напрямую обратиться к ConsoleControl, созданному в конструкторе. В этом причина существования статического объекта Dictionary по имени consdict (то есть console dictionary, или словарь консоли). Обработчик события должен получить ConsoleControl, относящийся к конкретному элементу управления, чьи события регистрируются в журнале. Обертка Справившись с элементом управления EventLogger, мы вольны делать все, что заблагорассудится. Класс PropertiesAndEventsDialog — это немодальное диалоговое окно, в котором размещаются элементы управления PropertyGrid, ReadOnlyPropertyGrid и EventLogger, аккуратно размежеванные разделителями. PropertiesAndEventsDialog.cs // // PropertiesAndEventsDialog.cs (с) 2005 by Charles Petzold // From ControlExplorer program // using System; using System.Drawing; using System.Windows.Forms; class PropertiesAndEventsDialog : Form { PropertyGrid propgrid; ReadOnlyPropertyGrid ropropgrid; EventLogger evtlst; // SelectedObject распределяет свойство на другие элементы управления, public object SelectedObject { get { return propgrid.SelectedObject; } set { propgrid.SelectedObject = value; ropropgrid.SelectedObject = value; evtlst.SelectedObject = value; }
364 ГЛАВА 7 // Конструктор. public PropertiesAndEventsDialog() { MaximizeBox = false; ShowInTaskbar = false; ClientSize = new Size(800, 600); SplitContainer sd = new SplitContainerO; sd. Parent = this; sd.Dock = DockStyle.Fill; sd.SplitterDistance = ClientSize.Height / 2; sd. Orientation = Orientation. Horizontal; SplitContainer sc2 = new SplitContainerO; sc2. Parent = sc1. Panel"!; sc2.Dock = DockStyle.Fill; sc2.SplitterDistance = ClientSize.Width / 2; // Элемент управления PropertyGrid. propgrid = new PropertyGridQ; propgrid.Parent = sc2.Panel1; propgrid.Dock = DockStyle.Fill; // Элемент управления ReadOnlyPropertyGrid. ropropgrid = new ReadOnlyPropertyGridO; ropropgrid.Parent = sc2.Panel2; ropropgrid.Dock = DockStyle.Fill; // Элемент управления EventLogger. evtlst = new EventLoggerQ; evtlst. Parent = sd.Panel2; evtlst.Dock = DockStyle.Fill; } } У самого диалогового окна есть свойство SelectedObject, которое используется для передачи объекта трем дочерним окнам. Второе диалоговое окно проекта ControlExplorer — AboutDialog. AboutDialog.es // // AboutDialog.cs (с) 2005 by Charles Petzold // using System;
Два настоящих приложения 365 using System.Diagnostics; using System.Drawing; using System.Reflection; using System.Windows.Forms; class AboutDialog : Form { protected FlowLayoutPanel flow; protected Button btnOk; public AboutDialog(string strResource) { // Получаем информацию о текущей сборке. Assembly a = GetType().Assembly; // Получаем название программы. AssemblyTitleAttribute asmblytitle = (AssemblyTitleAttribute) a.GetCustomAttributes(typeof(AssemblyTitleAttribute), false)[0]; string strTitle = asmblytitle.Title; // Получаем версию программы. AssemblyFileVersionAttribute asmblyvers = (AssemblyFileVersionAttribute) a.GetCustomAttributes(typeof(AssemblyFileVersionAttribute), false)[0]; string strVersion = asmblyvers.Version.Substring(0, 3); // Получаем информацию о правообладателе. AssemblyCopyrightAttribute asmblycopy = (AssemblyCopyrightAttribute) a.GetCustomAttributes(typeof(AssemblyCopyrightAttribute), false)[0]; string strCopyright = asmblycopy.Copyright; // Инициализируем атрибуты. Text = "About " + strTitle; AutoSize = true; AutoSizeMode = AutoSizeMode.GrowAndShrink; FormBorderStyle = FormBorderStyle.FixedDialog; ControlBox = false; MinimizeBox = false; MaximizeBox = false; ShowInTaskbar = false; Icon = ActiveForm.Icon; StartPosition = FormStartPosition.Manual; Location = ActiveForm.Location + Systemlnformation.CaptionButtonSize + Systemlnformation.FrameBorderSize; // Создаем элементы управления, flow = new FlowLayoutPanel(); flow.Parent = this;
366 ГЛАВА 7 flow.AutoSize = true; flow.FlowDirection = FlowDirection.TopDown; FlowLayoutPanel flow2 = new FlowLayoutPanelO; flow2.Parent = flow; flow2.AutoSize = true; flow2.Margin = new Padding(Font.Height); PictureBox picbox = new PictureBoxO; picbox.Parent = flow2; picbox. Image = Icon.ToBitmapO; picbox.SizeMode = PictureBoxSizeMode.AutoSize; picbox.Anchor = AnchorStyles.None; Label lbl = new LabelQ; lbl.Parent = flow2; lbl.AutoSize = true; lbl.Anchor = AnchorStyles.None; lbl.Text = strTitle + " Version " + strVersion; Ibl.Font = new Font(FontFamily.GenericSerif, 24, FontStyle.Italic); lbl = new Label(); lbl.Parent = flow; lbl.Text = "From the Microsoft Press book:"; lbl.AutoSize = true; lbl.Anchor = AnchorStyles.None; lbl.Margin = new Padding(Font.Height); Ibl.Font = new Font(FontFamily.GenericSerif, 16); picbox = new PictureBoxO; picbox.Parent = flow; picbox.Image = new Bitmap(GetType(), strResource + ".BookCover.png"); picbox.SizeMode = PictureBoxSizeMode.AutoSize; picbox.Anchor = AnchorStyles.None; LinkLabel Ink = new LinkLabelQ; Ink.Parent = flow; lnk.AutoSize = true; Ink.Anchor = AnchorStyles.None; Ink.Margin = new Padding(Font.Height); Ink.Text = "\x00A9 2005 by Charles Petzold"; Ink.Font = Ibl.Font; // new Font(FontFamily.GenericSerif, 16); Ink.LinkArea = new LinkArea(10, 15); lnk.LinkClicked += delegate { Process.Start("http://www.charlespetzold.com"); };
Два настоящих приложения 367 btnOk = new Button(); btnOk.Text = "OK"; btnOk.Parent = flow; btnOk.AutoSize = true; btnOk.Anchor = AnchorStyles.None; btnOk.Margin = new Padding(Font.Height); btnOk.DialogResult = DialogResult.OK; } } Я уже говорил, что даже в окне About используется отражение — вначале конструктора вызывается метод GetCustomAttributes для получения информации о сборке, в частности названия программы, версии файла и информации о правообладателе. (Часть этой информации можно получить, вызвав статические методы в Company- Name, ProductName и ProductVersion объекта Application?) Я хотел использовать одно диалоговое окно для обеих программ этой главы, а получение этой информации из сборки — отличный ход на пути к обобщению кода. Конструктор инициализирует свойство Icon диалогового окна значением одноименного свойства объекта ActiveForm. Последний представляет собой программу, вызвавшую диалоговое окно. Конструктор также получает аргумент, указывающий на ресурс «пространство имен». (Как вы помните, Visual Studio задает этому ресурсу имя, идентичное имени проекта.) Конструктору AboutDialog эта информация нужна, чтобы загрузить файл BookCover.png, который был добавлен в проект и отмечен как внедренный ресурс (Embedded Resource). Также в проект включается и помечается как внедренный ресурс файл Control- Explorer.ico. Один файл значка может содержать несколько «картинок» с разным разрешением. Если создавать значок в Visual Studio, вы получите два значка стандартных размеров: 1бх1б и 32x32 пикселов. Меньший значок используется для заголовка формы, панели задач и меню Пуск (Start), а больший размещается на рабочем столе. В зависимости от контекста проводник автоматически выбирает тот или иной значок. Вот большой значок, который я создал средствами Visual Studio — это кнопка с текстом «foo»: А вот и сам класс ControlExplorer. Он начинается с нескольких атрибутов сборки, которые позволяют идентифицировать ЕХЕ-файл.
368 ГЛАВА 7 ControlExplorer.es // // ControlExplorer.cs (с) 2005 by Charles Petzold // using System; using System.Drawing; using System.Reflection; using System.Windows.Forms; [assembly: AssemblyTitle("Control Explorer")] [assembly: AssemblyDescription("Developer Tool for Windows Forms")] [assembly: AssemblyCompany("www.charlespetzold.com")] [assembly: AssemblyProduct("Control Explorer")] [assembly: AssemblyCopyright("(c) 2005 by Charles Petzold")] [assembly: AssemblyVersion("1.0.*")] [assembly: AssemblyFileVersion("1.0.0.0")] class ControlExplorer: Form { const string strResource = "ControlExplorer"; Panel pnl; [STAThread] public static void Main() { Application.EnableVisualStyles(); Application.Run(new ControlExplorerO); } public ControlExplorerO { Text = "Control Explorer"; Icon = new Icon(GetType(), strResource + ".ControlExplorer.ico"); pnl = new Panel(); pnl.Parent = this; pnl.Dock = DockStyle.Fill; MenuStrip menu = new MenuStripQ; menu.Parent = this; menu.Items.Add(new ControlMenuItem(MenuItemOnClick)); ToolStripMenuItem itemHelp = new ToolStripMenuItem("&Help"); menu.Items.Add(itemHelp); ToolStripMenuItem itemAbout = new ToolStripMenuItem();
Два настоящих приложения 369 itemAbout.Text = "&About Control Explorer..."; itemAbout.Click += AboutOnClick; itemHelp.DropDownltems.Add(itemAbout); void MenuItemOnClick(object objSrc, EventArgs args) { // Получаем информацию о меню Item и о классе, который в нем указан. ToolStripMenuItem item = objSrc as ToolStripMenuItem; Type typ = (Type)item.Tag; // Подготовка к созданию объекта заданного типа. Constructorlnfo ci = typ.GetConstructor(System.Type.EmptyTypes); Control Ctrl; // Попытка создания объекта заданного типа. try { Ctrl = (Control)ci.Invoke(null); } catch (Exception exc) { MessageBox.Show(exc.Message, Text); return; } // Создаем диалоговое окно с элементом управления PropertyGrid. PropertiesAndEventsDialog dig = new PropertiesAndEventsDialogO; dig.Owner = this; dig.Text = item.Text + " Property Grid"; dlg.SelectedObject = Ctrl; dig.Closed += new EventHandler(DialogOnClosed); dlg.Show(); // Если свойству Parent не удается задать значение, // это, скорее всего, форма, для которой нужно вызвать Show. try { Ctrl.Parent = pnl; } catch { ctrl.Show(); }
370 ГЛАВА 7 // При закрытии диалогового окна Properties // удаляем элемент управления. void DialogOnClosed(object objSrc, EventArgs args) { PropertiesAndEventsDialog dig = (PropertiesAndEventsDialog)objSrc; Control Ctrl = (Control)dlg.SelectedObject; ctrl.Dispose(); } void AboutOnClick(object objSrc, EventArgs args) { new AboutDialog(strResource).ShowDialog(); } } Конструктор загружает значок и создает панель и меню. Когда пользователь выбирает элемент в меню Control, обработчик MenuItemOnClick создает объект выбранного типа и окно PropertiesAndEventsDialog для размещения объекта. При удалении диалогового окна, элемент управления уничтожается. Если у программы есть ресурс-значок, его можно загружать и инициализировать им свойство Icon формы — значок будет отображаться в заголовке окна программы. В Visual Studio нужно открыть окно свойств проекта и на вкладке Application и в поле Icon задать значок программы. После этого другие программы (например, оболочка Windows) смогут извлекать значок из ЕХЕ-файла программы и отображать его на экране. Установка ClickOnce В реальных приложениях очень важна организация процесса развертывания. Как программа попадает на машину пользователя? Понятно, что пользователями чисто программистского приложения ControlExplorer являются программисты, поэтому очень соблазнительно передать только файл ControlExplorer.exe и предоставить пользователю самостоятельно решать как, где, зачем и что с ним делать. Однако в реальных приложениях, адресованных обычным пользователям, не обойтись без установщика, например Microsoft Installer (MSI), который предоставляет пользователю ряд знакомых диалоговых окон. MSI (или аналогичная установочная утилита) лучше всего подходит для установки приложений, которым требуется внести изменения в системный реестр, определить ассоциации расширений файлов и настроить массу других параметров. Но для более простых программ в .NET Framework 2.0 появился новый установщик — он называется ClickOnce, и именно о нем мы сейчас поговорим. ClickOnce автоматизирует несколько стандартных операций по установке программы. Если на машине пользователя не установлен каркас .NET Framework 2.0, установщик предложит установить его, впрочем, как и другие необходимые для
Два настоящих приложения 371 нормальной работы программы компоненты (например, Microsoft SQL Server). ClickOnce разместит ярлык программы в меню Пуск (Start) и обеспечит возможность удалить программу средствами соответствующей утилиты из панели управления (Control Panel). ClickOnce также может выяснить, вышла ли более новая версия программы и выполнит загрузку и установку этой версии. Аплет Установка и удаление программ (Add Or Remove Programs) также позволяет возвращаться к предыдущей версии программы. ClickOnce не размещает файлы программы в папке Program Files, как это делает обычный установщик. Вместо этого программа устанавливается для конкретного пользователя и хранится в папке-кэше приложений ClickOnce — подпапке Application Data папки Local Settings в профиле локального пользователя. ClickOnce можно использовать для создания установочного компакт-диска, но лучше всего он подходит для загрузки приложений с Web-сайта. Вы вправе создать необходимые файлы вручную или средствами «генератора и редактора манифеста» — утилиты mage.exe или mageuLexe, но Visual Studio предоставляет прекрасные средства подготовки и размещения файлов на Web-сервере. Среди этих файлов обычно есть setup.exe, который иногда называют «начальным загрузчиком», потому что при необходимости он установит .NET Framework 2.0, чтобы оставшаяся часть установки могла пройти без сучка и задоринки. Visual Studio также при необходимости создает HTML-страницу (в некоторых бета-версиях Visual Studio 2005 она называется publish.htm), которая содержит JavaScript-код, проверяющий удовлетворяет ли машина пользователя системным требованиям. Также в установочный пакет входят XML-файлы с расширениями .application и manifest и двоичные файлы с расширением .deploy, содержащие ЕХЕ-файлы и любые другие файлы, необходимые для нормальной работы приложения (например, файлы справочной системы). Создание процедуры установки ClickOnce называется «публикацией» приложения. Я подробно расскажу, как публиковал ControlExplorer на своем Web-сайте. Понятно, что ваш процесс публикации на Web-сайте будет немного отличаться. Загрузив проект в Visual Studio, откройте окно свойств проекта и перейдите на вкладку Publish Заглавной странице нужна некоторая информация. Поле Publishing Location указывает, где должны размещаться файлы приложения. Это могут быть локальные файлы или локальный HTTP-сервер. В моем случае нужно было скопировать файлы на мой Web-сайт, что я обычно делаю, копируя файлы на сайт EarthLink по протоколу FTP: ftp://ftp.business.earthlink.net/www/winforms/ControlExplorer/ Этот адрес ссылается на подкаталог ControlExplorer (который Visual Studio создаст автоматически) каталога winforms моего Web-сайта. Следующее поле, Installation URL, требуется Visual Studio для запуска процесса установки. В моем случае это адрес: http://www.charlespetzold.com/winforms/ControlExplorer/
372 ГЛАВА 7 В области Install Mode and Settings предлагается выбрать один из двух вариантов: всегда выполнять программу с Web-страницы или устанавливать ее на машине пользователя и создать ярлык в меню Пуск. Я выбрал второй вариант. Указываемый в поле Publish Version номер публикуемой версии никак не зависит любых других номеров версий или файлов. Чтобы не вводить пользователей в заблуждение, желательно аккуратно отслеживать по крайней мере основную (Major) и вспомогательную (Minor) части номера версии. Visual Studio будет автоматически увеличивать номер Revision при каждой публикации новой версии приложения, но только до 9- На вкладке Publish есть также четыре кнопки. Кнопка Application Files позволяет задать дополнительные относящиеся к установочному пакету файлы. (Я продемонстрирую, как использовать эту кнопку позже в программе MdiBrowser.) Кнопка Prerequisites позволяет удостовериться, что пользователь выбрал для загрузки .NET Framework 2.0, и (если только у вас нет железной уверенности, что у все пользователи установят .NET Framework 2.0), вероятно, потребуется создать файл установки всех необходимых для работы программы компонентов. Нужно организовать загрузку таких компонентов с сайта поставщика (то есть Microsoft). Есть несколько вариантов для публикации и развертывания обновлений. Можно вообще отключить проверку обновлений, проверять на наличие обновлений до установки или при первом запуске на исполнение. Проверка обновлений до запуска позволяет гарантировать, что пользователь будет всегда использовать самую свежую версию, но это увеличивает время запуска программы. Если проверять наличие обновлений после запуска приложения (это мой любимый вариант), пользователю разрешается использовать устаревшую версию, но при следующем запуске программы откроется диалоговое окно, предлагающее пользователю загрузить и установить более свежую версию. В диалоговом окне Publish Options, которое открывается по щелчку кнопки Publish, предлагается требует указать издателя (поле Publisher name) и имя (поле Product name) продукта. Эти два имени указываются на Web-странице, которую создает Visual Studio, и в меню Пуск Если оставить эти поля пустыми, Visual Studio вместо имени издателя укажет название компании, определенное при установке Windows, и вместо имени продукта — название проекта. Я указал «Petzold Programming Windows Forms» в поле Publisher name, и «Control Explorer» — в поле Product name, то есть более понятное пользователю название, состоящее из двух, а не одного слова. Можно также указать URL-адрес сайта поддержки и сопровождения в поле Support URL. Visual Studio разместит ссылку на сайт на созданной Web-странице и в меню Пуск рядом с ярлыком программы. Я предпочел оставить это поле пустым. В поле Deployment web page я ввел publish.htm. (В некоторых бета-версиях Visual Studio 2005 оно заполнялось значением по умолчанию.) Под этим полем есть флажки, из которых я установил два, чтобы автоматически генерировать HTML-
Два настоящих приложения 373 файлы (Automatically generate deployment web page after every publish) и отображать их по завершении публикации (Open deployment page after publish). Другие флажки я оставил в состоянии по умолчанию. Вопросы безопасности При публикации приложений на Web-сайте нужно соблюсти определенные требования. Два основных — определение того, что делает программа, и кто ее создал. Пользователи хотят быть уверенными, что программу, которую они загрузят, не разрушит их систему, и поэтому очень важно доверие Web-сайту, откуда они получают программу. По умолчанию флажок Enable ClickOnce Security Settings на вкладке Security окна свойств проекта Project Properties установлен. Вы вправе выбрать уровень доверия пользователя вашему приложению: полное доверие — This is a full trust application, или частичное — This is a partial trust application. Какой вариант выбрать? По умолчанию выбран переключатель полного доверия. Большинству программ Windows Forms обычно требуется выполнить действия (например, доступ к файлам), которые требуют полного доверия со стороны пользователя. Создавая приложение с частичным доверием, нужно внимательно изучить посвященный безопасности .NET Framework раздел в документации к используемым в программе классам, методам и свойствам. В этом разделе (если он есть) говорится, какие разрешения требуются для нормальной работы программы. Загруженная через Интернет программа с частичным доверием обладает только разрешениями FileDialogPer- mission, IsolatedStorageFilePermission, SecurityPermission, UlPermission и PrintingPermis- sion. (Это классы в пространствах имен SystemSecurity.Permissions и System.Drawing- Printing.) В программе ControlExplorer используются методы, требующие разрешения ReJlectionPermission, поэтому ControlExplorer недостаточно частичного доверия. Большинству приложений требуется полное доверие. Поэтому нужно как-то убедить потенциальных пользователей в том, что программа происходит из надежного источника и не подверглась модификации с момента публикации. Это делается путем подписывания кода с использованием технологии Authenticode компании Microsoft. Сначала нужно получить в центре сертификации цифровой сертификат для подписания кода. Наиболее известный центр сертификации — VeriSign, но «свободные художники» (не официально зарегистрированные компании) вроде меня VeriSign неинтересны. Частные лица могут приобрести сертификат в других компаниях, например в Comodo (www.instantssl.com). Для меня процесс получения сертификата для подписания кода вылился в создание на локальном компьютере двух файлов: с расширением spc (software publisher certificate — сертификат издателя ПО) и pvk (private key — закрытый ключ), последний защищен паролем. Visual Studio требуется предоставить файл с расширени-
374 ГЛАВА 7 ем pfx (personal information exchange — частный обмен информацией). Он создается на основе SPC- и PVK-файлов с помощью утилиты pvkimprt.exe (PVK Digital Certificate Files Importer), которую можно скачать на Web-странице http://office.- microsoft.com/downloads/2000/pvkimprt.aspx. Чтобы использовать полученный PFX-файл для подписания приложения, на вкладке Signing в окна Project Properties установите флажок Sign the CiickOnce manifests. Щелкните кнопку Select from File и укажите PFX-файл. Центр сертификации должен также предоставить URL-адрес, который нужно указать в поле Timestamp server URL. Если не предоставить сертификат для подписания кода, Visual Studio создаст в процессе публикации временный сертификат, но с точки зрения пользователя код будет считаться не подписанным. Публикация приложения Теперь мы готовы вернуться к вкладке Publish окна Project Properties. Процесс завершается щелчком кнопки Publish Now. При попытке подключиться к вашему FTP-сайту, Visual Studio скорее всего предложит указать имя пользователя и пароль. По завершении установки можно посмотреть на созданные Visual Studio каталоги и файлы на Web-сайте. Помимо publish.htm и setup.exe вы увидите XML-файлы с расширениями .application и .manifest, а также двоичный файл .deploy. Отдельный каталог создается для каждой публикуемой версии. Внимание: Web-сервер нужно уведомить о MIME-типе application- и manifest- файлов. Это обычно делается добавлением следующих двух строк в файл с расширением btaccess в корневом каталоге Web-сайта: AddType application/x-ms-application application AddType application/x-ms-application manifest Конечно же, мне пришлось изменить страницу indexhtml, расположенную в каталоге winforms, чтобы она вызывала страницу publish.htm из подкаталога Control- Explorer. Задача решается простым HTML-кодом, например таким: Install <a href=MControlExplorer/publish.htm">Control Explorer</a> ... Когда-нибудь я обновлю страницу publish.htm, чтобы она была больше похожа на остальные страницы моего Web-сайта, но пока и этого достаточно. При установке программы с помощью CiickOnce пользователь видит диалоговое окно с заголовком-предупреждением Application Install - Security Warning. Если приложение не подписано, диалоговое окно будет содержать предупреждения, например Publisher cannot be verified («не удается аутентифицировать издателя») или Publisher of this software is unknown («неизвестный издатель ПО»). Если программа подписана, никаких предупреждений не будет, зато будет инфор-
Два настоящих приложения 375 мация об издателе. Если приложение требует полного доверия, диалоговое окно будет содержать соответствующее предупреждение, например Application requires potentially unsafe access to your computer («приложению требуется опасно высокий уровень доступа к компьютеру»). Для программ требующих частичного доверия, в диалоговом окне сообщается Application can only access your computer in ways deemed safe («приложению предоставляется доступ к компьютеру с уровнем, считающимся безопасным»). Пример 2: MdiBrowser Элемент управления Windows Forms, известный как WebBrowser, поддерживает так много функциональных возможностей Internet Explorer, что мне даже неудобно его использовать. Чтобы получить представление о его основных возможностях, достаточно создать элемент управления WebBrowser в ControlExplorer и ввести URL- адрес в свойство Url. Хотя кнопки Назад (Back) и это, казалось бы должно исключить навигацию, но можно нажать кнопку Backspace или щелкнуть правой кнопкой элемент управления и выбрать Назад (Back). Контекстное меню также позволяет распечатать страницу, поэтому понятно, что поддержка печати также встроена в элемент управления. Ясно, что WebBrowser, созданный на основе ActiveX-элемента управления, и есть не кто иной, как сам Internet Explorer. Поэтому удивляться тут, собственно, нечему. Следующая программа, о которой я расскажу, — Web-браузер с многодокументным, или MDI-интерфейсом (Multiple Document Interface). За последние годы архитектура MDI значительно потеряла популярность, но в Windows Forms она поддерживается и, кажется, является неплохим вариантом для приложения-браузера. (В браузере Opera также используется многодокументный интерфейс.) В основном окне программы размещаются несколько дочерних окон, в каждом из которых может отображаться своя Web-страница, причем навигация выполняется независимо. Во всем остальном я бесстыдно копирую пользовательский интерфейс Internet Explorer. В MdiBrowser есть панель инструментов, где размещаются кнопки Back, Forward, Stop, Refresh, Home и Print, а также панель адреса (фактически, это еще один элемент управления ToolStrip), где можно вводить URL-адрес и щелчком кнопки Go инициировать загрузку Web-страницы. А вот строку состояния, я решил, реализовать лучше, чем в оригинале — у каждого дочернего окна должна быть собственная строка состояния. Если предполагается, что разные дочерние окна выполняют загрузку разных страниц, им действительно нужны отдельные строки состояния. Вот как выглядит окно программы после первого запуска:
376 ГЛАВА 7 Естественно, я как создатель этой программы бессовестным образом воспользовался своим правом задать домашнюю страницу по умолчанию. MDI-интерфейс В MDI-программе есть одно основное окно, но в клиентской области размещаются несколько дочерних форм, причем в каждой — отдельный документ. Дочерние окна практически идентичны окнам обычного приложения за исключением того, что у них нет меню. При переключении между дочерними окнами основное меню программы и панели инструментов (если они есть) меняются в соответствии с состоянием и параметрами активного дочернего окна. Например, в панели инструментов браузера всегда есть кнопки Назад (Back) и Вперед (Forward), но кнопка Назад активна только, если страница позволяет навигацию назад по истории, а кнопка Вперед становится доступной только если пользователь уже «ушел» со страницы, щелкнув кнопку Назад. При переключении между дочерними окнами состояние этих кнопок должно меняться в зависимости от доступности тех или иных действий в выбранном окне. Форма приложения становится контейнером многодокументного интерфейса после инициации свойства IsMdiContainer значением true. После этого создается элемента управления типа MdiClient, который заполняет клиентскую область и становится фактическим «родителем» всех дочерних документов. Вместе с тем, взаимодействовать с MdiClient напрямую не принято. Дочернее окно является экземпляром класса Form или его потомка. При создании дочерней формы ее свойство MdiParent инициируется формой приложения и вызывается метод Show.
Два настоящих приложения 377 При свертывании дочернее окно отображается внизу клиентской области приложения, уменьшенное так, что видна только часть заголовка окна. Если дочернее окно развернуть в максимальный размер, заголовок окна потомка исчезает, а текст, который там был, заключается в квадратные скобки и добавляется в конец текста заголовка окна приложения. Значок и кнопки развертывания, свертки и закрытия дочернего окна становится частью меню приложения. Если используется элемент управления MenuStrip, а не использовавшийся ранее MainMenu, нужно инициализировать свойство MainMenuStrip формы объектом MenuStrip — только в этом случае все будет работать, как должно. Свойство IsMdiChild позволяет дочернему окну выяснить, является ли оно частью MDI-интерфейса. Для получения набора потомков из родительского окна служит свойство MdiChildren. Дочернее окно с подсвеченным заголовком, располагающееся поверх остальных потомков, называется активным потомком. Из формы приложения можно активизировать конкретного потомка вызовом метода ActivateMdiChild. Форма приложения получает информации об активном в данный момент потомке путем считывания свойства ActiveMdiChild. Для выявления смены активного потомка в форме приложения устанавливают обработчик события MdiChildActivate или (что делают чаще) переопределяют метод OnMdiChildActivate. Нет ничего необычного, что в MDI-программе элемент меню верхнего уровня называется Window. Обычно это последний элемент меню перед элементом Help. Обычно в этом меню есть команды, позволяющие свернуть или расположить дочерние окна каскадом или замостить ими клиентскую область родителя. Реализовать эти команды меню можно методом LayoutMdi. В соответствии с принятой практикой меню Window также содержит список всех текущих дочерних окон, позволяя пользователю выбирать нужное. Если дочерних окон больше девяти, внизу меню появится команда, открывающая небольшое диалоговое окно, в котором можно выбрать нужное дочернее окно. Эту функцию специально реализовывать не нужно — достаточно приравнять свойство MdiWindoiv- Listltem элемента управления MenuStrip к ToolStripMenuItem элемента Window. Решение и проект До сих пор я создавал проекты в Visual Studio так, чтобы файлы проекта (.csproj) и решения (.sin) оказывались в одной папке, имя которой совпадало с именем проекта. Вместе с тем при создании нового проекта в диалоговом окне New Project можно установить переключатель Create directory for solution. Тогда папка проекта будет подпапкой папки решения (по умолчанию имя последней совпадает с именем создаваемого проекта). Именно так я создал проект MdiBrowser. Позже в этой главе мы узнаете, как создавать файл справочной системы для программы. Я хотел, чтобы файлы справ-
378 ГЛАВА 7 ки располагались в Help, подпапке папки решения MdiBrowser, но папки проекта MdiBrowser и справочной системы Help должны находиться на одном уровне. Избранное и параметры настройки Если бы я серьезно планировал создать Web-браузер, конкурирующий с Internet Explorer, я бы постарался обеспечить интеграцию с хранилищем избранных страниц (Favorites) Internet Explorer или хотя бы возможность импорта этих данных. Поскольку MdiBrowser создается практически исключительно для целей обучения, я посчитал, что полезнее все-таки реализовать собственную логику хранения и отображения избранного, не пытаясь обеспечить интеграцию с Internet Explorer. В результате логика программы оказалась достаточно непоследовательной: если для создания записи об избранной странице задействовать меню, MdiBrowser сохранить ее в своем хранилище, а вот при использовании контекстного меню элемента управления WebBrowser страница будет размещена в Избранное браузера Internet Explorer. Моя реализация избранных страниц не поддерживает папки и не позволяет изменять текст, описывающий избранные страницы, на что-то отличное от заголовка Web-страницы. Логика управления избранными страницами инкапсулируется в классе Favorite. Favorite.cs // // Favorite.cs (с) 2005 by Charles Petzold // From MdiBrowser Program // using System; public class Favorite : IComparable<Favorite> { string strTitle, strUrl; // Открытые свойства, public string Title { get { return strTitle; } set { strTitle « value; } } public string Url { get { return strUrl; } set { strUrl = value; } } // Конструкторы, public Favorite()
Два настоящих приложения 379 { } public Favorite(string strTitle, string strUrl) { Title = strTitle; Url = strUrl; } // Метод, реализующий интерфейс IComparable. public int CompareTo(Favorite fav) { return Title.CompareTo(fav.Title); } } Здесь все просто. Класс реализует интерфейс IComparable, что позволяет хранить избранное в свойстве Title. Класс определен как открытый и содержит конструктор без параметров, поэтому он поддается сериализации классом XmlSerializer. Основной класс для сохранения и получения параметров настройки программы называется MdiBrowserSettings. MdiBrowserSettings.cs // // MdiBrowserSettings.cs (с) 2005 by Charles Petzold // From MdiBrowser Program // using System; using System.Collections.Generic; using System.Drawing; using System.10; using System.Windows.Forms; using System.Xml.Serialization; public class MdiBrowserSettings { // Параметры по умолчанию. public Rectangle WindowBounds = new Rectangle(0, 0, 800, 600); public FormWindowState WindowState = FormWindowState.Normal; public Rectangle ChildWindowBounds = new Rectangle(0, 0, 660, 400); public FormWindowState ChildWindowState = FormWindowState.Normal; public string Home = "http://www.charlespetzold.com"; public bool ViewToolBar = true; public bool ViewAddressBar = true;
380 ГЛАВА 7 } // Список избранного и введенных в адресную строку URL-адресов, public List<Favorite> Favorites = new List<Favorite>(); public List<string> ManualUrls = new List<string>(); // Загрузка параметров из файла. public static MdiBrowserSettings Load(string strAppData) { StreamReader sr; MdiBrowserSettings settings; XmlSerializer xmlser = new XmlSerializer(typeof(MdiBrowserSettings)); try { sr = new StreamReader(strAppData); settings = (MdiBrowserSettings)xmlser.Deserialize(sr); sr.Close(); } catch { settings = new MdiBrowserSettingsO; } return settings; } // Сохранение параметров в файле, public void Save(string strAppData) { Directory.CreateDirectory(Path.GetDirectoryName(strAppData)); StreamWriter sw = new StreamWriter(strAppData); XmlSerializer xmlser = new XmlSerializer(GetTypeO); xmlser.Serialize(sw, this); sw.Close(); } Параметры настройки хранятся как открытые поля, а не как свойства, только из соображений простоты и ясности. WindowBounds и WindowState хранят информацию о начальном размере окна приложения и его состоянии — свернуто, развернуто или обычный размер. ChildWindowBounds и CbildWindowState — хранят те же свойства дочерних окон (хотя пока программа не умеет менять эти параметры). Следующее поле — домашняя страница по умолчанию. Следующие два поля управляют отображением панели инструментов и поля адреса. Также есть два объекта List — один из них содержит объекты Favorite, а второй называется ManualUrls и хранит все URL-адреса, введенные пользователем вручную в поле адреса или в диалоговое окно File Open.
Два настоящих приложения 381 Класс заканчивается методами Load и Save — первый служит для загрузки существующего XML-файла с параметрами настройки, а второй — для хранения текущего объекта MdiBrowserSettings. Имя файла, используемое в этих методах, будет определено в классе MdiBrowser, о котором пойдет речь чуть позже. Дочернее окно Класс дочернего окна называется BrowserChild и является наследником Form. Конструктор создает элемент управления WebBrowser, который занимает практически всю клиентскую область, а также строку состояния StatusStrip в нижней части формы. BrowserChild.cs // // BrowserChild.cs (с) 2005 by Charles Petzold // From MdiBrowser Program // using System; using System.Drawing; using System.Windows.Forms; class BrowserChild : Form { WebBrowser wb; ToolStripStatusLabel statlbl; ToolStripProgressBar statprog; // Конструктор, public BrowserChild() { wb = new WebBrowserO; wb.Parent = this; wb.Dock = DockStyle.Fill; StatusStrip status = new StatusStripO; status.Parent = this; statlbl = new ToolStripStatusLabelO; statlbl.TextAlign = ContentAlignment.MiddleLeft; statlbl.Spring = true; status.Items.Add(statlbl); statprog = new ToolStripProgressBarO; statprog.Visible = false; status.Items.Add(statprog);
382 ГЛАВА 7 // Теперь, когда у нас есть StatusStrip, можно без опаски // переходить к созданию обработчиков события в WebBrowser. wb.DocumentTitleChanged += OnDocumentTitleChanged; wb.StatusTextChanged += OnStatusTextChanged; wb.ProgressChanged += OnProgressChanged; } // Открытое свойство. public WebBrowser WebBrowser { get { return wb; } } // Обработчики событий заголовка и строки состояния, void OnDocumentTitleChanged(object objSrc, EventArgs args) { WebBrowser wb = objSrc as WebBrowser; Text = wb.DocumentTitle; if (wb.Url != null && wb.Url.ToStringO.Length > 0) Text += " \x2014 " + wb.Url.ToStringO; } void OnStatusTextChanged(object objSrc, EventArgs args) { WebBrowser wb = objSrc as WebBrowser; statlbl.Text = wb.StatusText; } void OnProgressChanged(object obj, WebBrowserProgressChangedEventArgs args) { if (statprog.Visible = (args.CurrentProgress != args.MaximumProgress)) statprog.Value = (int)(100 * args.CurrentProgress / args.MaximumProgress); } protected override void 0nClosed(EventArgs args) { base.OnClosed(args); WebBrowser.Dispose(); > } Этот класс отвечает за обновление области заголовка и строки состояния окна — работы у него, честно говоря, немного: три реализованные в WebBrowser события — DocumentTitleCbanged, StatusTextCbanged и ProgressCbanged информируют класс о любых изменениях.
Два настоящих приложения 383 Форма приложения По мере разработки программы, класс формы приложения (который я назвал MdiBrowsef) становился все объемнее. Этот класс должен был содержать код создания меню и панелей инструментов, а также определять обработчики событий меню и событий Click панели инструментов. Как нельзя кстати пришлось появившееся в С# 2.0 ключевое слово partial. Оно позволяет разнести код класса на несколько файлов. Таким образом, программа может содержать несколько файлов с определением класса, которые выглядят примерно таю partial class MdiBrowser { } Я решил каждый элемент меню верхнего уровня реализовать в отдельном файле. Это файлы MdiBrowser.FileMenu.cs, MdiBrower.ViewMenu.cs, MdiBrowser.Favorites- Menu.cs, MdiBrowser.WindowMenu.cs и MdiBrowser.HelpMenu.cs. Каждый из них содержит метод, создающий часть меню и обработчики событий этой части. Панель инструментов и поле адреса также реализованы в отдельных файлах — MdiBrowser. - ToolBar.cs и MdiBrowser.AddrBar.cs, соответственно. После вынесения всех этих частей кода в другие файлы от MdiBrowser.cs практически ничего не осталось. MdiBrowser.cs // // MdiBrowser.cs (с) 2005 by Charles Petzold // From MdiBrowser Program // using System; using System.Drawing; using System.10; using System.Reflection; using System.Windows.Forms; [assembly: AssemblyTitle("MDI Browser")] [assembly: AssemblyDescription("Multiple Document Interface Web Browser")] [assembly: AssemblyCompany("www.charlespetzold.com")] [assembly: AssemblyProduct("MDI Browser")] [assembly: AssemblyCopyrightC(c) 2005 by Charles Petzold")] [assembly: AssemblyVersion("1.0.*")] [assembly: AssemblyFileVersion("1.0.0.0")] partial class MdiBrowser : Form {
384 ГЛАВА 7 static string strAppData = Path.Combine( Environment.GetFolderPath( Environment.SpecialFolder.LocalApplicationData), "Petzold\\MdiBrowser\\MdiBrowser.Settings.xml"); MdiBrowserSettings settings; MenuStrip menu; ToolStrip addr, tool; [STAThread] public static void Main() { Application.EnableVisualStyles(); Application.Run(new MdiBrowserO); } public MdiBrowserO { // Определение основных свойств Form. Text = "MDI Browser"; Icon = new Icon(GetType(), "MdiBrowser.MdiBrowser.ico"); IsMdiContainer = true; // Загрузка параметров. settings = MdiBrowserSettings.Load(strAppData); // Определение размера и состояния окна. Bounds = settings.WindowBounds; WindowState = settings.WindowState; // Создание строки адреса. addr = CreateAddressBarC'MdiBrowser"); // in MdiBrowser.AddrBar.es // Создание панели инструментов. tool = CreateToolBarC'MdiBrowser"); // in MdiBrowser.ToolBar.es // Создание меню. menu = new MenuStripO; menu.Parent = this; MainMenuStrip = menu; // This is good for MDI. menu.Items.Add(FileMenuO); // in MdiBrowser.FileMenu.es menu.Items.Add(ViewMenuO); // в MdiBrowser.ViewMenu.es menu.Items.Add(FavoritesMenuO);// в MdiBrowser.FavoritesMenu.es menu.Items.Add(WindowMenuO); // в MdiBrowser.WindowMenu.es menu.Items.Add(HelpMenuO); // в MdiBrowser.HelpMenu.es
Два настоящих приложения 385 } // Загрузка окна и открытие в нем домашней страницы. Go(settings.Home, false); } // Метод Go осуществляет переход по URL-адресу и // при необходимости добавляет его в список, void Go(string strUrl, bool bAddToList) { BrowserChild bcNew = new BrowserChildO; bcNew.MdiParent = this; bcNew.Icon = Icon; bcNew.WebBrowser.Navigate(strUrl); // Мы задаем, а не обновляем параметры, извлеченные из объекта параметров. // Для этого нужно установить обработчик события Resize // и сохранять измененные пользователем параметры. bcNew.Bounds = settings.ChildWindowBounds; bcNew.WindowState = settings.ChildWindowState; bcNew.Show(); // Если URL-адрес был введен вручную, добавляем его в поле со списком. if (bAddToList) { if (settings.ManualUrls.IndexOf(strUrl) == -1) { settings.ManualUrls.Add(strUrl); settings.ManualUrls.Sort(); settings.Save(strAppData); } } } // Сохранение параметров при закрытии формы, protected override void OnClosed(EventArgs args) { settings.WindowState = WindowState; settings.WindowBounds = WindowState == FormWindowState.Normal ? Bounds : RestoreBounds; settings.Save(strAppData); base.OnClosed(args); } Обратите внимание, что этот класс инициализирует свойство IsMdiContainer значением true, что необходимо для организации многодокументного интерфейса. Далее программа пытается загрузить файл параметров настройки. Где хранятся эти параметры? Вообще говоря, приложения размещают индивидуальные для раз-
386 ГЛАВА 7 ных пользователей параметры настройки в специальной области, известной как «данные приложения пользователя». Для программы по имени MdiBrowser, издаваемой компанией Petzold и установленной пользователем Deirdre, параметры настройки будут сохранены в следующей папке: \Documents и Settings\Deirdre\Application Data\Petzold\MdiBrowser Однако для приложений с поддержкой установки ClickOnce, рекомендуется размещать параметры в области «данные приложения локального пользователя»: \Documents и Settings\Deirdre\Local Settings\Application Data\Petzold\MdiBrowser Обе области относятся к конкретному пользователю, но обычные данные приложения предназначены для мобильного пользователя — того, что может входить в систему с разных машин, а локальные данные относятся к конкретному пользователю на конкретной (локальной) машине. Я задействовал статический метод Environment.GetFolderPath для получения папки Local Application Data текущего пользователя, хотя также можно использовать метод Application.LocalUserAppDataPath, который автоматически создает папку на основании свойств CompanyName, ProductName и ProductVersion. Если файла параметров настройки не существует, класс MdiBrowserSettings создаст объект с параметрами по умолчанию. Затем создаются адресная строка, панель инструментов и меню, а завершается все это вызовом метода Go по отношению к домашней странице пользователя. Этот метод создает новый дочерний объект BrowserCbild с заданным URL-адресом. Переопределенный обработчик OnClosed сохраняет новые параметры настройки программы. Сохранять параметры по завершении программы обычно проще (и быстрее), чем при каждом их изменении. Однако и этот подход не идеален. Допустим, работая в программе, вы добавили несколько страниц в Избранное, после чего запустили второй экземпляр программы. Но первый экземпляр не сохранил Избранное, второй экземпляр о них ничего не узнает. Далее вы завершаете первый экземпляр, он сохраняет все параметры настройки, включая новый состав избранных страниц. Позже, завершая свою работу, второй экземпляр тоже сохранит Избранное, но в устаревшем составе — все добавленные в Избранное страницы безвозвратно потеряны. Одно из решений заключается в запрещении запуска нескольких экземпляров, например путем создания объект-мьютекса Mutex, который является стандартным механизмом синхронизации в рамках всей системы. Первый экземпляр успешно получит Mutex, а второй уже не сможет. В конце концов я принял решение все-таки разрешить запуск многих экземпляров, но сохранять объект параметров настройки сразу после добавления новых элементов в список ManualUrls или Favorites. Значок MdiBrowser.ico в окне MDI — это изображение земного шара:
Два настоящих приложения 387 Меню File Меню File в MdiBrowser содержит команды New, Open, Save As, три относящихся к печати команды, Properties и Exit. За исключением New, Open и Exit, все элементы реализуют в WebBrower простой вызов методов. Все, что требуется от обработчиков события Click, это получить текущего потомка, представляющего собой объект типа BrowserChild, и считать его свойство WebBrowser. MdiBrowser.FileMenu.cs // _____ // MdiBrowser.FileMenu.cs (с) 2005 by Charles Petzold // From MdiBrowser Program // _ using System; using System.Drawing; using System.Windows.Forms; partial class MdiBrowser: Form { ToolStripMenuItem itemSaveAs, itemPrint, itemPreview, itemProps; ToolStripMenuItem FileMenu() { ToolStripMenuItem itemFile = new ToolStripMenuItem("&File"); itemFile.DropDownOpening += FileOnDropDownOpening; ToolStripMenuItem item = new ToolStripMenuItem("&New"); item.ShortcutKeys = Keys.Control | Keys.N; item.Click += NewOnClick; itemFile.DropDownltems.Add(item); item = new ToolStripMenuItem("&Open..."); item.ShortcutKeys = Keys.Control | Keys.O; item.Click += OpenOnClick; itemFile.DropDownItems.Add(item);
388 ГЛАВА 7 itemSaveAs = new ToolStripMenuItem("Save &As..."); itemSaveAs.Click += SaveAsOnClick; itemFile.DropDownltems.Add(itemSaveAs); itemFile.DropDownItems.Add(new ToolStripSeparatorO); item = new ToolStripMenuItem("Page Set&up..."); item.Click += PageSetupOnClick; itemFile.DropDownltems.Add(item); itemPrint = new ToolStripMenuItem("&Print..."); itemPrint.ShortcutKeys = Keys.Control | Keys.P; itemPrint.Click += PrintDialogOnClick; itemFile.DropDownItems.Add(itemPrint); itemPreview = new ToolStripMenuItem("Print Pre&view..."); itemPreview.Click += PreviewOnClick; itemFile.D ropDownltems.Add(itemP review); itemFile.DropDownItems.Add(new ToolStripSeparatorO); itemProps = new ToolStripMenuItemC'P&roperties"); itemProps.Click += PropertiesOnClick; itemFile.DropDownltems.Add(itemProps); itemFile.DropDownItems.Add(new ToolStripSeparatorO); item = new ToolStripMenuItem("E&xit"); item.Click += CloseOnClick; itemFile.DropDownItems.Add(item); return itemFile; } void FileOnDropDownOpening(object objSrc, EventArgs args) { BrowserChild bcActive = ActiveMdiChild as BrowserChild; itemSaveAs.Enabled = (bcActive != null); itemPrint.Enabled = (bcActive != null); itemPreview.Enabled = (bcActive != null); itemProps.Enabled = (bcActive != null); } void NewOnClick(object objSrc, EventArgs args) { BrowserChild bcActive = ActiveMdiChild as BrowserChild; string strUrl;
Два настоящих приложения 389 if (bcActive != null) strUrl = bcActive. WebBrowser. Url.ToStringO; else strUrl = settings.Home; Go(strUrl, false); } void OpenOnClick(object objSrc, EventArgs args) { OpenDialog dig = new OpenDialog(settings); if (dlg.ShowDialogO == DialogResult.OK) { Go(dlg.Url, true); } } void SaveAsOnClick(object objSrc, EventArgs args) { BrowserChild bcActive = ActiveMdiChild as BrowserChild; if (bcActive != null) bcActive.WebB rowse r.ShowSaveAsDialog(); } void PageSetupOnClick(object objSrc, EventArgs args) { BrowserChild bcActive = ActiveMdiChild as BrowserChild; if (bcActive != null) bcActive. WebBrowser. ShowPageSetupDialogO; } void PrintDialogOnClick(object objSrc, EventArgs args) { BrowserChild bcActive = ActiveMdiChild as BrowserChild; if (bcActive != null) bcActive. WebBrowser. ShowPrintDialogO; } void PreviewOnClick(object objSrc, EventArgs args) { BrowserChild bcActive = ActiveMdiChild as BrowserChild; if (bcActive != null) bcActive.WebBrowser.ShowPrintPreviewDialogO; } void PropertiesOnClick(object objSrc, EventArgs args)
390 ГЛАВА 7 { BrowserChild bcActive = ActiveMdiChild as BrowserChild; if (bcActive != null) bcActive. WebBrowser. ShowPropertiesDialogO; } void ExitOnClick(object objSrc, EventArgs args) { Close(); } } При активизации команды New в меню File в Internet Explorer создается новый экземпляр программы, в который загружается та же Web-страница, что и в исходном окне. Я решил сделать так же в MdiBrowser — должен создаваться новый Browser- Child с той же WejD-страницей, что и у активного потомка. Если активных потомков нет, обработчик события создает новый BroivserCbild и открывает в нем домашнюю страницу С командой Open придется потрудиться. Она сначала вызывает маленькое диалоговое окно, реализованное в следующем классе. OpenDialog.cs // // OpenDialog.cs (с) 2005 by Charles Petzold // From MdiBrowser Program // using System; using System.Drawing; using System.Windows.Forms; class OpenDialog : Form { ComboBox combo; Button btnOk; // Открытое свойство, public string Url { get { return combo.Text; } set { combo.Text = value; } } // Конструктор. public OpenDialog(MdiBrowserSettings settings) {
Два настоящих приложения 391 // Инициализация нескольких свойств. Text = "Open"; AutoSize = true; AutoSizeMode = AutoSizeMode.GrowAndShrink; FormBorderStyle = FormBorderStyle.FixedDialog; ControlBox = false; MinimizeBox = false; MaximizeBox = false; ShowInTaskbar = false; Icon = ActiveForm.Icon; StartPosition = FormStartPosition.Manual; Location = ActiveForm.Location + Systemlnformation.CaptionButtonSize + Systemlnformation.FrameBorderSize; // Создание структуры таблицы и расположение элементов управления. TableLayoutPanel table = new TableLayoutPanelO; table.Parent = this; table.AutoSize = true; table.Padding = new Padding(Font.Height); table.RowCount = 3; table.ColumnCount = 4; Label lbl = new LabelO; lbl.Text = "Enter a URL or filename, or\r\n" + "Select a previously visited site from the list, or\r\n" + "Press Browse to select a file."; lbl.AutoSize = true; table.Controls.Add(lbl, 1, 0); table.SetColumnSpan(lbl, 3); lbl = new LabelO; lbl.Text = "Open: "; lbl.AutoSize = true; lbl.Anchor = AnchorStyles.Left; table.Controls.Add(lbl, 0, 1); combo = new ComboBoxO; combo.AutoSize = true; combo.Anchor = AnchorStyles.Left | AnchorStyles.Right; combo.TextChanged += ComboBoxOnTextChanged; combo. BeginUpdateO; foreach (string str in settings.ManualUrls) combo.Items.Add(str); combo. EndUpdateO;
392 ГЛАВА 7 } table.Controls.Add(combo, 1, 1); table.SetColumnSpan(combo, 3); btnOk = new ButtonO; btnOk.Text = "OK"; btnOk.AutoSize = true; btnOk.DialogResult = DialogResult.OK; btnOk.Enabled = false; btnOk.Margin = new Padding(Font.Height); table.Controls.Add(btnOk, 1, 2); AcceptButton = btnOk; Button btn = new ButtonO; btn.Text = "Cancel"; btn.AutoSize = true; btn.DialogResult = DialogResult.Cancel; btn.Margin = new Padding(Font.Height); table.Controls.Add(btn, 2, 2); CancelButton = btn; btn = new ButtonO; btn.Text = "Browse..."; btn.AutoSize = true; btn.Margin = new Padding(Font.Height); btn.Click += BrowseOnClick; table.Controls.Add(btn, 3, 2); } void ComboBoxOnTextChanged(object objSrc, EventArgs args) { ComboBox combo = objSrc as ComboBox; btnOk.Enabled = combo.Text != null && combo.Text.Length > 0; } void BrowseOnClick(object objSrc, EventArgs args) { OpenFileDialog dig = new OpenFileDialogO; if (dlg.ShowDialogO == DialogResult.OK) { Url = dlg.FileName; } Этот элемент управления ComboBox содержит все URL-адреса из набора Manual- Urls объекта settings. Новые объекты добавляются в этот набор методом Go (из MdiBrowser.cs), когда второй аргумент равен true, то есть когда пользователь вво-
Два настоящих приложения 393 дит в это диалоговое окно URL-адрес. Это окно также поддерживает отображение стандартного окна OpenFileDialog, которое позволяет пользователю выбирать файл для загрузки в браузер. Меню View В меню View реализованы команды трех различных типов. Первые две позволяют пользователю отключить отображение панели инструментов и строки адреса. Обработчики Click просто меняют соответствующее свойство Visible и сохраняют значение в объекте settings. Следующий тип — команды Back, Forward, Stop, Refresh и Home, первый четыре из которых реализованы просто — они вызывают соответствующие методы WebBrowser. И, наконец, в меню View есть команда Source, которая позволяет просмотреть HTML-код страницы, загруженной в элемент управления WebBrowser. MdiBrowser.ViewMenu.cs // // MdiBrowser.ViewMenu.cs (с) 2005 by Charles Petzold // From MdiBrowser Program // using System; using System.Drawing; using System.Windows.Forms; partial class MdiBrowser : Form { ToolStripMenuItem itemBack, itemForward, itemStop; ToolStripMenuItem itemRefresh, itemHome, itemSource; ToolStripMenuItem ViewMenuQ { ToolStripMenuItem itemView = new ToolStripMenuItem("&View"); itemView.DropDownOpening += ViewOnDropDownOpening; ToolStripMenuItem item = new ToolStripMenuItem("&Tool Bar"); item.Checked = settings.ViewToolBar; item.CheckOnClick = true; item.Click += ViewToolBarOnClick; itemView.DropDownItems.Add(item); item = new ToolStripMenuItem("&Address Bar"); item.Checked = settings.ViewAddressBar; item.CheckOnClick = true; item.Click += ViewAddressBarOnClick; itemView.DropDownltems.Add(item);
394 ГЛАВА 7 itemView.DropDownltems.Add(new ToolStripSeparator()); itemBack = new ToolStripMenuItem("&Back"); itemBack.Click += BackOnClick; itemView.DropDownltems.Add(itemBack); itemForward = new ToolStripMenuItem("&Forward"); itemForward.Click += ForwardOnClick; itemView.DropDownltems.Add(itemForward); itemStop = new ToolStripMenuItem("Sto&p"); itemStop.Click += StopOnClick; itemView.DropDownltems.Add(itemStop); itemRefresh = new ToolStripMenuItem("&Refresh"); itemRefresh.Click += RefreshOnClick; itemView.DropDownItems.Add(itemRefresh); itemHome = new ToolStripMenuItem("&Home"); itemHome.Click += HomeOnClick; itemView.DropDownltems.Add(itemHome); itemView.D ropDownltems.Add(new ToolSt ripSepa rato r()); itemSource = new ToolStripMenuItem("Sour&ce"); itemSource.Click += ViewSourceOnClick; itemView.DropDownltems.Add(itemSource); return itemView; } // Обработчик, раскрывающий меню View, void ViewOnDropDownOpening(object objSrc, EventArgs args) { BrowserChild be = ActiveMdiChild as BrowserChild; bool bActiveChild = be != null; // Делаем недоступными все эти команды, если нет активного дочернего окна. itemBack.Enabled = bActiveChild; itemForward.Enabled = bActiveChild; itemStop.Enabled = bActiveChild; itemRefresh.Enabled = bActiveChild; itemHome.Enabled = bActiveChild; itemSource.Enabled = bActiveChild; // Если активное дочернее окно есть, // активизируем команды в соответствии со значениями свойств.
Два настоящих приложения 395 if (bActiveChild) { itemBack.Enabled = be.WebBrowser.CanGoBack; itemForward.Enabled = be.WebBrowser.CanGoForward; } } // Отображение или сокрытие панели инструментов и строки адреса, void ViewToolBarOnClick(object objSrc, EventArgs args) { ToolStripMenuItem item = objSrc as ToolStripMenuItem; tool.Visible = settings.ViewToolBar = item.Checked; } void ViewAddressBarOnClick(object objSrc, EventArgs args) { ToolStripMenuItem item = objSrc as ToolStripMenuItem; addr.Visible = settings.ViewAddressBar = item.Checked; } // Реализация команд Implement Back, Forward, Stop, Refresh и Home, void BackOnClick(object objSrc, EventArgs args) { BrowserChild be = ActiveMdiChild as BrowserChild; if (be != null) be.WebBrowser.GoBack(); } void ForwardOnClick(object objSrc, EventArgs args) { BrowserChild be = ActiveMdiChild as BrowserChild; if (be != null) be.WebB rowse r.GoFo rwa rd(); } void StopOnClick(object objSrc, EventArgs args) { BrowserChild be = ActiveMdiChild as BrowserChild; if (be != null) be.WebBrowser. StopO; } void RefreshOnClick(object objSrc, EventArgs args) { BrowserChild be = ActiveMdiChild as BrowserChild; if (be != null) be.WebB rowse r.Ref resh(); }
396 ГЛАВА 7 void HomeOnClick(object objSrc, EventArgs args) { BrowserChild be = ActiveMdiChild as BrowserChild; if (be != null) be.WebBrowser.Url = new Uri(settings.Home); > // Команда View Source меню, void ViewSourceOnClick(object objSrc, EventArgs args) { BrowserChild be = ActiveMdiChild as BrowserChild; if (be != null) { Form frm = new Form(); frm.Text = be.WebBrowser.DocumentTitle; TextBox txtbox = new TextBox(); txtbox.Parent = frm; txtbox.Multiline = true; txtbox.Wordwrap = false; txtbox.ScrollBars = ScrollBars.Both; txtbox.Dock = DockStyle.Fill; txtbox.Text = be.WebBrowser.DocumentText; txtbox.Select(0, 0); frm.Show(); } } } Меню Favorites Помимо очевидного перечисления элементов набора Favorites объекта settings, в меню Favorites содержится команда добавления в этот набор Web-страницы, открытой в активном дочернем окне. В отличие от Internet Explorer, эта команда добавляет страницу немедленно, не открывая диалоговое окно и не предоставляя возможности переименовать заголовок, поменять порядок или удалить все или отдельные избранные страницы. В этом же меню я разместил команду определения домашней страницы — выбирается Web-страница, открытая в активном дочернем окне. MdiBrowser.FavoritesMenu.cs // // MdiBrowser.FavoritesMenu.cs (с) 2005 by Charles Petzold // From MdiBrowser Program
Два настоящих приложения 397 // using System; using System.Drawing; using System.Windows.Forms; partial class MdiBrowser : Form { ToolStripMenuItem itemAdd, itemSetHome; ToolStripMenuItem FavoritesMenu() { ToolStripMenuItem itemFavorites = new ToolStripMenuItemC'F&avorites"); itemFavorites.DropDownOpening += FavoritesOnDropDownOpening; return itemFavorites; } void FavoritesOnDropDownOpening(object objSrc, EventArgs args) { ToolStripMenuItem itemFavorites = objSrc as ToolStripMenuItem; BrowserChild be = ActiveMdiChild as BrowserChild; // Удаляем все элементы из выпадающего меню. itemFavorites. DropDownltems.ClearO; itemAdd = new ToolStripMenuItem("&Add to favorites"); itemAdd.Enabled = (be != null); itemAdd.Click += AddOnClick; itemFavo rites.D ropDownltems.Add(itemAdd); itemSetHome = new ToolStripMenuItem("&Make this your home"); itemSetHome.Enabled = (be != null); itemSetHome.Click += SetHomeOnClick; itemFavo rites.D ropDownltems.Add(itemSetHome); itemFavorites.DropDownItems.Add(new ToolStripSeparatorO); // Добавляем в меню избранные страницы, foreach (Favorite fav in settings.Favorites) { ToolStripMenuItem item = new ToolStripMenuItem(); item.Text = fav.Title; item.Tag = fav.Url; item.Click += FavoriteOnClick; itemFavorites.DropDownltems.Add(item); } }
398 ГЛАВА 7 void AddOnClick(object objSrc, EventArgs args) { BrowserChild be = ActiveMdiChild as BrowserChild; if (be != null) { Favorite fav = new Favorite(bc.WebBrowser.DocumentTitle, be. WebBrowser. U rl.ToStringO); settings.Favorites.Add(fav); settings.Favorites.Sort(); settings.Save(st rAppData); } } void SetHomeOnClick(object objSrc, EventArgs args) { BrowserChild be = ActiveMdiChild as BrowserChild; if (be != null) { settings. Home = bc.WebBrowser.Url.ToStringO; } } void FavoriteOnClick(object objSrc, EventArgs args) { ToolStripMenuItem item = objSrc as ToolStripMenuItem; Go((string)item.Tag, false); } } Ожидается, что метод FavoritesMenu создает меню Favorites, но его деятельность ограничивается созданием элемента меню верхнего уровня и установкой обработчика его события DropDownOpening. При каждом вызове обработчик обновляет все раскрывающиеся меню. Каждому элементу избранного соответствует свой обработчик FavoriteOnClick события Click; URL-адрес он получает из свойства Tag. Меню Window Метод WindowMenu создает меню Window, обычно присутствующее во всех приложениях с MDI-интерфейсом. Я думаю второй оператор метода WindowMenu наиболее важный: menu.MdiWindowListltem = itemWindow; Определение свойства MdiWindowListltem элемента управления MenuStrip говорит о том, что это меню, где должны перечисляться дочерние окна, поэтому эту часть работы над меню Window можно считать законченной.
Два настоящих приложения 399 Первые четыре команды меню — Cascade, Tile Horizontal, Tile Vertical и Arrange Icons — соответствуют четырем членам перечисления MdiLayout. Я задействовал один обработчик событий для всех четырех команд, приравняв свойство Tag команды меню соответствующему члену перечисления. MdiBrowser.WindowMenu.cs // // MdiBrowser.WindowMenu.cs (с) 2005 by Charles Petzold // From MdiBrowser Program // using System; using System.Drawing; using System.Windows.Forms; partial class MdiBrowser : Form { ToolStripMenuItem WindowMenuQ { ToolStripMenuItem itemWindow = new ToolStripMenuItem("&Window"); menu.MdiWindowListltem = itemWindow; ToolStripMenuItem item = new ToolStripMenuItem("&Cascade"); item.Tag = MdiLayout.Cascade; item.Click += WindowArrangeOnClick; itemWindow.DropDownltems.Add(item); item = new ToolStripMenuItem("Tile &Horizontal"); item.Tag = MdiLayout.TileHorizontal; item.Click += WindowArrangeOnClick; itemWindow.DropDownltems.Add(item); item = new ToolStripMenuItem("Tile &Vertical"); item.Tag = MdiLayout.TileVertical; item.Click += WindowArrangeOnClick; itemWindow.DropDownltems.Add(item); item = new ToolStripMenuItem("&Arrange Icons"); item.Tag = MdiLayout.Arrangelcons; item.Click += WindowArrangeOnClick; itemWindow.DropDownltems.Add(item); itemWindow. DropDownItems.Add(new ToolStripSeparatorO); item = new ToolStripMenuItem("C&lose All Windows"); item.Click += CloseAllOnClick; itemWindow.DropDownltems.Add(item);
400 ГЛАВА 7 itemWindow.DropDownItems.Add(new ToolStripSeparatorO); return itemWindow; } // Два небольших обработчика Click, void WindowArrangeOnClick(object objSrc, EventArgs args) { ToolStripMenuItem item = objSrc as ToolStripMenuItem; LayoutMdi((Mdil_ayout)item.Tag); } void CloseA110nClick(object objSrc, EventArgs args) { while (MdiChildren.Length > 0) MdiChildren[0].Close(); } } В меню Window также есть команда закрытия всех окон, Close All Windows, которая оказывается очень кстати, когда нужно удалить все дочерние окна и начать работу с чистого листа. Меню Help Меню Help содержит две команды: Help и About. По событию Click команды Help обработчик просто вызывает статический метод Help.ShowHelp, указывая файл Mdi- Browser.chm. В завершение этой главы, я покажу, как создать такой файл, разместить его в той же папке, что и файл MdiBrowser.exe и опубликовать на Web-сайте вместе с MdiBrowser.exe. MdiBrowser.HelpMenu.cs // // MdiBrowser.HelpMenu.cs (с) 2005 by Charles Petzold // From MdiBrowser Program // using System; using System.Drawing; using System.Windows.Forms; partial class MdiBrowser : Form { ToolStripMenuItem HelpMenuO { ToolStripMenuItem itemHelp = new ToolStripMenuItem("&Help");
Два настоящих приложения 401 ToolStripMenuItem item = new ToolStripMenuItem("&Help"); item.Click += HelpOnClick; itemHelp.DropDownltems.Add(item); item = new ToolStripMenuItem("&About MDI Browser..."); item.Click += AboutOnClick; itemHelp.DropDownltems.Add(item); return itemHelp; } void HelpOnClick(object objSrc, EventArgs args) { Help.ShowHelp(this, "MdiBrowser.chm"); } void AboutOnClick(object objSrc, EventArgs args) { AboutDialog2 dig = new AboutDialog2("MdiBrowser"); dlg.ShowDialog(); } } Я хотел, чтобы в диалоговом окне About приложения MdiBrowser отображалось свойство Version элемента управления WebBrowser. Проект MdiBrowser ссылается на файл AboutDialog.cs (файл и BookCover.png) из проекта ControlExplorer, а также включает класс AboutDialog2, который является потомком AboutDialog. AboutDialog2.cs // // AboutDialog2.cs (с) 2005 by Charles Petzold // From MdiBrowser Program // using System; using System.Drawing; using System.Windows.Forms; class AboutDialog2 : AboutDialog { public AboutDialog2(string strResource): base(strResource) { // Создание элемента-метки Label. Label lbl = new Label(); lbl.Parent = flow; lbl.Font = new Font(FontFamily.GenericSerif, 14); lbl.AutoSize = true; lbl.Anchor = AnchorStyles.None;
402 ГЛАВА 7 lbl.Text = "Using Microsoft Internet Explorer " + new WebBrowser().Version.ToString(); // Перемещение кнопки OK в конец набора элементов управления. btnOk.SendToBackO; } } AboutDialog2 создает метку со свойством Version. Поскольку конструктор из AboutBox2 выполняется после конструктора из AboutBox, эта новая метка обычно оказывается внизу окна, под кнопкой ОК, но один вызов метода SendToBack возвращает эту кнопку в конец набора Controls, где ей и место. Автор остальных прекрасных преобразований — этот чудесный элемент управления FlowLayoutPanel Два элемента управления ToolStrip В MdiBrowser два элемента управления ToolStrip: в верхнем (его обычно называют панелью инструментов) есть кнопки Back, Forward, Stop, Refresh, Home и Print, a нижний в исходном тексте и справочных файлах называется строкой адреса. Значки для кнопок панели инструментов я взял из подпапки icons\WinXP библиотеки значков Visual Studio (\Program Files\Microsoft Visual Studio 8\Common7\VS2005- ImageLibraryizip) и отметил их как внедренные ресурсы (Embedded Resource). (К сожалению, в Visual C# 2005 Express Edition этой библиотеки нет.) Не все выбранные значки годятся для иллюстрации выполняемых кнопками функций. Чтобы создать кнопку Forward, код метода CreateToolBar зеркально отражает значок кнопки Back. При работе над настоящими приложениями для создания значков и других графических элементов все-таки лучше прибегать к услугам профессиональных технических дизайнеров. Файлы значков обычно содержат изображения нескольких размеров и форматов. Среди поставляемых в составе Visual Studio, значков различных размеров особенно много. При создании ImageList из объекта Icon копируется изображение только одного размера. Программист сам задает нужный размер, задавая свойство ImageSize объекта ImageList. В следующем коде этому свойству присваивается значение, полученное от Systemlnformation.IconSize — в большинстве систем это квадрат размером 32x32 пиксела. MdiBrowser.ToolBar.cs // // MdiBrowser.ToolBar.cs (с) 2005 by Charles Petzold // From MdiBrowser Program // using System;
Два настоящих приложения 403 using System.Drawing; using System.Windows.Forms; partial class MdiBrowser { ToolStripButton btnBack, btnForward; ToolStripButton btnStop, btnRefresh, btnHome, btnPrint; BrowserChild bcLastActive; ToolStrip CreateToolBar(string strResource) { // ImageList для ToolStrip. ImageList imglst = new ImageListO; imglst.ImageSize = Systemlnformation.IconSize; imglst.Images.AddC'back" imglst.Images.Add("stop" imglst.Images.Add("rfsh" imglst.Images.AddC'home" imglst.Images.Add("prnt" new Icon(GetType() new Icon(GetType() new Icon(GetType() new Icon(GetType() new Icon(GetType() // 32x32, probably. "MdiBrowser.hotplug.ico")) "MdiBrowser.error.ico")); "MdiBrowser.idr_dll.ico")) "MdiBrowser.homenet.ico")) "MdiBrowser.printer.ico")) // Создание значка Forward из значка Back. Image img = imglst.Images["back"]; img.RotateFlip(RotateFlipType.RotateNoneFlipX); imglst.Images.Add("frwd", img); // Элемент управления ToolStrip с кнопками. ToolStrip tool = new ToolStripO; tool.Parent = this; tool.Visible = settings.ViewToolBar; tool. ImageList = imglst; tool.ImageScalingSize = imglst.ImageSize; // Обработчики событий этих пяти кнопок находятся в файле MdiBrowser.ViewMenu.es. btnBack = new ToolStripButtonO; btnBack.Text = "Back"; btnBack.ImageKey = "back"; btnBack.Click += BackOnClick; tool.Items.Add(btnBack); btnForward = new ToolStripButtonO; btnForward.Text = "Forward"; btnForward.ImageKey = "frwd"; btnForward.Click += ForwardOnClick; tool. Items.Add(btnForward);
404 ГЛАВА 7 btnStop = new ToolStripButton(); btnStop.ImageKey = "stop"; btnStop.ToolTipText = "Stop"; btnStop.Click += StopOnClick; tool.Items.Add(btnStop); btnRefresh = new ToolStripButtonO; btnRefresh.ImageKey = "rfsh"; btnRefresh.ToolTipText = "Refresh"; btnRefresh.Click += RefreshOnClick; tool.Items.Add(btnRefresh); btnHome = new ToolStripButtonO; btnHome.ImageKey = "home"; btnHome.ToolTipText = "Home"; btnHome.Click += HomeOnClick; tool.Items.Add(btnHome); tool.Items.Add(new ToolStripSeparatorO); btnPrint = new ToolStripButtonO; btnPrint.ImageKey = "prnt"; btnPrint.ToolTipText = "Printer"; btnPrint.Click += PrintOnClick; tool.Items.Add(btnPrint); return tool; } // При смене активного MDI-потомка обновляем обработчики событий и ToolStrip. protected override void OnMdiChildActivate(EventArgs args) { base.OnMdiChildActivate(args); if (bcLastActive != null) { bcLastActive.WebBrowser.CanGoBackChanged -= OnCanGoBackChanged; bcLastActive.WebBrowser.CanGoForwardChanged -= OnCanGoForwardChanged; } BrowserChild be = ActiveMdiChild as BrowserChild; if (be != null) { be.WebBrowser.CanGoBackChanged += OnCanGoBackChanged; be.WebBrowser.CanGoForwardChanged += OnCanGoForwardChanged;
Два настоящих приложения 405 } btnBack.Enabled = be.WebBrowser.CanGoBack; btnForward.Enabled = be.WebBrowser.CanGoForward; } else { btnBack.Enabled = false; btnForward.Enabled = false; } btnStop.Enabled = (be != null); btnRefresh.Enabled = (be != null); btnHome.Enabled = (be != null); btnPrint.Enabled = (be != null); bcLastActive = be; } // Обработчики событий, обеспечивающие работу кнопок Back и Forward, void OnCanGoBackChanged(object objSrc, EventArgs args) { WebBrowser wb = objSrc as WebBrowser; btnBack.Enabled = wb.CanGoBack; } void OnCanGoForwardChanged(object objSrc, EventArgs args) { WebBrowser wb = objSrc as WebBrowser; btnForward.Enabled = wb.CanGoForward; } void PrintOnClick(object objSrc, EventArgs args) { BrowserChild bcActive = ActiveMdiChild as BrowserChild; if (bcActive != null) bcActive.WebBrowser.Print(); } Большинство обработчиков событий Click этих кнопок описаны в файле Mdi- Browser.ViewMenu.cs, однако кнопка Print — особый случай. Команда Print меню File открывает диалоговое окно вывода печать, вызывая метод ShowPrintDialog элемента управления WebBrowser, а должна просто выводить текущую страницу на печать и не открывать никаких диалоговых окон. Поэтому у нее собственный обработчик события Click, вызывающий метод Print элемента управления WebBrowser. Чтобы правильно приводить в активное/неактивное состояние эти кнопки, программе нужно иметь информацию об активном дочернем окне. Вот зачем нужен файл, где определен метод, переопределяющий OnMdiChildActivate. Этот метод
406 ГЛАВА 7 вызывается при любой смене активного дочернего окна. Кнопки Back и Forward должны активизироваться, только если свойства CanGoBack и CanGoForward активного дочернего окна равны true, а остальные кнопки должны становиться неактивными при полном отсутствии активного дочернего окна. Такое переключение между активным и неактивным состояниями похоже на логику работы меню View, но оно происходит при смене активных дочерних окон, а не при отображении меню View. Метод OnMdiChildActivate должен прикрепить обработчики событий OnCanGo- BackChanged и OnCanGoForwardChanged к активному дочернему окну, чтобы обеспечить смену состояний в процессе работы пользователя в этом окне. При этом OnMdiChildActivate должен отсоединить обработчики от дочернего окна, которое было активным до этого. Отсоединение обработчиков событий происходит в программах намного реже, чем подключение, но в данном случае без этого не обойтись. Строка адреса по большей части дублирует функции диалогового окна Open из меню File и содержит всего три элемента управления: метку, поле со списком и кнопку Go. MdiBrowse.AddrBar.cs // // MdiBrowser.AddrBar.cs (с) 2005 by Charles Petzold // From MdiBrowser Program // using System; using System.Drawing; using System.Windows.Forms; partial class MdiBrowser { ToolStripComboBox comboUrl; ToolStrip CreateAddressBar(string strResource) { // Поле Address для ввода URL-адреса. ToolStrip addr = new ToolStripO; addr.Parent = this; addr.GripStyle = ToolStripGripStyle.Hidden; addr.Visible = settings.ViewAddressBar; addr.SizeChanged += AddressBarOnSizeChanged; ToolStripLabel lbl = new ToolStripLabel("Address:"); addr.Items.Add(lbl); // Поле со списком, где можно ввести или выбрать URL-адрес. comboUrl = new ToolStripComboBoxO; comboUrl.AutoSize = false;
Два настоящих приложения 407 } comboUrl.BeginUpdate(); foreach (string str in settings.ManualUrls) comboUrl.Items.Add(str); comboUrl.EndUpdate(); addr.Items.Add(comboUrl); ToolStripButton btn = new ToolStripButtonQ; btn.Text = "Go" btn.Alignment = ToolStripItemAlignment.Right; btn.Click += GoOnClick; addr.Items.Add(btn); // Инициация размера поля со списком Combobox. AddressBarOnSizeChanged(addr, EventArgs.Empty); return addr; } // Обработчик AddressBarOnSizeChanged. void AddressBarOnSizeChanged(object objSrc, EventArgs args) { ToolStrip tool = objSrc as ToolStrip; tool.Items[1].Width = tool.Width - tool.Items[0].Width - tool.Items[2].Width - 6 * tool.Items[1].Margin.Horizontal; } // Обработчик событий кнопки Go в строке адреса, void GoOnClick(object objSrc, EventArgs args) { if (comboUrl.Text != null && comboUrl.Text.Length > 0) Go(comboUrl.Text, true); } Я хотел, чтобы, как и в Internet Explorer, поле со списком занимало всю ширину строки адреса, но я не нашел другого способа как установить обработчик события, который уведомляется об изменении размера строки адреса и пересчитывает размер поля со списком. Справочная система в формате HTML Help В идеальном мире приложения настолько понятные и очевидные, что вообще не нуждаются в справочной системе. В более-менее терпимом мире справочную систему создают до кодирования приложения — в этом случае проектирование и кодирование проходят без сучка и задоринки по готовым инструкциям.
408 ГЛАВА 7 Увы, но в безумном мире, где приходится жить нам с вами, файл справочной системы часто стряпают на скорую руку в самом конце проекта. Должен покаяться: затолкав посвященный справке раздел в самый конец последней главы книги, я тоже не способствую исправлению ситуации. Современная справочная система для Windows называется HTML Help, так как в ее основе лежит набор HTML-файлов. (Предыдущий стандарт справочной системы назывался WinHelp и в нем использовались RTF-файлы.) Она создается программой HTML Help Workshop (или аналогом), которая на основе проектного файла с расширением .hhp, собирает в единый скомпилированный двоичный файл с расширением .chm следующие ресурсы: файл оглавления (расширение .hhc), файл предметного указателя (.hhk), файлы разделов (.htm или html) и графические файлы (.Ьтр, png, .gif'или jpeg). Как правило, СНМ-файл устанавливается на машине пользователя в ту же папку, что и исполняемый файл приложения. Как вы уже видели, программа Windows Forms отображает файл справки с помощью статических методов ShowHelp класса Help. HTML Help Workshop можно скачать с Web-сайта MSDN: откройте сайт http:// msdn.microsoft.com, а затем перейдите по ссылке Library. В левой панели последовательно раскройте узлы Win32 and COM Development и Tools и щелкните узел HTML Help. В правой панели перейдите по ссылке на страницу HTML Help SDK. В новой странице выберите ссылку перехода на страницу загрузки и загрузите файлы HtmlHelp.exe (это установщик программы HTML Help Workshop) и HelpDocs.zip (несколько справочных файлов в формате HTML). Наиболее полезные из содержащихся в HelpDocs.zip файлов — htmlhelp.chm (документация по HTML Help Workshop) и htmlref.chm (удобный справочник по HTML-тэгам). Вот как я использовал HTML Help Workshop для создания справочного файла программы MdiBrowser: в меню File я последовательно выбрал New и Project и затем создал подпапку Help в папке решения MdiBrowser (Help расположился на одном уровне с каталогом проекта MdiBrowser). Я дал проектному файлу имя MdiBrowser- .hhp. По умолчанию HTML Help Workshop создает скомпилированный файл по имени MdiBrowser.chm. Щелкнув самую верхнюю кнопку в левой панели HTML Help Workshop, можно задать другое имя файла, а также изменить другие параметры проекта. Несколько раз, используя последовательность команд File/New/HTML File, я создал 10 файлов. При создании каждый файл получал заголовок (Title), который размещался в коде HTML-файла. К каждому новому файлу я применял команду Save File As из меню File и сохранял их под именами, которые указаны в таблице.
Два настоящих приложения 409 Заголовок страницы Имя файла Описание Welcome to MDI Browser welcome.html Program Overview overview.html The Multiple Document Interface mdi.html The File Menu file.html The View Menu view.html The Favorites Menu favorites.html The Window Menu window.html The Help Menu help.html The Tool Bar toolbar.html The Address Bar addrbar.html Вводная страница справочной системы Общая инфомация о программе Описание многодокументного интерфейса приложения Описание меню File Описание меню View Описание меню Favorites Описание меню Window Описание меню Help Описание меню Tool Описание меню Address Далее я щелкнул кнопку Add/Remove Topic Files, а затем — кнопку Add, чтобы добавить все 10 HTML-файлов в проект. Они указываются в разделе [FILES] проектного ННР-файла. Следующий шаг — создание оглавления с использованием вкладки Contents. По умолчанию оглавление сохраняется в файле Table ofContentshhc. Оглавление создается с использованием кнопок на левой панели инструментов — они позволяют создавать новые записи оглавления и связывать их со страницами. Страница обозначается в оглавлении небольшим значком страницы и всегда связана с HTML- файлом раздела. Раздел с несколькими страницами и/или другими разделами отмечается значком папки. Я сначала создал раздел (Heading) и назвал его Welcome to MDI Browser. Щелкнув кнопку Add, я сопоставил разделу файл Welcome to MDI Browser (welcome.html). В раздел я добавил страницу с заголовком Program Overview и, щелкнув Add, связал ее с одноименным файлом. Так же я добавил страницу The Multiple Document Interface. Затем я создал второй раздел, Exploring the Menu, но не связал его с файлом. HTML Help Workshop создал этот раздел как потомок раздела Welcome to MDI Browser, но ситуация легко поправляется щелчком кнопки со стрелкой влево. В этом разделе я создал 5 страниц с описанием команд меню. Я добавил еще один раздел верхнего уровня, Exploring the Tool Bars, и разместил в нем две оставшихся страницы. Чтобы изменить параметры проекта, нужно перейти на вкладку Project и щелкнуть самую верхнюю кнопку. Я назначил проекту заголовок MDI Browser Help и welcome.html в качестве файла, открываемого по умолчанию. На вкладке Compiler я установил флажок Compile full-text search Information, чтобы обеспечить возможность поиска по всей справочной системе. Теперь возможно скомпилировать проект, выбрав команду Compile в меню File или щелкнув кнопку Compile. В файле журнала фиксируется ход процесса и воз-
410 ГЛАВА 7 можные неполадки. После компиляции можно посмотреть результат, выбрав в меню View команду Compiled File или щелкнув кнопку View Compiled File (с картинкой очков). Оглавление отображается без проблем, но ни в одной странице нет текста. Здесь, собственно, и начинается главная работа по редактированию HTML- кода, но это, увы, не предмет этой книги. Можно также создать файл предметного указателя, но я посчитал, что для такой куцей справочной системы это будет лишним. Ясно, что в больших справочных системах без предметного указателя не обойтись. Подготовив СНМ-файл, можно возвратиться к Visual Studio и присоединить файл к проекту. В меню Project выберите Add Existing Item. В папке Help выберите СНМ-файл и щелкните Add As Link. Откройте панель свойств СНМ-файла. В свойстве Copy To Output File выберите Copy if newer. Это означает, что при сборке проекта Visual Studio скопирует СНМ-файл в ту же папку, что и исполняемый файл, поэтому файл справочной системы будет легко загружать, указав лишь имя файла. Build Action — еще одно свойство, значение которого часто изменяют на Content. Теперь если на странице свойств проекта перейти на вкладку Publish и щелкнуть кнопку Application Files, вы увидите, что СНМ-файл указан вместе с исполняемым файлом в списке файлов для публикации на Web-сайте. После публикации файлы доступны для загрузки пользователями. Да будут ваши пользователи многочисленными, справочные системы — информативными, а ошибки — нечастыми!
Петцольд Чарльз Программирование с использованием Microsoft® Windows® Forms Мастер-класс Перевод с английского под общей редакцией А. Р. Врублевского Главный редактор А. И. Козлов Совместный проект издательства «Русская Редакция» и издательства «Питер» М. РУССКАЯ РЕДАКЦИЯ С#пптер« Подписано в печать 22.06.06. Формат 70x100/16.Усл. п. л. 34,83. Тираж 3000 экз. Заказ №2051. ООО «Питер Пресс», 198206, Санкт-Петербург, Петергофское шоссе, д. 73, лит. А29. Налоговая льгота — общероссийский классификатор продукции ОК 005-93, том 2; 953005 — литература учебная. Отпечатано с диапозитивов в ОАО «Печатный двор» им. А. М. Горького. 197110, Санкт-Петербург, Чкаловский пр., 15.
?3*#S5 Официальные учебные пособия Microsoft гарантия Вашей квалификации! ||111111|1111Ш11111111|щ111!11^ШШШШШШ| iiiiliiii щ|р:л>.*»; Необходимы для самостоятельного освоения программного обеспечения Microsoft Обязательны для учебных центров IT Academy Незаменимы при подготовке к сертификационным экзаменам Microsoft Microsoft www.microsoft.com Щ.ШСШ П1ЩП £^ППТЕР' www.rusedit.com www.piter.com
MICROSOFT PRESS — ДЛЯ ПРОФЕССИОНАЛОВ Стив Макноииелл Совершенный код. Мастер-класс, see стр., 2005 г. Каков бы ни был ваш профессиональный уровень, с какими бы средствами разработками вы ни работали, какова бы ни была сложность вашего проекта, в этой книге вы найдете нужную информацию, она заставит вас размышлять и поможет создать совершенный код. «Это исчерпывающее исследование тактических аспектов создания хорошо спроектированных программ. Книга Макконнелла охватывает такие разные темы, как архитектура, стандарты кодирования, тестирование, интеграция и суть разработки ПО». (Гради Буч, автор книги «Object Solutions*) Джеффри Рихтер Программирование на платформе Microsoft „NET Framework* Мастер-класс, э* изд., si2 стр., гоов г. В книге подробно описано внутреннее устройство и функционирование общеязыковой исполняющей среды (CLR) Microsoft .NET Framework. Подробно изложена развитая система типов .NET Framework и разъясняются способы управления типами исполняющей среды. Книга ориентирована на разработчиков любых видов приложений на платформе ,НЕТ Framework: Windows Forms, Web Forms, Web-сервисов, консольных приложений, служб и пр. Шеферд Джордж Программирование на Microsoft Visual C++ .NET. МаСТер-КЛаСС. 2-е издание. 928 отр„ 2005 г, Эта книга — настоящая «библия» программирования в среде Microsoft Visual C++ ,NET с применением библиотеки классов MFC. Она адресована профессионалам, владеющим языком C++ и приступающим к разработке как «классических» 32-разрядных приложений Windows, так и приложений для новой среды ,NET, где используются возможности Visual C++ и Microsoft .NET Framework, Руссинович М. и Соломон Д. Внутреннее устройство Microsoft Windows; Windows Server 2003, Windows XP и Windows 2000. Мастер-класс. 992 стр., 2005 r. Книга посвящена внугреннему устройству и алгоритмам работы основных компонентов ОС Microsoft Windows; Windows Server 2003, Windows XP и Windows 2000 - и файловой системы NTFS, Детально рассмотрены системные механизмы: диспетчеризация ловушек и прерываний, DPC, АРС, LPC, RPC, синхронизация, системные рабочие потоки, глобальные флаги и др. Описываются все этапы загрузки операционной системы и завершения ее работы, рассматриваются детали реализации поддержки аппаратных платформ AMD х64 и Intel IA64. Microsoft' www.microsoft.com Н.РКСШ РЕДАКЦИЯ №ППТЕР* www.rusedit.com www.piter.com
|М| [л 1 ■ ш ■ЯШ 'vw-^w ^^^^J^, В: fiM^rtii magazine P>i:«t*»P<4i;«-n»x «««. л *h . !WhtfXW4*FJcW ?v,' »v.i;»i/ir'Vi'v. j 4. >,*. - л. 1 fis* л»зд&*и* J г*ь'л J*a i w*w« 1 i*juffl2»0|wr»3|tfkilr«»c i t-W*«J«W»^*K>»WW«** jfttrttyEW* v ' .• , TechMm РУООКАЯ ПГЛАК14ИЯ ARCHITECTS JOURNAL Русская Редакция SQZ Server www.ITbook.ru . " .^v.. _. о "Ч1Т А&чиниаприроьинын ияпеи Windows и UnttX ■■■1 Подбери г« дли своих Ш7л<*0и>а~лрила«енми г-* * ~"\: ~~ JOURNALS fir PI10FEJ
КЛУБ П Р(0 /\Ж С С И О Е А Л Основанный Издательским домом «Питер» в 1997 году, книжный клуб «Профессионал» собирает в своих рядах знатоков своего дела, которых объединяет тяга к знаниям и любовь к книгам. Для членов клуба проводятся различные мероприятия и, разумеется, предусмотрены привилегии. Привилегии для членов клуба: • карта члена «Клуба Профессионал»; • бесплатное получение клубного издания - журнала «Клуб Профессионал»; • дисконтная скидка на всю приобретаемую литературу в размере 10% или 15%; • бесплатная курьерская доставка заказов по Москве и Санкт-Петербургу; • участие во всех акциях Издательского дома «Питер» в розничной сети на льготных условиях. Как вступить в клуб? Для вступления в «Клуб Профессионал» вам необходимо: • совершить покупку на сайте www.piter.com или в фирменном магазине Издательского дома «Питер» на сумму от 800 рублей без учета почтовых расходов или стоимости курьерской доставки; • ознакомиться с условиями получения карты и сохранения скидок; • выразить свое согласие вступить в дисконтный клуб, отправив письмо на адрес: postbook@piter.com; • заполнить анкету члена клуба (зарегистрированным на нашем сайте этого делать не надо). Правила для членов «Клуба Профессионал»: • для продления членства в клубе и получения скидки 10%, в течение каждых шести месяцев нужно совершать покупки на общую сумму 800 до 1500 рублей, без учета почтовых расходов или стоимости курьерской доставки; • Если же за указанный период вы выкупите товара на сумму от 1501 рублей, скидка будет увеличена до 15% от розничной цены издательства. Заказать наши книги вы можете любым удобным для вас способом: • по телефону: (812) 703-73-74; • по электронной почте: postbook@piter.com; • на нашем сайте: www.piter.com; • по почте: 197198, Санкт-Петербург, а/я 619 ЗАО «Питер Пост». При оформлении заказа укажите: • ваш регистрационный номер (если вы являетесь членом клуба), фамилию, имя, отчество, телефон, факс, e-mail; • почтовый индекс, регион, район, населенный пункт, улицу, дом, корпус, квартиру; • название книги, автора, количество заказываемых экземпляров. пзалтЕпьскпй пом I® WWW.PITER.COM
Программирование с использованием Microsoft* Windows Forms Новые возможности технологии Windows Forms Практическое руководство по программированию от Чарльза Петцольда — признанного гуру программирования для Windows И опытные программисты, и новички найдут в этой книге массу полезных сведений о программировании с использованием Windows Forms на базе Microsoft .NET Framework 2.0, познакомятся с принципами и методами использования возможностей Windows Forms, проиллюстрированными множеством примеров программ на языке С#. Основная тематика книги: • анализ архитектуры программ Windows Forms; • создание стандартных элементов управления Windows, в том числе кнопок, полос прокрутки и полей ввода; • создание панелей инструментов, меню и строк состояния; • совершенствование и комбинирование существующих, а также создание совершенно новых нестандартных элементов управления; • применение динамического размещения с помощью панели FlowLayoutPanel; • поддержка строк и столбцов с абсолютными, пропорциональными или автоматически определяемыми размерами за счет использования элемента управления TableLayoutPanel; • привязка элементов управления к источникам данных; • отображение табличных данных с помощью нового элемента управления DataGridView; • создание с нуля, сборка и развертывание двух полнофункциональных приложений Windows Forms. Об авторе Чарльз Петцольд (Charles Petzold) — признанный авторитет в сообществе разработчиков программного обеспечения, автор самой первой журнальной статьи о программировании для Windows, а также книги «Windows Programming» — классического руководства по программированию для Windows, все издания которого (а недавно вышло шестое) пользуются неизменным успехом у читателей. Его перу также принадлежат популярные книги «Программирование в тональности С#» и «Код». В 1994 году Чарльз Петцольд был удостоен учрежденной основателем Microsoft Биллом Гейтсом и журналом Windows Magazine награды Windows Pioneer Award. Издательский дом Питер Санкт-Петербург Б. Сампсониевский пр., 29а E-mail: sales@piter.com Internet: www.piter.com Тел./факс: (812) 703-7383 Издательство Русская Редакция Москва Шелепихинская наб., 32 E-mail: info@rusedit.com Internet: www.rusedit.com Тел./факс: (495) 256-7145 ISBN 5-91180-041-1 ISBN 5-7502-0284-4 9"7859 1 1 "8004 1 3' 9»785750"202843И Microsoft