Вы находитесь на странице: 1из 415

Аннотация

В книге рассмотрены приемы визуального проектирования автономных приложений на языке


программирования C# с использованием инструментальных средств Microsoft Visual Studio .NET.
Рассмотрены принципы функционирования приложений Windows, управляемых событиями, на
платформе Microsoft .NET Framework. Приведена методика использования Microsoft Visual Studio .NET
для создания и отладки проектов приложений Windows на языке C#.
Приведена информация, необходимая для быстрого создания деловых приложений со сложным
пользовательским интерфейсом, интегрированных с базами данных. Описаны приемы проектирования
диалоговых окон, элементов управления и других компонентов. Имеется большое количество примеров
исходных программ, снабженных необходимыми комментариями.
Авторы: А.В. Фролов, Г.В. Фролов

Книга вышла в издательстве Кудиц-Образ

Оглавление
 Предисловие
 Глава 1. Принципы работы приложений Windows
 Глава 2. Работа с Microsoft Visual Studio .NET
 Глава 3. Формы в приложениях C#
 Глава 4. Создание редактора текста
 Глава 5. Диалоговые окна
 Глава 6. Использование элементов управления
 Глава 7. Многооконный пользовательский интерфейс
 Глава 8. Приложения с базами данных
 Глава 9. Интеграция с MS SQL Server
 Глава 10. Графический интерфейс GDI+
 Приложение 1. Исходные тексты приложения SimpleNotepad
 Библиографический список

Визуальное проектирование
приложений C#
А.В. Фролов, Г.В. Фролов

Аннотация
В книге рассмотрены приемы визуального проектирования автономных приложений на
языке программирования C# с использованием инструментальных средств Microsoft
Visual Studio .NET. Рассмотрены принципы функционирования приложений Windows,
управляемых событиями, на платформе Microsoft .NET Framework. Приведена методика
использования Microsoft Visual Studio .NET для создания и отладки проектов
приложений Windows на языке C#.
Приведена информация, необходимая для быстрого создания деловых
приложений со сложным пользовательским интерфейсом, интегрированных с базами
данных. Описаны приемы проектирования диалоговых окон, элементов управления и
других компонентов. Имеется большое количество примеров исходных программ,
снабженных необходимыми комментариями.
Предисловие
Создавая сложные и очень сложные приложения, профессиональные программисты
часто используют специальные средства ускоренной разработки приложений (Rapidly
Application Development, RAD). Такие средства позволяют значительно сократить сроки
разработки и отладки, так что с их использованием приложение, возможно, будет
создано еще до того, как в нем отпадет необходимость.
Средства RAD широко используются для проектирования таких информационных
систем, как бухгалтерские системы и системы управления хозяйственной
деятельностью предприятий, справочно-информационные системы с базами данных,
управляющие системы и т.п. Кроме того, в связи с переносом бизнеса в Интернет,
сегодня возросла актуальность применения средств RAD и для создания Web-
приложений, функционирующих в Интернете и в интрасетях компаний.
За счет чего средства RAD ускоряют разработку приложений?
Во многом это происходит благодаря применению графических средств
проектирования пользовательского интерфейса.
Напомним, что первое время приложения для операционной системы (ОС)
Microsoft Windows создавались при помощи инструментов, предполагающих
использование обычных редакторов текста, дополненных средствами синтаксического
выделения конструкций языка, средствами отладки и справочно-информационными
системами.
Справедливости ради отметим, что в таких инструментальных средствах, как
Microsoft Visual C++, Borland C++ и аналогичных были предусмотрены так называемые
редакторы ресурсов. Механизм ресурсов приложений Microsoft Windows был разработан
для того, чтобы отделить элементы пользовательского интерфейса (диалоговые окна,
кнопки, списки и т.д.) от программного кода. При этом описание элементов интерфейса
хранится внутри исполнимого файла приложения и поддается редактированию без
перетрансляции исходных текстов самого приложения.
Формально Microsoft Visual C++ в комплекте с библиотекой классов Microsoft
Foundation Classes (MFC) может выступать в качестве системы визуального
проектирования приложений. Однако на деле создание с его помощью программ,
имеющих по-настоящему сложный пользовательский интерфейс, совсем не просто.
К сожалению, внешний вид компонентов пользовательского интерфейса,
хранящихся в ресурсах обычных приложений Microsoft Windows, уже не отвечает
требованиям сегодняшнего дня. Избалованные хорошо проработанным дизайном Web-
сайтов, пользователи требуют аналогичных дизайнерских решений и от автономных
приложений Windows. Теоретически механизм ресурсов приложений, библиотека
классов MFC и программный интерфейс Win32 API (Application Program Interface, API)
позволяют придать приложению самый замысловатый внешний вид, однако это
достигается использованием нетривиальных приемов программирования.
В итоге реализация сложного пользовательского интерфейса приложения при
помощи Microsoft Visual C++ и MFC доступна только опытным программистам, хорошо
знакомыми с архитектурой и принципами работы ОС Microsoft Windows, а также
искушенными в использовании программного интерфейса Win32 API.
Библиотека MFC тоже не свободна от недостатков. Ее объектно-ориентированная
реализация оставляет желать лучшего, и в добавок вместе с приложениями приходится
распространять специальную библиотеку динамической компоновки (Dynamic Load
Library, DLL). К настоящему времени существует несколько различных версий этот
библиотеки, что создает проблемы при установке приложений MFC на компьютеры
пользователей. Эта проблема известна под названием «ада DLL» (Dll Hell).
Успех существующих систем RAD
Среди ранее созданных и наиболее удачных инструментов ускоренной разработки
приложений заслуживает упоминание Borland Delphi. Эта система получила огромную
популярность, в том числе благодаря по-настоящему удобным средствам визуального
проектирования. Эти средства дополняли стандартные элементы интерфейса Microsoft
Windows новыми средствами, имеющими современный и привлекательный внешний
вид. Помимо визуальных средств разработки приложений Microsoft Windows, в Borland
Delphi имелись компоненты, предназначенные для интеграции приложений с базами
данных, а также средства создания активных компонентов Web-сайтов.
Другим инструментом быстрой разработки приложений, несомненно,
заслуживающим упоминания, является Microsoft Visual Basic.
Упрощенно, процесс визуальной разработки приложений в Borland Delphi и
Microsoft Visual Basic заключается в графическом проектировании внешнего вида
(дизайна) приложений, с последующей привязкой программного кода к элементам
пользовательского интерфейса. При этом для решения самых нужных задач (таких,
например, как обращение к базам данных) используется богатая библиотека
программных компонентов.
Создавая новую программу, разработчик имеет дело с графическим
представлением ее главного окна и всех других окон. Перемещая мышью в эти окна из
специальных палитр значки элементов интерфейса (кнопок, меню и т.д.), а также
значки программных компонентов (соединений с базами данных, компонентов
управления меню и др.), программист может очень быстро получить работающий скелет
программы. При этом размещение элементов интерфейса и настройка их размеров
производится простым и понятным образом. Фактически, программист просто «рисует»
окна своего приложения, быстро достигая необходимого результата.
Когда скелет программы создан, его следует наполнить необходимой
функциональностью. Щелкая мышью элементы пользовательского интерфейса и значки
программных компонентов в окне проектируемого приложения, программист вводит в
редакторе текста программный код, предназначенный для работы с этими элементами.
В результате намного упрощается процедура привязки программного кода к элементам
пользовательского интерфейса и другим компонентам приложения.
Такая технология во много раз ускоряет разработку приложений со сложным
пользовательским интерфейсом, что и привело к росту популярности Borland Delphi,
Microsoft Visual Basic и других аналогичных средств.

Системы RAD для Web-приложений


Процесс создания Web-приложений достаточно сложен. Разрабатывая активные Web-
сайты, способные взаимодействовать с посетителями интерактивным образом,
обращаться к базам данных, почтовым серверам и другим системам, приходится
использовать множество технологий и инструментальных средств. Об этом мы
рассказывали в [1] и [2].
Хотя было бы заманчиво использовать для проектирования Web-приложений
средства RAD, на сегодняшний день большинство активных Web-сайтов создается
традиционным способом.
При этом страницы сайта готовятся на языке разметки гипертекста (HyperText
Markup Language, HTML) создаются с помощью специальных редакторов, таких как
HomeSite, Dreamwaver, FrontPage и т.п. Что же касается активных компонентов, то они
разрабатываются с помощью специальных языков программирования (Perl, PHP, ASP и
др.), а также соответствующих инструментальных средств проектирования и отладки,
таких как Microsoft InterDev и Cold Fusion Studio.
Используя специальные средства вроде шаблонов HTML и компонентов времени
выполнения (Design Time Control, DTC), удается отделить дизайн активного Web-сайта
от программного кода. При этом Web-дизайнер может создавать внешний вид страниц
Web-сайта, не разбираясь в программировании. В свою очередь, программист может
разрабатывать программные компоненты активного сайта, не вникая в детали языка
HTML и в особенности реализации дизайна.
Тем не менее, на наш взгляд, достаточно удобных и доступных средств
ускоренной разработки Web-приложений, в рамках которых были бы охвачены все
необходимые аспекты, до сих пор создано не было. А все существующие системы,
претендующие на звание систем ускоренной разработки Web-приложений, обладают
многочисленными недостатками, ограничивающими их применение.

Платформа Microsoft .NET Framework


Не будет большим преувеличением сказать, что создание компанией Microsoft новой
платформы Microsoft .NET Framework можно рассматривать как значительное
достижение в области систем RAD. В книге [3] мы рассказали об основных
возможностях этой платформы, а также детально рассмотрели язык программирования
C#, наилучшим образом подходящий для создания приложений Microsoft .NET
Framework.
Платформа Microsoft .NET Framework в сочетании с удобными инструментальными
средствами пакета Microsoft Visual Studio .NET позволяет создавать по технологии RAD
как автономные программы Microsoft Window, так и активные Web-приложения.
Большим достижением Microsoft является то, что при создании автономных
программ и Web-приложений можно использовать одинаковые приемы и
инструментальные средства. Таким образом, изучая инструментальные средства
Microsoft Visual Studio .NET, можно овладеть универсальными приемами ускоренной
разработки автономных программ и активных Web-приложений.
Не вдаваясь в подробное описание преимуществ платформы Microsoft .NET
Framework (они описаны в [3]) заметим, что эта платформа предполагает
использование так называемого управляемого кода. При этом исходный текст
программы преобразуется не в машинные коды, а в так называемый промежуточный
язык Microsoft Intermediate Language (MSIL), интерпретируемый средой исполнения
Microsoft .NET Framework.
Программы, созданные при помощи Borland Delphi, Microsoft Visual C++ и других
аналогичных средств, представляют собой обычные исполнимые модули. Ошибка в
таких программах может привести к «зависанию» ОС или даже к потерям данных.
Что же касается управляемых программ Microsoft .NET Framework, то им не
разрешается выполнять опасные операции, поэтому даже при возникновении в них
серьезных ошибок ОС продолжит свою работу. Сделаем, однако, оговорку — ошибки в
самой среде исполнения Microsoft .NET Framework могут привести к зависанию ОС или
другим неприятностям, т.к. эта среда представляет собой набор обычных программных
модулей.
Что нужно для работы с книгой
Так как наша книга посвящена описанию методик визуальной разработки приложений
для платформы Microsoft .NET Framework на языке C#, то для работы с ней потребуется
Microsoft Visual C# или более мощный инструмент Microsoft Visual Studio .NET.
Для использования таких инструментальных средств потребуется достаточно
мощный компьютер с процессором не хуже Intel Pentium-III и тактовой частотой не
менее 600 Мгц. А еще лучше оснастить компьютер процессором Pentium-4. Объем
оперативной памяти должен быть не меньше 256 Мбайт (а лучше 512 Мбайт). Что же
касается дисковой памяти, то будет вполне достаточно диска объемом 40-60 Гбайт.
Особое внимание мы рекомендуем обратить на видеоадаптер и монитор
компьютера. Для работы с Microsoft Visual Studio .NET больше всего подойдет
разрешение не менее 1152х864, а лучше 1280х1024 пикселов. В крайнем случае,
можно работать и при разрешении 1024х768, однако ни о каком комфорте здесь
говорить уже не приходится.
Требования к читателю
Мы предполагаем, что перед чтением этой книги читатель знаком с основными
принципами, положенными в основу платформы Microsoft .NET, а также владеет языком
программирования C#. Вся эта информация была изложена нами в [3].
Чтение глав, рассказывающих об интеграции приложений C# с базами данных,
требует базовых знаний теории реляционных СУБД, а также навыков работы с
сервером Microsoft SQL Server. Читатель без труда найдет всю эту информацию в
книжных магазинах, так как базам данных посвящено очень много различных изданий.
Какого-либо опыта создания приложений для ОС Microsoft Windows не требуется,
хотя, без сомнения, этот опыт будет очень полезен. Мы, однако, полагаем, что Вы
хорошо владеете приемами работы с готовыми приложениями Windows на уровне
опытного пользователя.
Содержимое книги
В первой главе книги мы рассказали о принципах, положенных в основу работы ОС
Microsoft Windows. По своей структуре и принципам работы приложения Microsoft
Windows значительно отличаются от старых программ MS-DOS. Если Вы никогда не
создавали ранее программ для ОС Microsoft Windows, то мы настоятельно рекомендуем
ознакомиться с этой главой. Опытные программисты, знакомые с программным
интерфейсом WinAPI 32 или библиотекой MFC, могут сразу переходить к чтению второй
главы.
Вторая глава рассказывает о работе с инструментальным средством
проектирования приложений Microsoft Visual Studio .NET. На момент создания книги это
был единственный инструмент, позволяющий создавать приложения для платформы
Microsoft .NET, хотя авторам известно о планах компании Borland выпустить
инструментарий аналогичного назначения.
Во второй главе Вы научитесь создавать проекты приложений и отлаживать
приложения C# с визуальным графическим интерфейсом.
Глава 3 расскажет Вам о создании форм в приложениях C#. Формы представляют собой
важнейший компонент приложений, с помощью которого создаются окна программы.
Вы научитесь добавлять в формы такие элементы управления, как текстовые поля и
кнопки, настраивать внешний вид и поведение формы, создавать обработчики событий,
расположенных в окне формы.
В 4 главе на практическом примере редактора текста мы рассмотрим многие
аспекты создания приложений с визуальным интерфейсом. Вы научитесь добавлять в
форму меню и строку состояния, загружать, сохранять и распечатывать файлы
текстовых документов, словом, все основное, что нужно для создания большинства
приложений.
Глава 5 посвящена диалоговым окнам, с которыми приходится сталкиваться
практически каждому разработчику приложений с визуальным графическим
интерфейсом. Мы рассмотрим как простейшие диалоговые окна, предназначенные для
отображения сообщений, так и сложные, способные получать, обрабатывать и
возвращать данные. Вы узнаете о модальных и немодальных окнах, а также о способах
проверки данных, введенных пользователями в формах ввода, реализованных с
помощью диалоговых окон.
В 6 главе мы подробно расскажем об использовании элементов управления, таких
как кнопки, флажки с зависимой и независимой фиксацией, списки, элементы
управления TrackBar и ProgressBar, полосы прокрутки. Вы узнаете и о более сложных
элементах управления, таких как календарь, научитесь работать с таймером и
создавать диалоговые окна с закладками и всплывающие подсказки.
Многооконный пользовательский интерфейс рассмотрен в 7 главе. Здесь мы
рассказали о создании оконных приложений с фреймами, а также с многооконным
интерфейсом документов MDI. Попутно мы рассмотрели элементы управления TreeView
и ListView, которые очень часто используются именно в приложениях с многооконным
интерфейсом.
Глава 8 посвящена созданию приложений с базами данных . В ней мы рассмотрели
различные методы доступа к базам данных и применяемые в этих методах доступа
программные интерфейсы, а также подробно рассмотрели работу с элементами
управления DataSet и DataGrid. Эти элементы управления позволяют создавать
приложения с базами данных, расположенных в оперативной памяти, и отображать их
содержимое в табличном виде, соответственно. Использование упомянутых элементов
управления демонстрируется на примере исходных текстов простейшего приложения с
базой данных — телефонного справочника.
В главе 9 мы рассказали об интеграции приложений C# с сервером СУБД Microsoft SQL
Server. Этот вопрос имеет большое значение, так как многие приложения бизнеса
требуют хранения информации в базе данных. Вы узнаете о том, как создавать базы
данных и связывать их с наборами данных DataSet, рассмотренными в 8 главе.
Кроме этого, мы расскажем о том, как создавать клиент-серверные приложения
C#, устанавливая соединения с базой данных и выполняя запросы SQL. Вы научитесь
выполнять такие запросы напрямую, или делать это с применением механизма
хранимых процедур сервера Microsoft SQL Server. В качестве практического примера
мы создадим приложение, позволяющее создавать в базе данных и редактировать
древовидную структуру данных.
Глава 10 рассказывает о графическом интерфейсе GDI+, с помощью которого
приложения C# могут рисовать в своих окнах практически любые изображения, а
также текст со шрифтовым оформлением. В этой главе мы рассмотрели основные
понятия, знание которых необходимо для работы с интерфейсом GDI+, в частности,
рассмотрели механизм перерисовки окон и обработку события Paint. Мы ввели понятие
контекста отображения и показали, как его использовать для рисования. Прочитав 10
главу, вы научитесь рисовать различные геометрические фигуры (прямые линии,
эллипсы и др.), кривые линии с использованием обычных сплайнов и сплайнов Безье,
выбирать необходимые для рисования кисти и перья.
Мы расскажем Вам также в 10 главе о рисовании изображений, хранящихся в
файлах различных графических форматов. Мы создадим программу просмотра таких
файлов, способную отображать изображения, пиктограммы которых пользователь
может перетаскивать методом буксировки из папок в окно программы просмотра. И в
заключение мы расскажем о шрифтах и способах выбора шрифтов для рисования
текста.
И, наконец, в приложении к нашей книге мы привели довольно полные исходные
тексты редактора текста Simple Notepad, которые мы будем подробно обсуждать в 4 и 5
главе книги.
Как связаться с авторами книги
Свои замечания по этой книге и предложения присылайте авторам по адресу
alexandre@frolov.pp.ru. Информацию о других наших книгах и проектах можно найти по
адресам http://www.frolov.pp.ru, http://www.datarecovery.ru.
Благодарности
Мы благодарим Сергея Ноженко, который натолкнул нас на идею использования языка
C# в системе удаленного восстановления данных через Интернет для службы
DataRecovery.Ru, а также на идею создания этой книги.

Визуальное проектирование
приложений C#
А.В. Фролов, Г.В. Фролов
ГЛАВА 1. ПРИНЦИПЫ РАБОТЫ ПРИЛОЖЕНИЙ WINDOWS
РАЗНОВИДНОСТИ ПРОГРАММ
Синхронное и асинхронное выполнение программ
Однопоточные и многопоточные программы
СООБЩЕНИЯ
Создание сообщений
Очередь сообщений
Обработка сообщений
Фокус ввода
Цикл обработки сообщений
Функция окна
Передача сообщений
СОЗДАНИЕ И УНИЧТОЖЕНИЕ ОКНА
СТРУКТУРА ПРИЛОЖЕНИЯ С ОБРАБОТКОЙ СООБЩЕНИЙ
РЕСУРСЫ ПРИЛОЖЕНИЙ MICROSOFT WINDOWS
КАК ВСЕ НЕПРОСТО В МИРЕ WINDOWS

Глава 1. Принципы работы приложений


Windows
Прежде чем приступить к созданию приложений Windows для платформы Microsoft .NET
Framework, необходимо сделать некоторые общие замечания относительно принципов
работы приложений Windows. В частности, необходимо разобраться с механизмом
сообщений, лежащим в основе всех программ Microsoft Windows с графическим
пользовательским интерфейсом.
Если Вы никогда раньше не создавали программы для Microsoft Windows,
механизм сообщений может показаться довольно запутанным. Среда разработки
приложений Microsoft Visual Studio .NET скрывает от программиста многие
второстепенные детали, поэтому нет необходимости вникать во все тонкости этого
механизма. Однако хотя бы краткое знакомство с ним будет крайне полезно.
Если же Вы раньше создавали приложения Microsoft Windows на языках C или C+
+, и хорошо знакомы с механизмом передачи и обработки сообщений, можете
пропустить этот материал и сразу переходить к чтению следующей главы.
Разновидности программ
К настоящему времени в операционной системе Microsoft Windows существуют
программы самых разных типов — консольные программы и программы с графическим
интерфейсом, однопоточные и многопоточные, работающие синхронно или асинхронно.
Прежде чем двигаться дальше, попробуем разобраться с этим.
Синхронное и асинхронное выполнение программ
Прежде всего, заметим, что по способу выполнения все программы можно разделить на
синхронные и асинхронные.
Синхронные программы состоят из одного или нескольких программных модулей,
и полностью контролируют процесс передачи управления от одной команды программы
к другим или от одного модуля к другим.
Изменение последовательности выполнения команд, фрагментов или модулей
асинхронных программ, напротив, могут исполняться под воздействием внешних
событий, возникающих при работе устройств ввода, вывода или системного таймера.
При асинхронном выполнении программа не в состоянии контролировать процесс
передачи управления между своими модулями, т.к. он может происходить в
непредсказуемое время под влиянием внешних воздействий.
Программы, создаваемые для некогда популярной операционной системы MS-
DOS, выполняются синхронно. Это означает, что программа начинает свою работу с
определенной точки входа и затем действует в соответствии с заложенным в нее
алгоритмом. Как правило, команды программы выполняются последовательно одна за
другой. С помощью условных операторов и операторов цикла управление может
передаваться на те или иные фрагменты программы.
Нормальный ход выполнения программы MS-DOS могут нарушать так называемые
аппаратные и программные прерывания (interrupt).
Аппаратные прерывания (hardware interrupt) вырабатываются устройствами ввода
или вывода, когда им требуется обслуживание со стороны операционной системы.
Использование аппаратных прерываний позволяет совместить относительно медленные
операции ввода и вывода с работой других программ или модулей. Программные
прерывания (software interrupt) инициируются самой программой.
Рассмотрим, например, процесс печати документа.
При синхронном выполнении программа должна вывести на принтер блок данных,
дождаться, пока эти данные будут распечатаны, а затем перейти к печати следующего
блока данных. Печать — медленный процесс, поэтому во время синхронной печати
компьютер не удастся использовать для выполнения каких-либо других работ. И это
несмотря на то, что процессор все время будет простаивать, дожидаясь завершения
печати очередного блока данных. Заметим, что этот способ печати был характерен для
старых программ MS-DOS и сейчас, конечно, уже не используется.
Асинхронная процедура печати выполняется следующим образом.
Вначале программа инициирует печать блока данных, передавая его
драйверу принтера. Затем, не дожидаясь завершения печати, программа переходит к
выполнению другой работы. Если печать выполнялась, например, из текстового
редактора, то во время печати программа может позволить пользователю продолжить
редактирование.
Когда печать блока будет завершена, адаптер принтера выработает аппаратное
прерывание. В результате работа программы, инициировавшей печать, будет прервана,
и она сможет вывести на принтер очередной блок данных.
Однопоточные и многопоточные программы
Операционная система MS-DOS допускает в любой момент времени работу только
одной программы. Это означает, что после запуска программы пользователь должен
дождаться ее завершения, и только потом он сможет запустить другую программу.
В отличие от MS-DOS, современные операционные системы, такие как Microsoft
Windows и Linux, работают в так называемом многопоточном (multithreading) режиме.
При этом ресурсы одного или нескольких центральных процессоров используются
для одновременного выполнения нескольких программ. Этим процессом управляет
специальный системный процесс — планировщик, периодически выделяя каждому
потоку выполнения свой квант времени. Кванты времени выделяются по прерываниям
таймера.
Современные программы Microsoft Windows работают в среде многопоточной
операционной системы одновременно с множеством других программ. Каждая такая
программа, в свою очередь, тоже может быть многопоточной и состоять из нескольких
потоков выполнения. Работа программ Microsoft Windows (и их потоков) может
прерываться в произвольное время как результат завершения операций ввода или
вывода, а также по сигналам системного таймера. Потоки могут синхронизовать свою
работу, отслеживая возникновение или завершение тех или иных событий, таких,
например, как завершение операции ввода или вывода, выполнение запроса к базе
данных и т.д.
Сообщения
Обрисованную выше картину дополнительно усложняют так называемые сообщения
Microsoft Windows (Windows Messages), вырабатываемые в программах Microsoft
Windows.
Что это за сообщения и откуда они берутся?
Чтобы ответить на этот вопрос, вспомним, что в среде Microsoft Windows могут
выполняться консольные программы и программы с графическим оконным
интерфейсом.
Создание консольных программ на языке C# было нами детально описано в [3]. В
том, что касается пользовательского интерфейса, такие программы напоминают
программы MS-DOS и утилиты Unix-подобных операционных систем. Пользователь
запускает программу, вводя путь к ее загрузочному файлу в командном приглашении
ОС. Далее программа может выводить на консоль текстовые строки и воспринимать
ввод данных с клавиатуры.
Консольные программы очень хорошо подходят для изучения языков
программирования, однако все современные настольные приложения Microsoft Windows
снабжаются графическим оконным интерфейсом. Мы полагаем, что Вы хорошо владеете
приемами работы с готовыми приложениями Windows на уровне опытного
пользователя, поэтому не будем вдаваться в подробное описание элементов
пользовательского интерфейса оконных программ.
Для нас сейчас важен внутренний механизм, обеспечивающий работу таких
элементов управления оконными программами, как меню, поля ввода, списки, кнопки,
флажки и т.п. Этот механизм предполагает создание и обработку упомянутых выше
сообщений Microsoft Windows.
Создание сообщений
Большинство сообщений создают драйверы периферийных устройств ввода и вывода,
таких, как клавиатура, мышь или таймер. Драйверы создают сообщения при
поступлении аппаратных прерываний.
Например, когда Вы нажимаете и затем отпускаете клавишу, драйвер
обрабатывает прерывания от клавиатуры и создает несколько сообщений. Аналогично,
сообщения создаются при перемещении мыши или в том случае, когда Вы нажимаете
кнопки, расположенные на корпусе мыши. Можно сказать, что драйверы периферийных
устройств компьютера транслируют аппаратные прерывания в сообщения.
Очередь сообщений
Куда направляются сообщения, созданные драйверами?
Прежде всего, сообщения попадают в системную очередь сообщений Microsoft
Windows, существующую в единственном экземпляре. Далее из системной очереди
сообщения распределяются в очереди сообщений отдельных приложений. При этом для
каждого приложения создается своя собственная очередь сообщений.
Очередь сообщения приложений может пополняться не только из системной
очереди. Любое приложение может послать сообщение любому другому сообщению, в
том числе и само себе.
Обработка сообщений
Основная работа, которую должно выполнять приложение, заключается в
обслуживании собственной очереди сообщений. Обычно приложение в цикле
опрашивает свою очередь сообщений. Обнаружив сообщение, приложение с помощью
специальной функции из программного интерфейса Windows (Win32 API) распределяет
его нужному программному модулю, называемому функции окна. Эта функция и
выполняет обработку сообщения.
Структура приложения Microsoft Windows состоит из инициализирующей части,
вслед за которой идет так называемый цикл обработки сообщений (message loop).
Обращаем внимание, что инициализирующая часть не выполняет никакой работы,
имеющей отношение к поведению приложения. Ее задача заключается к подготовке
цикла обработки сообщений и в запуске этого цикла. Что же касается реализации
функциональности приложения Microsoft Windows, то она целиком ложится на
программные модули (функции), вызываемые внутри цикла обработки сообщений.
В этом смысле приложения Microsoft Windows похожи на загружаемые драйверы
MS-DOS. Эти драйверы также состоят из инициализирующего фрагмента и функции,
обрабатывающей команды, выдаваемые драйверу. Приложение Microsoft Windows после
инициализации переходит в состояние постоянного опроса собственной очереди
сообщений. Как только происходит какое-либо событие, имеющее отношение к
приложению, в очереди приложения появляется сообщение и приложение начинает его
обрабатывать. После обработки приложение вновь возвращается к опросу собственной
очереди сообщений. Иногда функция окна может получать сообщения
непосредственно, минуя очередь приложения.
Фокус ввода
Так как Microsoft Windows — многопоточная ОС, ее разработчики должны были
предусмотреть механизм совместного использования несколькими одновременно
работающими приложениями таких ресурсов, как мышь и клавиатура. Все сообщения,
создаваемые драйверами мыши и клавиатуры, попадают в системную очередь
сообщений, поэтому должен существовать способ распределения этих сообщений
между различными приложениями.
В ОС Microsoft Windows существует понятие фокуса ввода (input focus),
помогающее в распределении сообщений.
Фокус ввода — это атрибут, который в любой момент времени может относиться
только к одному окну. Если окно имеет фокус ввода, все сообщения от клавиатуры
распределяются сначала в очередь сообщений приложения, создавшего окно, а
затем — функции окна, владеющего фокусом ввода.
Нажимая определенные клавиши, пользователь может перемещать фокус ввода от
одного окна или поля к другому.
Например, при работе с диалоговой панелью, содержащей несколько полей ввода
текста, с помощью клавиши Tab можно переключать фокус ввода с одного такого поля
на другое и вводить текст в различные поля диалоговой панели. С помощью
комбинации клавиш Alt+Tab пользователь может переключить фокус ввода на другое
приложение. При их использовании фокус ввода будет отдан окну, принадлежащему
другому приложению.
Сообщения от драйвера мыши всегда передаются функции того окна, над которым
находится курсор мыши. При необходимости приложение может выполнить операцию
захвата мыши (mouse capturing). В этом случае все сообщения от мыши будут
поступать в очередь приложения, захватившего мышь, вне зависимости от положения
курсора мыши.
Цикл обработки сообщений
Итак, теперь Вы знаете, что работа приложений Microsoft Windows управляется
сообщениями, обрабатываемыми в специальном цикле, называемом циклом обработки
сообщений (message loop).
Простейший цикл обработки сообщений состоит из вызовов двух функций Win32
API с именами GetMessage и DispatchMessage.
Функция GetMessage предназначена для выборки сообщения из очереди
приложения. Сообщение выбирается из очереди и записывается в область данных,
принадлежащую приложению.
Функция DispatchMessage предназначена для распределения выбранного из
очереди сообщения нужной функции окна. Так как приложение обычно создает много
окон, и эти окна используют различные функции окна, необходимо распределить
сообщение именно тому окну, для которого оно предназначено. Поэтому приложение
должно распределить сообщение после его выборки из очереди приложения, в котором
находятся сообщения для всех окон.
ОС Microsoft Windows оказывает приложению существенную помощь в решении
этой задачи — для распределения приложению достаточно вызвать функцию
DispatchMessage.
Вот как выглядит простейший вариант цикла обработки сообщений в программе,
составленной на языке C:
MSG msg;
while(GetMessage(&msg, 0, 0, 0))
{
DispatchMessage(&msg);
}
Завершение цикла обработки сообщений происходит при выборке из очереди
специального сообщения, в ответ на которое функция GetMessage возвращает нулевое
значение.
Структура MSG хранит данные сообщения. Вот как она определена:
typedef struct tagMSG
{
HWND hwnd;
UINT message;
WPARAM wParam;
LPARAM lParam;
DWORD time;
POINT pt;
} MSGMSG;
Структура содержит уникальный для Microsoft Windows код сообщения message и
другие параметры, имеющие отношение к сообщению.
В поле hwnd хранится идентификатор получателя сообщения, т.е. идентификатор
окна, для которого это сообщение предназначено.
Через поля wParam и lParam передается информация, связанная с сообщением.
Смысл содержимого данных полей зависит от кода сообщения message. Если
сообщение вызвано, например, нажатием клавиши на клавиатуре компьютера, эти поля
содержат код нажатой клавиши. Сообщения, генерируемые мышью, содержат код
клавиши и координаты курсора.
Поле time хранит время отправления сообщения, а поле pt — координаты,
связанные с сообщением.
В дальнейшем мы не будем непосредственно работать с полями этой структуры,
однако понимание назначения ее полей позволит лучше разобраться в принципах
функционирования приложений Microsoft Windows.
Функция окна
Итак, теперь Вы знаете, что в основе приложений Microsoft Windows с графическим
интерфейсом лежит цикл обработки сообщений. Когда приложение инициализируется,
оно создает цикл обработки сообщений, а также выделяет функцию, которой будут
передаваться сообщения. Эта функция называется функцией окна (window procedure).
Почему такое название?
Работа окон Microsoft Windows основана на обработке сообщений. Каждое окно
имеет свой цикл обработки сообщений, а также свою функцию окна, которой
передаются сообщения, извлеченные из очереди приложения.
Функция окна анализирует код сообщения, записанный в поле message только
что описанной структуры MSG, и в зависимости от этого кода выполняет те или иные
действия. При этом функция окна может использовать (а может игнорировать) другие
поля сообщения: hwnd, wParam, lParam, time и pt.
Таким образом, если приложение создает несколько окон, то в нем может быть
несколько циклов обработки сообщений и несколько функций окон. В процессе
обработки сообщений функция окна может создавать другие окна, запуская для них
дополнительные циклы обработки сообщений.
Процесс обработки сообщений схематически показан на рис. 1-1.
Рис. 1-1. Процесс обработки сообщений

На этом рисунке слева показаны ресурсы и подсистемы ОС Microsoft Windows,


справа — ресурсы и подсистемы приложения. Сообщения поступают от драйверов
устройств, таких как клавиатура, мышь, таймер, и попадают в системную очередь
сообщений. Из системной очереди сообщений ОС Microsoft Windows выбирает
сообщения, предназначенные для приложения, и помещает их в очередь сообщения
приложения.
Если Вы раньше создавали программы MS-DOS на языке программирования C, то
помнете, что при запуске программы управление передается функции с именем main. В
оконных приложениях ОС Microsoft Windows эту роль играет функция WinMain.
Получив управление, функция WinMain выбирает сообщения из очереди
сообщений приложения в цикле обработки сообщений с помощью функции GetMessage
и затем распределяет их соответствующим функциям окон, вызывая функцию
DispatchMessage.
На рис. 1-1 для простоты показано только одно приложение, в котором
определена только одна функция окна. Как мы уже говорили, реально существует
много приложений, и каждое приложение может иметь несколько функций окна.
Каждое приложение вызывает в цикле обработки сообщений функцию GetMessage,
выбирая сообщения из своей очереди. Затем каждое приложение распределяет
выбранное сообщение одной из своих функций окна, для чего используется функция
DispatchMessage.
Передача сообщений
Передача сообщений — это способ, при помощи которого в Microsoft Windows
организован обмен информацией между отдельными подсистемами, приложениями или
между отдельными модулями одного и того же приложения.
Как мы уже говорили, ОС Microsoft Windows содержит в себе системную очередь
сообщений. Сообщения поступают туда от драйверов устройств (при завершении
операции ввода или вывода) или от приложений, а также несколько очередей
сообщений для каждого приложения.
Когда Вы нажимаете клавиши, перемещаете мышь по поверхности стола или
нажимаете кнопки на корпусе мыши, соответствующий драйвер (клавиатуры или мыши)
вырабатывает сообщения, отражающие выполняемые Вами действия. Эти сообщения
попадают вначале в общую системную очередь и затем распределяются в очереди
отдельных приложений.
Если Вы обратите внимание на внешний вид приложений Windows, то обнаружите,
что окно приложения содержит множество элементов управления, таких, как кнопки,
переключатели, полосы просмотра и т. д. Действие, выполненное над любым элементом
управления, приводит к генерации соответствующего сообщения и помещения этого
сообщения в очередь приложения.
Приложение Microsoft Windows постоянно анализирует содержимое своей очереди
сообщений. Когда в очереди появляется сообщение от какого-либо элемента
управления, приложение выполняет соответствующее действие.
Такой механизм значительно упрощает программирование, так как приложение не
занимается, например, отслеживанием текущих координат курсора мыши для того,
чтобы определить, на какую кнопку Вы нажали, или какую выбрали строку в меню.
Приложение получает готовое сообщение о том, что нажата кнопка с определенным
идентификатором или выбрана строка из меню с определенным идентификатором, а
вся остальная работа, связанная с отслеживанием текущих координат курсора и
определением использованной кнопки, выполняется ОС. Идентификаторы кнопок и
других элементов управления определяются программистом. Это просто числа,
однозначно соответствующие элементам управления.
Обычно приложение имеет главное окно, в котором располагаются такие
элементы управления, как кнопки, меню, полосы прокрутки, флажки и т. д. Работая с
приложением, пользователь выбирает строки меню, нажимает кнопки или используете
другие элементы управления.
Каждый элемент управления (кнопка или строка меню) имеет свой
идентификатор. Когда Вы нажимаете на кнопку или выбираете строку меню, в очередь
сообщений приложения ОС заносит сообщение, содержащее идентификатор
использованного элемента управления.
Приложение анализирует очередь сообщений и выполняет обработку сообщений.
Например, если Вы нажали кнопку с надписью Exit, приложение завершает свою
работу.
Следует отметить, что в Microsoft Windows используется многоуровневая система
сообщений.
Сообщения низкого уровня вырабатываются, когда Вы перемещаете мышь или
нажимаете клавиши мыши или клавиатуры. В эти сообщения входит информация о
текущих координатах курсора мыши или кодах нажатых клавиш. Обычно приложения
редко анализируют сообщения низкого уровня. Все эти сообщения передаются ОС
Microsoft Windows, которая на их основе формирует сообщения более высокого уровня.
Когда Вы нажимаете кнопку на диалоговом окне или выбираете строку из меню
приложения Windows, Ваше приложение получает сообщение о том, что нажата та или
иная клавиша или выбрана та или иная строка в меню. Вам не надо постоянно
анализировать координаты курсора мыши или коды нажимаемых клавиш — ОС
Microsoft Windows сама вырабатывает соответствующее сообщение высокого уровня.
Таким образом, Вы можете возложить на Windows всю работу, связанную с
«привязкой» мыши и клавиатуры к элементам управления.
Но, тем не менее, приложение может вмешаться и в обработку сообщений низкого
уровня. Такая схема очень удобна и позволяет, с одной стороны, упростить
приложение, возложив на Microsoft Windows всю работу с сообщениями нижнего
уровня, с другой стороны, позволяет при необходимости вмешаться в процесс
обработки сообщений низкого уровня.
Программы MS-DOS в «классическом» исполнении работают по-другому. Как
правило, такие программы выполняются линейно, ожидая от пользователя ввода той
или иной команды и блокируя нежелательные на данный момент действия.
Логика работы приложений Microsoft Windows называется логикой, управляемой
событиями. Под событием понимается обнаружение в очереди сообщений приложения
того или иного сообщения. В любой момент времени пользователю разрешается
использовать любые элементы управления любого запущенного приложения (хотя
приложение при необходимости может выполнить блокировку отдельных элементов
управления).
Операционная система Windows направляет сообщение от использованного
элемента управления в очередь того приложения, к которому принадлежит данный
элемент управления. Поэтому приложение не должно беспокоиться о том, что в любой
момент времени Вы можете приступить к работе с другим приложением. Просто каждое
приложение занимается обработкой своей очереди сообщений, а ОС Microsoft Windows
заботится о том, чтобы все сообщения попадали в нужную очередь.
Так как все приложения используют совместно общий экран видеомонитора, при
этом работая каждое в своем окне, ОС Microsoft Windows обеспечивает обработку
ситуаций, при которых одно окно перекрывает другое. Приложения не следят за тем,
чтобы их окна не перекрывались окнами других приложений. Но они все должны быть
способны обработать специальное сообщение, по которому необходимо перерисовать
содержимое всего своего окна или части окна.
Создание и уничтожение окна
Функция окна получает сообщения при создании окна, в процессе работы приложения,
а также при разрушении окна.
Сообщение с кодом WM_CREATE передается функции окна в момент создания
окна. При обработке этого сообщения функция окна выполняет инициализирующие
действия (аналогично конструктору класса в языке C++). Коды сообщений определены
в файле windows.h, включаемом в исходные тексты любых приложений Microsoft
Windows, написанных на языке C или C++.
В процессе работы приложения функция окна может получать сообщения с
различными кодами, как через очередь сообщений приложения, так и
непосредственно, в обход очереди сообщений. Обработчики этих сообщений,
определенные в функции окна, являются методами для работы с окном как с объектом.
При разрушении структуры данных окна (при уничтожении окна) функция окна
получает сообщение с кодом WM_DESTROY. Обработчик этого сообщения действует как
деструктор. Если Ваша функция окна во время обработки сообщения WM_CREATE
создала какие-либо структуры данных, эти структуры должны быть разрушены (а
заказанная для них память возвращена ОС) во время обработки сообщения
WM_DESTROY.
Структура приложения с обработкой сообщений
Рассказывая в [4] о создании оконных приложений Microsoft Windows, мы в самом
начале книги привели исходный текст простейшей программы с обработкой сообщений.
И хотя теперь мы будем создавать приложения Microsoft Windows совершенно по-
другому и на другом языке программирования, мы повторим описание этой программы
в сильно сокращенном виде. Это поможет Вам лучше понять механизм обработки
сообщений. Желающие могут найти полный текст этой книги и других наших книг
серии «Библиотека системного программиста» на сайте службы восстановления данных
DataRecovery.Ru по адресу http://info.datarecovery.ru.
Для простоты наше приложение имеет только одно окно и одну функцию окна. Его
исходный текст приведен в листинге 1-1.
Листинг 1-1. Приводится с сокращениями
#include <windows.h>

// Имя класса окна
char const szClassName[] = "WindowAppClass";

// Заголовок окна
char const szWindowTitle[] = "Window Application";

int PASCAL WinMain(… [параметры опущены]…)


{
MSG msg; // структура для работы с сообщениями
HWND hwnd; // идентификатор главного окна приложения

// Проверяем, не запускалось ли это приложение ранее


if(!hPrevInstance)
{
// Если нет, вызываем функцию InitApp для инициализации
// приложения. Если инициализацию выполнить не удалось,
// завершаем приложение
if(!InitApp(hInstance))
return FALSE;
}

// Если данное приложение уже работает,


// выводим сообщение о том, что допускается
// запуск только одной копии приложения, и
// затем завершаем приложение
else
{
MessageBox(NULL,
"Можно запускать только одну копию приложения",
"Ошибка", MB_OK | MB_ICONSTOP);
return FALSE;
}

// После успешной инициализации приложения создаем


// главное окно приложения
hwnd = CreateWindow(
szClassName, // имя класса окна
szWindowTitle, // заголовок окна
… [параметры приведены с сокращениями] );

// Если создать окно не удалось, завершаем приложение


if(!hwnd)
return FALSE;

// Рисуем окно. Для этого после функции ShowWindow,


// рисующей окно, вызываем функцию UpdateWindows,
// посылающую сообщение WM_PAINT в функцию окна

ShowWindow(hwnd, nCmdShow);
UpdateWindow(hwnd);

// Запускаем цикл обработки сообщений


while(GetMessage(&msg, 0, 0, 0))
{
DispatchMessage(&msg);
}

return msg.wParam;
}

// =====================================
// Функция InitApp
// Вызывается из функции WinMain для
// инициализации приложения.
// Выполняет регистрацию класса окна
// =====================================
BOOL InitApp(HINSTANCE hInstance)
{
ATOM aWndClass; // атом для кода возврата
WNDCLASS wc; // структура для регистрации класса окна
memset(&wc, 0, sizeof(wc));


// Указатель на функцию окна, обрабатывающую
// сообщения, предназначенные для всех окон,
// созданных на основе данного класса
wc.lpfnWndProc = (WNDPROC) WndProc;


// Имя, которое присваивается создаваемому
// классу и используется при создании
// окон данного класса
wc.lpszClassName = (LPSTR)szClassName;

// Регистрация класса
aWndClass = RegisterClass(&wc);

// Возврат результата регистрации класса


return (aWndClass != 0);
}

// =====================================
// Функция окна WndProc
// НЕ ВЫЗЫВАЕТСЯ ни из одной функции приложения. Эту функцию вызывает
// ОС в процессе обработки сообщений. Для этого адрес функции WndProc
// указывается при регистрации класса окна. Функция выполняет
// обработку сообщений главного окна приложения
// =====================================
LRESULT CALLBACK _export
WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
// Выполняем обработку сообщений. Идентификатор
// сообщения передается через параметр msg
switch (msg)
{
// Это сообщение приходит, когда вы поместили курсор
// мыши в область главного окна приложения и нажали
// левую клавишу мыши
case WM_LBUTTONDOWN:
{
MessageBox(NULL,
"Нажата левая клавиша мыши",
"Сообщение", MB_OK | MB_ICONINFORMATION);
return 0;
}

// Это сообщение приходит, когда вы поместили курсор


// мыши в область главного окна приложения и нажали
// правую клавишу мыши
case WM_RBUTTONDOWN:
{
MessageBeep(-1); // звуковой сигнал
MessageBox(NULL,
"Нажата правая клавиша мыши",
"Сообщение", MB_OK | MB_ICONINFORMATION);
return 0;
}

// Это сообщение приходит, когда вы завершаете


// работу приложения стандартным для
// Windows способом
case WM_DESTROY:
{
// Инициируем завершение работы приложения,
// помещая в очередь приложения сообщение
// WM_QUIT. Это приведет к завершению
// цикла обработки сообщений в функции WinMain
PostQuitMessage(0);
return 0;
}
}

// Все сообщения, которые не обрабатываются нашей


// функцией окна, ДОЛЖНЫ передаваться функции
// DefWindowProc
return DefWindowProc(hwnd, msg, wParam, lParam);
}
Если посмотреть исходный текст нашего приложения, то легко заметить, что оно
имеет несколько необычную (с точки зрения программиста, составляющего программы
для MS-DOS) структуру. В частности, функция WinMain после выполнения
инициализирующих действий входит в цикл обработки сообщений. После выхода из
этого цикла работа приложения завершается. Функция WndProc вообще не вызывается
ни из какой другой функции приложения, хотя именно она выполняет всю «полезную»
работу.
Составляя программы для MS-DOS, программисты привыкли к тому, что за весь
сценарий работы программы отвечает функция main. Эта функция выполняет вызов
всех остальных функций (за исключением функций обработки прерываний), из которых
и состоит программа.
Логика работы приложения Microsoft Windows другая. Прежде всего, выполняются
инициализирующие действия, связанные, например, с определением классов, на базе
которых в дальнейшем (или сразу) будут создаваться окна приложения. Для каждого
класса необходимо указать адрес функции окна. Эта функция будет обрабатывать
сообщения, направляемые окнам, создаваемым на базе класса.
Однако процесс распределения сообщений функциям окон, созданных
приложением, происходит не сам по себе. Приложение должно само организовать этот
процесс, для чего после выполнения инициализирующих действий в функции WinMain
запускается цикл обработки сообщений.
Обработка сообщений, которые операционная система Windows посылает в
очередь приложения (во время цикла обработки сообщений), выполняется
соответствующей функцией окна. Наше приложение определяет один класс окна и на
его базе создает одно, главное окно. Для обработки сообщений, поступающих в это
окно, приложение определяет одну функцию окна.
Функция окна обрабатывает три сообщения с кодами WM_LBUTTONDOWN,
WM_RBUTTONDOWN и WM_DESTROY.
Сообщение WM_LBUTTONDOWN записывается в очередь приложения и передается
функции окна, когда Вы устанавливаете курсор мыши внутри главного окна
приложения и нажимаете левую клавишу мыши. В ответ на это сообщение функция
окна выводит диалоговую панель с сообщением о том, что нажата левая клавиша
мыши.
Сообщение WM_RBUTTONDOWN аналогично предыдущему, но оно записывается в
очередь сообщений приложения при нажатии правой кнопки мыши. В ответ на это
сообщение функция выводит диалоговую панель с сообщением и дополнительно
выдает звуковой сигнал.
Последнее сообщение, WM_DESTROY, передается приложению при разрушении
структуры данных, связанной с окном. В нашем примере при завершении работы
приложения разрушается главное (и единственное) окно. В ответ на это сообщение
функция окна помещает в очередь приложения специальное сообщение с
идентификатором WM_QUIT. Выборка этого сообщения в цикле обработки сообщений
приводит к завершению цикла и, соответственно, к завершению работы приложения.
Схематически алгоритм работы функции WinMain приложения можно представить
следующим образом:
if(приложение уже было запущено)
{
Завершение работы текущей копии приложения
}
Создание класса окна
Создание главного окна приложения на основе созданного ранее класса
Отображение окна на экране
while(в очереди нет сообщения WM_QUIT)
{
Выборка сообщения и распределение его функции окна
}
Завершение работы приложения
Адрес функции окна указывается при создании класса окна. Этот адрес
используется ОС Microsoft Windows для вызова функции окна (напомним, что
приложение само не вызывает функцию окна). Приведем алгоритм работы функции
окна нашего простейшего приложения:
switch(Идентификатор сообщения)
{
case WM_LBUTTONDOWN:
{
Вывод диалоговой панели с сообщением о том,
что была нажата левая кнопка мыши
}
case WM_RBUTTONDOWN:
{
Выдача звукового сигнала
Вывод диалоговой панели с сообщением о том,
что была нажата правая кнопка мыши
}
case WM_DESTROY:
{
Запись в очередь приложения сообщения WM_QUIT,
при выборке которого завершается цикл обработки сообщений
}
}
Вызов функции DefWindowProc, обрабатывающей все
остальные сообщения, поступающие в функцию окна
Из приведенного выше алгоритма работы функции окна видно, что наша функция
обрабатывает только три сообщения. Все остальные сообщения, которые попадают в
очередь приложения и распределяются функции окна (а их очень много), также
должны быть обработаны. Для этого необходимо использовать функцию программного
интерфейса Microsoft Windows с именем DefWindowProc.
Если функция окна проигнорирует вызов функции DefWindowProc для тех
сообщений, которые она сама не обрабатывает, Microsoft Windows не сможет
обработать такие сообщения. Это может привести к неправильной работе, блокировке
приложения или даже всей ОС.
Ресурсы приложений Microsoft Windows
Формат загрузочного модуля оконного приложения Microsoft Windows сложнее формата
загрузочного модуля программы MS-DOS. Кроме выполняемого кода и констант в
загрузочном модуле такого приложения находятся дополнительные данные — ресурсы.
Что такое ресурсы?
Приложение Microsoft Windows может хранить в виде ресурсов текстовые строки,
значки (icons), курсоры различной формы, графические изображения, меню,
диалоговые окна, произвольные массивы данных и т. д.
Физически ресурсы находятся внутри exe-файла приложения. Они могут
загружаться в оперативную память автоматически при запуске приложения или по
запросу приложения (явному или неявному). Такой механизм обеспечивает экономное
расходование оперативной памяти, так как все редко используемые данные можно
хранить на жестком диске и загружать в оперативную память только при
необходимости.
Например, приложение может иметь сложную систему диагностики ошибочных
ситуаций, содержащую различные диалоговые окна, массивы сообщений об ошибках и
т. п. Когда приложение работает без ошибок (что, очевидно, является нормальным
явлением), ненужные диагностические ресурсы лежат на диске, не перегружая
оперативную память. Но как только возникает ошибка, и приложение вызывает
функцию обработки ошибки, эти ресурсы будут автоматически загружены.
Ресурсы можно редактировать без повторной трансляции программного проекта.
Это означает, что можно легко перевести все сообщения и тексты меню приложения на
любой другой национальный язык, отредактировать графические изображения или
любые другие ресурсы, даже не имея в своем распоряжении исходные тексты
приложения.
Как все непросто в мире Windows
Как видите, для описания принципов работы даже самого примитивного приложения
Microsoft Windows c одним-единственным окном нам потребовалось больше десяти
страниц текста. Очевидно, программировать такие приложения на языке C — непростая
задача, особенно если приложение содержит десятки и сотни окон, множество меню,
списков и других элементов управления. А ведь именно так и выглядят настоящие
приложения Microsoft Windows!
Для того чтобы создание программ для Microsoft Windows не превратилось в
кошмар, были разработаны различные средства, некоторых из них мы упоминали во
введении к книге. Эти средства позволяют до некоторой степени скрыть внутреннюю
сложность приложений Microsoft Windows, позволяя разработчику сконцентрировать
свои усилия на реализации прикладной задачи.
В нашей книге мы рассмотрим средства ускоренной разработки приложений RAD,
доступные программистам при использовании Microsoft Visual Studio .NET. Как Вы
увидите, эти средства включают в себя не только мощные и удобные инструменты
визуального проектирования дизайна приложений, но и весьма обширную библиотеку
классов. Эта библиотека позволяет использовать готовые решения задач, чаще всего
встающих перед разработчиком приложений для Microsoft Windows.
В частности, класс System.Windows.Forms, о котором мы еще будем рассказывать
подробно, позволяет кардинально упростить задачу создания окон и управления ими.
Разработчику приложений C# нет необходимости самостоятельно организовывать цикл
обработки сообщений и определять функции окна — все это скрыто внутри класса
System.Windows.Forms.
Программист работает на уровне обработчиков событий, возникающих, например,
при использовании элементов управления. Соответствующие средства удобны и
встроены непосредственно в язык C#. Более того, инструментальная система Microsoft
Visual Studio .NET умеет автоматически создавать «пустые» обработчики событий,
привязанные к элементам управления, так что программисту остается лишь наполнить
эти обработчики необходимым содержимым.
Заметим, что объекты класса System.Windows.Forms — это больше, чем обычные
окна Microsoft Windows. Перетаскивая из панели инструментов изображения элементов
управления и настраивая различные параметры, программист может придать такому
окну любой необходимый внешний вид.
Теперь при разработке дизайна приложения программист не ограничен серыми
стандартными кнопками и списками ОС Microsoft Windows. Помимо готовых элементов
управления, значки которых расположены в инструментальных панелях Microsoft Visual
Studio .NET, можно использовать любые другие элементы управления, созданные при
помощи все той же системы разработки.
Например, нетрудно создать элемент управления для ввода данных кредитной
карточки или любой другой подобный элемент управления. Создаваемые элементы
управления помещаются на ту же инструментальную панель, где находятся
стандартные элементы управления, поэтому их использование не вызовет никаких
затруднений.
Визуальное проектирование
приложений C#
А.В. Фролов, Г.В. Фролов
ГЛАВА 2. РАБОТА С MICROSOFT VISUAL STUDIO .NET
УСТАНОВКА MICROSOFT VISUAL STUDIO .NET
СОЗДАНИЕ ПЕРВОГО ПРОЕКТА HELLO
Проекты и решения
Использование мастера проектов
Главное окно Microsoft Visual .NET
Рабочее окно
Вкладка Start Page
Вкладка проектирования формы
Окно Solution Explorer
Редактирование значка приложения
Редактирование исходного текста приложения
Окно просмотра и редактирования свойств объектов
ЗАКРЫТИЕ И ПОВТОРНОЕ ОТКРЫТИЕ РЕШЕНИЙ И ФАЙЛОВ
ИСХОДНЫЙ ТЕКСТ ПРИЛОЖЕНИЯ HELLO
Пространства имен
Класс Form1
Поле components
Конструктор
Метод Dispose
Метод InitializeComponent
Метод Main
ПРИЛОЖЕНИЕ С ОБРАБОТКОЙ СОБЫТИЙ
Добавление кнопки в форму приложения
Изменения в исходном тексте после добавления кнопки
Обработка событий от кнопки
ОТЛАДКА ПРИЛОЖЕНИЙ
Пошаговое выполнение программ
Точка останова
Просмотр содержимого переменных и полей

Глава 2. Работа с Microsoft Visual Studio .NET


Система разработки Microsoft Visual Studio .NET представляет собой современный
инструмент, при помощи которого можно создавать любые приложения, программы и
программные компоненты для ОС Microsoft Windows. Это могут быть как автономные
приложения, такие как информационно-справочные и бухгалтерские системы, так и
Web-приложения, предназначенные для работы в Интернете или в корпоративной
интрасети.
Инструмент этот настолько сложен, что его полное описание заняло бы не одну
книгу. Рассказывая о возможностях языка C# в [3], мы научили Вас создавать с его
помощью консольные программы. Теперь мы сосредоточим внимание на использование
Microsoft Visual Studio .NET для визуального проектирования автономных (или, как их
еще называют, настольных) приложений Microsoft Windows.

Установка Microsoft Visual Studio .NET


Дистрибутив Microsoft Visual Studio .NET занимает несколько компакт-дисков, и на его
установку уйдет немало времени.
Процедура установки запускается обычным образом — Вы вставляете первый
компакт-диск дистрибутива в устройство чтения CD-ROM, и на экране появляется
первое диалоговое окно мастера установки (рис. 2-1).
Рис. 2-1. Первое окно мастера установки Microsoft Visual Studio .NET

Если компьютер будет использоваться для разработки Web-приложений на


платформе Microsoft .NET, то перед установкой Microsoft Visual Studio .NET необходимо
установить Web-сервер Microsoft Internet Information Server (IIS) с расширением
FrontPage Server Extensions. Это можно сделать при помощи управляющей панели ОС.
Обнаружив отсутствие перечисленный выше компонентов, мастер установки
выведет на экран предупреждающее сообщение, показанное на рис. 2-2.

Рис. 2-2. Предупреждающее сообщение о необходимости установки IIS

Так как мы пока не будем разрабатывать Web-приложения, щелкните кнопку


Continue для продолжения установки. Впоследствии, если появится необходимость
разработки Web-приложений (или если она у Вас уже есть), отсутствующие компоненты
можно будет легко добавить.
Мастер установки проверит конфигурацию компьютера и при необходимости
потребует установить дополнительные компоненты с диска Windows Component Update.
В частности, в ОС Microsoft Windows 2000 Professional будет установлен пакет
обновлений Service Pack 2, браузер Microsoft Internet Explorer версии 6.0, среда
исполнения Microsoft .NET Framework и другие компоненты. Список устанавливаемых
компонентов для ОС Microsoft Windows XP показан на рис. 2-3 (этот список может
меняться в зависимости от конфигурации Вашей системы).
Рис. 2-3. Список обновляемых компонентов

Далее Вам будет предложено установить диск Windows Component Update (это
пятый диск дистрибутива). Щелкните ссылку Install Now! (рис. 2-3), и процесс
установки будет продолжен.
Возможно, что в процессе установки обновлений ОС будет несколько раз
перезагружаться. Для удобства можно сообщить программе установки идентификатор и
пароль пользователя для автоматического входа в систему после перезагрузки. В этом
случае процесс установки упростится и фактически будет сведен к своевременной
замене компакт-дисков в приводе CD-ROM.
Когда обновление компонентов ОС будет закончено, щелкните ссылку Done,
расположенную в нижней части окна списка компонентов, и запустите программу
установки заново. Теперь Вы сможете щелкнуть кнопку 2. Visual Studio .NET для
продолжения установки. Вам будет нужно снова вставить в привод CD-ROM первый
диск дистрибутива.
На следующем этапе мастер запросит серийный номер, а также параметры
установки Microsoft Visual Studio .NET. Вам будет предложен список устанавливаемых
компонентов, показанный на рис. 2-4.
Рис. 2-4. Выбор компонентов Microsoft Visual Studio .NET для установки

В минимальном варианте нужно отметить флажки, показанные на рис. 2-4. Это


флажки Visual C# .NET, Enterprise Development Tools, .NET Framework SDK, Tools for
Redistributing Applications и Documentations. Если впоследствии Вам потребуется
установить другие компоненты, то это можно будет сделать позже, запустив программу
установки еще раз.
Те из Вас, кто раньше пользовался языком программирования Basic, возможно,
заинтересуются полностью переработанной версией этого языка и отметят флажок
Visual Basic .NET. Новый Basic способен реализовать практически все возможности
платформы Microsoft .NET, хотя, на наш взгляд, язык C# делает это лучше.
Если у Вас есть большие наработки на языке C++, то Вы сможете интегрировать
их в новые приложения C#. Для этого Вам необходимо отметить галочку Visual C+
+ .NET и модифицировать свои проекты соответствующим образом.
В любом случае мы настоятельно рекомендуем установить документацию (флажок
Documentation). В комплекте с Microsoft Visual Studio .NET идет обширная документация
(рис. 2-5) в виде специальной версии библиотеки Microsoft Developer Network (MSDN).
Рис. 2-5. Документация для Microsoft Visual Studio .NET

Документация необходима, т.к. наша книга (и никакая другая!) не сможет


вместить весь гигантский объем информации, требующийся разработчику приложений
Microsoft Windows.
Информация MSDN доступна также через Интернет на сайте
http://msdn.microsoft.com. Заметим, однако, что в Microsoft Visual Studio .NET встроена
очень удобная система контекстной помощи, для работы которой рекомендуется
установить документацию на диск компьютера.
Для продолжения установки щелкните ссылку Install Now! (рис. 2-4) и следуйте
инструкциям, появляющимся на экране.
При необходимости более подробные инструкции по установке Microsoft Visual
Studio .NET смотрите в только что упомянутой документации. Там, в частности, описан
административный режим установки и установка на удаленный компьютеры. Это может
быть интересно при коллективной работе над проектами или при удаленной отладке
приложений.
И еще один важный момент.
После завершения установки Microsoft Visual Studio .NET необходимо установить
пакет обновления Microsoft .NET Framework Service Pack. Этот пакет исправляет ошибки
в среде исполнения Microsoft .NET Framework и ликвидирует бреши в безопасности.
Самый новый пакет обновления можно найти на сайте компании Microsoft по адресу
http://www.Microsoft.com.
Кроме того, следует установить пакеты обновления и для самой системы
разработки Microsoft Visual Studio .NET (когда они появятся). Это можно сделать при
помощи ссылки 3. Service Releases (рис. 2-1).
Создание первого проекта Hello
Книги, посвященные языкам программирования, почти всегда начинают изложение
материала с традиционной программы, отображающей строку «Hello, World!» в окне
консольного приложения. В самом деле, такая программа позволяет
продемонстрировать наиболее простой способ использования того или иного языка для
создания программы.
Что же касается визуального проектирования приложений, то здесь, на наш
взгляд, нужно начинать с программы, отображающей диалоговое окно с элементами
управления, такими как поля и кнопки. Именно диалоговые окна по большей части
образуют пользовательский интерфейс автономных приложений Microsoft Windows, а
также активных Web-приложений.
Однако прежде чем приступить к созданию приложения, нам нужно изучить
методы управления проектами, реализованные в Microsoft Visual Studio .NET.
Проекты и решения
Практически все существующие системы разработки программного обеспечения
используют так называемую концепцию проекта (project). Проект объединяет все
файлы, необходимые для редактирования, трансляции и отладки приложения. Это
файлы исходных текстов, объектных и загрузочных модулей, файлы ресурсов
приложений и т.п.
В простейшем случае исходный текст программы помещается в одном файле.
Исходные тексты сложных приложений могут насчитывать десятки и сотни файлов
различного типа. Объединение в проект всех файлов, имеющих отношение к
разработке приложения, облегчает управление всем этим файловым хозяйством.
Но ситуация может быть и еще более сложной, если для реализации поставленной
задачи нужно создавать много различных проектов. В системе Microsoft Visual
Studio .NET можно объединять проекты в решения (solution), предоставляющие более
высокий уровень иерархии управления процессом разработки программных
комплексов.
Примеры приложений, приведенных в нашей книге, будут реализованы с
использованием одного решения, в котором находится только один проект. Однако при
необходимости Вы сможете создавать и более сложные решения.
Использование мастера проектов
Теперь мы закончили с теорией, и переходим непосредственно к практике визуального
проектирования приложений Microsoft Windows. Мы считаем, что Вы установили
Microsoft Visual Studio .NET и эта система готова к работе.
Для создания нашего самого первого проекта запустите Microsoft Visual
Studio .NET и выберите из меню File строку New, а затем из меню второго уровня —
строку Project. В результате на экране появится окно мастера проектов, показанное на
рис. 2-6.
Рис. 2-6. Мастер проектов

Список Project Types, расположенный в левом верхнем углу окна, позволяет


выбрать нужный тип проекта. Нас интересуют проекты, создаваемые на языке C#,
поэтому выберите в этом списке строку Visual C# Projects.
Далее необходимо выбрать шаблон создаваемого проекта. Это можно сделать при
помощи списка Templates, занимающего верхний левый угол окна мастера проектов.
Мы будем создавать оконные приложения Microsoft Windows, поэтому выберите здесь
шаблон Windows Application. Заметим, что когда мы рассказывали в [3] о создании
консольных приложений C#, то просили вас выбрать шаблон Console Application.
На следующем этапе следует выбрать каталог, в котором будут создаваться
файлы нашего проекта. Это нужно сделать при помощи кнопки Browse, расположенной
справа от поля Location.
В поле Name следует задать имя проекта, используя для этого латинские буквы.
Мы назвали наш первый проект Hello.
После определения всех необходимых параметров проекта щелкните кнопку OK
для создания файлов проекта. Через некоторое время проект будет создан, а окно
Microsoft Visual .NET примет вид, показанный на рис. 2-7.

Рис. 2-7. Проект Hello создан

Обратите внимание на окно Form1, расположенное в центре рисунка. Это форма


класса System.Windows.Forms, которая будет играть роль главного окна нашего
приложения. Пока это окно пустое, но скоро мы добавим сюда поля, кнопки и другие
элементы управления. Затем мы обеспечим добавленные элементы управления нужной
нам функциональностью.
Для трансляции и запуска проекта нажмите клавишу F5 или выберите из меню
Debug строку Start. Программа будет запущена, а ее окно появится на экране (рис. 2-
8).
Рис. 2-8. Работает программа Hello

Это окно ведет себя подобно обычным окнам Microsoft Windows. Оно имеет
заголовок со значком системного меню, а также со значками, позволяющими изменять
размеры окна и закрывать окно. Заметьте, все это мы сделали, не написав ни одной
строчки программного кода (точнее говоря, за нас все сделал мастер проектов).
Главное окно Microsoft Visual .NET
Теперь, когда мы научились создавать простейшие проекты, посмотрим внимательнее
на главное окно системы разработки Microsoft Visual .NET, чтобы идентифицировать
расположенные там наиболее важные элементы. По мере изложения материала мы еще
не раз будем возвращаться к этому окну.
Рабочее окно
В центре главного окна Microsoft Visual .NET расположено рабочее окно с вкладками.
На рис. 2-7 видны вкладки Start Page и Form1.cs [Design]. По мере работы с проектом в
рабочем окне будут появляться и другие вкладки.
Вкладка Start Page
Вкладка Start Page (рис. 2-9) открывает доступ к списку проектов (вкладка Projects),
системе поиска примеров приложений (вкладка Find Samples), а также ко многим
важным ресурсам Интернета, имеющим отношение к разработке приложений для
платформы Microsoft .NET.
Рис. 2-9. Вкладка Start Page

При помощи ссылок, показанных в левой части рис. 2-9, можно получить доступ
on-line к ресурсам сайта http://msdn.microsoft.com (т.е. к библиотеке MSDN).
Разумеется, для этого компьютер должен быть подключен к Интернету.
Ссылка What’s New позволит просмотреть различные новости, интересные
разработчикам приложений.
Если Вам нужна помощь других пользователей Microsoft Visual .NET и экспертов,
воспользуйтесь ссылкой Online Community. Эта ссылка откроет Вам доступ к
многочисленным конференциям и чатам, где Вы сможете задать свой вопрос и найти на
него ответ.
Воспользовавшись ссылкой Headlines, можно ознакомиться с самыми новыми
ресурсами и новостями, касающимися самых разных аспектов разработки приложений.
Для поиска какой-либо информации по ключевым словам в библиотеке Microsoft
MSDN предназначена ссылка Search Online.
Ссылка Download отправит Вас в раздел сайта MSDN, из которого можно загрузить
различные обновления и дополнения к системе разработки Microsoft Visual .NET,
документацию и другие файлы.
С помощью ссылки XML Web Services можно отыскать нужный Web-сервис, или
зарегистрировать свой собственный. Эта ссылка, а также ссылка Web Hosting,
предназначенная для поиска провайдеров, будет интересна разработчикам Web-
приложений.
И, наконец, на странице My Profile можно настроить некоторые параметры работы
системы Microsoft Visual .NET. Например, здесь Вы можете указать, что при запуске
Microsoft Visual .NET необходимо автоматически загружать проект, с которым работали
в прошлый раз. Для этого в списке At startup нужно выбрать строку Load last loaded
solution (этот список появится в нижней части страницы, открываемой щелчком ссылки
My Profile).
Вкладка проектирования формы
Визуальное проектирование формы осуществляется на вкладке Form1.cs [Design]. Эта
вкладка показана на рис. 2-7.
При необходимости Вы можете изменить размеры создаваемой формы и цвет ее
фона, разместить в форме текст, графические изображения и различные элементы
управления. Для этого нужно щелкнуть левой клавишей мыши панель Toolbox, раскрыв
ее, как это показано на рис. 2-10.

Рис. 2-10. Панель элементов управления

Далее, опять же при помощи мыши, нужно перетащить значки необходимых Вам
элементов управления на поверхность создаваемой формы. На рис. 2-10 мы разместили
таким способом в нашей форме две кнопки и два текстовых поля.
Если это требуется, то с помощью мыши можно отрегулировать размеры и
расположение элементов управления, добавленных в форму, а также размеры самой
формы.
Окно Solution Explorer
Окно Solution Explorer, показанное на рис. 2-11, позволяет просматривать и
редактировать файлы проекта. Просмотр может осуществляться по файлам или по
классам (вкладка Class View).

Рис. 2-11. Окно Solution Explorer

Для нашего приложения было создано одно решение. На рис. 2-11 ему
соответствует корневой значок дерева файлов, обозначенный как Solution ‘Hello’ (1
project).
На следующем уровне иерархии находится значок проекта с надписью Hello.
Далее следуют папки и файлы, входящие в проект.
Папка References содержит перечень классов, на которые имеются ссылки в
файлах исходных текстов проекта. Обратите внимание, что среди перечисленных в
этой папке классов имеется класс System.Windows.Forms, на базе которого создана
наша форма.
Еще в проекте имеются три файла с именами App.ico, AssemblyInfo.cs и Form1.cs.
Первый из этих файлов содержит изображение значка приложения (application
icon). Значок появляется на экране при переключении окон, а также в панели задач
рабочего стола Microsoft Windows. Второй файл с именем AssemblyInfo.cs содержит
описание сборки (assembly) проекта, и, наконец, третий файл Form1.cs хранит
собственно исходный текст нашего приложения, написанный на языке C#.
Редактирование значка приложения
Если дважды щелкнуть имя файла значка левой клавишей мыши, в рабочем окне
системы Microsoft Visual Studio .NET появится окно редактирования значка приложения
(рис. 2-12).
Рис. 2-12. Окно редактирования значка приложения

Сейчас мы не будем останавливаться подробно на том, как пользоваться этим


окном. Заметим только, что редактор значков, встроенный в Microsoft Visual
Studio .NET, сильно напоминает по своим возможностям и приемам работы приложение
Paint, входящее в состав ОС Microsoft Windows.
Редактирование исходного текста приложения
Теперь мы снова вернемся к окну Solution Explorer, показанному на рис. 2-11. Если
щелкнуть имя файла Form1.cs правой клавишей мыши, то на экране появится
контекстное меню. С помощью этого меню можно выбрать один из двух режимов
просмотра файла — в режиме визуального проектирования (строка View Designer) и в
режиме редактирования исходного текста программы (строка View Code).
После того как мастер проектов создал для нас готовый проект приложения Hello,
он открыл файл Form1.cs в режиме визуального проектирования (рис. 2-7). Сейчас же
нас интересует режим редактирования исходного текста приложения. Выбрав из
упомянутого выше контекстного меню строку View Code, Вы откроете тем самым окно
редактирования, показанное на рис. 2-13.
Рис. 2-13. Окно редактирования исходного текста приложения

Это окно содержит полный исходный текст нашего приложения, созданного


автоматически мастером проектов. По мере добавления в форму новых элементов
управления, этот исходный текст будет автоматически модифицироваться. Вам тоже
придется «приложить руки» к редактированию этого текста, так как программирование
обработчиков событий, создаваемых элементами управления, выполняется вручную.
Позже мы рассмотрим этот исходный текст подробнее, а сейчас расскажем об
одной важной особенности редактора исходного текста программ системы Microsoft
Visual Studio .NET. Этот редактор позволяет при просмотре временно скрывать
фрагменты исходного текста, которые представляются несущественными.
Обратите внимание на вертикальные линии, расположенные в левой части окна
редактирования (рис. 2-13). Эти линии, снабженные флажками в виде
прямоугольников, ограничивают фрагменты текста. Щелкая эти флажки, можно
свертывать или разворачивать соответствующие фрагменты текста.
Когда фрагмент текста свернут, соответствующий флажок отмечен знаком плюс, а
когда развернут — знаком минус.
На рис. 2-14 мы свернули список пространств имен, включаемых оператором
using, все комментарии, исходный текст конструктора класса Form1, а также исходный
текст метода Dispose. Кроме того, мы оставили свернутым блок кода, созданный
мастером форм и обозначенный на этом рисунке как Windows Form Designer generated
code.

Рис. 2-14. Сокрытие фрагментов исходного текста

Возможность сворачивать и разворачивать фрагменты редактируемого теста Вы


оцените, когда перейдете к работе над крупными проектами. Она позволит Вам
быстрее находить нужные фрагменты текста, избавив от необходимости
последовательного просмотра листинга.
Окно просмотра и редактирования свойств объектов
Практически каждый компонент создаваемого приложения, такой как форма или
элемент управления, имеет свой набор свойств. Для формы, например, можно задавать
такие свойства, как цвет фона, заголовок, наличие фонового графического
изображения, внешний вид рамки и т.д. Кнопка обладает таким свойством, как цвет,
текст надписи и др.
В правой нижней части главного окна Microsoft Visual Studio .NET (рис. 2-7)
находится окно просмотра и редактирования свойств объектов. Это окно мы показали
на рис. 2-15.
Рис. 2-15. Окно просмотра и редактирования свойств объектов

Пользоваться им очень просто. В левой части этого окна на вкладке Properties


располагается список свойств и групп свойств, а в правой — значения свойств. В
зависимости от разных обстоятельств некоторые из свойств поддаются
редактированию, а некоторые — нет.
Создавая приложения, мы будем постоянно ссылаться на это окно, т.к. настройка
свойств объектов выполняется очень часто. Заметьте, что некоторые числовые и
строчные значения можно вводить в полях редактирования, а некоторые нужно
выбирать из списков. Для выбора цвета на экран выводятся специальные палитры.
Вкладка Dynamic Help (рис. 2-14) поможет получить контекстно-чувствительную
справку. Содержимое этой вкладки определяется исходя из того, какие окна Microsoft
Visual Studio .NET открыты, и какие над ними выполняются действия.
Закрытие и повторное открытие решений и файлов
После того как Вы закончили работу с решением, его нужно закрыть. Решение
закрывается автоматически при завершении работы Microsoft Visual Studio .NET, но его
можно закрыть и явным образом. Для этого нужно выбрать строку Close Solution из
меню File.
Для того чтобы открыть созданное ранее решение, выберите из меню File строку
Open Solution. На экране появится стандартное диалоговое окно выбора файла,
озаглавленное Open Solution. Найдите с его помощью на дисках компьютера каталог
решения и выберите нужный файл с расширением имени *.sln. Этот файл содержит
полное определение решения.
В предыдущих разделах этой главы мы создавали приложение Hello. Для него
мастер проектов создает файл Hello.sln с описанием решения. Именно его и нужно
выбрать, если Вы вначале закрыли решение, а потом решили продолжить работу над
приложением.
Для того чтобы вернуться к работе с тем или иным решением, предназначена
строка Recent Projects меню File. Выбрав эту строку, Вы увидите меню второго уровня
из названий нескольких решений, с которыми недавно работали на этом компьютере.
Заметим, что помимо решений и проектов, Вы можете открывать и закрывать
обычные файлы. Для этого в меню File предусмотрены строки Open и Close. Новый
файл можно создать при помощи строки New, а вернуться к работе над закрытым ранее
файлом — с помощью строки Recent Files меню File.
Исходный текст приложения Hello
Прежде чем вносить какие-либо изменения в приложение Hello, созданное для нас
мастером проектов, мы проведем полное исследование созданного им исходного текста
программы. Это позволит лучше разобраться в структуре программы. Мы сопоставим
эту программу с той, что была написана на языке C и приведена в предыдущей главе.
Полный исходный текст первого варианта программы Hello Вы найдете в листинге
2-1. Далее мы будем исследовать его по частям.
Листинг 2-1. Файл ch02\hello\Form1.cs
using System;
using System.Drawing;
using System.Collections;
using System.ComponentModel;
using System.Windows.Forms;
using System.Data;

namespace Hello
{
/// <summary>
/// Summary description for Form1.
/// </summary>
public class Form1 : System.Windows.Forms.Form
{
/// <summary>
/// Required designer variable.
/// </summary>
private System.ComponentModel.Container components = null;

public Form1()
{
//
// Required for Windows Form Designer support
//
InitializeComponent();

//
// TODO: Add any constructor code
// after InitializeComponent call
//
}

/// <summary>
/// Clean up any resources being used.
/// </summary>
protected override void Dispose( bool disposing )
{
if( disposing )
{
if (components != null)
{
components.Dispose();
}
}
base.Dispose( disposing );
}

#region Windows Form Designer generated code


/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
private void InitializeComponent()
{
this.components = new System.ComponentModel.Container();
this.Size = new System.Drawing.Size(300,300);
this.Text = "Form1";
}
#endregion

/// <summary>
/// The main entry point for the application.
/// </summary>
[STAThread]
static void Main()
{
Application.Run(new Form1());
}
}
}

Пространства имен
В самое начало фала исходного текста приложения мастер проектов вставил строки,
подключающие несколько пространств имен (name space) с помощью ключевого слова
using:
using System;
using System.Drawing;
using System.Collections;
using System.ComponentModel;
using System.Windows.Forms;
using System.Data;
Пространство имен System содержит определение фундаментальных и базовых
классов, определяющих типы данных, события, обработчики событий и другие,
необходимые в каждом приложении компоненты.
Пространство имен System.Drawing необходимо для доступа к интерфейсу
графических устройств (Graphics Device Interface, GDI), а точнее, к его расширенной
версии GDI+. Классы, определенные в этом пространстве имен, необходимы для
рисования в окнах приложений текста, линий, двухмерных фигур, изображений и
других графических объектов.
В пространстве имен System.Collections определены классы, реализующие
функциональность таких контейнеров, как массивы, списки, словари, хэши и т.п.
Классы пространства System.ComponentModel используются для реализации
необходимого поведения компонентов и элементов управления приложения на этапе
его разработки и выполнения.
Пространство имен System.Windows.Forms мы уже упоминали — в нем определены
классы, реализующие поведение оконных форм, составляющих базу оконных
приложений Microsoft windows на платформе Microsoft .NET Frameworks.
Класс System.Data необходим приложениям, работающим с базами данных
посредством интерфейса ADO.NET. Этот интерфейс мы рассмотрим позже в 8 и 9
главах, посвященных интеграции приложений с базами данных.
Наше приложение также определяет собственное пространство имен Hello:
namespace Hello
{

}
В этом пространстве располагается определение класса Form1 приложения Hello.
Класс Form1
Наше приложение содержим в пространстве имен Hello определение только одного
класса Form1:
/// <summary>
/// Summary description for Form1.
/// </summary>
public class Form1 : System.Windows.Forms.Form
{

}
Этот класс создается мастером проектов на базе класса
System.Windows.Forms.Form, о котором мы уже говорили ранее. Обратите внимание,
что мастер проектов добавил перед определением класса комментарий специального
вида, включающий в себя тег <summary> XML-описания класса. Это описание Вы
можете отредактировать по своему усмотрению, отразив в нем какую-либо
осмысленную информацию о назначении класса. При необходимости подробное
описание языка XML Вы найдете в [9].
Наличие подобных комментариев упрощает документирование проекта.
Рассмотрим поля и методы, определенные в классе Form1.
Поле components
В классе Form1 мастер проекта создал одно поле components типа private и класса
System.ComponentModel.Container:
/// <summary>
/// Required designer variable.
/// </summary>
private System.ComponentModel.Container components = null;
Оно представляет собой контейнер, предназначенный для хранения компонентов,
размещаемых в форме. Как Вы увидите дальше, с этим полем работает мастер форм.
Конструктор
Задача конструктора класса Form1, вызываемого при создании новой формы,
заключается в инициализации всех компонентов, размещенных в форме. С этой целью
конструктор вызывает метод InitializeComponent, определенный в классе Form1:
public Form1()
{
//
// Required for Windows Form Designer support
//
InitializeComponent();

//
// TODO: Add any constructor code
// after InitializeComponent call
//
}
Мастер проектов добавил в исходный текст конструктора комментарий, в котором
говорится, что Вы можете добавить после вызова метода InitializeComponent другой
необходимый инициализирующий код.
Метод Dispose
Деструктор класса Form1 в явном виде отсутствует. Однако для освобождения ресурсов
приложения после закрытия формы в этом классе определен метод Dispose:
/// <summary>
/// Clean up any resources being used.
/// </summary>
protected override void Dispose( bool disposing )
{
if( disposing )
{
if (components != null)
{
components.Dispose();
}
}
base.Dispose( disposing );
}
В его задачу входит освобождение ресурсов всех компонентов, хранящихся в
упомянутом выше контейнере components, при закрытии формы. Далее этот метод
вызывает метод Dispose базового класса (т.е. класса System.Windows.Forms.Form) для
выполнения действий по освобождению ресурсов, определенных в этом классе.
Метод InitializeComponent
Мы уже говорили, что конструктор класса Form1 вызывает для инициализации
компонентов приложения метод InitializeComponent. Обратите внимание на то, что
исходный текст этого метода заключен внутри блока, образованного ключевыми
словами #region и #endregion:
#region Windows Form Designer generated code
/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
private void InitializeComponent()
{
this.components = new System.ComponentModel.Container();
this.Size = new System.Drawing.Size(300,300);
this.Text = "Form1";
}
#endregion
В автоматически созданном комментарии к методу InitializeComponent говорится о
том, что этот метод используется мастером форм, и его не следует модифицировать
вручную. В режиме просмотра исходного текста данный блок по умолчанию
отображается в свернутом виде (рис. 2-14), но при необходимости его можно
развернуть.
Что делает метод InitializeComponent?
Прежде всего, этот метод создает новый контейнер для хранения компонентов, и
записывает его в описанное нами ранее поле components:
this.components = new System.ComponentModel.Container();
Далее метод InitializeComponent устанавливает два атрибута формы — ее размер
Size и текст заголовка Text:
this.Size = new System.Drawing.Size(300,300);
this.Text = "Form1";
Так как пока наша форма не содержит никаких элементов управления, на этом
инициализация формы будет закончена.
Метод Main
В классе Form1 определен статический метод Main, получающий управление при
запуске нашего приложения:
/// <summary>
/// The main entry point for the application.
/// </summary>
[STAThread]
static void Main()
{
Application.Run(new Form1());
}
Этот метод играет роль точки входа приложения, с которой и начинается его
работа.
Метод Main очень прост. Он состоит всего из одной строки, в которой вызывается
метод Application.Run:
Application.Run(new Form1());
В качестве параметра методу Application.Run передается ссылка на новый, только
что созданный объект класса Form1 (т.е. на нашу форму).
Как это работает?
Вначале конструктор создает объект класса Form1 — новую форму, выступающую
в качестве главного окна нашего приложения. Метод Application.Run, получая при
вызове ссылку на эту форму, инициализирует цикл обработки сообщений и запускает
его.
При этом метод Application.Run берет на себя регистрацию функции окна и
обработку сообщений, о которых мы рассказывали в 1 главе нашей книги. Поэтому
программисту, создающему оконное приложение на языке C#, не надо об этом
заботиться.
Когда пользователь закрывает главное окно приложения, метод Application.Run
возвращает управление, и приложение завершает свою работу.
В форме нашего первого приложения нет никаких компонентов и элементов
управления, поэтому нет и обработчиков событий, создаваемых этими компонентами.
Теперь мы немного усовершенствуем приложение, добавив в него кнопку и
предусмотрев обработку событий, связанных с этой кнопкой.
Приложение с обработкой событий
В исходном виде главное окно (форма) приложения Hello обладает весьма небольшой
функциональностью. Наше первое усовершенствование приложения будет заключаться
в добавлении кнопки. Если щелкнуть эту кнопку, которой на экране должно будет
появиться простейшее диалоговое окно с сообщением.
Добавление кнопки в форму приложения
Откройте проект приложения Hello, если Вы его закрыли. Затем перетащите левой
клавишей мыши значок кнопки с надписью Button на изображение нашей формы,
открытой в режиме визуального проектирования (рис. 2-16).

Рис. 2-16. Добавление кнопки в форму

По умолчанию на кнопке появится надпись button1. Чтобы ее изменить,


воспользуйтесь окном просмотра свойств, которое показано на рис. 2-17. Напомним,
что это окно находится в левом нижнем углу главного окна Microsoft Visual Studio .NET.
Рис. 2-17. Изменение надписи на кнопке

Отыщите в списке свойство Text (оно выделено на рис. 2-17), и замените его
значение — вместо button1 напишите там строку «Сообщение». Теперь если убрать
текстовый курсор из поля Text, то надпись на кнопке изменится.
Добавив кнопку и отредактировав надпись, нажмите клавишу F5 для трансляции и
запуска программы. Теперь в окне нашей программы появится кнопка, которую можно
нажимать (рис. 2-18).

Рис. 2-18. Работающее приложение с кнопкой

Изменения в исходном тексте после добавления кнопки


Убедившись, что приложение работает должным образом и только что добавленная
кнопка Сообщение нажимается, откройте форму в режиме просмотра исходного текста.
Для этого в окне Solution Explorer щелкните имя файла Form1.cs правой кнопкой мыши
и выберите из контекстного меню строку View Code.
После добавления кнопки в классе Form1 появилось новое поле button1,
предназначенное для хранения ссылки на кнопку:
private System.Windows.Forms.Button button1;
Кнопка создается как объект класса System.Windows.Forms.Button.
Самые большие изменения затронули метод InitializeComponent, отвечающий за
инициализацию формы. Теперь в нем появились строки, отвечающие за создание и
размещение кнопки внутри формы:
#region Windows Form Designer generated code
private void InitializeComponent()
{
this.button1 = new System.Windows.Forms.Button();

this.SuspendLayout();

//
// button1
//
this.button1.Location = new System.Drawing.Point(200, 16);
this.button1.Name = "button1";
this.button1.TabIndex = 0;
this.button1.Text = "Сообщение";

//
// Form1
//
this.AutoScaleBaseSize = new System.Drawing.Size(5, 13);
this.ClientSize = new System.Drawing.Size(292, 273);
this.Controls.AddRange(new System.Windows.Forms.Control[] {
this.button1});
this.Name = "Form1";
this.Text = "Form1";

this.ResumeLayout(false);
}
#endregion
Прежде всего, метод InitializeComponent создает кнопку как объект класса
System.Windows.Forms.Button, и сохраняет ссылку на этот объект в поле button1 для
дальнейшего использования:
this.button1 = new System.Windows.Forms.Button();
После этого начинается процесс размещения кнопки на поверхности формы.
Этот процесс начинается с вызова метода SuspendLayout:
this.SuspendLayout();
Это делается для временного отключения механизма генерации сообщений,
связанных с размещением элементов в окне формы. Такое отключение позволяет
снизить непроизводительные затраты ресурсов процессора при размещении в форме
нескольких компонентов сразу.
После того как все необходимые компоненты будут размещены, механизм
генерации упомянутых выше сообщений включается снова при помощи метода
ResumeLayout:
this.ResumeLayout(false);
В промежутке между вызовами методов SuspendLayout и ResumeLayout программа
добавляет и размещает элементы управления, а также настраивает их свойства.
Первым делом в форму добавляется наша кнопка:
this.button1.Location = new System.Drawing.Point(200, 16);
Для рисования кнопки внутри окна формы в определенной позиции программа
вызывает метод System.Drawing.Point. Координаты места расположения кнопки по
горизонтальной и вертикальной оси координат передаются методу через параметры.
Далее программа устанавливает свойства кнопки:
this.button1.Name = "button1";
this.button1.TabIndex = 0;
this.button1.Text = "Сообщение";
Свойство Name хранит идентификатор кнопки. При необходимости этот
идентификатор можно отредактировать в окне, показанном на рис. 2-17.
В свойстве Text хранится надпись на кнопке. Что же касается свойства TabIndex,
то оно приобретает значение, когда в форме имеется несколько элементов управления,
таких как кнопки, поля редактирования, списки и т.д. Это свойство задает порядок
выбора элементов в окне формы при использовании клавиши табуляции. Напомним,
что с помощью клавиши табуляции можно перемещать фокус ввода между элементами
управления обычных диалоговых окон Microsoft Windows.
После добавления кнопки и настройки ее свойств метод InitializeComponent задает
свойства самой формы:
this.AutoScaleBaseSize = new System.Drawing.Size(5, 13);
this.ClientSize = new System.Drawing.Size(292, 273);
this.Controls.AddRange(
new System.Windows.Forms.Control[]
{
this.button1
}
);
Свойство AutoScaleBaseSize задает базовые размеры, которые используются
формой для автоматического масштабирования. При этом за основу берется размер
системного шрифта.
При помощи свойства ClientSize программа определяет размеры так называемой
клиентской области окна (client area) нашей формы. Эта область не включает в себя
заголовок окна, рамку и полосы прокрутки.
И, наконец, при помощи метода Controls.AddRange программа добавляет в форму
контейнер с массивом System.Windows.Forms.Control[] элементов управления. В нашем
случае этот массив состоит из одной кнопки с идентификатором button1.
Последнее, что делает метод InitializeComponent, это настройка свойств формы
Name и Text:
this.Name = "Form1";
this.Text = "Form1";
Первое из них задает имя (идентификатор) формы, а второе — текстовую строку,
отображаемую в заголовке формы.
Заметьте, что мы сделали лишь несколько движений мышью, добавив кнопку в
окно формы. При этом мастер форм автоматически добавил весь код, необходимый для
создания и размещения кнопки.
Обработка событий от кнопки
Хотя в усовершенствованной версии приложения есть кнопка, она не несет никакой
полезной нагрузки. Теперь нашей задачей будет создание для этой кнопки обработчика
события. Когда пользователь щелкает кнопку мышью, этот обработчик должен будет
выводить на экран компьютера диалоговое окно с сообщением.
Чтобы обеспечить обработку событий от кнопки, дважды щелкните мышью ее
изображение в окне дизайнера форм (рис. 2-16). Сразу после этого в рабочей области
Microsoft Visual Studio .NET откроется окно редактирования исходного текста
программы, в который будет добавлен метод button1_Click:
private void button1_Click(object sender, System.EventArgs e)
{

}
Пока этот метод, получающий управление при щелчке кнопки, ничего не делает,
однако мы скоро изменим данное обстоятельство.
Метод button1_Click — это обработчик события, возникающего, когда
пользователь щелкает кнопку. Чтобы этот метод заработал, его нужно подключить на
этапе инициализации формы. Такое подключение обеспечивает следующая строка,
добавленная дизайнером формы в метод InitializeComponent:
this.button1.Click += new System.EventHandler(this.button1_Click);
Чтобы в процессе обработки сообщения от кнопки вывести на экран сообщение,
измените исходный текст метода button1_Click следующим образом:
private void button1_Click(object sender, System.EventArgs e)
{
MessageBox.Show("Для Вас есть сообщение!");
}
Мы вставили сюда вызов метода MessageBox.Show. Этот метод отображает на
экране диалоговое окно с текстом сообщения, переданного в качестве параметра (рис.
2-19).
Рис. 2-19. Диалоговое окно с сообщением

Итак, мы в течение нескольких минут сделали вполне работоспособное


приложение Microsoft Windows, добавив вручную только одну строку кода с вызовом
метода MessageBox.Show. Всю остальную работу за нас провернул дизайнер форм.
Отладка приложений
Система разработки приложений Microsoft Visual Studio .NET содержит мощные средства
отладки, необходимые при разработке сложных программ и программных комплексов.
Мы расскажем только о некоторых возможностях, которые пригодятся Вам при отладке
автономных приложений C#.
Пошаговое выполнение программ
Один из часто применяемых способов отладки программ заключается в их пошаговом
выполнении. При этом отладчик выполняет программу построчно, предоставляя на
каждом шаге возможность просмотра и редактирования содержимого переменных и
других объектов программы.
Чтобы приступить к пошаговой отладке программы, откройте решение и затем
выберите из меню Debug строку Step Into или Step Over. Вы также можете
использовать вместо этих строк клавиши F11 и F10, соответственно.
Клавиша F11 предназначена для пошагового выполнения программ с входом в
тело методов и функций. Что же касается клавиши F10, то она тоже нужна для
пошагового выполнения программ, но при ее использовании вход в тело методов и
функций не выполняется.
Таким образом, если нужно отладить функцию, то ее вызов следует выполнить
клавишей F11, а если функция должна быть выполнена без отладки, — то клавишей
F10.
Давайте попробуем выполнить по шагам наше приложение Hello. Для этого
откройте решение в программе Microsoft Visual Studio .NET и нажмите клавишу F11.
Через некоторое время в окне Microsoft Visual Studio .NET появится исходный текст
программы, причем первая строка метода Main будет выделена желтым цветом. Кроме
того, эта строка будет отмечена слева стрелкой (рис. 2-20).
Рис. 2-20. Запуск программы в режиме пошаговой отладки

Если теперь нажать клавишу F11, отладчик приступит к созданию объекта класса
Form1. Вначале будет проинициализировано поле components, а затем, при
последующих нажатиях клавиши F11, отладчик перейдет к пошаговому выполнению
конструктора класса Form1. На рис. 2-21 показан как раз этот момент.

Рис. 2-21. Отладка конструктора Form1

Теперь если нажать кнопку F10, метод InitializeComponent будет выполнен без
отладки. При помощи кнопки F11 Вы сумеете отладить строки метода
InitializeComponent в пошаговом режиме.
Еще Вам может оказаться полезной клавиша Shift-F12, соответствующая строке
Step Out меню Debug. При помощи этой строки можно перевести программу из
пошагового режима в автоматический. Однако после выхода из тела текущего метода
программа снова перейдет в пошаговый режим.
Для того чтобы продолжить выполнение с текущего места без отладки, нажмите
клавишу F5 или выберите строку Continue меню Debug.
Точка останова
Отлаживая программу Hello, Вы можете столкнуться с одной проблемой. Выполняя
программу по шагам, Вы не сможете отладить обработчик события, создаваемого в
результате нажатия кнопки Сообщение. В самом деле, после прохода конструктора
класса Form1 программа будет работать уже без отладки.
Чтобы отладить обработчик события, необходимо установить так называемую
точку остановки (breakpoint). Для этого щелкните левой клавишей мыши в узкое
вертикальное поле слева от той строки кода, на которой нужно установить точку
останова. После этого строка будет выделена красно-кирпичным цветом, а слева
напротив нее появится жирная точка (рис. 2-22).
Рис. 2-22. Установка точки останова

Таким способом Вы можете установить в своей программе несколько точек


останова.
Чтобы убрать точку останова, щелкните жирную точку, отмечающую строку
программы, еще раз.
Установив точку останова напротив вызова метода MessageBox.Show, нажмите
кнопку F5 для запуска программы без отладки. Теперь, когда на экране появится окно
программы, щелкните расположенную в нем кнопку Сообщение. Когда в процессе
своей работы программа дойдет до отмеченной строки, произойдет останов и переход в
режим пошагового выполнения программы. Строка, выделенная на рис. 2-22, будет
отмечена желтым цветом, а слева от нее в кружке появится стрелка.
Заметим, что при отладке Вы не сможете «войти» внутрь функций библиотеки
Microsoft .NET Frameworks, так что ее исходные тексты будут Вам недоступны.
Точки останова удобно использовать и для отладки программ, содержащих циклы.
Если в цикле делается много итераций, то его прохождение в пошаговом режиме может
отнять слишком много времени. Выход простой — установите точку останова после
цикла и продолжите работу программы без отладки.
Просмотр содержимого переменных и полей
В процессе пошагового выполнения программы или при достижении точки останова Вы
можете просмотреть содержимое переменных и полей объектов отлаживаемой
программы. В этом Вам поможет вкладка Locals окна просмотра Locals, отображаемое
при отладке в левом нижнем углу главного окна приложения Microsoft Visual
Studio .NET (рис. 2-23).
Рис. 2-23. Просмотр содержимого переменных и полей

В левой части этой вкладки находится дерево объектов программы. Раскрывая его
ветви, Вы можете получить доступ ко всем необходимым Вам переменным и полям.
Вкладка Autos (рис. 2-24) удобна для контроля текущих изменений значений в
полях и переменных.

Рис. 2-24. Вкладка Autos

На этой вкладке автоматически появляются те поля и переменные, значения


которых были изменены в результате выполнения текущей строки кода. При этом
новые значения выделяются красным цветом.
Для отслеживания содержимого каких либо переменных или полей на протяжении
отладки программы предназначена вкладка Watch. На ней отображается список
отслеживаемых полей и соответствующие значения (рис. 2-25).

Рис. 2-25. Вкладка Watch

Чтобы добавить значение поля или переменной на вкладку Watch, необходимо


выделить это значение левой клавишей мыши, а потом щелкнуть его правой клавишей
мыши. На экране появится контекстное меню, из которого следует выбрать строку Add
Watch.
Другая очень удобная возможность просмотра содержимого полей и переменных
предоставляется в окне редактирования исходного текста программы. Если Вам нужно
узнать содержимое переменной или поля, установите на нее курсор мыши. Через
некоторое время около курсора появится необходимое содержимое, выделенное тонкой
рамкой (рис. 2-26).

Рис. 2-26. Просмотр значений полей в редакторе исходного текста

На этом, однако, возможности просмотра значений не исчерпываются. Еще можно


щелкнуть правой клавишей мыши нужное поле, а затем выбрать из контекстного меню
строку QuickWatch. На экране появится одноименное окно, показанное на рис. 2-27.
Рис. 2-27. Окно QuickWatch

Здесь, пользуясь деревом полей выбранного Вами объекта, можно просмотреть и,


при необходимости, изменить значение полей.

Визуальное проектирование
приложений C#
А.В. Фролов, Г.В. Фролов
ГЛАВА 3. ФОРМЫ В ПРИЛОЖЕНИЯХ C#
НАСТРОЙКА ВНЕШНЕГО ВИДА И ПОВЕДЕНИЯ ФОРМЫ
Изменение заголовка окна
Изменение значка формы
Выбор рамки
Кнопки изменения размеров формы
Изменение цвета фона формы
Добавление фонового изображения
Изменение формы курсора
ДОБАВЛЕНИЕ ЭЛЕМЕНТОВ УПРАВЛЕНИЯ
Добавление и настройка текстовых полей
Изменение текста надписи
Изменение шрифта
Цвет текста и фона
Рамка вокруг поля
Изменение курсора мыши
Выравнивание текста
Настройка свойств текстовых полей LinkLabel
Неиспользованные ссылки
Использованные ссылки
Активные ссылки
Заблокированные ссылки
Добавление графических изображений
ПОЗИЦИОНИРОВАНИЕ ЭЛЕМЕНТОВ УПРАВЛЕНИЯ В ФОРМЕ
Привязка к сетке
Настройка взаимного расположения элементов формы
Выравнивание
Установка размеров
Установка промежутков между элементами
Центровка элементов в окне формы
Передний и задний план
ФОРМА И КЛАВИША ТАБУЛЯЦИИ
Свойство TabStop
Свойство TabIndex
ОБРАБОТКА СОБЫТИЙ
Обработка идентификатора и пароля
Закрытие формы
Ссылки на ресурсы Интернета
Изменение внешнего вида ссылки
Запуск браузера
Отправка почтового сообщения
Полный исходный текст приложения UserLogin
Пространства имен
Класс Form1
Поля класса Form1
Инициализация приложения
Инициализация текстовых полей
Инициализация полей ввода текста
Инициализация графических изображений
Инициализация кнопок
Инициализация полей LinkLabel
Инициализация формы
УДАЛЕНИЕ ОБРАБОТЧИКА СОБЫТИЙ

Глава 3. Формы в приложениях C#


В предыдущей главе мы познакомили Вас с формами, создаваемыми на базе класса
System.Windows.Forms и подробно рассмотрели исходный текст несложного
приложения Hello. Вы научились размещать на поверхности форм элементы
управления, перетаскивая их мышью из панели инструментов, создавать обработчики
событий, создаваемых этими элементами управления. Вы также познакомились с
основными приемами отладки приложений C#.
В этой главе мы рассмотрим формы подробнее и научимся задавать для них
различные свойства.
Настройка внешнего вида и поведения формы
Когда мастер проектов создает форму, она имеет название Form1, размеры и другие
свойства, принятые по умолчанию. В нем нет меню, инструментальных панелей, строки
состояния, полос прокрутки и других атрибутов, которыми обычно снабжаются окна
Microsoft Windows.
Теперь мы научим Вас настраивать внешний вид формы, чтобы придать ей
необходимый дизайн. Как и следовало ожидать, большинство операций по настройке
внешнего вида формы выполняется визуально без «ручного» программирования.
Изменение заголовка окна
Для начала поставим перед собой цель создать форму, предназначенную для
идентификации пользователей. В этой форме нужно будет предусмотреть поля ввода
идентификатора пользователя и пароля, а также кнопки Войти и Отменить. С помощью
первой кнопки пользователь сможет попытаться «войти» в систему, а с помощью
второй — отказаться от этой операции.
Вначале Вам следует создать приложение с названием UserLogin, пользуясь для
этого приемами, описанными в предыдущей главе. Пока не добавляйте в окно никакие
элементы управления, мы займемся этим позже.
Первое, что мы сделаем, это изменим заголовок формы.
Щелкните правой кнопкой мыши форму Form1, открытую в окне визуального
проектирования приложения, и выберите из контекстного меню строку Properties. При
этом в окне свойств, расположенном в правом нижнем углу главного окна Microsoft
Visual Studio .NET, будут показаны свойства нашей формы (рис. 3-1).
Рис. 3-1. Просмотр свойств формы Form1

Чтобы изменить заголовок окна, отредактируйте свойство Text. Замените строку


Form1 строкой «Идентификация пользователя». После этого нажмите клавишу F5,
чтобы оттранслировать и запустить приложение. Теперь Вы увидите измененный
заголовок окна (рис. 3-2).

Рис. 3-2. Мы изменили заголовок окна

Изменение значка формы


В верхнем левом углу формы находится значок (icon) формы. Этот значок представляет
окно формы на панели задач Microsoft Windows, а также в окне, появляющимся на
экране компьютера при использовании клавиши табуляции для переключения между
окнами различных приложений.
По умолчанию мастер проектов добавляет в создаваемый проект файл значка с
именем App.ico с изображением цветных прямоугольников. Однако каждое приложение
и, возможно, каждая форма приложения, должна иметь собственные значки, чтобы их
было легче различать.
Чтобы нарисовать новый значок, нужно обладать определенными
художественными способностями. Вы можете поручить эту работу профессиональному
дизайнеру или воспользоваться готовыми значками.
Много платных и свободно доступных рисунков можно найти в Интернете,
воспользовавшись любой поисковой системой и ключевыми словами «free clipart».
Большая коллекция значков поставляется вместе с системой Microsoft Visual studio .NET.
После установки этой системы Вы сможете найти ее в папке Program files\Microsoft
Visual Studio .NET\Common7\Graphics\icons.
Для того чтобы изменить значок нашей формы идентификации пользователей,
скопируйте файл SECUR04.ICO (или любой другой, который Вам понравится) из
каталога Program files\Microsoft Visual Studio .NET\Common7\Graphics\icons\Misc в
каталог с файлами проекта приложения UserLogin.
Затем найдите свойство формы с названием Icon, и щелкните его левой клавишей
мыши. Затем щелкните появившуюся кнопку с многоточием (рис. 3-3). Наличие
многоточия свидетельствует о том, что для редактирования данного свойства будет
открыто дополнительное диалоговое окно.
Рис. 3-3. Изменение значка формы

Выберите только что скопированный файл значка SECUR04.ICO в стандартном


окне выбора файлов. После этого новый значок появится в верхнем левом углу
создаваемой формы.
Далее нужно включить новый файл значка в проект нашего приложения.
Добавление новых файлов и объектов в проект выполняется при помощи строки
Add New Item меню Project главного окна Microsoft Visual Studio .NET. Существующие
файлы и объекты добавляются строкой Add Existing Item того же меню.
Так как мы будем добавлять готовый файл значка SECUR04.ICO, воспользуйтесь
строкой Add Existing Item. После ее выбора на экране появится одноименное
диалоговое окно. Выберите в этом окне файл добавляемого значка. Как только Вы это
сделаете, имя файла появится в окне Solution Explorer (рис. 3-4).

Рис. 3-4. Файл значка добавлен в проект

При необходимости можно отредактировать файл значка. Для этого нужно дважды
щелкнуть левой клавишей мыши его имя в окне Solution Explorer.
Заметим, что файл значка *.ico может содержать не одно, а сразу несколько
изображений, каждое из которых имеет свой размер и свою палитру (т.е. набор
цветов). В использованном нами файле SECUR04.ICO имеются два изображения, одно
из которых (большое) имеет размер 32х32 пиксела, а другое (маленькое) — размер
16х16 пикселов. Максимальное количество цветов обоих изображений равно 16.
Для того чтобы отредактировать нужное изображение, выберите из меню Image
главного окна Microsoft Visual Studio .NET строку Current Icon Image Types.
Чтобы значок приложения смотрелся лучше при разных размерах и экранных
разрешениях, возможно, следует добавить в файл пиктограммы еще несколько
изображений. Для этого, находясь в режиме редактирования значка, выберите из меню
Image строку New Icon Image Type. При этом на экране появится список типов
изображений, показанный на рис. 3-5.
Рис. 3-5. Добавление изображения нового типа

Если Вы предполагаете, что создаваемое приложение будет работать на


компьютерах с монохромным монитором, добавьте изображения, отмеченные как
Monochrome. С помощью кнопки Custom можно создать значки с произвольным
размером.
Проделав все описанные выше действия по изменению значка формы,
оттранслируйте и запустите приложение на выполнение кнопкой F5. Теперь окно
формы примет вид, показанный на рис. 3-6.

Рис. 3-6. Новый значок формы

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

Рис. 3-7. Выбор рамки для нашей формы

По умолчанию это свойство имеет значение Sizable. Чтобы пользователь не мог


изменять размеры окна при помощи рамки, нужно выбрать одно из значений данного
свойства, в названии которого присутствует слово Fixed. Словом Sizable отмечены типы
рамок, с помощью которых допускается изменение размеров окна формы.
Если выбрать значение None, у формы вообще не будет ни рамки, ни заголовка.
Закрыть такое окно можно будет только при помощи комбинации клавиш Alt+F4 (если,
конечно, в окне нет кнопки, предназначенной для закрытия формы).
Типы рамок, в названии которых присутствует строка ToolWindow, предназначены
для создания инструментальных панелей, таких как панели элементов управления и
компонентов Microsoft Visual Studio .NET или инструментальных панелей графического
редактора Adobe Photoshop.
Вы можете провести эксперименты, выбирая разные типы рамок и запуская наше
тестовое приложение UserLogin. На рис. 3-8 мы показали внешний вид формы, для
которой установлен тип рамки Fixed3D. Это трехмерная рамка, не допускающая
изменение размеров окна формы.

Рис. 3-8. Рамка типа Fixed3D

Кнопки изменения размеров формы


Хотя мы запретили изменение размеров окна при помощи рамки, пользователь все же
может раскрыть окно на весь экран (максимизировать окно) или свернуть его в панель
задач (минимизировать) при помощи управляющих кнопок, расположенных в правой
части заголовка формы.
Если по логике работы приложения такие действия недопустимы, следует
блокировать некоторые или все упомянутые выше кнопки, настраивая
соответствующим образом свойства формы.
Свойства MaximizeBox и MinimizeBox по умолчанию имеют значение True, в
результате чего в заголовке окна появляются разблокированные кнопки максимизации
и минимизации формы. Установив для этих свойств значение False, можно получить
окно, в котором имеется только кнопка закрытия формы (рис. 3-9).

Рис. 3-9. Размеры этого окна изменить нельзя

Установив значение свойства ControlBox, равным False, можно вообще убрать все
кнопки из заголовка формы. Если же задать значение True свойствам ControlBox и
HelpButton, в заголовке окна появятся две кнопки, первая из которых предназначена
для получения подсказки, а вторая — для закрытия окна (рис. 3-10).

Рис. 3-10. Появилась кнопка запроса подсказки

Если щелкнуть кнопку подсказки, курсор мыши будет снабжен вопросительным


знаком. Пользователь сможет подвести такой курсор к интересующему его объекту
формы и, щелкнув этот элемент, узнать его назначение.
Изменение цвета фона формы
По умолчанию формы, создаваемые мастером проектов, имеют скучный серый цвет, как
и все стандартные диалоговые окна ОС Microsoft Windows первых версий. К счастью,
Вы можете легко задать необходимый фоновый цвет формы и даже указать для формы
фоновое изображение, редактируя ее свойства.
Чтобы изменить цвет фона формы, необходимо отредактировать свойство
BackColor. Щелкнув кнопку раскрытия списка, расположенную справа от названия
данного свойства, Вы откроете диалоговое окно с тремя вкладками, показанное на рис.
3-11.

Рис. 3-11. Диалоговое окно выбора цвета

На вкладке System находится список так называемых системных цветов ОС


Microsoft Windows. Эти цвета может отображать любой видеоконтроллер, даже
работающий в «доисторическом» режиме VGA.
Вкладка Web содержит более обширный список цветов, которые гарантированно
отображаются браузерами при низком цветовом разрешении видеоадаптера (в режиме
отображения 256 цветов).
И, наконец, с помощью вкладки Custom можно задать любой цвет, выбрав его из
палитры или указав значения отдельных составляющих компонентов цвета.
Большинство современных компьютеров оборудовано видеоадаптерами,
способными отображать более 17 млн. цветов в так называемом режиме TrueColor.
Однако для совместимости с более низкими режимами цветового разрешения Вы
можете ограничить палитру цветов приложения, выбирая цвета на вкладке Web или
даже System. Заметим, что монохромные режимы, а также режимы с низким цветовым
разрешением могут встречаться в карманных компьютерах, а также в старых
компьютерах типа Notebook и телевизионных приставках.
Добавление фонового изображения
При создании Web-страниц на языке HTML Web-дизайнеры очень часто пользуются
таким приемом, как добавление на страницу фонового изображения. Здесь обычно
используется изображение небольшого размера, которое браузер тиражирует
(размножает) по поверхности страницы.
Этот дизайнерский прием позволяет, с одной стороны, улучшить внешний вид
страницы, а с другой — обеспечить небольшое время загрузки страницы по медленным
каналам Интернета. Последнее достигается благодаря небольшому размеру файла
фонового изображения.
Для нашего приложения UserLogin создайте в любом графическом редакторе
(например, в Adobe Photoshop), файл bkg.gif с размерами 1х4000 пикселов. Левую
часть этого узкого и длинного изображения раскрасьте, например, в темно-синий цвет,
а правую — в светло-голубой или белый цвет.
Затем скопируйте файл bkg.gif в каталог проекта и добавьте его в проект при
помощи строки Add Existing Item меню Project.
Для того чтобы назначить это изображение в качестве фонового, отредактируйте
свойство формы с именем BackgroundImage. Щелкнув кнопку с изображением
многоточия, расположенную справа от имени этого свойства, выберите в появившемся
на экране диалоговом окне файл bkg.gif.
Теперь окно нашей формы примет вид, показанный на рис. 3-12.
Рис. 3-12. Фоновое изображение в окне формы

В левой части этого окна появилась широкая полоса, образовавшаяся в


результате тиражирования фонового изображения по вертикали.
Заметьте, мы создали файл bkg.gif с очень большой длиной по горизонтали,
превышающей горизонтальное разрешение современных мониторов. Это сделано для
того, чтобы данное изображение не тиражировалось по горизонтали, иначе вместо
одной вертикальной полосы в широком окне может появиться несколько таких полос.
Этот прием используется и при создании Web-страниц.
В качестве фона автономного приложения можно применить практически любое
графическое изображение, однако учтите, что на загрузку изображения размером в
несколько мегабайт может уйти немало времени (особенно если производительность
компьютера не слишком высока). Поэтому мы рекомендуем не перегружать форму
фоновыми изображениями большого размера без особой на то необходимости.
Хотя длина нашего изображения bkg.gif по горизонтали велика, размер файла
очень мал (всего 153 байта). Дело в том, что в изображении используется всего два
цвета, а его высота составляет всего один пиксел. Поэтому на загрузк файла bkg.gif в
окно формы будет происходить очень быстро.
Изменение формы курсора
По умолчанию курсор мыши в окне формы изображается в виде стрелки. Это
стандартный курсор для ОС Microsoft Windows. При необходимости нетрудно выбрать
курсор другой формы или даже нарисовать свой собственный курсор.
Чтобы выбрать курсор, необходимо отредактировать свойство Cursor формы. По
умолчанию это свойство имеет значение Default, что соответствует обычному курсору в
виде стрелки. При редактировании свойства Cursor Вам будет предложен список,
показанный на рис. 3-13.

Рис. 3-13. Выбор формы курсора мыши

Курсор, в изображении которого присутствуют, например, песочные часы, обычно


используют для индикации состояния ожидания, когда приложение занято какой-то
работой, и пользователю нужно немного подождать. Назначение курсоров различной
формы описано в пользовательской документации к ОС Microsoft Windows.
На рис. 3-14 мы показали окно приложения UserLogin, в котором используется
курсор мыши в виде руки.

Рис. 3-14. Курсор в виде руки

Мы, однако, не рекомендуем сильно увлекаться использованием курсоров мыши


нестандартной формы, т.к. это может запутать пользователя.
Добавление элементов управления
Как мы уже говорили в предыдущей главе, в форму можно добавлять различные
элементы управления, формирующие пользовательский интерфейс, а также
программные компоненты. Эта операция выполняется простым перетаскиванием
мышью значков необходимых элементов или компонентов в форму из
инструментальных панелей.
Особенности добавления и обработки событий для наиболее важных компонентов
будут предметом изложения довольно значительной части нашей книги. В этом разделе
мы расскажем только о добавлении таких компонентов, как текстовые надписи,
графические изображения, поля ввода текстовой информации и кнопки. Именно эти
компоненты потребуются нам для завершения работы над формой идентификацией
пользователей в приложении UserLogin.
При добавлении компонентов мы будем пользоваться инструментальной панелью,
показанной на рис. 3-15.

Рис. 3-15. Значки элементов управления на инструментальной панели Toolbox

В итоге всех действий, описанных в этой главе книги, форма приложения


UserLogin должна принять вид, показанный на рис. 3-16.
Рис. 3-16. Готовая форма идентификации пользователей

Как видите, по внешнему виду эта форма мало похожа на стандартные


диалоговые окна Microsoft Windows. Она скорее напоминает формы, размещаемые на
страницах Web-сайтов. На наш взгляд, сегодня имеет смысл придавать приложениям
именно такой вид, привычный для современных пользователей Интернета.
Что нового появилось в этой форме?
Прежде всего, мы добавили логотип и текстовые надписи. Кроме этого, в форме
появились два поля ввода текстовой информации, первое из которых (обычное)
предназначено для ввода идентификатора пользователя, а второе (специальное) — для
ввода пароля. Свойства этого поля настроены таким образом, что введенные символы
пароля не отображаются, а заменяются звездочками.
Далее, в нижнюю часть формы мы добавили две ссылки на ресурсы Интернета.
Когда обработчики событий формы будут готовы, щелчок первой из этих ссылок
запустит браузер и загрузит в его окно главную страницу службы восстановления
данных DataRecovery.Ru. Вторая ссылка может быть использована для отправки
электронного сообщения автору этой книги.
Назначение кнопок Войти и Отменить очевидно. Первая из них позволяет войти в
систему администрирования, а вторая просто закрывает форму без выполнения каких-
либо дополнительных действий.
Добавление и настройка текстовых полей
В инструментальной панели Toolbox предусмотрено два типа полей для создания
текстовых надписей в формах. Это поля Label и LinkLabel (рис. 3-15).
При помощи поля Label можно добавлять в форму обычные надписи, выполненные
заданным шрифтом. Поле LinkLabel предназначено для добавления в форму ссылок на
ресурсы Интернета, такие как адреса Web-сайтов и серверов FTP, адреса электронной
почты и пр. Разумеется, Вы можете использовать эти поля и другим образом. Например,
когда пользователь щелкнет такое поле мышью, приложение может вывести на экран
новую форму.
Создавая форму, показанную на рис. 3-16, мы перетащили из панели
инструментов несколько полей Label, разместив их на поверхности формы. Для каждого
поля необходимо настроить свойства, изменив нужным образом текст надписи, шрифт и
цвет текста. Кроме того, нужно отрегулировать мышью размеры областей, выделенных
для поля.
Рассмотрим основные свойства полей Label, которые чаще всего требуется
редактировать.
Изменение текста надписи
Текст надписи задается свойством Text. Вы можете задать здесь любую текстовую
строку с использованием латинского алфавита или символов кириллицы, цифр и знаков
пунктуации.
Изменение шрифта
Чтобы задать шрифт, которым отображается текстовая строка, отредактируйте свойство
Font. При этом вам будет предложено выбрать шрифт из списка шрифтов,
установленных на Вашем компьютере (рис. 3-17). Здесь можно выбрать название
шрифта, его стиль, размер и (что важно для отечественных разработчиков), написание
(в поле Script).
Рис. 3-17. Выбор шрифта для отображения текстовых надписей

Создавая приложения, которые предназначены для работы на различных


компьютерах, не увлекайтесь особенно выбором нестандартных шрифтов. Если
указанного шрифта не найдется на компьютере пользователя, то вместо него ОС
подберет другой, наиболее подходящий с ее точки зрения, шрифт. И вовсе не
обязательно, что внешний вид Вашей формы от этого улучшится.
Мы рекомендуем по возможности пользоваться только самыми основными
шрифтами, входящими в комплект ОС Microsoft Windows, такими как Arial, Times New
Roman, Courier New. В тех же случаях, когда необходимо использовать какой-то
особенный шрифт, следует упомянуть об этом в инструкции по установке. Если у Вас
есть лицензия на распространение шрифта, то его можно поставлять вместе с
приложением.
Более подробно мы расскажем о шрифтах в 10 главе нашей книги.
Цвет текста и фона
Для того чтобы указать цвет символов добавляемого текстового поля, отредактируйте
свойство ForeColor. При этом Вам будет предложено выбрать цвет из диалогового окна
с вкладками, показанными ранее на рис. 3-11.
При необходимости можно изменить и цвет фона надписи. Это можно сделать,
изменив значение свойства BackColor.
Рамка вокруг поля
Свойство BorderStyle указывает, надо ли заключать текстовое поле в рамку, и если
надо, то в какую. По умолчанию это свойство имеет значение None, в результате чего
рамка не используется. Если нужно заключить поле в тонкую рамку, задайте для этого
свойства значение FixedSingle, а если в трехмерную — значение Fixed3D.
Изменение курсора мыши
Если отредактировать значение свойства Cursor, то можно изменить внешний вид
курсора, когда он оказывается над текстовым полем.
Надо сказать, что для обычных надписей, выполненных с использованием полей
Label, эта операция выполняется достаточно редко. Что же касается полей LinkLabel,
применяемых для размещения в формах ссылок, то по умолчанию внутри этих полей
курсор мыши приобретает форму руки. Это поведение полностью согласуется с
поведением курсора мыши внутри окна браузера при просмотре обычных Web-
страниц — когда курсор мыши указывает на ссылку, он превращается в «указующую»
руку.
Выравнивание текста
Еще одно важное свойство TextAlign текстовых полей задает способ выравнивания
текста в прямоугольной области, отведенной для поля. Если попытаться его
отредактировать, на экране появится окно выбора способа выравнивания, показанное
на рис. 3-18.
Рис. 3-18. Выбор выравнивания текста

Чтобы задать нужный способ выравнивания, щелкните одну из расположенных в


этом окне кнопок. По умолчанию текст выравнивается по левой верхней границе,
поэтому соответствующая кнопка изображена в нажатом состоянии.
Нажимая те или иные кнопки, нетрудно выровнять текст по вертикали и по
горизонтали. При этом в окне дизайнера форм Вы сразу увидите результат своей
работы.
Настройка свойств текстовых полей LinkLabel
Как мы уже говорили, поля LinkLabel предназначены для добавления в форму ссылок
на ресурсы Интернета или на другие формы. В этом разделе мы расскажем о настройке
основных свойств этого поля на этапе проектирования формы.
Большинство настроек свойств полей LinkLabel выполняются точно таким же
образом, что и полей Label. Например, текст надписи задается свойством Text, а
шрифт — свойством Font. Вид рамки определяется при помощи свойства BorderStyle,
форма курсора — свойства Cursor, а выравнивание — свойства TextAlign.
Однако есть несколько свойств, специфических для полей LinkLabel. Их мы и
рассмотрим в этом разделе. Самые важные из них — это свойства LinkColor,
VisitedLinkColor, ActiveLinkColor и DisabledLinkColor.
Данные свойства определяют цвета ссылок, находящихся в различных
состояниях. Чтобы автономное приложение было больше похоже по своему дизайну на
Web-приложение, ссылки LinkLabel ведут себя таким же образом, что и ссылки,
расположенные на Web-страницах. При этом они даже раскрашиваются в цвета,
привычные для посетителей Web-сайтов.
Неиспользованные ссылки
Свойство LinkColor определяет цвет ссылки, которая еще ни разу не использовалась. По
умолчанию такие ссылки имеют светло-синий цвет.
Использованные ссылки
Когда пользователь делает переход по ссылке, а затем вновь возвращается к работе с
той же формой, то цвет ссылки изменяется. Цвет использованных ссылок задается при
помощи свойства VisitedLinkColor. По умолчанию это фиолетовый цвет.
Активные ссылки
Если навести курсор мыши на ссылку, то она станет активной. Цвет активной ссылки
задается свойством ActiveLinkColor. По умолчанию это красный цвет.
Заблокированные ссылки
И, наконец, ссылка может быть заблокирована. Цвет заблокированной ссылки по
умолчанию темно-синий и определяется свойством DisabledLinkColor.
Добавление графических изображений
Ранее мы уже рассказывали о том, что для формы можно задавать фоновое
изображение. Помимо этого имеется возможность размещать любые графические
изображения на поверхности формы.
Возьмите файл изображения логотипа своей компании или какого-либо другого
изображения подходящего размера, запишите его в каталог приложения UserLogin, а
затем добавьте в проект.
Теперь перетащите мышью в форму из инструментальной панели значок
PictureBox. Разместите получившуюся в результате область прямоугольного размера в
нужном месте формы и придайте ей необходимые размеры.
Теперь нужно привязать к данной области файл графического изображения. Это
нужно сделать, отредактировав свойство поля PictureBox с названием Image. Просто
выберите нужный файл в диалоговом окне Open (рис. 3-19).
Рис. 3-19. Выбор файла графического изображения

Сразу после этого выбранное изображение появится в форме. Теперь останется


только отрегулировать расположение изображения и размеры выделенной для него
области. Учтите, что Вы не сможете выполнить масштабирование изображения, поэтому
данная операция должна выполняться еще при подготовке файла изображения.
Позиционирование элементов управления в форме
Самый простой способ позиционирования элементов управления, добавленных в форму
из окна инструментальной панели, заключается в их перемещении мышью. Тонкая
доводка расположения элементов «по месту» может выполняться и клавишами
перемещения курсора.
Однако в системе Microsoft Visual Studio .NET имеются специальные средства,
упрощающие процесс позиционирования элементов управления на поверхности формы.
Об этих средствах мы сейчас и будем рассказывать.
Привязка к сетке
Для удобства позиционирования элементов в режиме проектирования формы
используется специальная точечная сетка (рис. 3-20).

Рис. 3-20. Сетка для точного позиционирования элементов управления

Настраивая свойства формы и самих элементов управления, можно установить


такой режим проектирования, при котором элементы привязываются узлам сетки и не
могут занимать промежуточные положения. Режим привязки облегчает выравнивание
границ элементов и самих элементов по вертикали и горизонтали.
Управление режимом привязки к сетке выполняется путем настройки свойств
формы. Щелкните форму правой клавишей мыши в таком месте, где нет элементов
управления (или в окне редактирования формы, но рядом с формой). Затем в
контекстном меню выберите строку Properties. Далее в свойствах формы отыщите
группу свойств с названием Design (рис. 3-21).
Рис. 3-21. Свойства формы для этапа проектирования

Обратите внимание, что свойство DrawGrid по умолчанию имеет значение True.


Если заменить его значением False, сетка исчезнет. Скрытие сетки поможет Вам точнее
оценить дизайн формы еще на этапе проектирования.
Свойство GridSize состоит из свойств GridSize.Width и GridSize.Height. Первое из
них задает размер сетки по горизонтали, а второй — по вертикали. В зависимости от
требований к точности позиционирования Вы можете оставить значения по умолчанию
(8 пикселов), или задать другие.
Когда разработка дизайна формы завершена, мы рекомендуем Вам установить
значение свойства Locked равным True. После этого изменение (намеренное или
случайное) взаимного размеров формы будет невозможно.
Заметим, что свойство Locked определено не только для формы, но и для
элементов управления. Если Вам нужно зафиксировать расположение в форме какого-
то одного элемента управления, установите свойство Locked этого элемента, равным
True.
И, наконец, назначив свойству SnapToGrid значение False, можно отменить
привязку к сетке. После выполнения этой операции элементы можно будет располагать
не дискретно, а в любом месте формы. Кроме того, им можно будет придавать любые
размеры.
Настройка взаимного расположения элементов формы
Для точной настройки взаимного расположения элементов управления предназначено
меню Format главного окна системы разработки Microsoft Visual Studio .NET. Строки
этого меню дублируются для удобства в панели инструментов выравнивания,
показанной на рис. 3-22.

Рис. 3-22. Панель инструментов выравнивания элементов управления

Чтобы выровнять по горизонтали, например, поля ввода идентификатора


пользователя и пароля, нужно выделить их по очереди левой клавишей мыши,
удерживая при этом на клавиатуре в нажатом состоянии клавишу Ctrl. То поле (или
другой элемент управления), которое будет выделено последним, станет эталонным.
Именно по его границе и будут выделены остальные выделенные Вами элементы
управления.
Выравнивание
После выделения выберите из упомянутого выше меню Format строку Align, после чего
на экране появится меню инструментов выравнивания (рис. 3-23).
Рис. 3-23. Меню Align

Если выбрать из этого меню строку Lefts, выделенные ранее элементы управления
будут выровнены по левой границе того элемента, который был выбран последним.
Заметим, что вместо меню можно использовать панель инструментов, показанную
на рис. 3-22. Строке Lefts в этой панели соответствует вторая кнопка слева. Самая
левая кнопка панели инструментов выравнивания обеспечивает привязку выбранного
элемента к сетке.
Средствами меню Align можно выровнять выделенные элементы по вертикали и
горизонтали. Их можно выровнять левой (строка Lefts), правой (Rights), верхней (Tops)
и нижней (Bottoms) границам. Можно выполнить центровку элементов по вертикали
(Centers) и горизонтали (Middles), а также привязать границы элементов к сетке (to
Grid).
Установка размеров
Строки меню Make the Same Size позволяют сделать одинаковыми размеры
выделенных элементов управления по вертикали и горизонтали (рис. 3-24).

Рис. 3-24. Меню Make the Same Size

С помощью строк Width и Height можно сделать одинаковыми размеры


выделенных элементов управления, соответственно, по ширине и высоте. Строка Both
позволяет уравнять одновременно и ширину, и высоту элементов.
Если же выбрать строку Size to Grid, то размеры выделенных элементов
изменятся таким образом, чтобы оказаться привязанными к сетке. Эта операция
удобна, если вначале размеры элементов не были привязаны к сетке, а затем Вы
решили выполнить такую привязку.
Установка промежутков между элементами
Меню Horizontal spacing (рис. 3-25) и Vertical spacing (рис. 3-26) позволяют
уровнять расстояние между элементами по горизонтали и вертикали, соответственно. В
этой операции задействуют, как правило, не менее трех элементов.

Рис. 3-25. Меню Horizontal spacing


Рис. 3-26. Меню Vertical spacing

С помощью строк Make Equal этих меню можно автоматически расположить


элементы таким образом, чтобы между ними было равное расстояние.
Строки Increase и Decrease позволяют, соответственно, увеличить и уменьшить
расстояние между элементами.
И, наконец, строка Remove позволяет ликвидировать свободное пространство
между элементами, расположив их вплотную.
Центровка элементов в окне формы
Меню Center in Form (рис. 3-27) предназначено для центровки элементов внутри окна
формы.

Рис. 3-27. Меню Center in Form

С помощью строки Horizontally можно выполнить центровку по горизонтали, а с


помощью строки Vertically — по вертикали.
Передний и задний план
Заметим, что элементы в форме могут пересекаться, перекрывая друг друга. С
помощью меню Order (рис. 3-28) разработчик может изменить порядок расположения
элементов, выдвигая некоторые элементы на передний план (строка Bring to Front)
или убирая некоторые элементы на задний план (строка Send to Back).

Рис. 3-28. Меню Order

Форма и клавиша табуляции


Как известно, с приложениями Microsoft Windows можно работать и без мыши, с
использованием одной только клавиатуры. Нельзя сказать, чтобы этот способ был
удобный или широко распространенный, но, строго говоря, если Вы создаете
программу с графическим интерфейсом для этой ОС, то она должна быть пригодна к
употреблению даже на компьютерах, не оборудованных мышью, трекболом или
аналогичным устройством.
Стандартные диалоговые окна Microsoft Windows позволяют обходиться без мыши.
Для того чтобы изменить состояние того или иного элемента управления,
расположенного в таком окне, пользователь вначале должен передать этому элементу
фокус ввода. Эта операция выполняется при помощи клавиши табуляции. Когда
пользователь нажимает клавишу табуляции, фокус ввода последовательно передается
от одного элемента управления к другому. Перебирая элементы управления,
пользователь находит нужный и затем изменяет его состояние необходимым образом с
помощью клавиатуры или мыши.
Точно такое же поведение можно организовать и в формах, создаваемых на базе
класса System.Windows.Forms. Для этого нужно настроить свойства TabStop и TabIndex.
Свойство TabStop
Свойство TabStop может иметь два значения — True и False. Если у элемента
управления, расположенного в форме, это свойство имеет значение, равное True,
пользователь может передать фокус ввода данному элементу управления с помощью
клавиши табуляции. В том случае, когда значение свойства TabStop равно False, при
нажатии на клавишу табуляции фокус ввода будет «перескакивать» через
соответствующий элемент управления.
Таким образом, для всех элементов управления, которые должны получать фокус
ввода с помощью клавиши табуляции, необходимо установить данное свойство равным
True. Обычно это такие элементы управления, как поля ввода текстовой информации,
кнопки, списки, таблицы, деревья, флажки и т.п.
В нашей программе UserLogin свойство TabStop имеет по умолчанию значение
True для полей ввода идентификатора пользователя и пароля, а также для кнопок
Войти и Отменить.
Свойство TabIndex
Только что описанное свойство TabStop управляет возможностью получения фокуса
элементами управления, расположенными в форме, а свойство TabIndex определяет
порядок, в котором эти элементы управления получают фокус ввода при использовании
клавиши табуляции.
Для каждого элемента управления формы свойство TabIndex имеет свое
собственной числовое значение. Во время проектирования дизайнер формы назначает
эти значения автоматически в соответствии с порядком, в котором эти элементы были
добавлены в форму.
Когда пользователь нажимает клавишу табуляции, фокус ввода вначале
передается тому элементу управления, который имеет наименьшее значение свойства
TabIndex, и для которого значение свойства TabStop равно True. При втором нажатии
клавиши табуляции фокус управления передается другому элементу управления,
значение свойства TabIndex которого больше, и т.д.
Вы можете управлять порядком передачи фокуса ввода при использовании
клавиши табуляции, изменяя соответствующим образом значения свойства TabIndex
элементов управления, расположенных в форме. Но при этом необходимо следить,
чтобы все элементы управления имели разные значения этого свойства, иначе логика
передачи фокуса ввода будет нарушена.
Обработка событий
Итак, мы добавили в форму приложения UserLogin все необходимые текстовые строки,
изображения и элементы управления. Вы можете оттранслировать и запустить
приложение, нажав в Microsoft Visual Studio .NET кнопку F5.
Однако пока наша форма никак не реагирует на использование элементов
управления, т.к. мы еще не добавили обработчики событий. Как мы уже говорили,
чтобы добавить обработчик события для того или иного элемента управления, нужно
щелкнуть его дважды левой клавишей мыши.
Обработка идентификатора и пароля
Когда пользователь щелкает кнопку Войти, приложение UserLogin должно отобразить
на экране идентификатор пользователя и пароль (реальные приложения, разумеется,
никогда не должны показывать пароли пользователей).
Чтобы реализовать такое поведение, добавьте обработчик события для кнопки
Войти. Для этого в окне дизайнера форм нужно дважды щелкнуть левой клавишей
мыши указанную кнопку. В ответ дизайнер форм создаст для Вас пустое тело метода
button1_Click. Этот метод получит управление во время работы приложения, когда
пользователь щелкнет кнопку Войти.
Добавьте в тело метода button1_Click вызов метода MessageBox.Show, как это
показано ниже:
private void button1_Click(object sender, System.EventArgs e)
{
MessageBox.Show("User: " + textBox1.Text + "\n" +
"Password: " + textBox2.Text);
}
Обратите внимание, что мы передаем методу MessageBox.Show текстовую строку,
составленную из свойств textBox1.Text и textBox2.Text элементов управления,
предназначенных для ввода идентификатора пользователя и пароля, соответственно.
Свойство Text содержит текстовую строку, представляющую текущее состояние
элемента управления.
Чтобы пароль пользователя отображался с новой строки, мы использовали символ
перевода строки "\n". В результате диалоговое окно с идентификатором пользователя и
паролем будет иметь вид, показанный на рис. 3-29.
Рис. 3-29. Диалоговое окно с идентификатором пользователя и паролем

Закрытие формы
Кнопка Отменить предназначена для закрытия окна формы. Эта операция выполняется
при помощи метода Close. Добавьте обработчик событий для кнопки Отменить, а затем
добавьте вызов метода Close в этот обработчик событий:
private void button2_Click(object sender, System.EventArgs e)
{
Close();
}
Теперь окно нашего приложения можно будет закрывать не только с помощью
кнопки, расположенной в заголовке окна, но и с помощью кнопки Отменить.
Ссылки на ресурсы Интернета
Как мы уже говорили, по своему внешнему виду элемент управления LinkLabel очень
похож на ссылку, размещенную на обыкновенной Web-странице. Используя такие
элементы управления, можно сделать формы приложений похожими по внешнему виду
на страницы Web-сайтов.
В нашем приложении UserLogin элемент управления LinkLabel применяется для
ссылки на Web-сайт службы восстановления данных http://www.datarecovery.ru, а
также для отправки электронного сообщения автору этой книги. Как Вы узнаете позже,
эти элементы управления могут быть использованы для отображения на экране новых
форм или любых других объектов.
Чтобы ссылки, сделанные с помощью элемента управления LinkLabel, заработали,
необходимо создать для них обработчики событий.
Первую из этих ссылок (с надписью www.datarecovery.ru) необходимо обеспечить
таким обработчиком событий:
private void linkLabel1_LinkClicked(object sender,
System.Windows.Forms.LinkLabelLinkClickedEventArgs e)
{
linkLabel1.Links[linkLabel1.Links.IndexOf(e.Link)].Visited = true;
System.Diagnostics.Process.Start(linkLabel1.Text);
}
Изменение внешнего вида ссылки
Первая строка этого обработчика событий обеспечивает изменение внешнего вида
ссылки после ее использования:
linkLabel1.Links[linkLabel1.Links.IndexOf(e.Link)].Visited = true;
Как она работает?
Обработчик события получает два параметра. Через параметр sender типа object
передается ссылка на объект, вызвавший событие. В нашем случае это будет элемент
управления типа LinkLabel с надписью www.datarecovery.ru.
Параметр e типа System.Windows.Forms.LinkLabelLinkClickedEventArgs
предназначен для передачи параметров от элемента управления, вызвавшего
появление события. В частности, через свойство e.Link передается ссылка, которую
щелкнул пользователь.
Элемент управления LinkLabel может содержать информацию о нескольких
ссылках. Полный список ссылок элемента linkLabel1 (с адресом Web-сайта) можно
получить при помощи свойства Links. Далее, метод IndexOf позволяет получить из этого
списка заданную ссылку, а именно, ссылку e.Link.
Теперь остается отметить эту ссылку как использованную, установив значение
свойства Visited, равное true.
Запуск браузера
Итак, внешний вид ссылки мы изменили. Теперь обработчик события должен
отобразить на экране компьютера содержимое главной страницы Web-сайта. Это можно
сделать при помощи метода System.Diagnostics.Process.Start:
System.Diagnostics.Process.Start(linkLabel1.Text);
Данный метод пытается запустить на локальном компьютере программу или
документ, путь к которому передается через единственный параметр. Мы передали
этому методу адрес URL нашего Web-сайта www.datarecovery.ru. В результате
выполнения метода будет запущен браузер, и в его окне появится загружена искомая
Web-страница (рис. 3-30).

Рис. 3-30. Появилась главная страница Web-сайта www.datarecovery.ru

Отправка почтового сообщения


Вторая ссылка работает аналогичным образом. Для отправки электронного сообщения
мы передаем методу System.Diagnostics.Process.Start наш адрес электронной почты:
private void linkLabel2_LinkClicked(object sender,
System.Windows.Forms.LinkLabelLinkClickedEventArgs e)
{
linkLabel2.Links[linkLabel2.Links.IndexOf(e.Link)].Visited = true;
System.Diagnostics.Process.Start("mailto:alexandre@frolov.pp.ru");
}
После использования этой ссылки на экране появится окно почтовой программы,
зарегистрированной в системе по умолчанию (рис. 3-31).
Рис. 3-31. Отправка электронного сообщения

Полный исходный текст приложения UserLogin


Хотя Microsoft Visual Studio .NET позволяет создавать достаточно сложные приложения с
помощью дизайнера форм, и Вы можете при этом никогда не увидеть его полного
исходного текста, мы будем приводить полные исходные всех наших приложений. Это
позволит Вам, с одной стороны, лучше изучить структуру приложений C# с
графическим интерфейсом, а с другой — разрабатывать такие приложения на уровне
исходных текстов без применения дизайнера форм.
Исходный текст приложения UserLogin мы привели в листинге 3-1.
Листинг 3-1. Файл ch03\UserLogin\Form1.cs
using System;
using System.Drawing;
using System.Collections;
using System.ComponentModel;
using System.Windows.Forms;
using System.Data;

namespace UserLogin
{
/// <summary>
/// Summary description for Form1.
/// </summary>
public class Form1 : System.Windows.Forms.Form
{
private System.Windows.Forms.Label label1;
private System.Windows.Forms.Label label2;
private System.Windows.Forms.Label label3;
private System.Windows.Forms.TextBox textBox1;
private System.Windows.Forms.TextBox textBox2;
private System.Windows.Forms.Button button1;
private System.Windows.Forms.Button button2;
private System.Windows.Forms.PictureBox pictureBox1;
private System.Windows.Forms.Label label4;
private System.Windows.Forms.LinkLabel linkLabel1;
private System.Windows.Forms.LinkLabel linkLabel2;
private System.Windows.Forms.Label label5;
/// <summary>
/// Required designer variable.
/// </summary>
private System.ComponentModel.Container components = null;

public Form1()
{
//
// Required for Windows Form Designer support
//
InitializeComponent();

//
// TODO: Add any constructor code after
// InitializeComponent call
//
}

/// <summary>
/// Clean up any resources being used.
/// </summary>
protected override void Dispose( bool disposing )
{
if( disposing )
{
if (components != null)
{
components.Dispose();
}
}
base.Dispose( disposing );
}

#region Windows Form Designer generated code


/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
private void InitializeComponent()
{
System.Resources.ResourceManager resources = new
System.Resources.ResourceManager(typeof(Form1));
this.label1 = new System.Windows.Forms.Label();
this.label2 = new System.Windows.Forms.Label();
this.label3 = new System.Windows.Forms.Label();
this.textBox1 = new System.Windows.Forms.TextBox();
this.textBox2 = new System.Windows.Forms.TextBox();
this.button1 = new System.Windows.Forms.Button();
this.button2 = new System.Windows.Forms.Button();
this.pictureBox1 = new System.Windows.Forms.PictureBox();
this.label4 = new System.Windows.Forms.Label();
this.linkLabel1 = new System.Windows.Forms.LinkLabel();
this.linkLabel2 = new System.Windows.Forms.LinkLabel();
this.label5 = new System.Windows.Forms.Label();
this.SuspendLayout();
//
// label1
//
this.label1.Font = new System.Drawing.Font("Haettenschweiler", 26.25F,
System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((System.Byte)
(204)));
this.label1.ForeColor = System.Drawing.Color.FromArgb(((System.Byte)(67)),
((System.Byte)(128)), ((System.Byte)(165)));
this.label1.Location = new System.Drawing.Point(128, 8);
this.label1.Name = "label1";
this.label1.Size = new System.Drawing.Size(216, 40);
this.label1.TabIndex = 0;
this.label1.Text = "DataRecovery.Ru";
//
// label2
//
this.label2.Font = new System.Drawing.Font("Microsoft Sans Serif", 8.25F,
System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point, ((System.Byte)
(204)));
this.label2.Location = new System.Drawing.Point(40, 158);
this.label2.Name = "label2";
this.label2.Size = new System.Drawing.Size(48, 16);
this.label2.TabIndex = 1;
this.label2.Text = "Имя:";
//
// label3
//
this.label3.Font = new System.Drawing.Font("Microsoft Sans Serif", 8.25F,
System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point, ((System.Byte)
(204)));
this.label3.Location = new System.Drawing.Point(40, 184);
this.label3.Name = "label3";
this.label3.Size = new System.Drawing.Size(48, 23);
this.label3.TabIndex = 2;
this.label3.Text = "Пароль:";
//
// textBox1
//
this.textBox1.Location = new System.Drawing.Point(104, 158);
this.textBox1.Name = "textBox1";
this.textBox1.Size = new System.Drawing.Size(104, 20);
this.textBox1.TabIndex = 3;
this.textBox1.Text = "";
//
// textBox2
//
this.textBox2.Location = new System.Drawing.Point(104, 184);
this.textBox2.Name = "textBox2";
this.textBox2.PasswordChar = '*';
this.textBox2.Size = new System.Drawing.Size(104, 20);
this.textBox2.TabIndex = 4;
this.textBox2.Text = "";
//
// button1
//
this.button1.BackColor = System.Drawing.Color.FromArgb(((System.Byte)(224)),
((System.Byte)(224)), ((System.Byte)(224)));
this.button1.Location = new System.Drawing.Point(264, 160);
this.button1.Name = "button1";
this.button1.TabIndex = 5;
this.button1.Text = "Войти";
this.button1.Click += new System.EventHandler(this.button1_Click);
//
// button2
//
this.button2.BackColor = System.Drawing.Color.FromArgb(((System.Byte)(224)),
((System.Byte)(224)), ((System.Byte)(224)));
this.button2.Location = new System.Drawing.Point(264, 192);
this.button2.Name = "button2";
this.button2.TabIndex = 6;
this.button2.Text = "Отменить";
this.button2.Click += new System.EventHandler(this.button2_Click);
//
// pictureBox1
//
this.pictureBox1.Image = ((System.Drawing.Bitmap)
(resources.GetObject("pictureBox1.Image")));
this.pictureBox1.Location = new System.Drawing.Point(40, 16);
this.pictureBox1.Name = "pictureBox1";
this.pictureBox1.Size = new System.Drawing.Size(72, 88);
this.pictureBox1.TabIndex = 7;
this.pictureBox1.TabStop = false;
//
// label4
//
this.label4.Font = new System.Drawing.Font("Microsoft Sans Serif", 9.75F,
System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point, ((System.Byte)
(204)));
this.label4.Location = new System.Drawing.Point(128, 56);
this.label4.Name = "label4";
this.label4.Size = new System.Drawing.Size(216, 48);
this.label4.TabIndex = 8;
this.label4.Text = "Вход в систему администрирования сайта службы
восстановления данных";
//
// linkLabel1
//
this.linkLabel1.Location = new System.Drawing.Point(40, 224);
this.linkLabel1.Name = "linkLabel1";
this.linkLabel1.Size = new System.Drawing.Size(120, 16);
this.linkLabel1.TabIndex = 9;
this.linkLabel1.TabStop = true;
this.linkLabel1.Text = "www.datarecovery.ru";
this.linkLabel1.LinkClicked += new
System.Windows.Forms.LinkLabelLinkClickedEventHandler(this.linkLabel1_LinkClicked);
//
// linkLabel2
//
this.linkLabel2.Location = new System.Drawing.Point(40, 240);
this.linkLabel2.Name = "linkLabel2";
this.linkLabel2.Size = new System.Drawing.Size(136, 16);
this.linkLabel2.TabIndex = 10;
this.linkLabel2.TabStop = true;
this.linkLabel2.Text = "alexandre@frolov.pp.ru";
this.linkLabel2.LinkClicked += new
System.Windows.Forms.LinkLabelLinkClickedEventHandler(this.linkLabel2_LinkClicked);
//
// label5
//
this.label5.Location = new System.Drawing.Point(40, 136);
this.label5.Name = "label5";
this.label5.Size = new System.Drawing.Size(144, 16);
this.label5.TabIndex = 11;
this.label5.Text = "Введите имя и пароль:";
//
// Form1
//
this.AutoScaleBaseSize = new System.Drawing.Size(5, 13);
this.BackColor = System.Drawing.Color.FromArgb(((System.Byte)(245)),
((System.Byte)(248)), ((System.Byte)(255)));
this.BackgroundImage = ((System.Drawing.Bitmap)
(resources.GetObject("$this.BackgroundImage")));
this.ClientSize = new System.Drawing.Size(362, 271);
this.Controls.AddRange(new System.Windows.Forms.Control[] {
this.label5, this.linkLabel2, this.linkLabel1,
this.label4, this.pictureBox1, this.button2,
this.button1, this.textBox2, this.textBox1,
this.label3, this.label2, this.label1});
this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedDialog;
this.HelpButton = true;
this.Icon = ((System.Drawing.Icon)(resources.GetObject("$this.Icon")));
this.MaximizeBox = false;
this.MinimizeBox = false;
this.Name = "Form1";
this.Text = "Идентификация пользователя";
this.ResumeLayout(false);

}
#endregion

/// <summary>
/// The main entry point for the application.
/// </summary>
[STAThread]
static void Main()
{
Application.Run(new Form1());
}

private void button1_Click(object sender, System.EventArgs e)


{
MessageBox.Show("User: " + textBox1.Text + "\n" + "Password: " + textBox2.Text);
}

private void button2_Click(object sender, System.EventArgs e)


{
Close();
}

private void linkLabel1_LinkClicked(object sender,


System.Windows.Forms.LinkLabelLinkClickedEventArgs e)
{
linkLabel1.Links[linkLabel1.Links.IndexOf(e.Link)].Visited = true;
System.Diagnostics.Process.Start(linkLabel1.Text);
}

private void linkLabel2_LinkClicked(object sender,


System.Windows.Forms.LinkLabelLinkClickedEventArgs e)
{
linkLabel2.Links[linkLabel2.Links.IndexOf(e.Link)].Visited = true;
System.Diagnostics.Process.Start("mailto:alexandre@frolov.pp.ru");
}
}
}
Пространства имен
В приложении UserLogin используются те же самые пространства имен, что и в
приложении Hello, рассмотренном в предыдущей главе:
using System;
using System.Drawing;
using System.Collections;
using System.ComponentModel;
using System.Windows.Forms;
using System.Data;
Если у Вас появится необходимость подключения каких-либо других пространств
имен, просто добавьте новые операторы using в конец этого списка.
Класс Form1
Приложение UserLogin состоит из одного класса Form1, определенного в пространстве
имен UserLogin:
namespace UserLogin
{
public class Form1 : System.Windows.Forms.Form
{

}
}
По мере добавления в окно формы элементов управления, дизайнер форм создает
в этом классе различные поля и методы.
Поля класса Form1
Для каждого элемента управления, расположенного в форме, дизайнер форм создает
по одному полю соответствующего класса:
private System.Windows.Forms.Label label1;
private System.Windows.Forms.Label label2;
private System.Windows.Forms.Label label3;
private System.Windows.Forms.TextBox textBox1;
private System.Windows.Forms.TextBox textBox2;
private System.Windows.Forms.Button button1;
private System.Windows.Forms.Button button2;
private System.Windows.Forms.PictureBox pictureBox1;
private System.Windows.Forms.Label label4;
private System.Windows.Forms.LinkLabel linkLabel1;
private System.Windows.Forms.LinkLabel linkLabel2;
private System.Windows.Forms.Label label5;
Эти поля хранят ссылки, необходимые для выполнения всех операций с
элементами управления формы.
Если для реализации функциональности приложения Вам потребуются новые
поля, добавьте их после приведенного выше фрагмента кода.
Инициализация приложения
Для инициализации приложения конструктор класса Form1 вызывает метод
InitializeComponent, о котором мы рассказывали в предыдущей главе:
public Form1()
{
InitializeComponent();

//
// TODO: Add any constructor code after
// InitializeComponent call
//
}
Этот метод создает и инициализирует все элементы управления формы, а также
задает их размеры и расположение в окне.
При необходимости выполнить какую-либо дополнительную инициализацию Вы
можете добавить вызовы других методов, расположив их в теле конструктора после
вызова метода InitializeComponent.
На самом первом шаге инициализации, на базе класса
System.Resources.ResourceManager создается система управления ресурсами,
необходимая для работы с национальными языками и параметрами:
System.Resources.ResourceManager resources = new
System.Resources.ResourceManager(typeof(Form1));
В качестве параметра конструктору класса System.Resources.ResourceManager
передается тип создаваемого ресурса. В данном случае это тип формы Form1.
Далее метод InitializeComponent создает все необходимые элементы управления,
добавленные в форму на этапе проектирования:
this.label1 = new System.Windows.Forms.Label();
this.label2 = new System.Windows.Forms.Label();
this.label3 = new System.Windows.Forms.Label();
this.textBox1 = new System.Windows.Forms.TextBox();
this.textBox2 = new System.Windows.Forms.TextBox();
this.button1 = new System.Windows.Forms.Button();
this.button2 = new System.Windows.Forms.Button();
this.pictureBox1 = new System.Windows.Forms.PictureBox();
this.label4 = new System.Windows.Forms.Label();
this.linkLabel1 = new System.Windows.Forms.LinkLabel();
this.linkLabel2 = new System.Windows.Forms.LinkLabel();
this.label5 = new System.Windows.Forms.Label();
Когда все элементы управления созданы, необходимо их проинициализировать и
разместить в окне формы. На время выполнения этой операции метод
InitializeComponent отключает на время механизм генерации сообщений, связанных с
размещением элементов в окне формы:
this.SuspendLayout();
Инициализация и размещение элементов управления выполняется путем
изменения значений соответствующих свойств.
Инициализация текстовых полей
Рассмотрим процесс инициализации текстовых полей на примере поля с надписью
«DataRecovery.Ru»:
//
// label1
//
this.label1.Font = new System.Drawing.Font("Haettenschweiler",
26.25F, System.Drawing.FontStyle.Regular,
System.Drawing.GraphicsUnit.Point, ((System.Byte)(204)));
this.label1.ForeColor =
System.Drawing.Color.FromArgb(((System.Byte)(67)),
((System.Byte)(128)), ((System.Byte)(165)));
this.label1.Location = new System.Drawing.Point(128, 8);
this.label1.Name = "label1";
this.label1.Size = new System.Drawing.Size(216, 40);
this.label1.TabIndex = 0;
this.label1.Text = "DataRecovery.Ru";
Для отображения этой надписи мы использовали шрифт Haettenschweiler. При
этом мы создали этот шрифт как объект класса System.Drawing.Font, а затем присвоили
ссылку на этот объект свойству this.label1.Font. Тем самым был изменен шрифт,
использованный для отображения надписи.
Подробно о шрифтах мы расскажем в 10 главе, а сейчас только отметим, что
первый параметр конструктора класса System.Drawing.Font задает название шрифта,
второй — его размер, третий — стиль (обычный, наклонный, жирный и т.д.).
Четвертый параметр конструктора позволяет выбрать единицу измерения для
указания размера шрифта. В нашем случае это пункт (1/72 дюйма). И, наконец, пятый
параметр указывает номер страницы Unicode, который должен быть использован для
отображения текстовой строки. Значение 204 обозначает страницу с символами
кириллицы.
Чтобы задать цвет надписи, необходимо изменить свойство this.label1.ForeColor.
Дизайнер форм создал объект класса System.Drawing.Color.FromArgb, позволяющий
выбрать цвет как набор трех компонентов — красной, зеленой и голубой. Значения
компонентов цвета изменяются от 0 до 255.
Далее задается расположение надписи. С этой целью изменяется значение
свойства this.label1.Location. Дизайнер форм задал координаты в виде объекта класса
System.Drawing.Point, передав соответствующему конструктору через параметры
координаты надписи по горизонтальной и вертикальной оси.
Что же касается размеров надписи, то они задаются при помощи свойства
this.label1.Size. Дизайнер присвоил этому свойству ссылку на объект класса
System.Drawing.Size, задающий размер объекта по горизонтали и вертикали.
Свойство this.label1.Name задает идентификатор (имя) надписи. С помощью этого
идентификатора можно, например, изменять текста надписи или ее другие атрибуты.
Что же касается текста надписи, то он задается свойством this.label1.Text.
Хотя надпись и не получает фокус ввода, дизайнер форм присваивает ей
последовательный номер, определяющий порядок, в котором элементы управления
получают фокус ввода при использовании клавиши табуляции.
Как видите, инициализация и размещение простой текстовой надписи занимает
несколько строк программного текста. К счастью, дизайнер форм создает этот текст для
Вас автоматически.
Инициализация полей ввода текста
В процессе инициализации полей, предназначенных для ввода текста, определяется
значение пяти различных свойств:
//
// textBox1
//
this.textBox1.Location = new System.Drawing.Point(104, 158);
this.textBox1.Name = "textBox1";
this.textBox1.Size = new System.Drawing.Size(104, 20);
this.textBox1.TabIndex = 3;
this.textBox1.Text = "";
Свойства this.textBox1.Location и this.textBox1.Size задают, соответственно,
расположение и размеры поля. Мы рассказывали о них в предыдущем разделе.
Аналогично, дизайнер форм задал имя поля при помощи свойства this.textBox1.Name, и
порядковый номер для табуляции в поле this.textBox1.TabIndex.
Свойство this.textBox1.Text позволяет задать надпись, которая появится в поле
ввода сразу после отображения формы. Мы этой возможностью не пользуемся, и
поэтому в данное свойство записывается пустая строка.
Инициализация графических изображений
При размещении в форме графического изображения (такого, например, как логотип
компании), мы добавляли объект в ресурсы приложения, изменяя свойство Image.
Предварительно мы включали файл изображения в проект, добавляя его к ресурсам
приложения.
Вот как происходит инициализация элемента управления, содержащего наше
изображение:
//
// pictureBox1
//
this.pictureBox1.Image = ((System.Drawing.Bitmap)
(resources.GetObject("pictureBox1.Image")));
this.pictureBox1.Location = new System.Drawing.Point(40, 16);
this.pictureBox1.Name = "pictureBox1";
this.pictureBox1.Size = new System.Drawing.Size(72, 88);
this.pictureBox1.TabIndex = 7;
this.pictureBox1.TabStop = false;
Здесь с помощью метода resources.GetObject мы извлекаем данные нашего
изображения из ресурсов приложения. Эти данные затем приводятся к типу
System.Drawing.Bitmap (растровые графические изображения), а затем полученная
ссылка используется для инициализации свойства this.pictureBox1.Image.
Подробнее о графических изображениях мы расскажем позже в 10 главе,
посвященной рисованию в окне форм.
Инициализация кнопок
Размеры и расположение кнопок, их имя, текст и порядок табуляции задается таким же
образом, что и для текстовых полей:
//
// button1
//
this.button1.BackColor =
System.Drawing.Color.FromArgb(((System.Byte)(224)),
((System.Byte)(224)), ((System.Byte)(224)));
this.button1.Location = new System.Drawing.Point(264, 160);
this.button1.Name = "button1";
this.button1.TabIndex = 5;
this.button1.Text = "Войти";
Дополнительно кнопке назначается обработчик событий:
this.button1.Click += new System.EventHandler(this.button1_Click);
Этот обработчик получит управление, когда пользователь щелкнет кнопку.
Исходный текст обработчиков для кнопок нашего приложения уже был описан ранее в
этой главе:
private void button1_Click(object sender, System.EventArgs e)
{
MessageBox.Show("User: " + textBox1.Text + "\n" + "Password: " +
textBox2.Text);
}

private void button2_Click(object sender, System.EventArgs e)


{
Close();
}
Инициализация полей LinkLabel
Процедура инициализации полей LinkLabel очень похожа на процедуру инициализации
обыкновенных текстовых полей Label. Отличие заключается в том, что для наших полей
LinkLabel назначаются обработчики событий:
//
// linkLabel1
//
this.linkLabel1.Location = new System.Drawing.Point(40, 224);
this.linkLabel1.Name = "linkLabel1";
this.linkLabel1.Size = new System.Drawing.Size(120, 16);
this.linkLabel1.TabIndex = 9;
this.linkLabel1.TabStop = true;
this.linkLabel1.Text = "www.datarecovery.ru";
this.linkLabel1.LinkClicked += new
System.Windows.Forms.LinkLabelLinkClickedEventHandler(
this.linkLabel1_LinkClicked);
Исходные тексты обработчиков событий для полей нашей формы были описаны
ранее в разделе «Ссылки на ресурсы Интернета».
Инициализация формы
Последнее, что делает метод InitializeComponent, это настройка свойств самой формы
Form1.
Рассмотрим этот процесс подробнее.
Свойство this.AutoScaleBaseSize задает базовый размер для масштабирования,
используемый во время отображения формы:
//
// Form1
//
this.AutoScaleBaseSize = new System.Drawing.Size(5, 13);
При масштабировании формы этот размер сопоставляется с размером системного
шрифта.
Цвет фона формы задается при помощи свойства this.BackColor:
this.BackColor = System.Drawing.Color.FromArgb(
((System.Byte)(245)), ((System.Byte)(248)), ((System.Byte)(255)));
Этот цвет составляется из отдельных компонентов при помощи метода
System.Drawing.Color.FromArgb.
Наша форма имеет фоновое графическое изображение. Это изображение задается
свойством this.BackgroundImage:
this.BackgroundImage = ((System.Drawing.Bitmap)
(resources.GetObject("$this.BackgroundImage")));
Область формы, предназначенная для отображения элементов управления и
рисования, называется клиентской областью (client area). Размеры этой области
определяются свойством this.ClientSize:
this.ClientSize = new System.Drawing.Size(362, 271);
Для размещения элементов управления в окне формы используется метод
this.Controls.AddRange:
this.Controls.AddRange(new System.Windows.Forms.Control[] {
this.label5, this.linkLabel2, this.linkLabel1,
this.label4, this.pictureBox1, this.button2,
this.button1, this.textBox2, this.textBox1,
this.label3, this.label2, this.label1});
Этому методу передается ссылка на массив добавляемых элементов управления.
Стиль рамки, окантовывающей окно формы, задается свойством
this.FormBorderStyle:
this.FormBorderStyle =
System.Windows.Forms.FormBorderStyle.FixedDialog;
Так как мы запросили отображение кнопки помощи в заголовке формы, дизайнер
формы установил значение свойства this.HelpButton равным true:
this.HelpButton = true;
Далее в процессе инициализации метод InitializeComponent назначает форме
значок, с помощью которого форма будет представлена на панели задач и в окне
переключения приложений:
this.Icon =
((System.Drawing.Icon)(resources.GetObject("$this.Icon")));
Значок загружается из ресурсов приложения методом resources.GetObject.
Кнопки максимизации и минимизации формы не нужны, поэтому свойства
this.MaximizeBox и this.MinimizeBox получают значение false:
this.MaximizeBox = false;
this.MinimizeBox = false;
И последние шаги:
this.Name = "Form1";
this.Text = "Идентификация пользователя";
Здесь метод InitializeComponent задает имя формы (свойство this.Name), текст
заголовка окна формы (свойство this.Text).
Завершив размещение элементов управления в окне формы, метод
InitializeComponent разрешает обработку сообщений, связанных с размещением
элементов в окне формы:
this.ResumeLayout(false);
Напомним, что эта обработка была отключена в самом начале работы метода
InitializeComponent.
Удаление обработчика событий
Дизайнер форм позволяет назначить обработчик события для каждого элемента
управления, размещенного в форме. Если в процессе проектирования формы Вы
случайно дважды щелкнули элемент управления, для которого не нужно создавать
обработчик события, то этот обработчик можно удалить.
Допустим, Вы по ошибке дважды щелкнули окно формы в дизайнере форм. В
результате по умолчанию в исходный текст приложения будет добавлен пустой
обработчик событий Form1_Load:
private void Form1_Load(object sender, System.EventArgs e)
{

}
Чтобы удалить его, откройте окно дизайнера форм и щелкните форму один раз.
Далее в инструментальной панели, расположенной в верхней части окна свойств
формы Properties щелкните кнопку Events. На рис. 3-22 эта кнопка показана в нажатом
виде.

Рис. 3-32. Диалоговое окно событий

В результате Вы увидите список всех событий, возможных для объекта (в нашем


случае для формы). На рис. 3-22 мы показали, что для события Load (загрузка формы)
был создан обработчик события с именем Form1_Load. Чтобы удалить обработчик
события, просто сотрите его имя.
Кстати, в этом окне Вы можете не только удалять обработчики событий, но и
создавать их (что не менее важно). Для создания обработчика Вам достаточно
щелкнуть в этом окне имя нужного события.
Можно удалить обработчик событий и вручную, редактируя текст программы.
Однако чтобы это сделать, недостаточно стереть код обработчика события. Еще нужно
удалить код, подключающий этот обработчик к элементу управления.
Допустим, при подготовке формы приложения UserLogin Вы по ошибке дважды
щелкнули поле ввода идентификатора пользователя. Как результат, в исходном тексте
приложения появится тело обработчика сообщения:
private void textBox1_TextChanged(object sender, System.EventArgs e)
{

}
Этот обработчик позволяет отслеживать процесс ввода текста в поле
редактирования. Однако если эта возможность Вам не нужна, удалите исходный текст
метода.
Удалите также код, подключающий этот обработчик события к полю
редактирования:
this.textBox1.TextChanged +=
new System.EventHandler(this.textBox1_TextChanged);

Визуальное проектирование
приложений C#
А.В. Фролов, Г.В. Фролов
ГЛАВА 4. СОЗДАНИЕ РЕДАКТОРА ТЕКСТА
ПРИЛОЖЕНИЕ SIMPLENOTEPAD
ДОБАВЛЕНИЕ МЕНЮ
Переименование меню и строк меню
Подключение меню к форме
ВСТАВКА ПОЛЯ РЕДАКТИРОВАНИЯ
ОБРАБОТКА СОБЫТИЙ
РАБОТА С ФАЙЛАМИ ДОКУМЕНТОВ
Создание нового документа
Открытие существующего файла
Сохранение файла
ПЕЧАТЬ ДОКУМЕНТА
Добавление программных компонентов для печати
Редактирование меню File
Подключение пространств имен
Настройка параметров страницы документа
Предварительный просмотр документа перед печатью
Отображение окна печати документа
Обработка события PrintPage
Закрытие главного окна редактора текста
РЕАЛИЗАЦИЯ ФУНКЦИЙ МЕНЮ EDIT
РЕАЛИЗАЦИЯ ФУНКЦИЙ МЕНЮ FORMAT
Шрифт символов
Цвет символов
Стиль символов
Выравнивание параграфов
РЕАЛИЗАЦИЯ ФУНКЦИЙ МЕНЮ HELP
Добавление новой формы
Отображение формы
Редактирование класса HelpAboutForm
СОЗДАНИЕ ИНСТРУМЕНТАЛЬНОЙ ПАНЕЛИ
Добавление инструментальной панели в окно приложения
Подключение списка изображений
Наполнение списка изображений
Редактирование кнопок инструментальной панели
СТРОКА СОСТОЯНИЯ
Добавление строки состояния
Настройка свойств строки состояния
Привязка строки состояния к меню

Глава 4. Создание редактора текста


Если форма играет роль главного окна приложения, то в ней, как правило, имеется
меню, строки которого дублируются кнопками инструментальной панели. Кроме того,
такое окно обычно снабжается строкой состояния. В этой главе мы создадим
приложение SimpleNotepad, представляющее собой простейший текстовый редактор.
Окно этого редактора мы снабдим всеми перечисленными выше компонентами — меню,
инструментальной панелью и строкой состояния.
Наш редактор будет способен редактировать не только текстовые документы, но и
документы в формате RTF (Rich Text Format). При этом мы воспользуемся стандартным
для Microsoft Visual Studio Studio .NET компонентом RichTextBox.
Вот только основные возможности этого компонента:
 символы текста могут иметь любое шрифтовое оформление;
 доступны как растровые шрифты, так и шрифты TrueType;
 имеется возможность задавать оформление параграфов текста, такое как
выравнивание влево или вправо, центровка и задание отступов;
 не вызывает особого затруднения печать документов, загруженных для
редактирования;
 в компонент RichTextBox может работать как с текстом без шрифтового или
какого либо другого оформления, так с документами в формате RTF.
Формат RTF предназначен для хранения текста вместе со шрифтовым
оформлением и оформлением параграфов. Детальное изучение формата RTF выходит
за рамки нашей книги. При необходимости вы сможете найти полное описание этого
формата в MSDN (http://msdn.microsoft.com). Однако в большинстве случаев Вам не
придется создавать самостоятельно текстовые файлы в формате RTF, так как для этого
можно использовать многие текстовые процессоры (например, Microsoft Word for
Windows или WordPad).
Чтобы Вы получили некоторое представление о том, что представляет собой
текстовый файл в формате RTF, приведем небольшой пример.
Для преобразования в формат RTF мы взяли следующую текстовую строку:
This is RTF test file. This is RTF test file. This is RTF test file.
Мы загрузили эту строку в приложение SimpleNotepad (исходные тексты которого
будут приведены ниже). Затем мы задали для текста шрифтовое оформление и
сохранили как текст в формате RTF. Вот что получилось в результате:
{\rtf1\ansi\deff0\deftab720{\fonttbl{\f0\fnil MS Sans Serif;}{\f1\fnil\fcharset2 Symbol;}{\
f2\fswiss\fprq2 System;}{\f3\fswiss\fprq2 Arial;}{\f4\froman\fprq2 Calisto MT;}}
{\colortbl\red0\green0\blue0;}
\deflang1033\pard\plain\f4\fs38 This is RTF test file. This is RTF test file. This is RTF test
file.
\par
\par
\par
\par }
Компонент RichTextBox можно использовать для создания достаточно мощного
текстового редактора, напоминающего приложение WordPad. Однако для него можно
найти и другое применение. Например, приложение может отображать с его помощью
красиво оформленные сообщения.
Приложение SimpleNotepad
Создайте новое приложение с названием SimpleNotepad, используя для этого технику,
описанную в предыдущих главах нашей книги.
Отредактируйте имя формы, которая по умолчанию называется Form1. Для этого,
после того как мастер проектов создаст приложение, измените свойство формы Name,
заменив имя Form1 на SimpleNotepadForm. Далее, в окне Solution Explorer
переименуйте файл form1.cs в SimpleNotepadForm.sc.
Теперь откройте исходный текст формы, и отредактируйте метод Main таким
образом, чтобы методу Application.Run передавалась ссылка на новый объект класса
SimpleNotepadForm:

static void Main()


{
Application.Run(new SimpleNotepadForm());
}
Теперь наше пространство имен будет называться SimpleNotepad, а класс формы,
играющей роль главного окна приложения — SimpleNotepadForm:
namespace SimpleNotepad
{
public class SimpleNotepadForm : System.Windows.Forms.Form
{

}
}
Создавая приложения, старайтесь называть пространства имен и классы так,
чтобы из их названия было понятно назначение этих пространств имен и приложений.
Если Вам нужно создавать пространства имен, которые будут использоваться
сторонними разработчиками, добавляйте к названию пространства имен какую-либо
уникальную строку, например, название своей компании.
Далее замените стандартный значок, создаваемый системой Microsoft Visual Studio
.NET каким-нибудь другим. Используйте для этого технику, описанную в предыдущей
главе. Например, для приложения SimpleNotepad Вы можете использовать файл
note01.ico или другой из каталога Program Files\Microsoft Visual Studio .NET\Common7\
Graphics\icons\Writing.
Вы также можете изменить размеры окна, принятые по умолчанию, т.к. в окне
размером 300 на 300 пикселов редактировать текст неудобно.
Добавление меню
Для того чтобы добавить меню в главное окно нашего приложения, перетащите из
панели инструментов Toolbox значок меню с названием MainMenu.
Как только Вы это сделаете, окно дизайнера форм примет вид, показанный на
рис. 4-1.
Рис. 4-1. Добавление меню

В нижней части этого окна появится значок программного компонента — меню


mainMenu1. Кроме того, непосредственно под заголовком окна появится пустое пока
меню, представленное полем с надписью Type Here (что можно перевести как
«печатать здесь»).
Напечатайте в этом поле строку «&File». В результате этого в окне нашего
приложения появится меню File (рис. 4-2).

Рис. 4-2. Создание меню File

Обратите внимание, что первая буква в названии меню подчеркнута. Это


получилось потому, что мы поставили перед ней префикс &. Этим префиксом
отмечается буква, предназначенная для ускоренного выбора меню (или строки меню)
при помощи клавиатуры.
По мере того как Вы будете вводить названия строк меню File, поле ввода Type
Here будет опускаться вниз. Таким способом Вы сможете ввести все необходимые
строки данного меню.
Чтобы отредактировать строку меню, щелкните ее правой клавишей мыши. На
экране появится контекстное меню, показанное на рис. 4-3.
Рис. 4-3. Контекстное меню

С помощью строки Insert New Вы можете вставить новую строку меню между уже
существующих строк. Строка Insert Separator предназначена для вставки
разделительной линии между строками меню. И, наконец, при помощи строки Edit
Names можно отредактировать идентификаторы строк и меню.
Если же Вам нужно изменить введенные названия строк и меню, это можно
сделать по месту, выбрав нужную строку мышью.
Создайте меню File, чтобы оно было таким, как показано на рис. 4-4.

Рис. 4-4. Меню File

По своему назначению строки этого меню полностью соответствуют строкам


стандартного редактора текста Microsoft Notepad.
Далее создайте меню Edit (рис. 4-5).
Рис. 4-5. Меню Edit

В меню Edit мы реализуем только самые важные функции, опустив пока поиск,
замену и некоторые другие стандартные функции приложения Microsoft Notepad.
Меню Format (рис. 4-6) состоит только из одной строки Font, с помощью которой
пользователь сможет изменить шрифт текста.

Рис. 4-6. Меню Format

И, наконец, последнее меню нашего приложения, это меню Help (рис. 4-7).

Рис. 4-7. Меню Help

В этом меню мы предусмотрели только одну строку — About.


Переименование меню и строк меню
Прежде чем продолжить работу с меню, давайте переименуем идентификаторы меню и
строк меню таким образом, чтобы с ними было удобнее работать в программе. Для
этого в окне дизайнера формы щелкните правой клавишей мыши главное меню
приложения и затем выберите из контекстного меню строку Edit Names (рис. 4-8).

Рис. 4-8. Редактирование имен меню и строк меню

Как только Вы это сделаете, рядом с каждой строкой меню появится имя,
созданное дизайнером форм по умолчанию (рис. 4-9). Это имена вида menuItem1,
menuItem2 и т.д.
Рис. 4-9. Просмотр имен меню и строк меню

Отредактируйте имена меню и строк меню, как это показано на рис. 4-10.

Рис. 4-10. Новые имена меню File

Эту процедуру нужно будет повторить для каждого создаваемого Вами меню и для
каждой строки меню. При этом меню верхнего уровня File, Edit, Format и Help должны
называться menuFile, menuEdit, menuFormat и menuHelp. Имена строк меню
формируются путем добавления к имени меню текста, отображаемого в строке меню.
Например, строка New меню File называется menuFileNew, а строка About меню Help —
menuHelpAbout.
Разумеется, Вы можете придумать и свою собственную систему именования меню
и строк меню, лишь бы она была понятна для Вас и других разработчиков приложения,
которые, возможно, будут изучать его исходные тексты.
Подключение меню к форме
Теперь, когда меню готово, его нужно подключить к форме. Для этого отредактируйте
свойство формы с именем Menu. Раскройте список возможных значений этого свойства
и выберите в этом списке строку mainMenu1.
После этого Вы можете оттранслировать приложение и запустить его, нажав
кнопку F5. Убедитесь, что меню отображается, и Вы можете выбирать его строки. Если
все нормально, можно переходить к следующему этапу проектирования нашего
редактора текста.
Вставка поля редактирования
Редактирование текста в нашем приложении будет выполнять компонент RichTextBox.
Этот компонент очень похож на текстовый редактор TextBox, с которым мы уже имели
дело в предыдущей главе.
Перетащите из панели Toolbox в форму приложения simpleNotepad компонент
RichTextBox.
Настройте свойства компонента, чтобы он занимал все окно приложения. Далее
отредактируйте свойство Dock. Это свойство задает расположение выбранного
компонента внутри содержащей его формы (рис. 4-11).
Рис. 4-11. Редактирование свойства Dock

Выберите расположение по центру, щелкнув кнопку, показанную на рис. 4-8 в


нажатом состоянии.
И, наконец, сотрите текстовую строку, задающую значение свойства Text.
Напомним, что это значение определяет текст, который будет отображаться в поле
редактирования сразу после отображения формы. В нашем случае никакой
предварительной инициализации редактора текстом не требуется.
Запустите полученное приложение на выполнение. В результате на экране
должно появиться окно, показанное на рис. 4-12 (в нем мы уже набрали две строки
текста).

Рис. 4-12. Работа над дизайном редактора текста завершена

Обработка событий
Обработчики событий от строк меню создаются таким же образом, что и обработчики
событий от кнопок. Чтобы создать обработчик события для строки меню, ее нужно
щелкнуть дважды левой клавишей мыши.
Создайте обработчики событий для всех строк меню File, Edit, Format и Help,
дважды щелкнув каждую строку этих меню. Все обработчики событий создаются с
пустым телом:
private void menuFileNew_Click(object sender, System.EventArgs e)
{
}
Чтобы наполнить меню нужной нам функциональностью, нам придется написать
вручную код для этих обработчиков событий.
Работа с файлами документов
Прежде всего, мы создадим обработчики событий, необходимые для выполнения таких
функций, как создание нового документа, открытие и сохранение документов. Функции
печати документов, форматирования текста и некоторые другие мы реализуем позже в
этой главе.
Создание нового документа
Если пользователь выберет строку New из меню File, наш редактор текста
SimpleNotepad должен создать новый текстовый документ. В простейшем случае для
этого достаточно просто очистить содержимое окна редактирования, вызвав метод
Clear:
private void menuFileNew_Click(object sender, System.EventArgs e)
{
textBox1.Clear();
}
Метод Clear вызывается для объекта textBox1, т.е. для нашего окна
редактирования, занимающего все пространство формы.
Открытие существующего файла
Для того чтобы реализовать в нашем редакторе текста функцию открытия файла, нам
потребуется компонент OpenFileDialog. Перетащите значок этого компонента из панели
Toolbox в рабочее окно дизайнера формы.
Значок компонента OpenFileDialog будет показан в нижней части формы (рис. 4-
13), причем этот компонент получит идентификатор openFileDialog1. Этот
идентификатор будет использован нами в программном коде, открывающем исходный
файл.

Рис. 4-13. Добавление компонента OpenFileDialog

Добавьте в класс SimpleNotepadForm новый метод MenuFileOpen,


предназначенный для открытия существующего файла. Вот его исходный текст:
/// <summary>
/// Открытие существующего файла
/// </summary>
private void MenuFileOpen()
{
if(openFileDialog1.ShowDialog() ==
System.Windows.Forms.DialogResult.OK &&
openFileDialog1.FileName.Length > 0)
{
try
{
richTextBox1.LoadFile(openFileDialog1.FileName,
RichTextBoxStreamType.RichText);
}
catch (System.ArgumentException ex)
{
richTextBox1.LoadFile(openFileDialog1.FileName,
RichTextBoxStreamType.PlainText);
}

this.Text = "Файл [" + openFileDialog1.FileName + "]";


}
}
Далее добавьте вызов этого метода в обработчик события menuFileOpen_Click,
получающий управление при выборе строки Open из меню File:
private void menuFileOpen_Click(object sender, System.EventArgs e)
{
MenuFileOpen();
}
Если все сделано правильно, то после трансляции исходного приложения
редактор сможет открывать существующие файлы для редактирования.
Как работает метод MenuFileOpen?
Получив управление, он вызывает метод openFileDialog1.ShowDialog,
отображающий на экране стандартное диалоговое окно выбора файла (рис. 4-14).

Рис. 4-14. Стандартное диалоговое окно выбора файла

Настраивая свойства компонента openFileDialog1, можно изменять внешний вид


этого окна.
Задайте фильтр имен открываемых файлов. Для этого выделите компонент
openFileDialog1 в окне дизайнера форм левой клавишей мыши, а затем измените
свойство Filter. Присвойте этому свойству следующую текстовую строку:
RTF files|*.rtf|Text files|*.txt|All files|*.*
Строка фильтра состоит из блоков, разделенных символом |. Первый блок задает
название типа файла RTF files, отображаемое в поле Files of type диалогового окна
выбора файла (рис. 4-14), а второе — маску для имен файлов. Таким образом, для
открытия документов RTF используется маска *.rtf.
Далее следует название формата Text files, соответствующее обычным текстовым
файлам. Для этого формата применяется маска *.txt.
И, наконец, чтобы приложение могло открывать файлы любых типов (All files),
используется маска *.*.
Продолжим работу. Оттранслируйте и запустите наше приложение. Выбрав с
помощью только что описанного окна текстовый файл или файл RTF, щелкните кнопку
Open. Выбранный файл будет загружен в окно нашего редактора текста (рис. 4-15).
Рис. 4-15. Загружен файл для редактирования

Полный путь к редактируемому файлу будет отображен в заголовке главного окна


нашего приложения.
Подробнее о диалоговых окнах мы расскажем в главе 5 с названием «Диалоговые
окна». Сейчас же мы только отметим, что если в окне выбора файла пользователь
щелкнул кнопку Open, метод ShowDialog возвращает значение
System.Windows.Forms.DialogResult.OK. В этом случае мы также дополнительно
проверяем, что файл был выбран. Для этого мы сравниваем длину строки полного пути
к выбранному файлу openFileDialog1.FileName.Length с нулем:
if(openFileDialog1.ShowDialog() ==
System.Windows.Forms.DialogResult.OK &&
openFileDialog1.FileName.Length > 0)
{

}
В том случае, когда пользователь выбрал файл, метод MenuFileOpen
предпринимает попытку загрузить этот файл в редактор richTextBox1, вызывая для
этого метод LoadFile:
try
{
richTextBox1.LoadFile(openFileDialog1.FileName,
RichTextBoxStreamType.RichText);
}
catch (System.ArgumentException ex)
{
richTextBox1.LoadFile(openFileDialog1.FileName,
RichTextBoxStreamType.PlainText);
}
В качестве первого параметра этому методу передается путь к файлу, а в качестве
второго — тип файла RichTextBoxStreamType.RichText. Этот тип соответствует формату
RTF.
В том случае, если выбранный файл имеет формат, отличный от RTF, в методе
LoadFile возникает исключение System.ArgumentException. Наш обработчик этого
исключения выполняет повторную попытку загрузить файл, но на этот раз уже как
обычный текст. Для этого мы передаем в качестве второго параметра методу LoadFile
значение RichTextBoxStreamType.PlainText.
Сохранение файла
Чтобы добавить в наше приложение возможность сохранения документов, перетащите
из панели инструментов Toolbox в окно дизайнера форм компонент SaveFileDialog. Этот
компонент получит идентификатор saveFileDialog1 (рис. 4-16).
Рис. 4-16. Добавление компонента SaveFileDialog

После добавления этого компонента отредактируйте его свойства Filter и FileName.


Свойство Filter должно иметь значение, обеспечивающее работу с документами
RTF:
RTF files|*.rtf
Для свойства FileName задайте значение doc1.rtf. При этом по умолчанию
документы будут сохраняться в файле с этим именем (рис. 4-17).

Рис. 4-17. Стандартное диалоговое окно сохранения файла

Для сохранения документов в файле добавьте в класс SimpleNotepadForm


следующий метод с именем MenuFileSaveAs:
/// <summary>
/// Сохранение документа в новом файле
/// </summary>
private void MenuFileSaveAs()
{
if(saveFileDialog1.ShowDialog() ==
System.Windows.Forms.DialogResult.OK &&
saveFileDialog1.FileName.Length > 0)
{
richTextBox1.SaveFile(saveFileDialog1.FileName);
this.Text = "Файл [" + saveFileDialog1.FileName + "]";
}
}
Здесь метод saveFileDialog1.ShowDialog отображает стандартный диалог
сохранения файлов, показанный на рис. 4-17. Если пользователь указал имя файла и
щелкнул в этом диалоге кнопку Save, метод MenuFileSaveAs сохраняет документ в
файле, вызывая для этого метод richTextBox1.SaveFile.
Далее этот метод отображает имя сохраненного файла в заголовке окна нашего
приложения:
this.Text = "Файл [" + saveFileDialog1.FileName + "]";
Заметим, что существует несколько перегруженных вариантов метода SaveFile.
Если методу SaveFile задан один параметр (путь к сохраняемому файлу), то
документ будет сохранен в формате RTF.
Второй параметр позволяет выбрать тип сохраняемого файла.
Если передать через этот параметр значение RichTextBoxStreamType.PlainText,
документ будет сохранен в виде текстового файла с потерей форматирования. В том
случае, когда нужно сохранить документ в формате RTF, используйте значение
RichTextBoxStreamType.RichText. Можно также сохранить документ и как текст Unicode,
для чего нужно передать через второй параметр методу SaveFile значение
RichTextBoxStreamType.UnicodePlainText.
Определив в исходном тексте нашего приложения метод MenuFileSaveAs, добавьте
его вызов в обработчики событий от строк Save и Save As меню File:
private void menuFileSave_Click(object sender, System.EventArgs e)
{
MenuFileSaveAs();
}

private void menuFileSaveAs_Click(object sender, System.EventArgs e)


{
MenuFileSaveAs();
}
Для простоты мы сделаем функции этих строк меню одинаковыми.
Печать документа
Теперь, когда мы научились загружать документы в окно редактирования и сохранять
результат редактирования в файле, займемся печатью документов. Используя
компоненты инструментальной панели Toolbox, можно достаточно просто реализовать
все основные функции печати.
Добавление программных компонентов для печати
Добавьте в окно дизайнера форм компоненты PrintDocument, PrintDialog,
PrintPreviewDialog и PageSetupDialog, как это показано на рис. 4-18 (если все
программные компоненты не помещаются в видимую часто окна, щелкните окно с
компонентами правой клавишей мыши и выберите строку Line Up Icons, для того чтобы
упорядочить значки компонентов).
Рис. 4-18. Добавлены компоненты для печати документов

Далее Вам нужно будет настроить свойства добавленных компонентов.


Компонент PrintDocument предназначен для вывода данных документа на
принтер. Сразу после добавления этот компонент получает идентификатор
printDocument1.
Свойства компонента PrintDocument описывают, как именно нужно распечатывать
документ. Запишите в свойство DocumentName компонента PrintDocument строку
SimpleNotepad Document. Эта строка будет идентифицировать наш документ при
отображении состояния очереди печати. Значения остальных свойств оставьте без
изменения.
С помощью компонента PrintDialog наше приложение выведет на экран
стандартное диалоговое окно печати документа (рис. 4-19).

Рис. 4-19. Стандартное диалоговое окно печати документа

Перед использованием компонента PrintDialog задайте значение его свойства


Document, равным printDocument1. Этим Вы обеспечите связь компонента PrintDialog с
компонентом PrintDocument, указав таким способом документ, который следует
распечатать.
Настройка свойств компонентов PrintPreviewDialog и PageSetupDialog,
предназначенных для предварительного просмотра документа перед печатью и для
настройки параметров страницы распечатываемого документа так же сводится к
редактированию свойства Document. Вы должны записать в это свойство значение
printDocument1 для обоих компонентов.
Окно предварительного просмотра документа перед печатью показано на рис. 4-
20.

Рис. 4-20. Окно предварительного просмотра документа перед печатью

Здесь пользователь сможет увидеть документ в таком виде, в котором он появится


после печати на принтере. С помощью кнопки с изображением принтера пользователь
сможет отправить документ на печать прямо из окна предварительного просмотра.
Что же касается окна настройки параметров страницы документа (рис. 4-21), то с
его помощью пользователь сможет выбрать размер бумаги, расположение документа на
бумаге, поля и т.д.

Рис. 4-21. Окно настройки параметров страницы документа

Итак, Вы добавили в приложение компоненты, необходимые для печати и


настроили их свойства. Теперь, чтобы все это заработало, нужно будет создать все
необходимые обработчики событий.
Редактирование меню File
Прежде чем продолжить создание приложения SimpleNotepad, отредактируйте меню
File. В этом меню должны быть строки PageSetup, Print Preview и Print.
Создайте также обработчики сообщений для перечисленных выше строк меню.
Подключение пространств имен
Для работы с классами, предназначенными для выполнения операций с потоками и
печати, добавьте в начало листинга нашей программы следующие строки:
using System.IO;
using System.Drawing.Printing;
Первая из них нужна для использования потоков класса StringReader, а вторая —
для работы с компонентами, обеспечивающими возможность печати документов.
Настройка параметров страницы документа
Реализацию функциональности, имеющей отношение к печати документов, мы начнем с
самого простого — отображения диалогового окна, предназначенного для настройки
параметров страницы документа.
Добавьте в класс SimpleNotepadForm метод MenuFilePageSetup, предназначенный
для вывода на экран диалогового окна настройки параметров страницы документа,
показанного на рис. 4-21:
/// <summary>
/// Настройка параметров страницы
/// </summary>
private void MenuFilePageSetup()
{
pageSetupDialog1.ShowDialog();
}
Далее вставьте вызов этого метода в тело обработчика событий для строки
PageSetup меню File:
private void menuFilePageSetup_Click(object sender,
System.EventArgs e)
{
MenuFilePageSetup();
}
После трансляции и запуска приложения строка Page Setup меню File начнет
выполнять свои функции.
Предварительный просмотр документа перед печатью
Займемся теперь реализацией режима предварительного просмотра документа перед
печатью.
Прежде всего, добавьте в класс SimpleNotepadForm поля m_myReader и
m_PrintPageNumber, как это показано ниже:
/// <summary>
/// StringReader для печати содержимого редактора текста
/// </summary>
private StringReader m_myReader;

/// <summary>
/// Номер текущей распечатываемой страницы документа
/// </summary>
private uint m_PrintPageNumber;
Первое из этих полей предназначено для хранения ссылки на поток StringReader,
с помощью которого будет осуществляться чтение документа в процессе его печати, а
также предварительного просмотра перед печатью. Второе поле будет хранить номер
текущей распечатываемой страницы документа.
Добавьте в класс SimpleNotepadForm метод MenuFilePrintPreview,
предназначенный для вывода на экран окна предварительного просмотра документа,
показанного на рис. 4-20:
/// <summary>
/// Предварительный просмотр перед печатью документа
/// </summary>
private void MenuFilePrintPreview()
{
m_PrintPageNumber = 1;

string strText = this.richTextBox1.Text;


m_myReader = new StringReader(strText);
Margins margins = new Margins(100,50,50,50);

printDocument1.DefaultPageSettings.Margins = margins;
printPreviewDialog1.ShowDialog();

m_myReader.Close() ;
}
Кроме того, добавьте вызов метода MenuFilePrintPreview в тело обработчика
события строки Print Preview меню File:
private void menuFilePrintPreview_Click(object sender,
System.EventArgs e)
{
MenuFilePrintPreview();
}
Как работает метод MenuFilePrintPreview?
Прежде всего, этот метод записывает в поле m_PrintPageNumber значение 1, т.к.
просмотр начнется с первой страницы документа:
m_PrintPageNumber = 1;
Далее метод читает текущее содержимое окна редактирования в поток
m_myReader класса StringReader:
string strText = this.richTextBox1.Text;
m_myReader = new StringReader(strText);
Затем метод MenuFilePrintPreview отображает окно предварительного просмотра,
задавая для него размеры полей отступов на странице:
Margins margins = new Margins(100,50,50,50);
printDocument1.DefaultPageSettings.Margins = margins;
printPreviewDialog1.ShowDialog();
Конструктор Margins получает через свои параметры величину левого, правого,
верхнего и нижнего отступа в сотых долях дюйма.
Отобразив панель предварительного просмотра, метод MenuFilePrintPreview
закрывает поток m_myReader:
m_myReader.Close();
Если на этом этапе Вы попытаетесь запустить наше приложение, то окно
предварительного просмотра появится на экране пустым. Причина этого заключается в
том, что пока мы определили не все необходимые обработчики событий.
Отображение окна печати документа
Для отображения стандартного диалогового окна печати документов (рис. 4-19)
добавьте в класс SimpleNotepadForm метод MenuFilePrint:
/// <summary>
/// Печать документа
/// </summary>
private void MenuFilePrint()
{
m_PrintPageNumber = 1;

string strText = this.richTextBox1.Text;


m_myReader = new StringReader(strText);
Margins margins = new Margins(100,50,50,50);
printDocument1.DefaultPageSettings.Margins = margins;

if (printDialog1.ShowDialog() == DialogResult.OK)
{
this.printDocument1.Print();
}
m_myReader.Close() ;
}
Вызов этого метода должен осуществляться в обработчике событий строки Print
меню File:
private void menuFilePrint_Click(object sender, System.EventArgs e)
{
MenuFilePrint();
}
Расскажем о том, как работает метод MenuFilePrint.
Печать нашего документа будет начинаться с первой страницы, поэтому в поле
m_PrintPageNumber мы записываем значение 1:
m_PrintPageNumber = 1;
Далее мы читаем текущее содержимое окна редактирования текста в поток
m_myReader класса StringReader:
string strText = this.richTextBox1.Text;
m_myReader = new StringReader(strText);
Эта операция выполняется точно таким же способом, что и в только что
описанном методе MenuFilePrintPreview, предназначенном для отображения окна
предварительного просмотра документа.
Далее мы задаем границы отступов на распечатываемой странице и отображаем
диалоговое окно печати документа. Если пользователь щелкает в этом окне кнопку OK,
документ printDocument1 отправляется на печать методом Print:
Margins margins = new Margins(100,50,50,50);
printDocument1.DefaultPageSettings.Margins = margins;

if (printDialog1.ShowDialog() == DialogResult.OK)
{
this.printDocument1.Print();
}
Далее ненужный более поток m_myReader закрывается методом Close:
m_myReader.Close() ;
Заметим, что на данном этапе приложение еще не в состоянии распечатать
документ (точно так же, как оно пока не в состоянии заполнить окно предварительного
просмотра перед печатью). Причина этого в том, что приложение пока еще не знает,
каким именно образом нужно печатать наш документ.
Обработка события PrintPage
Чтобы в нашем приложении заработали функции предварительного просмотра
документа перед печатью и функция печати, необходимо создать обработчик события
PrintPageEventHandler. Для этого нужно дважды щелкнуть левой клавишей мыши
значок компонента printDocument1 (рис. 4-18). После этого дизайнер форм создаст
пустое тело этого обработчика событий.
Далее Вам придется набрать вручную довольно объемистый исходный текст
обработчика, представленный ниже:
/// <summary>
/// Обработка события PrintPage
/// </summary>
private void PrintPageEventHandler(object sender,
System.Drawing.Printing.PrintPageEventArgs e)
{
int lineCount = 0; // счетчик строк
float linesPerPage = 0; // количество строк на одной странице
float yLinePosition = 0; // текущая позиция при печати по
// вертикальной оси
string currentLine = null; // текст текущей строки

// Шрифт для печати текста


Font printFont = this.richTextBox1.Font;

// Кисть для печати текста


SolidBrush printBrush = new SolidBrush(Color.Black);

// Размер отступа слева


float leftMargin = e.MarginBounds.Left;

// Размер отступа сверху


float topMargin = e.MarginBounds.Top +
3*printFont.GetHeight(e.Graphics);

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


linesPerPage = (e.MarginBounds.Height -
6*printFont.GetHeight(e.Graphics)) /
printFont.GetHeight(e.Graphics);

// Цикл печати всех строк страницы


while(lineCount < linesPerPage &&
((currentLine=m_myReader.ReadLine()) != null))
{
// Вычисляем позицию очередной распечатываемой строки
yLinePosition = topMargin + (lineCount *
printFont.GetHeight(e.Graphics));

// Печатаем очередную строку


e.Graphics.DrawString(currentLine, printFont, printBrush,
leftMargin, yLinePosition, new StringFormat());

// Переходим к следующей строке


lineCount++;
}

// Печать колонтитулов страницы

// Номер текущей страницы


string sPageNumber = "Page " + m_PrintPageNumber.ToString();

// Вычисляем размеры прямоугольной области, занимаемой верхним


// колонтитулом страницы
SizeF stringSize = new SizeF();
stringSize = e.Graphics.MeasureString(sPageNumber, printFont,
e.MarginBounds.Right - e.MarginBounds.Left);

// Печатаем номер страницы


e.Graphics.DrawString(sPageNumber, printFont, printBrush,
e.MarginBounds.Right - stringSize.Width, e.MarginBounds.Top,
new StringFormat());
// Печатаем имя файла документа
e.Graphics.DrawString(this.Text, printFont, printBrush,
e.MarginBounds.Left, e.MarginBounds.Top, new StringFormat());

// Кисть для рисования горизонтальной линии,


// отделяющей верхний колонтитул
Pen colontitulPen = new Pen(Color.Black);
colontitulPen.Width = 2;

// Рисуем верхнюю линию


e.Graphics.DrawLine(colontitulPen,
leftMargin,
e.MarginBounds.Top + printFont.GetHeight(e.Graphics) + 3,
e.MarginBounds.Right, e.MarginBounds.Top +
printFont.GetHeight(e.Graphics) + 3);

// Рисуем линию, отделяющую нижний колонтитул документа


e.Graphics.DrawLine(colontitulPen,
leftMargin, e.MarginBounds.Bottom - 3,
e.MarginBounds.Right, e.MarginBounds.Bottom - 3);

// Печатаем текст нижнего колонтитула


e.Graphics.DrawString(
"SimpleNotepad, (c) Александр Фролов, http://www.frolov.pp.ru",
printFont, printBrush,
e.MarginBounds.Left, e.MarginBounds.Bottom, new StringFormat());

// Если напечатаны не все строки документа,


// переходим к следующей странице
if(currentLine != null)
{
e.HasMorePages = true;
m_PrintPageNumber++;
}

// Иначе завершаем печать страницы


else
e.HasMorePages = false;

// Освобождаем ненужные более ресурсы


printBrush.Dispose();
colontitulPen.Dispose();
}
Если при наборе Вы не допустили никаких ошибок, этот обработчик событий
обеспечит правильное функционирование приложения в режимах предварительного
просмотра документа и его печати. На рис. 4-22 мы показали внешний вид
распечатанной страницы документа.
Рис. 4-22. Внешний вид распечатанной страницы документа

Комментарии в тексте обработчика событий PrintPageEventHandler поясняют


назначение отдельных программных строк. Заметим, что для полного понимания
действий, выполняемых нашим обработчиком событий, требуется предварительное
знакомство с графической подсистемой Graphics Device Interface Plus (GDI+),
реализованной компанией Microsoft в рамках библиотеки классов .NET Framework. Мы
посвятим этой теме 10 главу нашей книги.
Пока же мы только отметим, что приложение распечатывает текст построчно в
цикле. После завершения печати всех строк текущей страницы обработчик событий
PrintPageEventHandler печатает верхний и нижний колонтитулы, а также рисует
горизонтальные линии, отделяющие текст колонтитулов от текста документа.
Учтите, что один и тот же обработчик событий PrintPageEventHandler используется
как в режиме предварительного просмотра, так и в режиме печати документа.
Хотя наше приложение допускает форматирование редактируемых документов и
даже вставку в них через универсальный буфер обмена Clipboard графических
изображений, на печать будет выводиться только текст. Для печати документа с учетом
форматирования и наличия графических изображений обработчик сообщений
PrintPageEventHandler нужно сильно усложнить.
Закрытие главного окна редактора текста
Окно нашего редактора текста должно быть закрыто, когда пользователь выбирает из
меню File строку Exit. Это легко достигается добавлением метода Close в тело
обработчика сообщения menuFileExit_Click:
private void menuFileExit_Click(object sender, System.EventArgs e)
{
this.Close(); // изменения в документе будут потеряны!
}
Однако здесь возникает проблема: окно редактора текста будет закрыто и в том
случае, если пользователь не сохранил сделанные им изменения.
Чтобы решить эту проблему, нам нужно каким-то образом отслеживать наличие
изменений в окне редактирования текста. К счастью, это сделать достаточно просто.
Определим в классе SimpleNotepadForm поле m_DocumentChanged, в котором
будем хранить флаг, отмечающий изменения, сделанные пользователем в документе:
private bool m_DocumentChanged = false;
В новом или только что загруженном документе изменений нет, поэтому
начальное значение этого флага равно false.
Далее откройте окно визуального проектирования формы и щелкните дважды
левой клавишей мыши редактор текста richTextBox1. В результате дизайнер форм
создаст для нас обработчик события richTextBox1_TextChanged. Этот обработчик
получит управление, как только пользователь внесет любые изменения в содержимое
редактируемого документа.
Вам остается только обеспечить изменение состояния флага m_DocumentChanged
внутри этого обработчика:
private void richTextBox1_TextChanged(object sender,
System.EventArgs e)
{
m_DocumentChanged = true;
}
Если пользователь редактировал документ, а потом решил создать новый, выбрав
из меню File строку New, изменения, внесенные в старый документ, могут быть
потеряны. Чтобы избежать этого, проверьте флаг m_DocumentChanged перед тем как
очищать содержимое редактора текста. Если в редакторе есть не сохраненные
изменения, необходимо вызвать метод MenuFileSaveAs, предназначенный для
сохранения документа:
private void menuFileNew_Click(object sender, System.EventArgs e)
{
if(m_DocumentChanged)
MenuFileSaveAs();
richTextBox1.Clear();
}
После того как пользователь сохранил изменения в документе, флаг
m_DocumentChanged необходимо сбросить. Для этого отредактируйте исходный текст
метода MenuFileSaveAs, добавив в него строку сброса указанного флага:
private void MenuFileSaveAs()
{
if(saveFileDialog1.ShowDialog() ==
System.Windows.Forms.DialogResult.OK &&
saveFileDialog1.FileName.Length > 0)
{
richTextBox1.SaveFile(saveFileDialog1.FileName);
m_DocumentChanged = false;
}
}
Теперь подготовьте обработчик события menuFileExit_Click следующим образом:
private void menuFileExit_Click(object sender, System.EventArgs e)
{
if(m_DocumentChanged)
MenuFileSaveAs();
this.Close();
}
И, наконец, надо выполнить еще одну проверку флага m_DocumentChanged — в
методе Dispose, который вызывается при закрытии окна приложения:
/// <summary>
/// Clean up any resources being used.
/// </summary>
protected override void Dispose( bool disposing )
{
if(m_DocumentChanged)
MenuFileSaveAs();

if( disposing )
{
if (components != null)
{
components.Dispose();
}
}
base.Dispose( disposing );
}
Теперь, когда пользователь попытается закрыть программу редактора с помощью
строки Exit меню File или с помощью соответствующей кнопки строки заголовка окна,
не сохранив сделанные изменения, на экране появится стандартное диалоговое окно,
предлагающее ему сохранить документ.
Реализация функций меню Edit
Реализация функций меню Edit, стандартных для всех редакторов текста и многих
других приложений, в нашем случае получается очень простой, т.к. элемент
управления RichTextBox имеет в своем составе все необходимые методы.
Все, что Вам нужно сделать, это вызвать эти методы в соответствующих
обработчиках событий. Например, для реализации функции копирования выделенного
фрагмента текста в универсальный буфер обмена Clipboard нужно добавить в тело
обработчика события строки Copy меню Edit вызов метода Copy.
Добавьте в меню Edit нашего редактора текста строку Redo. Затем подготовьте
обработчики событий от всех строк меню Edit следующим образом:
private void menuEditUndo_Click(object sender, System.EventArgs e)
{
richTextBox1.Undo();
}

private void menuItem1_Click(object sender, System.EventArgs e)


{
richTextBox1.Redo();
}

private void menuEditCut_Click(object sender, System.EventArgs e)


{
richTextBox1.Cut();
}

private void menuEditCopy_Click(object sender, System.EventArgs e)


{
richTextBox1.Copy();
}

private void menuEditPaste_Click(object sender, System.EventArgs e)


{
richTextBox1.Paste();
}

private void menuEditDelete_Click(object sender, System.EventArgs e)


{
richTextBox1.Cut();
}

private void menuEditSelectAll_Click(object sender,


System.EventArgs e)
{
richTextBox1.SelectAll();
}
Теперь наше приложение Simple Notepad сможет обмениваться текстовыми и
графическими данными с другими приложениями Microsoft Windows через
универсальный буфер обмена Clipboard.

Рис. 4-23. Мы вставили в документ текст и графическое изображение

Реализация функций меню Format


Прежде чем продолжить работу над нашим приложением, модифицируйте меню Format,
добавив в него строку Color, а также два меню второго уровня — Character Style (рис.
4-24) и Paragraph Alignment (рис. 4-25).

Рис. 4-24. Меню второго уровня Characters Style

Рис. 4-25. Меню второго уровня Paragraph alignment

Для того чтобы добавить меню второго уровня, Вам нужно вначале ввести с
клавиатуры его название (как Вы это делаете для строк меню первого уровня). Затем
необходимо ввести названия строк меню второго уровня в поле Type Here,
расположенном справа от только что введенной строки первого уровня.
Не забудьте также изменить имена строк меню таким образом, чтобы с ними было
легче работать в программе. Мы, например, назвали строку меню Left меню второго
уровня Paragraph Alignment как menuFormatParagraphAlignmentLeft. Остальные строки
меню второго уровня получили аналогичные названия.
Далее создайте обработчики событий для всех строк меню первого и второго
уровня, щелкнув их дважды левой клавишей мыши.
Шрифт символов
Чтобы пользователь мог выбирать шрифт фрагмента текста, выделенного в окне
редактирования программы SimpleNotepad, перетащите мышью из панели Toolbox в
окно дизайнера формы компонент FontDialog. Этот компонент отображает на экране
стандартное диалоговое окно выбора шрифта Font, показанное на рис. 4-26.

Рис. 4-26. Стандартное диалоговое окно выбора шрифта Font

Чтобы отобразить это диалоговое окно, добавьте вызов метода


fontDialog1.ShowDialog в тело обработчика события menuFormatFont_Click,
получающего управление при выборе строки Font из меню Format:
private void menuFormatFont_Click(object sender, System.EventArgs e)
{
if (fontDialog1.ShowDialog() == DialogResult.OK)
{
richTextBox1.SelectionFont = fontDialog1.Font;
}
}
После того как пользователь выбрал нужный ему шрифт, обработчик события
переписывает этот шрифт из свойства fontDialog1.Font в свойство
richTextBox1.SelectionFont. Свойство SelectionFont позволяет изменить шрифт
фрагмента текста, выделенного пользователем (или программой) в окне
редактирования.
Цвет символов
Чтобы пользователь мог изменять цвет выделенного фрагмента текста, добавьте в
проект компонент ColorDialog, перетащив его значок мышью из панели Toolbox в окно
дизайнера форм.
Этот компонент отображает на экране стандартное диалоговое окно выбора цвета
(рис. 4-27).
Рис. 4-27. Стандартное диалоговое окно выбора цвета Color

Далее модифицируйте обработчик событий для строки Color меню Format


следующим образом:
private void menuFormatColor_Click(object sender, System.EventArgs e)
{
if (colorDialog1.ShowDialog() == DialogResult.OK)
{
richTextBox1.SelectionColor = colorDialog1.Color;
}
}
Здесь программа вначале отображает диалоговое окно выбора цвета, вызывая для
этого метод colorDialog1.ShowDialog. Затем она переписывает выбранный цвет из
свойства colorDialog1.Color диалогового окна в свойство richTextBox1.SelectionColor
окна редактирования текста. В результате цвет фрагмента текста, выделенного
пользователем перед выполнением данной операции, будет изменен.
Стиль символов
Редактируя документы, пользователи часто применяют выделение слов и фрагментов
текста жирным шрифтом, наклоном, подчеркиванием или перечеркиванием. Для
подобного форматирования в нашей программе предусмотрены строки меню второго
уровня Character Style с названиями Bold, Italic, Underline и Strikeout, соответственно.

Рис. 4-28. Изменение стиля символов

Реализация соответствующей функциональности возложена на методы SetBold,


SetItalic, SetUnderline и SetStrikeout, которые мы добавили в обработчики сообщений
строк меню Character Style:
private void menuFormatFontCharacterStyleBold_Click(object sender,
System.EventArgs e)
{
SetBold();
}

private void menuFormatFontCharacterStyleItalic_Click(object sender,


System.EventArgs e)
{
SetItalic();
}

private void menuFormatFontCharacterStyleUnderline_Click(


object sender, System.EventArgs e)
{
SetUnderline();
}

private void menuFormatFontCharacterStyleStrikeout_Click(


object sender, System.EventArgs e)
{
SetStrikeout();
}
Все эти методы имеют одинаковую внутреннюю структуру, поэтому подробно мы
расскажем только об одном из них — о методе SetBold.
Вот исходный текст данного метода:
/// <summary>
/// Установка стиля символов Bold
/// </summary>
private void SetBold()
{
if (richTextBox1.SelectionFont != null)
{
System.Drawing.Font currentFont = richTextBox1.SelectionFont;
System.Drawing.FontStyle newFontStyle;

if (richTextBox1.SelectionFont.Bold == true)
{
newFontStyle = FontStyle.Regular;
}
else
{
newFontStyle = FontStyle.Bold;
}

richTextBox1.SelectionFont = new Font(


currentFont.FontFamily, currentFont.Size, newFontStyle);

CheckMenuFontCharacterStyle();
}
}
Получив управление, метод SetBold прежде всего, определяет шрифт фрагмента
текста, выделенного пользователем, анализируя свойство richTextBox1.SelectionFont.
Если шрифт определить не удалось, и это свойство содержит значение null, наша
программа не делает никаких изменений.
В противном случае программа сохраняет текущий шрифт в переменной
currentFont класса System.Drawing.Font.
Далее метод SetBold проверяет, был ли выделен фрагмент текста жирным
шрифтом, анализируя свойство richTextBox1.SelectionFont.Bold. Если это свойство
содержит значение true, то метод SetBold снимает выделение, если нет, то
устанавливает его.
Для снятия выделения программа записывает в переменную newFontStyle
значение FontStyle.Regular, а для установки — значение FontStyle.Bold.
В дальнейшем содержимое переменной newFontStyle будет использована методом
SetBold для изменения оформления выделенного фрагмента текста. Это изменение
выполняется следующим образом:
richTextBox1.SelectionFont = new Font(
currentFont.FontFamily, currentFont.Size, newFontStyle);
Здесь программа вначале создает новый шрифт как объект класса Font, передавая
конструктору через параметры семейство currentFont.FontFamily текущего шрифта
(установленного до выполнения операции), размер текущего шрифта currentFont.Size, а
также новый стиль шрифта из переменной newFontStyle.
Далее этот шрифт записывается в свойство richTextBox1.SelectionFont, что и
приводит к изменению стиля символов выделенного фрагмента текста.
Обратите также внимание, что при выделении фрагмента текста жирным шрифтом
мы одновременно отмечаем «галочкой» строку меню Bold. С этой целью мы вызываем
созданный нами метод CheckMenuFontCharacterStyle:
/// <summary>
/// Установка отметки строк меню Font->CharacterStyle
/// </summary>
private void CheckMenuFontCharacterStyle()
{
if(richTextBox1.SelectionFont.Bold == true)
{
menuFormatFontCharacterStyleBold.Checked = true;
}
else
{
menuFormatFontCharacterStyleBold.Checked = false;
}

if(richTextBox1.SelectionFont.Italic == true)
{
menuFormatFontCharacterStyleItalic.Checked = true;
}
else
{
menuFormatFontCharacterStyleItalic.Checked = false;
}

if(richTextBox1.SelectionFont.Underline == true)
{
menuFormatFontCharacterStyleUnderline.Checked = true;
}
else
{
menuFormatFontCharacterStyleUnderline.Checked = false;
}

if(richTextBox1.SelectionFont.Strikeout == true)
{
menuFormatFontCharacterStyleStrikeout.Checked = true;
}
else
{
menuFormatFontCharacterStyleStrikeout.Checked = false;
}
}
Метод CheckMenuFontCharacterStyle по очереди проверяет стилевое оформление
выделенных фрагментов, отмечая «галочками» соответствующие строки меню
CharacterStyle или снимая со строк этого меню отметки. Вот как, например, происходит
обработка отметки строки Bold меню CharacterStyle:
if(richTextBox1.SelectionFont.Bold == true)
{
menuFormatFontCharacterStyleBold.Checked = true;
}
else
{
menuFormatFontCharacterStyleBold.Checked = false;
}
Если фрагмент текста, выделенный пользователем, оформлен жирным шрифтом,
свойство richTextBox1.SelectionFont.Bold содержит значение true. В этом случае наша
программа отмечает «галочкой» строку Bold меню CharacterStyle, записывая значение
true в свойство Checked данной строки:
menuFormatFontCharacterStyleBold.Checked = true;
Когда оформление жирным шрифтом снимается, программа убирает галочку,
записывая в свойство Checked значение false:
menuFormatFontCharacterStyleBold.Checked = false;
Исходные тексты методов SetItalic, SetUnderline и SetStrikeout приведены ниже:
/// <summary>
/// Установка стиля символов Italic
/// </summary>
private void SetItalic()
{
if (richTextBox1.SelectionFont != null)
{
System.Drawing.Font currentFont = richTextBox1.SelectionFont;
System.Drawing.FontStyle newFontStyle;
CheckMenuFontCharacterStyle();

if (richTextBox1.SelectionFont.Italic == true)
{
newFontStyle = FontStyle.Regular;
}
else
{
newFontStyle = FontStyle.Italic;
}

richTextBox1.SelectionFont = new Font(


currentFont.FontFamily, currentFont.Size, newFontStyle);

CheckMenuFontCharacterStyle();
}
}

/// <summary>
/// Установка стиля символов Underline
/// </summary>
private void SetUnderline()
{
if (richTextBox1.SelectionFont != null)
{
System.Drawing.Font currentFont = richTextBox1.SelectionFont;
System.Drawing.FontStyle newFontStyle;
CheckMenuFontCharacterStyle();

if (richTextBox1.SelectionFont.Underline == true)
{
newFontStyle = FontStyle.Regular;
}
else
{
newFontStyle = FontStyle.Underline;
}

richTextBox1.SelectionFont = new Font(


currentFont.FontFamily, currentFont.Size, newFontStyle);

CheckMenuFontCharacterStyle();
}
}

/// <summary>
/// Установка стиля символов Strikeout
/// </summary>
private void SetStrikeout()
{
if (richTextBox1.SelectionFont != null)
{
System.Drawing.Font currentFont = richTextBox1.SelectionFont;
System.Drawing.FontStyle newFontStyle;

if (richTextBox1.SelectionFont.Strikeout == true)
{
newFontStyle = FontStyle.Regular;
}
else
{
newFontStyle = FontStyle.Strikeout;
}

richTextBox1.SelectionFont = new Font(


currentFont.FontFamily, currentFont.Size, newFontStyle);

CheckMenuFontCharacterStyle();
}
}
Эти методы мы оставляем Вам для самостоятельного изучения.
Выравнивание параграфов
Оформляя документы, пользователи часто изменяют выравнивание отдельных
параграфов. Применяется выравнивание по левой и правой границам документа, а
также центровка.
После небольшой доработки наш редактор текста тоже сможет выравнивать
параграфы текста, а также графические изображения, вставленные в документ через
универсальный буфер обмена Clipboard (рис. 4-29).
Рис. 4-29. Выравнивание параграфов текста

Для изменения выравнивания текста мы создали в меню Fomat меню второго


уровня Paragraph Alignment (рис. 4-25). Его строки Left, Right и Center обеспечивают
выравнивание, соответственно, по левой и правой границе документа, а также
центровку.
Подготовьте обработчики событий для строк меню Paragraph Alignment следующим
образом:
private void menuFormatFontParagraphAlignmentLeft_Click(
object sender, System.EventArgs e)
{
richTextBox1.SelectionAlignment = HorizontalAlignment.Left;
}

private void menuFormatFontParagraphAlignmentRight_Click(


object sender, System.EventArgs e)
{
richTextBox1.SelectionAlignment = HorizontalAlignment.Right;
}

private void menuFormatFontParagraphAlignmentCenter_Click(


object sender, System.EventArgs e)
{
richTextBox1.SelectionAlignment = HorizontalAlignment.Center;
}
Все эти обработчики событий строк меню Paragraph Alignment изменяют значение
свойства richTextBox1.SelectionAlignment, задающего выравнивание параграфа текста,
выделенного пользователем (для выделения параграфа с целью изменения
выравнивания достаточно установить в него текстовый курсор).
Чтобы текущий (выделенный) параграф выровнять по левой границе документа, в
это свойство нужно записать значение HorizontalAlignment.Left. Для выравнивания по
правой границе воспользуйтесь значением HorizontalAlignment.Right. И, наконец, для
центровки параграфа потребуется значение HorizontalAlignment.Center.
Реализация функций меню Help
В этом разделе мы займемся наделением функциональностью строки About меню Help.
Обычно при выборе этой строки в приложениях Microsoft Windows на экране появляется
диалоговое окно, содержащее сведения о приложении, такие как название, версия,
имена и электронные адреса авторов приложения и пр.
Для создания такого окна в приложении SimpleNotepad мы добавим в проект
новую форму. В окне этой формы и будет отображаться вся информация о нашем
приложении.
Добавление новой формы
Для того чтобы добавить в проект новую форму, щелкните правой клавишей мыши
строку названия проекта SimpleNotepad в окне Solution Explorer. Затем выберите из
меню Add строку Add Windows Form (рис. 4-30).

Рис. 4-30. Добавление в проект новой формы

На экране появится окно Add New Item, показанное на рис. 4-31. В правой части
этого окна, содержащей значки шаблонов, будет выделен шаблон Windows Form.

Рис. 4-31. Ввод имени новой формы

Введите в поле Name строку HelpAboutForm.cs, а затем щелкните кнопку Open. В


результате к проекту будет добавлена новая форма, а также новый класс
HelpAboutForm.
Далее отредактируйте эту форму, добавив в нее описание программы, а также
кнопку OK, предназначенную для закрытия окна формы. На рис. 4-32 мы показали вид
формы About SimpleNotepad, подготовленной для нашего приложения.
Рис. 4-32. Диалоговая форма About SimpleNotepad в окне дизайнера форм

Чтобы форма вела себя как стандартное диалоговое окно ОС Microsoft Windows,
необходимо настроить некоторые ее свойства. Проведите настройку в соответствии с
табл. 4-1.
Таблица 4-1. Настройка свойств формы About SimpleNotepad
Свойство Значение
FormBorderStyle FixedDialog
StartPosition CenterParent
ControlBox false
MinimizeBox false
MaximizeBox false
ShowInTaskbar false
Text About SimpleNotepad
Сделаем некоторые замечания к этой таблице.
Свойство FormBorderStyle определяет вид рамки, расположенной вокруг окна
формы. Если задать здесь значение FixedDialog, окно формы будет снабжено такой же
рамкой, как и все стандартные диалоговые окна Microsoft Windows.
Свойство StartPosition позволяет указать расположение формы в момент ее
отображения. Мы указали здесь значение CenterParent, в результате чего окно About
SimpleNotepad появится в центре главного окна нашего приложения. Вы также можете
выполнить центровку окна относительно экрана (значение Center Screen), позволить
ОС Microsoft Windows самой расположить окно на экране (значение Windows Default
Locations). Существуют и другие способы определения начального расположения окна
формы. Вы можете выбрать соответствующие значения из меню при редактировании
свойства StartPosition.
Для того чтобы убрать кнопки и управляющее меню из заголовка окна диалоговой
формы, мы установили значения свойств MinimizeBox, MaximizeBox и
ControlBox равными false.
Кроме того, мы «выключили» свойство ShowInTaskbar, записав в него значение
false, для того чтобы окно диалоговой формы не было представлено своим значком в
панели задач ОС Microsoft Windows.
И, наконец, свойство Text задает текст заголовка нашей диалоговой формы.
Отображение формы
Чтобы окно формы About SimpleNotepad появилось на экране при выборе
пользователем строки About меню Help, мы добавили две строки в исходный текст
обработчика событий menuHelpAbout_Click:
private void menuHelpAbout_Click(object sender, System.EventArgs e)
{
Form dlgAbout = new HelpAboutForm();
dlgAbout.ShowDialog();
}
Первая строка создает новую форму dlgAbout как объект класса HelpAboutForm, а
вторая отображает форму на экране, вызывая для этого метод dlgAbout.ShowDialog. Мы
показали форму на рис. 4-33 (на этом рисунке главное окно приложения снабжено
инструментальной панелью, которую мы скоро создадим).
Рис. 4-33. Отображение диалоговой формы About SimpleNotepad

Редактирование класса HelpAboutForm


Итак, теперь наша форма About SimpleNotepad появляется на экране, однако закрыть
ее можно только при помощи комбинации клавиш Alt-F4, используемой в ОС Microsoft
Windows для закрытия окон.
Чтобы заставить работать элементы управления, расположенные в форме About
SimpleNotepad, необходимо создать для них обработчики событий. Эта операция
выполняется таким же образом, что и для главной формы нашего приложения.
Создайте обработчики событий, щелкнув дважды все элементы управления формы
в окне дизайнера форм.
Для кнопки OK, закрывающей форму, добавьте в текст обработчика события
button1_Click вызов метода this.Close:
private void button1_Click(object sender, System.EventArgs e)
{
this.Close();
}
Напомним, что этот метод закрывает форму и убирает ее с экрана.
Что же касается элементов управления LinkLabel, то здесь мы использовали тот же
самый программный код, что и в нашем самом первом приложении Hello, описанном во
2 главе книги:
private void linkLabel1_LinkClicked(object sender,
System.Windows.Forms.LinkLabelLinkClickedEventArgs e)
{
linkLabel1.Links[linkLabel1.Links.IndexOf(e.Link)].Visited = true;
System.Diagnostics.Process.Start(linkLabel1.Text);
}

private void linkLabel2_LinkClicked(object sender,


System.Windows.Forms.LinkLabelLinkClickedEventArgs e)
{
linkLabel2.Links[linkLabel2.Links.IndexOf(e.Link)].Visited = true;
System.Diagnostics.Process.Start(" mailto:alexandre@frolov.pp.ru");
}

Создание инструментальной панели


Во многих приложениях, созданных для ОС Microsoft Windows, наиболее часто
использующиеся строки меню дублируются кнопками, расположенными на
инструментальных панелях. Такие панели есть, например, в главном окне самой
системы разработки приложений Microsoft Visual Studio .NET.
В этом разделе мы расскажем, как снабдить окно приложения SimpleNotepad
такой инструментальной панелью, и как создать для этой панели обработчик событий.
Добавление инструментальной панели в окно приложения
Чтобы добавить инструментальную панель в окно приложения, перетащите мышью ее
значок ToolBar из инструментальной панели Microsoft Visual Studio .NET в окно
проектирования формы нашего приложения.
По умолчанию окно инструментальной панели появится в верхней части формы. В
только что добавленной панели нет ни одной кнопки. На рис. 4-34 мы показали
инструментальную панель такой, какой она будет уже после добавления кнопок.

Рис. 4-34. Инструментальная панель в окне приложения SimpleNotepad

Сразу после добавления окно инструментальной панели будет расположено над


окном редактора текста. Чтобы исправить это положение, щелкните правой кнопкой
мыши окно редактора текста, а затем выберите из контекстного меню строку Bring to
Front. В результате окна примут правильное взаимное расположение.
Заметим, что строка Send to Back только что упомянутого меню позволяет
выполнить обратную операцию, а именно, переместить указанный элемент на задний
план.
Подключение списка изображений
В панели инструментов Toolbox системы Microsoft Visual Studio .NET имеется компонент
ImageList, специально предназначенный для хранения списков изображений. Этот
компонент можно использовать совместно с инструментальной панелью ToolBar.
Добавьте компонент ImageList в наш проект, перетащив его значок в нижнюю
часть окна проектирования формы. Он получит идентификатор imageList1 (рис. 4-35).

Рис. 4-35. Программные компоненты приложения SimpleNotepad

Теперь нужно подключить пустой пока список изображений imageList1 к


инструментальной панели. Для этого в окне редактирования свойств Properties
присвойте свойству ImageList инструментальной панели значение imageList1.
Наполнение списка изображений
Прежде чем приступать к наполнению списка изображениями, Вам нужно подготовить
эти изображения при помощи любого графического редактора. Что касается
инструментальной панели приложения SimpleNotepad, то здесь подойдет набор
изображений, входящий в комплект Microsoft Visual Studio .NET. Вы найдете их в
каталоге Program Files\Microsoft Visual Studio .NET\Common7\Graphics\bitmaps\OffCtlBr.
Создайте в папке проекта отдельную папку для хранения изображений
инструментальной панели, назвав ее, например, ImageList. Далее скопируйте в
созданную папку изображения, подготовленные для кнопок панели.
После этого Вам необходимо отредактировать свойство Images компонента
imageList1. При этом запустится редактор набора изображений, окно которого показано
на рис. 4-36.

Рис. 4-36. Окно редактора списка изображений

Для добавления изображения в список щелкните кнопку Add, а затем выберите


файл нужного изображения при помощи стандартного диалогового окна Open (рис. 4-
37).

Рис. 4-37. Выбор файла изображения

Добавив все необходимые изображения, расположите их в правильном порядке,


пользуясь кнопками с изображением стрелок. При необходимости можно удалять
отдельные изображения из списка при помощи кнопки Remove.
Редактирование кнопок инструментальной панели
Следующий этап — создание и редактирование кнопок инструментальной панели. Для
выполнения этой работы необходимо запустить специальный редактор кнопок (рис. 4-
38). Чтобы сделать это, измените свойство панели инструментов Buttons.
Рис. 4-38. Редактор кнопок инструментальной панели

Вначале с помощью кнопки Add добавьте в панель 10 кнопок. Восемь из них будут
играть роль настоящих кнопок, а две будут использованы для разделения трех групп
кнопок.
После добавление кнопки выделите кнопку с идентификатором toolBarButton1
(как это показано на рис. 4-38), и в окне toolBarButton1 Properties отредактируйте
свойство ImageIndex. Редактирование будет заключаться в выборе одного из
изображений, сохраненных Вами ранее в списке imageList1 (рис. 4-39).

Рис. 4-39. Редактирование свойства ImageIndex

Выполните эту операцию для кнопок с номерами 0-2, 4-6, 8 и 9.


Что же касается кнопок 3 и 7, то эти кнопки будут играть роль разделителей
между группами кнопок, ответственных за выполнение различных групп операций. Для
этих кнопок установите значение свойства Style, равным Separator.
После сохранения результатов и запуска приложения внешний вид
инструментальной панели будет соответствовать рис. 4-34.
Редактируя свойство кнопок с именем Text, Вы можете разместить на кнопках
инструментальной панели подписи с кратким описанием их назначения. Свойство
ToolTipText позволит Вам снабдить каждую кнопку окном с поясняющим сообщением,
которое появляется при наведении на кнопку курсора мыши (рис. 4-40).
Рис. 4-40. Усовершенствованная панель инструментов

По умолчанию подписи к кнопкам располагаются под графическими


изображениями. Но есть и другой вариант — подписи могут находиться справа, как это
показано на рис. 4-41.

Рис. 4-41. Изменение расположения подписей

Чтобы задать такое расположение, установите свойство TextAlign панели


инструментов, равным значению Right. По умолчанию это свойство имеет значение
UnderNeath, задающее расположение подписей в соответствии с рис. 4-40.
Установив свойство Divider равным значению false, можно избавиться от
горизонтальной линии, отделяющей главное меню приложения от инструментальной
панели.
Еще одно интересное свойство инструментальной панели — свойство Wrappable.
По умолчанию оно имеет значение true, из-за чего при уменьшении размера окна по
горизонтали может произойти свертка инструментальной панели (рис. 4-42).
Рис. 4-42. Свертка инструментальной панели

Чтобы этого не происходило, установите значение свойства Wrappable равным


false.
И, наконец, установив значение свойства Appearance, равным Flat, можно создать
панель инструментов с плоскими кнопками (рис. 4-43).

Рис. 4-43. Панель с плоскими кнопками

Строка состояния
Последнее добавление, которое мы сделаем к программе SimpleNotepad, — это
добавление строки состояния. Строка состояния представляет собой узкое окно,
располагающееся внизу окна приложения и предназначенная для отображения
текущего состояния программы.
Добавление строки состояния
Чтобы добавить строку состояния, перетащите мышью из панели Toolbox в форму
приложения значок элемента управления StatusBar.
Окно строки состояния окажется над окном редактора текста. Чтобы расположить
окна правильно, щелкните правой кнопкой мыши окно редактора текста, а затем
выберите из контекстного меню строку Bring to Front.
В результате этих действий главное окно нашего приложения примет вид,
показанный на рис. 4-44.
Рис. 4-44. В окно приложения добавлена строка состояния

Настройка свойств строки состояния


Строка состояния может работать в двух режимах — в простом режиме и в режиме
отображения панелей. Простой режим пригоден только для отображения одной-
единственной текстовой строки. В режиме отображения панелей окно строки состояния
можно разделить на панели, каждая из которых будет отображать различную
информацию.
Чтобы отобразить текстовую строку в простом режиме, достаточно присвоить эту
строку свойству stastusBar1.Text, например:
stastusBar1.Text = "Приложение SimpleNotepad";
Мы же настроим строку состояния для работы в более интересном режиме с
использованием панелей.
Прежде всего, мы приравняем свойство Text нашей панели пустой строке.
Для отображения панелей приравняйте свойству ShowPanels значение true.
Далее мы отредактируем свойство Panels, чтобы поделить строку состояния на
несколько панелей. Редактирование этого свойства выполняется при помощи редактора
набора панелей, показанного на рис. 4-45.

Рис. 4-45. Редактор набора панелей

Добавьте в строку состояния с помощью кнопки Add две панели. Первая панель с
идентификатором statusBarPanel1 будет использована для объяснения назначения
строк меню во время их выбора, а вторая — для индикации факта изменения
содержимого документа, загруженного в окно редактирования.
Первая панель (с идентификатором statusBarPanel1) должна изменять свой размер
в зависимости от ширины окна. Поэтому для нее установите значение свойства AutoSize
равным Spring.
Кроме того, присвойте свойству Text панелей statusBarPanel1 и statusBarPanel2
пустую строку. Это нужно сделать потому, что содержимое панелей будет определяться
программой.
Привязка строки состояния к меню
Для того чтобы отслеживать выбор строк меню, наша программа должна предусмотреть
специальный обработчик события Select. Это событие создается строками меню, когда
пользователь выбирает их при помощи мыши или клавиатуры.
Прежде всего, создадим обработчик событий. Для этого выделите мышью меню
File в окне дизайнера формы, а затем щелкните кнопку Events (с изображением
высоковольтного разряда) в окне Properties (рис. 4-46).

Рис. 4-46. Создаем обработчик события MunuSelect

Здесь находится список событий и обработчиков для них. Назначьте для события
Select обработчик MenuSelect, щелкнув соответствующую строку списка дважды левой
клавишей мыши или введя его имя с клавиатуры, как это показано на рис. 4-45. После
того как Вы нажмете клавишу Enter, будет создано пустое тело обработчика события
MenuSelect:
private void MenuSelect(object sender, System.EventArgs e)
{

}
На данном этапе наш обработчик будет вызываться только тогда, когда
пользователь раскроет меню File. Теперь нам нужно подключить этот обработчик ко
всем строкам главного меню приложения с целью обработки события Select.
Для этого Вам нужно по очереди выделить все строки меню нашего приложения, а
затем для каждой строки выбрать в только что упомянутом окне Properties из списка
события Select обработчик события MenuSelect (рис. 4-47).
Рис. 4-47. Выбираем из меню обработчик события MunuSelect

После этого обработчик события MenuSelect будет получать управление всякий


раз, когда пользователь выбирает из меню любую строку.
Далее измените исходный текст метода MenuSelect следующим образом:
private void MenuSelect(object sender, System.EventArgs e)
{
MenuItem mi = (MenuItem)sender;
string ms;
switch(mi.Text)
{
case "&New": ms = "Новый документ"; break;
case "&Open...": ms = "Открыть документ"; break;
case "&Save": ms = "Сохранить документ"; break;
case "&Save As...": ms = "Сохранить документ"; break;
case "Page Set&up...": ms = "Параметры страницы"; break;
case "Print Pre&view...": ms = "Предварительный просмотр";
break;
case "&Print...": ms = "Печатать документ"; break;
case "Exit": ms = "Завершение работы"; break;
case "&Undo": ms = "Отменить ввод"; break;
case "&Redo": ms = "Повторить ввод"; break;
case "Cu&t": ms = "Вырезать"; break;
case "&Copy": ms = "Скопировать"; break;
case "&Paste": ms = "Вставить"; break;
case "&Delete": ms = "Удалить"; break;
case "&Select All": ms = "Выделить все"; break;
case "&Font...": ms = "Выбрать шрифт"; break;
case "C&olor...": ms = "Выбрать цвет"; break;
case "&Bold": ms = "Жирный шрифт"; break;
case "&Italic": ms = "Наклонный шрифт"; break;
case "&Underline": ms = "Подчеркивание"; break;
case "&Strikeout": ms = "Перечеркивание"; break;
case "&Left": ms = "Выравнивание по левой границе"; break;
case "&Right": ms = "Выравнивание по правой границе"; break;
case "&Center": ms = "Центровка"; break;
case "&About...": ms = "О программе"; break;
default: ms = ""; break;
}
statusBarPanel1.Text = ms;
}
Здесь мы вначале используем первый параметр метода с именем sender, через
который передается ссылка на объект, вызвавший появление события. В нашем случае
это будет строка меню, выбранная пользователем.
Программа преобразует эту ссылку к типу MenuItem, предназначенному для
хранения такого рода ссылок, и записывает ее в переменную mi:
MenuItem mi = (MenuItem)sender;
Далее программа анализирует текст строки меню, хранящийся в свойстве mi.Text,
с помощью оператора switch. В зависимости от того, какая строка меню вызвала
событие, в переменную ms записывается тот или иной текст, поясняющий назначение
данной строки меню.
Затем этот текст записывается в левую панель строки состояния statusBarPanel1:
statusBarPanel1.Text = ms;
Что же касается правой панели строки состояния statusBarPanel2, то в запись него
выполняется обработчиком события richTextBox1_TextChanged. Измените его
следующим образом:
private void richTextBox1_TextChanged(object sender,
System.EventArgs e)
{
m_DocumentChanged = true;
statusBarPanel2.Text = "Изменено";
}
Напомним, что этот обработчик получает управление, когда пользователь
изменяет содержимое окна редактирования. При этом в правой панели строке
состояния будет отображаться слово «Изменено» (рис. 4-48).

Рис. 4-48. Теперь строка состояния нашего приложения работает

Для того чтобы очистить правую панель строки состояния после создания нового
документа, загрузки и сохранения документа, модифицируйте обработчики сообщений
menuFileNew_Click, а также методы MenuFileOpen и MenuFileSaveAs, как это показано
ниже:
private void menuFileNew_Click(object sender, System.EventArgs e)
{
if(m_DocumentChanged)
MenuFileSaveAs();

richTextBox1.Clear();
statusBarPanel2.Text = "";
m_DocumentChanged = false;
}

private void MenuFileOpen()


{
if(openFileDialog1.ShowDialog() ==
System.Windows.Forms.DialogResult.OK &&
openFileDialog1.FileName.Length > 0)
{
try
{
richTextBox1.LoadFile(openFileDialog1.FileName,
RichTextBoxStreamType.RichText);
}
catch (System.ArgumentException ex)
{
richTextBox1.LoadFile(openFileDialog1.FileName,
RichTextBoxStreamType.PlainText);
}
this.Text = "Файл [" + openFileDialog1.FileName + "]";
statusBarPanel2.Text = "";
m_DocumentChanged = false;
}
}

private void MenuFileSaveAs()
{
if(saveFileDialog1.ShowDialog() ==
System.Windows.Forms.DialogResult.OK &&
saveFileDialog1.FileName.Length > 0)
{
richTextBox1.SaveFile(saveFileDialog1.FileName);
this.Text = "Файл [" + saveFileDialog1.FileName + "]";
m_DocumentChanged = false;
statusBarPanel2.Text = "";
}
}
Во все эти методы необходимо добавить следующую строку, очищающую правую
панель строки состояния:
statusBarPanel2.Text = "";

Визуальное проектирование
приложений C#
А.В. Фролов, Г.В. Фролов
ГЛАВА 5. ДИАЛОГОВЫЕ ОКНА
ОКНО СООБЩЕНИЙ MESSAGEBOX
Перегруженные методы MessageBox.Show
Параметры метода MessageBox.Show
Возвращаемое значение
Доработка меню File приложения SimpleNotepad
СОЗДАНИЕ МОДАЛЬНЫХ ОКОН
Замена окну MassageBox
Создание новой формы
Создание обработчиков событий
Отображение формы
Альтернативный способ передачи информации из формы
Диалоговое окно регистрации программы
Создание формы регистрации
Поля ввода текстовой информации
Флажки с зависимой фиксацией
Флажки с независимой фиксацией
Список ComboBox
Добавление календаря
Кнопки для закрытия формы
Настройка свойств формы
Программирование формы регистрации
Обработка событий
Передача регистрационной информации
Добавление свойств
Свойства для текстовых полей
Свойства для флажков с зависимой фиксацией
Свойства для флажков с независимой фиксацией
Свойство для календаря
Отображение формы регистрации
НЕМОДАЛЬНЫЕ ДИАЛОГОВЫЕ ОКНА
Приложение PropertyApp
Форма главного окна приложения PropertyApp
Немодальная форма настройки свойств
ПРОВЕРКА ДАННЫХ ФОРМЫ
Подключение компонента ErrorProvider
Создание обработчика события Validating
Настройка свойств компонента ErrorProvider

Глава 5. Диалоговые окна


В предыдущей главе на примере приложения SimpleNotepad мы познакомились со
многими аспектами визуального проектирования приложений с использованием
системы разработки Microsoft Visual Studio .NET. В частности, мы создавали такие
объекты, как диалоговые окна (диалоговые формы).
Надо сказать, что значительная часть пользовательского интерфейса приложений
реализуется именно с применением диалоговых окон. Это окна, предназначенные для
открытия, сохранения, печати и закрытия документов, окна отображения и настройки
всевозможных параметров и т.д.
Диалоговые окна принято делить на модальные и немодальные окна. Когда
приложение открывает на экране модальное окно, его работа будет приостановлена до
тех пор, пока пользователь не закроет это окно. Что же касается немодальных окон, то
они работают одновременно с главным окном открывшего их приложения.
В составе ОС Microsoft Windows имеется набор готовых модальных диалоговых
окон, предназначенных для выполнения наиболее часто встречающихся операций,
таких как открытие файла, выбор шрифта или цвета. В предыдущей главе мы в общих
чертах рассказали о применении некоторых из этих окон. Применяя готовые
диалоговые окна (так же как и другие готовые компоненты), программист может
заметно сократить время разработки своего приложения.
Окно сообщений MessageBox
Во 2 главе нашей книги мы рассказывали Вам о том, как создать простейшее модальное
окно сообщений на базе класса MessageBox, входящего в библиотеку классов
Microsoft .NET Framework. Мы использовали это окно в приложении Hello (листинг 2-1).
Для удобства мы воспроизвели внешний вид окна класса MessageBox,
отображаемого приложением Hello, на рис.5-1.
Рис. 5-1. Диалоговое окно с сообщением

Для отображения диалогового окна мы использовали метод Show, передав ему


через параметр текст сообщения:
MessageBox.Show("Для Вас есть сообщение!");
Ниже мы привели прототип использованного здесь метода Show:
public static DialogResult Show(string message);
Когда пользователь щелкает кнопку OK, метод Show возвращает значение, равное
DialogResult.OK.
Перегруженные методы MessageBox.Show
Существует множество перегруженных вариантов метода MessageBox.Show,
позволяющих задать необходимый внешний вид диалоговой панели, а также
количество и тип расположенных на ней кнопок.
Прототип наиболее общего варианта метода MessageBox.Show, позволяющий
реализовать практически все возможности диалогового окна MessageBox, приведен
ниже:
public static DialogResult Show(
string message, // текст сообщения
string caption, // заголовок окна
MessageBoxButtons btns, // кнопки, расположенные в окне
MessageBoxIcon icon, // значок, расположенный в окне
MessageBoxDefaultButton defButton, // кнопка по умолчанию
MessageBoxOptions opt // дополнительные параметры
);
Параметры метода MessageBox.Show
Рассмотрим назначение отдельных параметров метода MessageBox.Show.
Параметр caption позволяет задать текст заголовка диалогового окна MessageBox.
С помощью параметра btns Вы можете указать, какие кнопки необходимо
расположить в окне диалогового окна. Этот параметр задается константами из
перечисления MessageBoxButtons (табл. 5-1).
Таблица 5-1. Перечисление MessageBoxButtons
Константа Кнопки, отображаемые в окне MessageBox
OK OK
OKCancel OK, Cancel
YesNo Yes, No
YesNoCancel Yes, No, Cancel
RetryCancel Retry, Cancel
AbortRetryIgnore Abort, Retry, Ignore

Параметр icon метода MessageBox.Show позволяет выбрать один из нескольких


значков для расположения в левой части диалогового окна. Он задается в виде
константы перечисления MessageBoxIcon (табл. 5-2).
Таблица 5-2. Перечисление MessageBoxIcon
Константа Значок
Asterisk

Error

Exclamation

Hand

Information

None Значок не отображается


Question

Stop

Warning

Как видите, для диалогового окна MessageBox можно выбирать один из четырех
значков. Если же эти значки Вам не подходят, придется отказаться от использования
диалогового окна MessageBox и создавать свое собственное модальное диалоговое
окно.
Параметр defButton метода MessageBox.Show предназначен для выбора кнопки,
которая получит фокус сразу после отображения диалогового окна. Этот параметр
должен иметь одно из значений перечисления MessageBoxDefaultButton (табл. 5-3).
Таблица 5-3. Перечисление MessageBoxDefaultButton
Константа Номер кнопки, получающей фокус ввода по
умолчанию
Button1 1
Button2 2
Button3 3

Если в диалоговом окне отображаются кнопки Yes, No и Cancel, то они будут


пронумерованы последовательно: кнопка Yes получит номер 1 (константа Button1),
кнопка No — номер 2 (константа Button2), а кнопка Cancel — номер 3 (константа
Button3).
И, наконец, последний параметр opt метода MessageBox.Show позволяет задать
дополнительные параметры. Эти параметры должны иметь значения из перечисления
MessageBoxOptions (табл. 5-4).
Таблица 5-4. Перечисление MessageBoxOptions
Константа Описание
DefaultDesktopOnly Окно с сообщением отображается только на рабочем
столе, выбранном по умолчанию (в системах с
несколькими рабочими столами)
RightAlign Текст сообщения выравнивается по правому краю
диалогового окна
RtlReading Текст отображается справа налево
ServiceNotification Окно отображается на активном рабочем столе, даже если
к системе не подключился ни один пользователь. Данный
параметр предназначен для приложений, работающих как
сервисы ОС Microsoft Windows
Возвращаемое значение
Если в окне диалогового окна MessageBox имеется несколько кнопок, то для того чтобы
определить, какую кнопку щелкнул пользователь, программа должна проанализировать
значение, возвращенное методом MessageBox.Show.
Метод MessageBox.Show может вернуть одно из нескольких значений
перечисления DialogResult (табл. 5-5).
Таблица 5-5. Перечисление DialogResult
Константа Кнопка, при щелчке которой возвращается эта
константа
Abort Abort
Cancel Cancel
Ignore Ignore
No No
None Модальное диалоговое окно продолжает работать
OK OK
Retry Retry
Yes Yes

Доработка меню File приложения SimpleNotepad


Вооружившись новыми знаниями относительно перегруженных методов
MessageBox.Show, доработаем наше приложение SimpleNotepad (на протяжении этой
главы мы будем дорабатывать это приложение, полные исходные тексты после всех
доработок Вы найдете в приложении 1 к нашей книге).
Сделаем так, чтобы при попытке создать или загрузить новый документ, когда
старый документ не был сохранен, программа запрашивала пользователя о
необходимости сохранения старого документа (рис. 5-2).

Рис. 5-2. Запрос на сохранение документа

Если пользователю нужно сохранить старый документ, он должен щелкнуть в


диалоговом окне Сохранение документа кнопку Yes. При этом на экране появится
диалоговое окно сохранения документа.
В том случае, когда пользователь решил отказаться от сохранения документа, он
щелкает кнопку No. При этом в окно редактирования будет загружен новый документ.
И, наконец, кнопка Cancel позволит отказаться от создания или загрузки нового
документа и вернуться к редактированию старого документа.
Для реализации этих усовершенствований необходимо внести изменения в методы
MenuFileNew, MenuFileOpen и menuFileExit_Click.
Вот новый исходный текст метода MenuFileNew:
/// <summary>
/// Создание нового файла
/// </summary>
private void MenuFileNew()
{
if(m_DocumentChanged)
{
DialogResult result = MessageBox.Show(
"Документ изменен. \nСохранить изменения?",
"Сохранение документа", MessageBoxButtons.YesNoCancel,
MessageBoxIcon.Warning, MessageBoxDefaultButton.Button1);

switch(result)
{
case DialogResult.Yes:
{
MenuFileSaveAs();
break;
}
case DialogResult.Cancel:
{
return;
}
}
}

richTextBox1.Clear();
statusBarPanel2.Text = "";
m_DocumentChanged = false;
}
Напомним, что при изменении содержимого окна редактирования в переменную
m_DocumentChanged записывается значение true. В этом случае метод MenuFileNew
вызовет метод MessageBox.Show для отображения на экране окна Сохранение
документа, показанного на рис. 5-2.
Далее метод MenuFileNew проанализирует значение, возвращенное методом
MessageBox.Show. Это значение зависит от того, какую кнопку щелкнул пользователь.
Соответственно, метод MenuFileNew предпримет различные действия.
Если пользователь щелкнул кнопку Yes, метод MessageBox.Show возвращает
значение DialogResult.Yes. В этом случае метод MenuFileNew приглашает пользователя
сохранить старый документ, вызывая для этого метод MenuFileSaveAs.
В том случае, когда пользователь отказался от сохранения старого документа,
метод MenuFileNew очищает окно редактора richTextBox1 и убирает строку сообщения
об изменении документа из правой панели строки состояния нашего приложения.
И, наконец, если пользователь щелкнул кнопку Cancel, метод MenuFileNew
возвращает управление, не предпринимая никаких действий.
Аналогичным образом доработаны методы MenuFileOpen и menuFileExit_Click:
/// <summary>
/// Открытие существующего файла
/// </summary>
private void MenuFileOpen()
{
if(m_DocumentChanged)
{
DialogResult result = MessageBox.Show(
"Документ изменен. \nСохранить изменения?",
"Сохранение документа", MessageBoxButtons.YesNoCancel,
MessageBoxIcon.Warning, MessageBoxDefaultButton.Button1);

switch(result)
{
case DialogResult.Yes:
{
MenuFileSaveAs();
break;
}
case DialogResult.Cancel:
{
return;
}
}
}

if(openFileDialog1.ShowDialog() ==
System.Windows.Forms.DialogResult.OK &&
openFileDialog1.FileName.Length > 0)
{
try
{
richTextBox1.LoadFile(openFileDialog1.FileName,
RichTextBoxStreamType.RichText);
}
catch (System.ArgumentException ex)
{
richTextBox1.LoadFile(openFileDialog1.FileName,
RichTextBoxStreamType.PlainText);
}
this.Text = "Файл [" + openFileDialog1.FileName + "]";
statusBarPanel2.Text = "";
m_DocumentChanged = false;
}
}

private void menuFileExit_Click(object sender, System.EventArgs e)


{
if(m_DocumentChanged)
{
DialogResult result = MessageBox.Show(
"Документ изменен. \nСохранить изменения?",
"Сохранение документа", MessageBoxButtons.YesNoCancel,
MessageBoxIcon.Warning, MessageBoxDefaultButton.Button1);

switch(result)
{
case DialogResult.Yes:
{
MenuFileSaveAs();
break;
}
case DialogResult.Cancel:
{
return;
}
}
}
this.Close();
}
Мы оставляем Вам эти методы для самостоятельного изучения.
Создание модальных окон
Как мы уже говорили, помимо только что рассмотренного модального диалогового окна
класса MessageBox программисту доступны стандартные модальные диалоговые окна,
предназначенные для выполнения основных операций. Такие окна мы использовали в
предыдущей главе для приложения SimpleNotepad.
В этом разделе мы расскажем подробнее о создании собственных модальных окон
и приведем примеры приложений, получающих от пользователей данные через такие
окна.
Замена окну MassageBox
Хотя в приложении SimpleNotepad мы уже создавали собственное модальное окно About
SimpleNotepad для отображения информации о программе (рис. 4-33), это окно не
возвращало никаких значений. Теперь мы создадим диалоговое окно, заменяющее окно
MessageBox и предназначенное для выдачи запроса пользователю о необходимости
сохранения документа в приложении SimpleNotepad. Т.е. мы заменим стандартное окно
Сохранение документа, показанное на рис. 5-2, своим собственным модальным
диалоговым окном.
Создание новой формы
Прежде всего, добавьте в проект новую форму. Для этого щелкните правой клавишей
мыши строку названия проекта SimpleNotepad в окне Solution Explorer. Затем выберите
из меню Add строку Add Windows Form (рис. 4-30). На экране появится окно Add New
Item, показанное на рис. 4-31. В правой части этого окна, содержащей значки
шаблонов, будет выделен шаблон Windows Form.
Введите в поле Name строку SaveDocumentNeededForm.cs, а затем щелкните
кнопку Open. В результате к проекту будет добавлена новая форма, а также новый
класс SaveDocumentNeededForm.
Отредактируйте эту форму, добавив в нее графическое изображение, текст с
предупреждением о необходимости сохранения документа, а также кнопки Да, Нет и
Отменить (рис. 5-3).

Рис. 5-3. Форма Сохранение документа

Чтобы форма вела себя как стандартное модальное диалоговое окно ОС Microsoft
Windows, необходимо настроить некоторые ее свойства. Проведите настройку в
соответствии с табл. 5-6.
Таблица 5-6. Настройка свойств формы SaveDocumentNeededForm
Свойство Значение
FormBorderStyle FixedDialog
StartPosition CenterParent
ControlBox false
MinimizeBox false
MaximizeBox false
ShowInTaskbar false
Text About SimpleNotepad
CancelButton buttonCancel
AcceptButton buttonYes
Почти все эти настройки мы уже использовали для диалогового окна About
SimpleNotepad (рис. 4-32). Это окно мы создавали в предыдущей главе для
отображения информации о приложении SimpleNotepad.
Здесь добавилась только две настройки — мы присвоили свойству CancelButton
идентификатор кнопки Отменить, а свойтсву AcceptButton — идентификатор кнопки Да.
В результате наше диалоговое окно можно будет закрыть при помощи клавиши
Esc. Кроме того, нажатие клавиши Enter на клавиатуре будет эквивалентно щелчку
мышью кнопки Да. Настройте эти свойства подобным образом и для упомянутого выше
диалогового окна About SimpleNotepad.
Заметим, что свойству CancelButton можно присвоить не только идентификатор
кнопки, но и также идентификатор любого объекта, реализующего интерфейс
IButtonControl, в частности, элемент управления LinkLabel.
При добавлении в форму кнопок, а также других элементов управления и
компонентов, дизайнер форм автоматически создает для них идентификаторы.
В предыдущей главе для удобства работы с исходным текстом приложения мы
меняли идентификаторы строк меню. Сейчас мы поменяем идентификаторы кнопок.
Для этого отредактируйте свойство кнопок Name. Установите значение этого
свойства для кнопки Да, равным buttonYes, а для кнопок Нет и Отменить — равным
buttonNo и buttonCancel, соответственно.
Создание обработчиков событий
Далее создайте для всех кнопок обработчики событий, а затем измените их следующим
образом:
private void buttonYes_Click(object sender, System.EventArgs e)
{
DialogResult = DialogResult.Yes;
}

private void buttonNo_Click(object sender, System.EventArgs e)


{
DialogResult = DialogResult.No;
}

private void buttonCancel_Click(object sender, System.EventArgs e)


{
DialogResult = DialogResult.Cancel;
}
Действия, выполняемые этими обработчиками событий, на первый взгляд не
очевидны. Расскажем о том, что же здесь происходит.
В каждом из обработчиков событий мы присваиваем свойству DialogResult формы
нашего диалогового окна одно из значений перечисления DialogResult. Когда это
происходит, форма исчезает с экрана, а метод ShowDialog, с помощью которого окно
диалоговой панели отображается на экране, возвращает значение свойства
DialogResult.
Отображение формы
Итак, мы добавили к проекту приложения SimpleNotepad новую форму и
настроили ее должным образом. Теперь измените исходный текст методов MenuFileNew,
MenuFileOpen и menuFileExit_Click.
Ниже мы привели новый исходный текст метода MenuFileNew:
private void MenuFileNew()
{
if(m_DocumentChanged)
{
SaveDocumentNeededForm dialog = new SaveDocumentNeededForm();
DialogResult result = dialog.ShowDialog();

switch(result)
{
case DialogResult.Yes:
{
MenuFileSaveAs();
break;
}
case DialogResult.Cancel:
{
return;
}
}
}

richTextBox1.Clear();
statusBarPanel2.Text = "";
m_DocumentChanged = false;
}
По сравнению с предыдущим вариантом этого метода мы заменили вызов метода
MessageBox.Show следующими строками, отображающими на экране диалоговое окно
Сохранение документа, показанное на рис. 5-3:
SaveDocumentNeededForm dialog = new SaveDocumentNeededForm();
DialogResult result = dialog.ShowDialog();
Обратите внимание, что значение, возвращенное методом dialog.ShowDialog,
сохраняется в переменной result и используется в дальнейшем для определения
кнопки, которую щелкнул пользователь в окне Сохранение документа.
Метод dialog.ShowDialog вернет то самое значение, которое обработчик событий
нажатой кнопки записал в свойство DialogResult формы SaveDocumentNeededForm.
Аналогичные изменения Вам нужно внести в исходный текст методов
MenuFileOpen и menuFileExit_Click.
Альтернативный способ передачи информации из формы
Мы только что рассмотрели способ передачи в вызывающую программу информации о
том, какой кнопкой пользователь завершил работу диалогового окна, основанный на
создании обработчиков событий. Однако существует и еще один, более простой способ.
Удалите из исходного текста формы SaveDocumentNeededForm обработчики
событий от кнопок, добавленные на предыдущем этапе. О том, как это правильно
делать, мы рассказали в разделе «Удаление обработчика событий» 3 главы.
Далее отредактируйте свойство DialogResult кнопок Да, Нет и Отменить (рис. 5-4).
Рис. 5-4. Настройка свойства DialogResult для кнопки Да

Присвойте свойству DialogResult кнопке Да значение Yes, выбрав это значение из


списка, показанного на рис. 5-4. Этому же самому свойству кнопок Нет и Отменить
присвойте, соответственно, значения No и Cancel.
Когда пользователь щелкает одну из наших кнопок, значение свойства
DialogResult щелкнутой кнопки будет присвоено свойству DialogResult диалогового
окна, в котором эта кнопка расположена. В нашем случае это окно
SaveDocumentNeededForm.
Заметим, что большинство кнопок, располагаемых в модальных диалоговых окнах,
создается для того, чтобы закрыть это окно. В зависимости от того, какая кнопка была
для этого использована, вызывающая программа предпринимает те или иные действия.
Благодаря тому, что в классе Button, на базе которого создаются кнопки,
предусмотрено свойство DialogResult, обработка событий от этих кнопок сильно
упрощается. Фактически для этого Вам не нужно писать вручную ни одной строчки
кода.
Диалоговое окно регистрации программы
Многие разработчики программ, особенно условно-бесплатных, предлагают
пользователям зарегистрировать копию программы. Как правило, для этого
пользователь должен заполнить регистрационную форму и отправить ее содержимое по
электронной почте. В ответ пользователь обычно получает ключ регистрации,
позволяющий использовать все возможности программы, бесплатное сопровождение и
пр.
В нашей программе SimpleNotepad мы тоже предусмотрим форму регистрации,
однако ее назначение ограничивается демонстрацией приемов ввода и обработки
информации в диалоговых окон. Никакого сопровождения программы SimpleNotepad мы
обеспечить не можем!
Создание формы регистрации
Добавьте в проект форму Регистрация программы, создав для нее файл RegisterForm.cs.
Процедура добавления в проект новых форм уже была описана ранее (например, в
разделе «Замена окну MassageBox» этой главы), поэтому мы не будем о ней подробно
рассказывать.
Внешний вид формы, которая должна у Вас получиться, показан на рис. 5-5.
Рис. 5-5. Диалоговое окно регистрации программы

Вам нужно будет добавить ряд элементов управления из инструментальной


панели Toolbox системы разработки приложений Microsoft Visual Studio .NET. Ниже мы
расскажем о том, какие элементы управления требуется добавить, и как настроить их
свойства. Затем мы займемся подготовкой обработчиков сообщений.
Поля ввода текстовой информации
Всего в форме Регистрация программы мы предусмотрели четыре поля ввода текстовой
информации. Это поля Ф.И.О., Адрес E-Mail, Оставьте Ваш комментарий, а также поле
ввода названия любимой ОС.
Добавив в форму указанные поля, измените их идентификаторы, хранящиеся в
свойстве Name, в соответствии с табл. 5-7.
Таблица 5-7. Идентификаторы полей ввода текстовой информации
Поле Идентификатор
Ф.И.О. textBoxName
Адрес E-Mail textBoxEmail
Оставьте Ваш textBoxComment
комментарий
Поле ввода названия textBoxFavoriteOS
любимой ОС

Кроме этого, во всех этих полях сотрите тестовые строки, задающие значение
свойства Text и снабдите их поясняющими надписями, как это показано на рис. 5-5.
Так как поле Оставьте Ваш комментарий предназначено для ввода многострочного
комментария, установите значение свойства Multiline равным true. После этого Вы
сможете отрегулировать не только ширину, но и высоту этого поля.
Далее, установите значение свойства Enabled поля ввода названия любимой ОС
textBoxFavoriteOS, равным false. В этом случае сразу после отображения формы данное
поле будет заблокировано. Разблокировка поля будет осуществляться обработчиком
событий от флажка Другая (укажите, какая), т.к. это поле нужно только в том случае,
если пользователь отметил этот флажок.
Флажки с зависимой фиксацией
Форма регистрации содержит две группы флажков с независимой фиксацией. Группа
флажков Пол позволяет указать пол владельца копии программы, а группа флажков
Любимая ОС — название любимой операционной системы владельца.
Заметим, что вторая группа флажков снабжена дополнительным полем ввода
текстовой информации. Если пользователю не нравится ни Microsoft Windows, ни Linux,
он может отметить флажок Другая (укажите, какая), а затем ввести название своей
любимой ОС в этом дополнительном текстовом поле.
Как видно из названия, из флажков с зависимой фиксацией, принадлежащих
одной группе, в отмеченном состоянии может находиться только один флажок. Таким
образом, нельзя указать одновременно и мужской, и женский пол, а из любимых ОС
можно выбрать только одну.
Для объединения флажков Муж. и Жен. в группу Пол перетащите из
инструментальной панели Toolbox элемент управления GroupBox. Запишите строку
«Пол» в свойство Text этого элемента управления.
Затем перетащите внутрь окна элемента управления GroupBox два флажка типа
RadioButton. Это и есть флажки с зависимой фиксацией. Свое название они получили
по аналогии с кнопками радиоприемника, которые можно нажимать только по одной.
Измените надпись около флажков в соответствии с рис. 5-5, отредактировав
свойства флажков Text.
Аналогичным образом создайте группу флажков Любимая ОС, добавив в нее три
флажка MS Windows, Linux и Другая (укажите, какая).
Добавив все флажки и расположив их внутри соответствующих окон GroupBox,
проверьте их идентификаторы, хранящиеся в свойстве Name, на соответствие табл. 5-
8.
Таблица 5-8. Идентификаторы флажков с зависимой фиксацией
Флажок Идентификатор
Муж. radioButton1
Жен. radioButton2
MS Windows radioButton3
Linux radioButton4
Другая (укажите, какая) radioButton5
Флажки с независимой фиксацией
Флажки с независимой фиксацией не нужно объединять в группы, т.к. они работают
независимо друг от друга. Вам нужно добавить в форму регистрации два таких флажка
класса CheckBox, перетащив их мышью из инструментальной панели Toolbox.
Флажок Подписка на новости должен иметь идентификатор checkBoxSendNews, а
флажок Подписка на электронную газету — идентификатор checkBoxSendLetter. Таким
образом, для этих флажков нужно настроить только свойства Name (идентификатор) и
Text (текст, расположенный около флажка).
Список ComboBox
Для выбора одного значения из нескольких возможных удобен элемент управления
ComboBox. Мы применили его для выбора квалификации пользователя из списка
Квалификация.
Перетащив элемент управления ComboBox в окно нашей формы, снабдите его
надписью Квалификация, как это показано на рис. 5-5.
Далее необходимо заполнить список текстовыми строками, отредактировав
свойство Items. Для этого будет запущен редактор набора строк String Collection Editor
(рис. 5-6).

Рис. 5-6. Редактор набора строк

Введите в окне этого редактора текстовые строки названий для различных


уровней квалификации пользователя.
Чтобы по умолчанию в списке Квалификация была выбрана строка Начальная,
запишите эту строку в свойство Text элемента управления ComboBox.
Добавление календаря
На панели Toolbox имеется очень удобный элемент управления MonthCalendar —
календарь, предназначенный для ввода дат и диапазонов дат. Перетащите этот
календарь на поверхность разрабатываемой формы и снабдите его надписью День
Вашего рождения, как это показано на рис. 5-5.
Вам нужно настроить только одно свойство календаря MaxSelectionCount,
определяющее максимальный диапазон дат, который может выбрать пользователь. В
нашем случае нужно выбрать только один день, поэтому установите значение этого
свойства, равное 1.
Кнопки для закрытия формы
Вам нужно добавить в форму Регистрация программы две кнопки с надписями
Зарегистрировать и Отменить. Первая из них предназначена для отправки по
электронной почте регистрационных данных, введенных пользователем, а вторая
отменяет регистрацию.
Кнопка Зарегистрировать должна иметь идентификатор button1, а кнопка
Отменить — идентификатор button2.
Запишите в свойство DialogResult кнопки Регистрация программы значение Yes, а
в свойство DialogResult кнопки Отменить — значение Cancel.
В результате при закрытии формы кнопкой Регистрация программы вызывающий
метод получит от метода ShowDialog значение DialogResult.Yes, а при закрытии формы
кнопкой Отменить — значение DialogResult.Cancel.
Настройка свойств формы
При редактировании свойств формы Вам нужно изменить ее название, хранящееся в
свойстве Text, а также назначить кнопки клавишам Esc и Enter.
Когда пользователь нажимает на клавиатуре клавишу Esc, процедура регистрации
должна быть отменена, как при щелчке кнопки Отменить. Поэтому свойству формы
CancelButton необходимо присвоить идентификатор кнопки Отменить (button2).
Что же касается клавиши Enter, то ее нужно связать с клавишей
Зарегистрировать. Это позволит завершить вод данных регистрации естественным
образом — нажатием клавиши Enter. Чтобы обеспечить такую функциональность,
присвойте свойству AcceptButton нашей формы значение идентификатора кнопки
Зарегистрировать (button1).
Программирование формы регистрации
Теперь, когда Вы подготовили форму регистрации и настроили свойства
расположенных в ней элементов управления, можно приступить к программированию
формы. Здесь нам нужно решить две задачи: создать обработчики событий нашей
формы, и обеспечить передачу регистрационной информации, введенной
пользователем, в вызывающую программу.
Обработка событий
Благодаря тому, что мы настроили свойство DialogResult кнопок Зарегистрировать и
Отменить, присвоив им значения DialogResult.Yes и DialogResult.Cancel соответственно,
нам не нужно обрабатывать события, создаваемые этими кнопками.
Когда пользователь щелкнет одну из этих кнопок, соответствующее значение
будет автоматически присвоено свойству DialogResult нашей формы. В результате
форма будет закрыта, а значение, соответствующее щелкнутой кнопке, будет передано
вызывающей программе.
Тем не менее, один обработчик событий нам все же придется создать. Речь идет
об обработчике событий для флажка Другая (укажите, какая), входящего в группу
флажков Любимая ОС.
Когда пользователь отмечает этот флажок, программа должна разблокировать
поле ввода текста textBoxFavoriteOS, для того чтобы позволить ему ввести название
любимой ОС. Аналогично, когда пользователь снимает отметку с данного флажка,
программа должна вновь заблокировать поле textBoxFavoriteOS.
Чтобы создать необходимый обработчик событий, дважды щелкните левой мышью
флажок Другая (укажите, какая). Затем подготовьте исходный текст созданного
обработчика событий radioButton5_CheckedChanged в следующем виде:
private void radioButton5_CheckedChanged(object sender,
System.EventArgs e)
{
RadioButton rb = (RadioButton)sender;

if(rb.Checked)
textBoxFavoriteOS.Enabled = true;
else
textBoxFavoriteOS.Enabled = false;
}
Получив управление, этот обработчик событий сохраняет ссылку на флажок,
создавший событие, в переменной rb. Далее программа проверяет значение,
хранящееся в свойстве rb.Checked.
Если это значение равно true, флажок отмечен. В этом случае обработчик событий
снимает блокировку с поля textBoxFavoriteOS, записывая в свойство
textBoxFavoriteOS.Enabled значение true.
В том случае, когда значение свойства rb.Checked равно false, событие было
вызвано тем, что пользователь снял отметку с нашего флажка. В этом случае
программа вновь блокирует поле textBoxFavoriteOS, записывая в свойство
textBoxFavoriteOS.Enabled значение false.
Передача регистрационной информации
Для того чтобы вызывающая программа смогла получить регистрационную
информацию, введенную пользователем в форме регистрации, нам надо будет
модифицировать класс RegisterForm, добавив в него несколько свойств.
Добавление свойств
Вы можете добавить свойства вручную, отредактировав исходный текст файла
RegisterForm.cs, однако Microsoft Visual Studio .NET может оказать Вам в этом
некоторую помощь.
Откройте вкладку Class View, расположенную в правом верхнем углу этой системы
разработки приложений (рис. 5-7).

Рис. 5-7. Добавление нового свойства

На этой вкладке раскройте папку проекта SimpleNotepad. В ней Вы увидите папки


классов — HelpAboutForm, RegisterForm, SaveDocumentNeededForm и
SimpleNotepadForm. Каждая такая папка создается для класса, реализующего ту или
иную форму нашего приложения.
Раскройте папку RegisterForm. В ней находятся методы, свойства, поля,
интерфейсы и другие объекты класса. Чтобы создать новый метод, щелкните правой
клавишей мыши строку названия класса RegisterForm, а затем выберите из
контекстного меню Add строку Add Property, как это показано на рис. 5-7.
В результате на экране появится окно мастера свойств (рис. 5-8).
Рис. 5-8. Указание параметров свойства

Выберите в поле Property access тип доступа public, тип свойства Property type —
string, а имя свойства Property name укажите как UserName. Это свойство будет
использовано нами для получения имени пользователя, введенного в форме
регистрации.
Далее, отметьте флажок get, чтобы создать один функцию доступа get для чтения
значения свойства. Наша программа не будет записывать в это свойство никаких
значений, поэтому функция доступа set не нужна.
Свойства для текстовых полей
В результате выполнения описанных выше действий мастер свойств создаст в файле
RegisterForm.cs пустое тело свойства UserName. Отредактируйте его следующим
образом:
public string UserName
{
get
{
return textBoxName.Text;
}
}
Обращаясь к свойству UserName, вызывающая программа получит текст,
введенный пользователем в поле textBoxName.
Подготовьте аналогичным образом свойства UserEmail, UserLevel и UserComment:
public string UserEmail
{
get
{
return textBoxEmail.Text;
}
}

public string UserLevel


{
get
{
return comboBoxLevel.Text;
}
}

public string UserComment


{
get
{
return textBoxComment.Text;
}
}
Все эти свойства выглядят одинаково, т.к. выполняют схожую функцию —
получают и возвращают текст, введенный (или выбранный) пользователем в
соответствующем элементе управления.
Свойства для флажков с зависимой фиксацией
Что же касается флажков, с помощью которых пользователь указывает свой пол, а
также любимую ОС, то для них нужно подготовить свойства по-другому.
Вот свойство, позволяющее вызывающей программе определить состояние
флажков группы Пол:
public string UserGender
{
get
{
for(int i=0; i < groupBoxGender.Controls.Count; i++)
{
RadioButton rb = (RadioButton)groupBoxGender.Controls[i];
if(rb.Checked)
return rb.Text;
}
return "";
}
}
Здесь мы в цикле перебираем все элементы управления, расположенные внутри
блока groupBoxGender. Количество таких элементов хранится в свойстве
groupBoxGender.Controls.Count.
Для каждого элемента мы получаем ссылку и сохраняем ее в переменной rb
класса RadioButton. Так как в группе groupBoxGender имеются только флажки класса
RadioButton, выполняемое при этом преобразование типов не вызывает никаких
проблем.
Далее программный код свойства UserGender проверяет состояние очередного
флажка. Если флажок отмечен, то в свойстве rb.Checked хранится значение true. В этом
случае функция доступа нашего свойства возвращает текст флажка, хранящийся в
свойстве rb.Text. Иначе программа переходит к проверке следующего флажка.
Ситуация, когда ни один из флажков не отмечен, возможна, только если форма
была подготовлена неправильно. В этом случае функция доступа get возвращает
пустую строку.
Что же касается группы флажков groupBoxOS, то в ней помимо собственно
флажков имеется текстовое поле textBoxFavoriteOS. Напомним, что если пользователь
не любит ни Microsoft Windows, ни Linux, то он может ввести в этом поле название
своей любимой ОС.
В процессе циклического перебора функция доступа обязательно столкнется с
текстовым полем textBoxFavoriteOS, и не сможет привести ссылку на него к типу
RadioButton. Для того чтобы исключить появление ошибки во время выполнения
программы, мы проверяем тип каждого элемента управления при помощи ключевого
слова is:
public string FavoriteOS
{
get
{
for(int i=0; i < groupBoxOS.Controls.Count; i++)
{
if(groupBoxOS.Controls[i] is RadioButton)
{
RadioButton rb = (RadioButton)groupBoxOS.Controls[i];

if(rb.Checked)
{
if(rb.Name != radioButton5.Name)
return rb.Text;
else
return textBoxFavoriteOS.Text;
}
}
}
return "";
}
}
Преобразование типа выполняется только для флажков, а текстовое поле
пропускается.
В том случае, если функция доступа обнаружила отмеченный флажок, она
проверяет его идентификатор. В том случае, если пользователь отметил флажок Другая
(укажите, какая), функция доступа читает свойство textBoxFavoriteOS.Text, а не
свойство Text отмеченной кнопки. В результате функция доступа вернет в этом случае
название ОС, введенное пользователем в текстовом поле.
Заметим, что использованную здесь проверку типов следует применить и в
свойстве UserGender, исходный текст которого был рассмотрен ранее. Это позволит
избежать ошибок в том случае, если Вы решите добавить в группу groupBoxGender
элементы, отличные от флажков RadioButton.
Свойства для флажков с независимой фиксацией
Свойства, предназначенные для передачи в вызывающую программу состояния
флажков с независимой фиксацией, выглядят достаточно просто:
public string SendNews
{
get
{
if(checkBoxSendNews.Checked)
return "Yes";
else
return "No";
}
}

public string SendLetter


{
get
{
if(checkBoxSendLetter.Checked)
return "Yes";
else
return "No";
}
}
Соответствующие функции доступа определяют, отмечены флажки или нет,
проверяя свойства checkBoxSendNews.Checked и checkBoxSendLetter.Checked . В
зависимости от этого они возвращают ту или иную текстовую строку.
Свойство для календаря
Свойство UserBirthDay, созданное нами для получения даты рождения, введенной
пользователем с помощью календаря, выглядит следующим образом:
public string UserBirthDay
{
get
{
DateTime dt = monthCalendar1.SelectionStart;
return dt.Day + "." + dt.Month + "." + dt.Year;
}
}
Здесь мы вначале получаем из свойства monthCalendar1.SelectionStart дату,
выделенную пользователем, и сохраняем эту дату в переменной dt типа DateTime.
Далее из отдельных компонентов даты (календарного числа dt.Day, номера месяца
dt.Month и года dt.Year) формируется текстовая строка. Эта строка и возвращается
функцией доступа get.
Отображение формы регистрации
Для отображения формы регистрации добавьте в меню Help приложения SimpleNotepad
строку Register. Затем создайте для этой строки следующий обработчик события
menuHelpRegister_Click:
private void menuHelpRegister_Click(object sender,
System.EventArgs e)
{
RegisterForm dialog = new RegisterForm();
if(DialogResult.Yes == dialog.ShowDialog())
{
string body = "Данные регистрации:";

body += " Name:" + dialog.UserName;


body += ", Email:" + dialog.UserEmail;
body += ", Level:" + dialog.UserLevel;
body += ", Gender:" + dialog.UserGender;
body += ", FavoriteOS:" + dialog.FavoriteOS;
body += ", SendNews:" + dialog.SendNews;
body += ", SendLetter:" + dialog.SendLetter;
body += ", BirthDay:" + dialog.UserBirthDay;
body += ", Comment:" + dialog.UserComment;

System.Diagnostics.Process.Start(
"mailto:alexandre@frolov.pp.ru?subject=Регистрация&body=" +
body);
}
}
Вначале этот обработчик создает форму регистрации как объект класса
RegisterForm и сохраняет ее идентификатор в переменной dialog. Далее форма
отображается на экране методом dialog.ShowDialog. На рис. 5-9 эта форма показана в
заполненном виде.
Рис. 5-9. Поля окна регистрации заполнены (все приведенные здесь данные
вымышлены)

Если пользователь щелкнул кнопку Зарегистрировать, метод dialog.ShowDialog


возвращает значение DialogResult.Yes. В этом случае обработчик события
menuHelpRegister_Click формирует текстовую строку body, записывая в нее значения,
извлеченные из свойств формы.
Далее обработчик события запускает почтовую программу, установленную на
компьютере пользователя, вызывая для этого метод System.Diagnostics.Process.Start:
System.Diagnostics.Process.Start(
"mailto:alexandre@frolov.pp.ru?subject=Регистрация&body=" + body);
В качестве параметра этому методу передается адрес электронной почты, а также
содержимое поля subject и тело сообщения. В результате на экране появится окно
редактора нового сообщения E-Mail, которое уже будет полностью подготовлено (рис.
5-10).

Рис. 5-10. Письмо с данными регистрации готово к отправке

Для отправки сообщения пользователю останется только щелкнуть кнопку


Отправить, расположенную в этом окне.
Немодальные диалоговые окна
Немодальные диалоговые окна «живут» и работают одновременно с породившим их
главным окном приложения. Таки окна часто используются для «плавающих»
инструментальных панелей, вроде панелей известного графического редактора Adobe
Photoshop. Эти окна могут применяться и для настройки различных параметров
приложения, причем отсутствие модальности позволяет использовать в приложении
измененные параметры, не закрывая окно настройки этих параметров.
Обычно немодальные окна связаны родственными отношениями с создавшими их
окнами. При этом если пользователь минимизирует или закрывает родительское окно,
дочерние немодальные окна также минимизируются или закрываются.
Немодальные окна, как и модальные, создаются на базе классов, произведенных
от класса System.Windows.Form. Однако для их отображения применяется не метод
ShowDialog, а метод Show. Кроме того, свойства формы немодального диалогового окна
настраиваются иначе, чем свойства формы модального окна.
Приложение PropertyApp
Для демонстрации приемов работы с немодальными окнами мы подготовили
приложение PropertyApp, главное окно которого показано на рис. 5-11.

Рис. 5-11. Главное окно приложения PropertyApp

В этом окне располагаются два поля ввода текстовой информации,


предназначенных для ввода адресов серверов электронной почты POP3 и SMTP (первый
из них предназначен для приема сообщений электронной почты, а второй — для
передачи).
Пользователь имеет возможность ввести эти адреса непосредственно в полях
Адрес сервера POP3 и Адрес сервера SMTP, расположенных в главном окне
приложения. Но есть и еще одна возможность — пользователь может щелкнуть кнопку
Настроить, и затем ввести адреса серверов в появившемся на экране немодальном окне
Настройка свойств, показанном на рис. 5-12.

Рис. 5-12. Немодальное окно для ввода адресов почтовых серверов

Наше окно Настройка свойств обладает одним интересным свойством — оно


создает события и передает их в главное окно приложения. Событие создается, когда
пользователь щелкает кнопку Изменить (рис. 5-12). Обработчик данного события,
предусмотренный в классе главного окна приложения, переписывает информацию из
полей окна Настройка свойств в соответствующие поля главного окна приложения
Серверы электронной почты.
Если пользователь вначале ввел адреса серверов в главном окне приложения, а
потом щелкнул расположенную там кнопку Настроить, то эти введенные адреса будут
использованы для инициализации полей немодального окна Настройка свойств. Таким
образом, с помощью окна Настройка свойств можно отредактировать значения,
введенные в главном окне, и сразу же увидеть результат редактирования.
Форма главного окна приложения PropertyApp
Создавая форму главного окна приложения (рис. 5-11), добавьте в нее два поля ввода
текста и две кнопки. Поле, обозначенное как Адрес сервера POP3, должно иметь
идентификатор Name, равный textBoxPOP3, а поле Адрес сервера SMTP —
идентификатор textBoxSMTP.
Для кнопки Закрыть подготовьте обработчик события button2_Click:
private void button2_Click(object sender, System.EventArgs e)
{
this.Close();
}
Когда пользователь щелкает эту кнопку, главное окно приложения исчезает с
экрана.
Вторая кнопка Настроить предназначена для отображения на экране
немодального окна Настройка свойств, показанного на рис. 5-12. Вам следует создать
для этой кнопки обработчик событий button1_Click в следующем виде:
private void button1_Click(object sender, System.EventArgs e)
{
PropertyForm dialog = new PropertyForm();
dialog.Owner = this;

dialog.SMTP = this.textBoxSMTP.Text;
dialog.POP3 = this.textBoxPOP3.Text;

dialog.ApplyHandler += new EventHandler(PropertyForm_Apply);

dialog.Show();
}
Здесь мы вначале создаем немодальное окно как объект класса PropertyForm,
сохраняя ссылку на него в переменной dialog. Далее мы настраиваем свойство Owner,
записывая в него ссылку на главное окно приложения:
dialog.Owner = this;
В результате создаются родительские отношения между главным окном
приложения и дочерним немодальным окном.
На следующем этапе обработчик событий button1_Click переписывает текст из
полей ввода адресов почтовых серверов в свойства dialog.SMTP и dialog.POP3
немодального диалогового окна:
dialog.SMTP = this.textBoxSMTP.Text;
dialog.POP3 = this.textBoxPOP3.Text;
Чуть позже мы создадим это окно и определим все его компоненты.
Следующая строка подключает обработчик события PropertyForm_Apply,
создаваемого дочерним немодальным окном:
dialog.ApplyHandler += new EventHandler(PropertyForm_Apply);
Подключив обработчик событий, программа отображает дочернее окно на экране
методом Show:
dialog.Show();
Обработчик событий PropertyForm_Apply нужно определить в классе формы
главного окна следующим образом:
private void PropertyForm_Apply(object sender, System.EventArgs e)
{
PropertyForm dialog = (PropertyForm)sender;
this.textBoxPOP3.Text = dialog.POP3;
this.textBoxSMTP.Text = dialog.SMTP;
}
Задача обработчика событий PropertyForm_Apply сводится к переписыванию
адресов почтовых серверов из свойств dialog.POP3 и dialog.SMTP дочернего окна в поля
редактирования главного окна. Т.е. этот обработчик предназначен для отображения в
главном окне нашей программы информации, введенной пользователем в дочернем
немодальном окне.
Немодальная форма настройки свойств
Теперь мы займемся формой для немодального окна. Создайте эту форму с именем
PropertyForm, создав класс с именем PropertyForm.
Расположите в форме текстовые поля ввода адресов почтовых серверов и две
кнопки, показанные на рис. 5-12. Идентификаторы текстовых полей ввода Адрес
сервера POP3 и Адрес сервера SMTP должны быть, соответственно, textBoxPOP3 и
textBoxSMTP.
Кнопка Отменить, предназначенная для закрытия немодального диалогового окна,
должна иметь идентификатор buttonCancel, а кнопка Изменить, создающая событие, —
идентификатор buttonApply.
Подготовив форму, отредактируйте исходный текст класса PropertyForm.
Прежде всего, добавьте ссылку ApplyHandler на событие, реализованное в виде
делегата EventHandler:
/// <summary>
/// Обработчик события от кнопки Изменить
/// </summary>
public event EventHandler ApplyHandler;
Далее подготовьте исходный текст обработчика события buttonApply_Click,
создаваемого кнопкой Изменить:
private void buttonApply_Click(object sender, System.EventArgs e)
{
if(ApplyHandler != null)
ApplyHandler(this, new EventArgs());
}
Анализируя содержимое поля ApplyHandler, этот метод проверяет, был ли
определен соответствующий обработчик события в вызывающей программе. Если был,
то метод buttonApply_Click вызывает его. В качестве первого параметра обработчику
при этом передается ссылка на дочернее окно, а в качестве второго — ссылка на новый
объект EventArgs, позволяющий задать параметры события.
Для кнопки Отменить подготовьте обработчик события buttonCancel_Click,
закрывающий окно дочерней немодальной формы:
private void buttonCancel_Click(object sender, System.EventArgs e)
{
this.Close();
}
Теперь Вам нужно определить два свойства с именами SMTP и POP3:
public string SMTP
{
get
{
return textBoxSMTP.Text;
}
set
{
textBoxSMTP.Text = value;
}
}

public string POP3


{
get
{
return textBoxPOP3.Text;
}
set
{
textBoxPOP3.Text = value;
}
}
Первое из них предназначено для хранения и передачи в вызывающую форму
адреса почтового сервера SMTP, а второе — сервера SMTP. Так как вызывающая
программа выполняет чтение и запись этих свойств, мы предусмотрели обе функции
доступа get и set.
Функция доступа get просто возвращает содержимое соответствующего поля
редактирования текста через свойство Text, а функция set — изменяет содержимое
этого поля, устанавливая новое значение свойства Text.
Как все это работает?
Когда форма главного окна приложения создает форму дочернего окна, она
передает ей через только что упомянутые свойства информацию, введенную
пользователем в полях главного окна.
Когда пользователь редактирует информацию в полях дочерней формы, свойства
SMTP и POP3 применяются для передачи информации в обратном направлении — из
дочерней формы в родительскую. Сигналом для начала этой передачи служит событие,
создаваемое кнопкой Изменить.
Проверка данных формы
При заполнении формы пользователь легко может ошибиться. Особенно часто
случаются ошибки при заполнении текстовых полей, таких как, например, поле адреса
электронной почты или номера кредитной карточки.
Добавив в форму компонент ErrorProvider, программист может организовать
проверку данных, которая будет выполняться в момент завершения ввода в окне того
или иного элемента управления. Например, когда пользователь ввел в текстовом поле
редактирования адрес электронной почты и нажал клавишу табуляции для перехода к
другому полю, программа может проверить этот адрес на наличие формальных ошибок.
Если были допущены ошибки, около поля появится значок, сигнализирующий наличие
ошибки.
Подключение компонента ErrorProvider
Давайте модифицируем форму регистрации, рассмотренную нами ранее в разделе
«Программирование формы регистрации» этой главы, добавив проверку данных,
введенных в полях Ф.И.О. и Адрес E-Mail (рис. 5-9).
Прежде всего, откройте окно редактирования свойств редактора textBoxName,
предназначенного для ввода имени, фамилии и отчества пользователя (рис. 5-13).
Рис. 5-13. Свойство CausesValidation

Обратите внимание, что значение свойства CausesValidation равно true. В


результате, когда окно редактирования textBoxName потеряет фокус ввода (т.е. когда
пользователь перейдет к заполнению другого поля формы), будет происходить
проверка содержимого формы.
Далее перетащите на поверхность формы из инструментальной панели Toolbox
системы Microsoft Visual Studio .NET значок программного компонента ErrorProvider. В
результате станет возможной обработка ошибок, допущенных при заполнении полей
этой формы.
Создание обработчика события Validating
На следующем этапе Вам нужно создать обработчик события Validating. Это событие
возникнет в тот момент, когда элемент управления потеряет фокус ввода. Для того
чтобы создать обработчик этого события, откройте вкладку событий в окне Properties и
введите в поле Validating имя обработчика события textBoxName_Validating (рис. 5-14).

Рис. 5-14. Создание обработчика события Validating

В результате будет создано пустое тело метода textBoxName_Validating, которое


нужно будет дополнить кодом, выполняющим все необходимые проверки.
Что касается проверки поля Ф.И.О., то мы должны убедиться, что оно не пустое.
Для этого мы проверяем длину текстовой строки, введенной пользователем:
private void textBoxName_Validating(object sender,
System.ComponentModel.CancelEventArgs e)
{
if(textBoxName.Text.Length == 0)
{
errorProvider1.SetError(textBoxName, "Не указано имя");
}
else
errorProvider1.SetError(textBoxName, "");
}
Если эта длина равна нулю, то обработчик события вызывает метод SetError. В
качестве первого параметра этому методу передается ссылка на проверяемый элемент
управления, а в качестве второго — текст сообщения об ошибке.
Для проверки содержимого поля Адрес E-Mail мы предусмотрели чуть более
сложный обработчик:
private void textBoxEmail_Validating(object sender,
System.ComponentModel.CancelEventArgs e)
{
string email = textBoxEmail.Text;

if(email.IndexOf('@') == -1 || email.IndexOf('.') == -1)


{
errorProvider1.SetError(textBoxEmail,
"Неправильный адрес E-Mail");
}
else
errorProvider1.SetError(textBoxEmail, "");
}
Здесь мы проверяем, что введенный адрес электронной почты содержит символ @
и точку. Если этих символов нет, то адрес E-Mail введен с ошибкой.
На рис. 5-15 мы показали проверку ошибок в действии.
Рис. 5-15. Сообщение об ошибке в адресе E-Mail

Обратите внимание, что пользователь указал неправильный адрес электронной


почты. При попытке перейти к полю Квалификация для продолжения работы с формой
около поля Адрес E-Mail возникла красный значок с восклицательным знаком,
сигнализирующий об ошибке. Если установить на этот значок курсор мыши, то на
экране появится всплывающее окно с текстом сообщения об ошибке.
Настройка свойств компонента ErrorProvider
Выделив в окне дизайнера форм значок компонента ErrorProvider, можно настроить его
свойства в окне Properties (рис. 5-16).

Рис. 5-16. Свойства компонента ErrorProvider

Рассмотрим самые необходимые свойства.


Отредактировав свойство Icon, Вы сможете заменить стандартный значок ошибки
(красный кружок с восклицательным знаком) любым другим значком, добавленным
предварительно в проект приложения.
Свойство BlinkRate определяет частоту мигания значка в миллисекундах.
Свойство BlinkStyle задает стиль значка и может иметь следующие значения:
 BlinkIfDifferentError (используется по умолчанию);
 AlwaysBlink;
 NeverBlink
Если задать свойству BlinkStyle значение BlinkIfDifferentError, то значок будет
мигать, если значок отображался ранее, но текст сообщения об ошибке стал другим.
Использование значения AlwaysBlink приводит к постоянному миганию значка, а
значения NeverBlink — к отмене мигания.

Визуальное проектирование
приложений C#
А.В. Фролов, Г.В. Фролов
ГЛАВА 6. ИСПОЛЬЗОВАНИЕ ЭЛЕМЕНТОВ УПРАВЛЕНИЯ
КНОПКИ
Надпись на кнопке
Текст
Шрифт
Цвет
Выравнивание текста
Стиль кнопки
Блокирование кнопки
Изображение на поверхности кнопки
Выбор файла изображения
Выравнивание изображения
Фоновое изображение для кнопки
События
ФЛАЖКИ С ЗАВИСИМОЙ ФИКСАЦИЕЙ
Приложение RadioButtonApp
Панели GroupBox и Panel
Добавление объектов в панели
Обработка событий
Использование изображений
ФЛАЖКИ С НЕЗАВИСИМОЙ ФИКСАЦИЕЙ
Приложение CheckBoxApp
Настройка свойств флажков CheckBox
Обработка событий
Переключение флажка в неопределенное состояние
Извлечение состояния флажков
СПИСКИ
Список ListBox
Создание списка
Настройка свойств
Получение списка выделенных строк
Список CheckedListBox
Список ComboBox
Список DomainUpDown
Элемент управления NumericUpDown
ЭЛЕМЕНТ УПРАВЛЕНИЯ TRACKBAR
Создание элемента управления TrackBar
Свойства элемента управления TrackBar
Обработка события Scroll
ЭЛЕМЕНТ УПРАВЛЕНИЯ PROGRESSBAR
ПОЛОСЫ ПРОКРУТКИ HSCROLLBAR И VSCROLLBAR
КАЛЕНДАРЬ
Создание календаря
Настройка свойств
ЭЛЕМЕНТ УПРАВЛЕНИЯ DATETIMEPICKER
Создание элемента управления DateTimePicker
Настройка свойств
ТАЙМЕР
Создание таймера
Свойства таймера
Методы таймера
Обработка события Tick
ЭЛЕМЕНТ УПРАВЛЕНИЯ TABCONTROL
Создание элемента управления TabControl
Добавление страниц
Редактирование свойств блокнота и страниц
ЭЛЕМЕНТ УПРАВЛЕНИЯ TOOLTIP
Добавление всплывающих подсказок
Настройка параметров всплывающих подсказок
Программный код

Глава 6. Использование элементов


управления
На примере приложений, описанных в предыдущих главах, Вы получили некоторое
представление о многообразии элементов управления, предназначенных для
размещения на поверхности форм. Надо заметить, что мы изучили далеко не все такие
элементы управления и познакомились не со всеми их свойствами.
В этой главе мы расскажем более подробно об использовании некоторых
наиболее часто встречающихся элементов управления, но из-за ограниченного объема
книги приведем описания не всех свойств и событий. Что же касается наиболее полной
информации о свойствах и событиях элементов управления, то Вы сможете ее найти в
справочно-информационной системе MSDN, входящей в комплект системы разработки
Microsoft Visual Studio .NET. Вы также сможете найти эту справочно-информационную
систему в Интернете по адресу http://msdn.microsoft.com.
Кнопки
Наряду с надписями и полями редактирования текстовой информации кнопки
встречаются чаще всего в пользовательском интерфейсе приложений.
Кнопки создаются на базе класса System.Windows.Forms.Button. В этом классе
предусмотрено значительное количество свойств, с помощью которого можно создавать
кнопки самого разного вида. Специально для демонстрации возможностей этого класса
мы подготовили приложение ButtonsApp, главное и единственное окно которого
показано на рис. 6-1.

Рис. 6-1. Кнопки различного вида

Программист может задавать внешний вид и поведение кнопки, помещать на ее


поверхность графические изображения, а также задавать для кнопки фоновое
графическое изображение. Шрифт текста, а также расположение текста и графического
изображения можно изменять. Расскажем об этом подробнее.
Надпись на кнопке
Как мы уже говорили, текст надписи, расположенной на кнопке, задается с помощью
свойства Text. Это свойство, как и другие свойства элементов управления, можно
задавать во время проектирования приложения (design time), а также программно во
время работы приложения (run time).
Текст
Вот, например, как программа может изменить текст надписи для кнопки buttonRed:
buttonRed.Text = "Красный";
Стандартные кнопки ОС Microsoft Windows не допускают изменение шрифта или
цвета надписей. Что же касается кнопок System.Windows.Forms.Button, то здесь перед
программистом открываются широкие возможности.
Шрифт
Свойство Font задает шрифт надписи. На этапе визуального проектирования
приложения можно выбрать этот шрифт из числа шрифтов, установленных в ОС, при
помощи стандартного диалогового окна. Программно шрифт задается следующим
образом:
buttonYellow.Font = new System.Drawing.Font("Comic Sans MS", 11.25F,
System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((System.Byte)
(204)));
Здесь мы создаем новый шрифт как объект класса System.Drawing.Font,
передавая конструктору через параметры такие атрибуты шрифта, как название,
размер, стиль, единицы измерения размера шрифта и кодировку. Подробнее о шрифтах
читайте в 10 главе нашей книги.
Цвет
Редактируя свойство ForeColor, можно выбрать для надписи любой цвет. Ранее мы уже
рассказывали о диалоговом окне выбора цвета. Это окно используется для указания
цвета любых элементов управления.
Программно цвет задается следующим образом:
buttonYellow.ForeColor = System.Drawing.Color.DarkGreen;
Здесь мы задали темно-зеленый цвет надписи, воспользовавшись для этого
статической константой System.Drawing.Color.DarkGreen.
Выравнивание текста
По умолчанию текст надписи центруется внутри окна кнопки. Однако, изменяя свойство
TextAlign, можно выровнять текст по правой, левой, верхней или нижней границам.
Кроме этого, можно выровнять текст по углам окна кнопки.
Программно выравнивание текста задается следующим образом:
buttonGreen.TextAlign = System.Drawing.ContentAlignment.BottomCenter;

Стиль кнопки
Учитывая стремление разработчиков приложений придавать своим программам
интерфейс, подобный интерфейсу Web-приложений, создатели класса
System.Windows.Forms.Button позаботились о возможности изменения стиля кнопки.
Стиль кнопки задается свойством FlatStyle. Это свойство может иметь следующие
значения:
 Flat;
 Popup;
 Standard;
 System
Стиль Standard предназначен для создания обычных «серых» кнопок, знакомых
Вам по старым приложениям Microsoft Windows.
Если выбрать стиль System, то внешний вид кнопки будет определяться
настройками ОС.
Кнопка Popup рисуется плоской. Однако когда пользователь указывает на нее
курсором мыши, кнопка принимает объемный вид. Этот стиль удобно использовать в
тех случаях, когда нужно создать несколько расположенных рядом кнопок. В этом
случае кнопка, над которой в настоящий момент находится курсор мыши, будет
выделяться своим объемным видом.
И, наконец, кнопка Flat всегда остается плоской. Но если пользователь
располагает над такой кнопкой курсор мыши, кнопка становится темнее.
Стиль кнопки можно определить средствами Microsoft Visual Studio .NET, а можно
задать программно, например:
buttonYellow.FlatStyle = System.Windows.Forms.FlatStyle.Popup;
Заметим, что кнопки, располагаемые на страницах Web-сайтов, обычно ведут себя
именно так, как только что описанные кнопки Popup и Flat. Поэтому если внешний вид
Вашего автономного приложения должен быть похож на внешний вид страниц Web,
применяйте именно эти стили.
Блокирование кнопки
Приложение может динамически во время своей работы блокировать и разблокировать
кнопки и элементы управления, расположенные в формах.
Для того чтобы заблокировать кнопку, установите значение свойства Enabled
равным true. Заблокированная кнопка отображается в форме, однако не реагирует на
мышь и клавиатуру, а также не создает событий.
При необходимости приложение может скрыть кнопку или другой элемент
управления, установив значение свойства Visible равным false. Скрытый элемент
управления не отображается в форме и не создает событий.
Изображение на поверхности кнопки
Можно значительно улучшить внешний вид кнопки, расположив на ее поверхности
графическое изображение. Мы пользовались этим приемом в 4 главе, создавая панель
инструментов в приложении SimpleNotepad.
Выбор файла изображения
Чтобы поместить на поверхность кнопки графическое изображение, необходимо
отредактировать свойство Image. Перед этим необходимо скопировать файл
изображения в каталог проекта, а затем добавить его в проект. Эти процедуры были
описаны ранее, когда мы рассказывали о размещении графических изображений на
поверхности формы.
Программно изображение на поверхности кнопки задается так:
buttonGreen.Image =
((System.Drawing.Bitmap)
(resources.GetObject("buttonGreen.Image")));
Здесь изображение извлекается из ресурсов приложения с помощью метода
resources.GetObject, а затем, после приведения типа к System.Drawing.Bitmap,
записывается в свойство Image.
Выравнивание изображения
Редактируя свойство ImageAlign, Вы можете изменить выравнивание изображения,
добавленного в кнопку (по умолчанию изображение центрируется в окне кнопки). Это
можно делать при помощи средств Microsoft Visual Studio .NET, или программно:
buttonGreen.ImageAlign = System.Drawing.ContentAlignment.TopCenter;
Как правило, если на кнопе имеется и текст, и изображение, нужно задать
соответствующее выравнивание и для текста, и для изображения. Например, можно
выровнять изображение влево по центру, а текст — вправо по центру, как это сделано
в кнопке Голубой на рис. 6-1. А можно выровнять изображение верх по центру, а
текст — вниз по центру (кнопка Зеленый на рис. 6-1)
Фоновое изображение для кнопки
Чтобы еще больше улучшить внешний вид кнопки, Вы можете задать для нее фоновое
изображение. Оно будет использовано таким же образом, как фоновое изображение
формы или Web-страницы.
Если размеры фонового изображения равны размерам окна кнопки или больше
этих размеров, то оно будет показано в окне кнопки полностью или частично. В
противном случае фоновое изображение будет размножено внутри окна кнопки до его
полного заполнения.
Чтобы задать фоновое изображение для кнопки, отредактируйте свойство
BackgroundImage. Это можно сделать во время разработки приложения или из
программы:
buttonWhite.BackgroundImage =
((System.Drawing.Bitmap)
(resources.GetObject("buttonWhite.BackgroundImage")));

События
До сих пор мы обрабатывали только одно событие Click, создаваемое при щелчке
кнопки мышью. В приложении ButtonsApp мы подготовили обработчики этого события,
предназначенные для изменения цвета панели panel1 класса Panel, расположенной в
правой части главного окна этого приложения. Создавая приложение, перетащите эту
панель в форму из инструментальной панели Toolbox.
Каждый такой обработчик события устанавливает свойство panel1.BackColor,
записывая в него тот или иной цвет:
private void buttonRed_Click(object sender, System.EventArgs e)
{
panel1.BackColor = System.Drawing.Color.Red;
}

private void buttonYellow_Click(object sender, System.EventArgs e)


{
panel1.BackColor = System.Drawing.Color.LightYellow;
}
private void buttonBlue_Click(object sender, System.EventArgs e)
{
panel1.BackColor = System.Drawing.Color.LightSkyBlue;
}

private void buttonGreen_Click(object sender, System.EventArgs e)


{
panel1.BackColor = System.Drawing.Color.MediumSpringGreen;
}

private void buttonWhite_Click(object sender, System.EventArgs e)


{
panel1.BackColor = System.Drawing.Color.White;
}
Цвет задается как статическая константа класса System.Drawing.Color.
Если в окне проектирования формы щелкнуть кнопку дважды, для нее по
умолчанию будет создан обработчик события Click. Однако кнопка, как и остальные
элементы управления, создает множество других событий. Чтобы увидеть список
событий, создаваемых кнопкой, выделите ее левой клавишей мыши в окне
проектирования формы, а затем откройте вкладку событий, расположенную в окне
Properties (рис. 6-2).

Рис. 6-2. Вкладка событий для кнопки

Как видите, для обработки события Click здесь назначен обработчик


buttonWhite_Click.
Все события, перечисленные на вкладке событий, разделены на группы. Группа
Action (действие) включает в себя только событие Click. Что же касается группы Mouse
(события, связанные с мышью), то в ней имеется целых шесть событий. Они создаются,
когда пользователь нажимает кнопку мыши в окне кнопки (событие MouseDown),
располагает курсор мыши над поверхностью окна кнопки (MouseEnter) или убирает
курсор мыши из этого окна (MouseLeave) и т.д. Выделяя по очереди левой клавишей
мыши названия событий, Вы можете прочитать в нижней части вкладки краткое
описание условий, при которых это событие возникает.
На вкладке событий Вы можете назначить обработчики для любых событий.
Выделите в окне дизайнера форм приложения ButtonsApp любую кнопку, а затем
создайте для событий MouseEnter и MouseLeave обработчики buttonsEnter и
buttonsLeave. Для этого введите имена обработчиков в соответствующих полях вкладки
событий (рис. 6-3).

Рис. 6-3. Добавление обработчиков событий

В результате мастер форм создаст для нас пустые тела обработчиков событий,
получающих управление при перемещении курсора мыши над кнопкой:
private void buttonsEnter(object sender, System.EventArgs e)
{

private void buttonsLeave(object sender, System.EventArgs e)


{

}
Обработчик buttonsEnter получит управление, когда курсор мыши входит в
область окна кнопки, а обработчик buttonsLeave — когда курсор мыши выходит из этой
области.
Выделив по очереди остальные кнопки, расположенные в окне нашей формы,
назначьте для них те же самые обработчики событий buttonsEnter и buttonsLeave, что и
для первой кнопки. Для этого достаточно выбрать имя существующего обработчика
событий из списка (рис. 6-4).

Рис. 6-4. Подключение существующего обработчика событий

Обработка событий buttonsEnter и buttonsLeave позволит Вам произвольным


образом менять внешний вид кнопки, над которой находится курсор мыши. В качестве
примера мы приведем исходные тексты обработчиков этих событий, меняющих фон
кнопки, надо которой находится курсор мыши, на красный цвет.
Кроме того, эти обработчики событий раскрашивают панель panel1 в цвет,
соответствующий той кнопке, над которой в данный момент находится курсор мыши.
Когда курсор мыши покидает область окна кнопки, цвет панели panel1 становится
светло-серым. Таким способом мы реализовали режим предварительного просмотра
цвета, в который будет раскрашена панель panel1 при помощи той или иной кнопки.
Модифицируйте исходные тексты приложения ButtonsApp.
Прежде всего, добавьте в класс Form1 поле oldColor, предназначенное для
хранения исходного цвета фона кнопки:
Color oldColor;
Далее добавьте исходный текст обработчика событий buttonsEnter,
подключенного ко всем кнопкам нашего приложения:
private void buttonsEnter(object sender, System.EventArgs e)
{
Button btn = (Button)sender;
oldColor = btn.BackColor;
btn.BackColor = Color.Red;

switch(btn.Text)
{
case "Красный":
{
panel1.BackColor = System.Drawing.Color.Red;
break;
}
case "Желтый":
{
panel1.BackColor = System.Drawing.Color.LightYellow;
break;
}
case "Голубой":
{
panel1.BackColor = System.Drawing.Color.LightSkyBlue;
break;
}
case "Зеленый":
{
panel1.BackColor = System.Drawing.Color.MediumSpringGreen;
break;
}
case "Белый":
{
panel1.BackColor = System.Drawing.Color.White;
break;
}
}
}
Получив управление, наш обработчик событий получает идентификатор кнопки,
вызвавшей событие, и сохраняет его в переменной btn. Затем обработчик сохраняет в
поле oldColor текущий цвет фона кнопки, получая его из свойства btn.BackColor, а
затем раскрашивает фон кнопки в красный цвет:
Button btn = (Button)sender;
oldColor = btn.BackColor;
btn.BackColor = Color.Red;
Далее обработчик события получает текст надписи на кнопке btn.Text и
определяет, какая кнопка вызвала появление события:
switch(btn.Text)
{
case "Красный":
{
panel1.BackColor = System.Drawing.Color.Red;
break;
}

}
Далее обработчик событий изменяет соответствующим образом цвет панели
panel1.
Когда мышь покидает окно наших кнопок, управление получает обработчик
событий buttonsLeave:
private void buttonsLeave(object sender, System.EventArgs e)
{
Button btn = (Button)sender;
btn.BackColor = oldColor;
panel1.BackColor = System.Drawing.Color.LightGray;
}
Его задача — восстановление исходного цвета фона кнопки, окно которой
покинул курсор мыши, а также раскрашивание панели panel1 в светло-серый цвет.
Создавая подобные обработчики событий buttonsEnter и buttonsLeave, Вы можете
динамически изменять графическое изображение, расположенное в окне кнопки.
Фоновое графическое изображение, текст надписи или шрифтовое оформление этого
текста. Словом, здесь все ограничивается только Вашей фантазией.
Обработка событий MouseDown и MouseUp позволяет задать, например,
произвольный вид кнопки в нажатом и отжатом состоянии.
Флажки с зависимой фиксацией
В разделе «Диалоговое окно регистрации программы» 5 главы мы кратко рассказывали Вам
об использовании флажков с зависимой и независимой фиксацией в форме
регистрации программы SimpleNotepad. В этом разделе мы рассмотрим свойства и
события флажков подробнее.
Прежде всего, займемся флажками с зависимой фиксацией. Напомним, что такие
флажки объединяются в группы, причем в отмеченном состоянии может находиться
лишь один флажок из группы.
Флажки с зависимой фиксацией создаются на базе класса
System.Windows.Forms.RadioButton.
Приложение RadioButtonApp
Мы будем рассказывать о флажках с зависимой фиксацией на примере приложения
RadioButtonApp, главное окно которого показано на рис. 6-5.

Рис. 6-5. Приложение RadioButtonApp

В левой части этого окна находятся флажки, предназначенные для изменения


цвета фона надписи «Выберите цвет фона и текста», а в правой — для изменения цвета
этой надписи.
Панели GroupBox и Panel
Создавая форму главного окна для этого приложения, сначала мы перетащили в нее из
инструментальной панели Toolbox элементы управления GroupBox и Panel. Элемент
управления GroupBox используется для объединения флажков, задающих цвет фона, а
элемент управления Panel — цвет текста.
Элемент управления GroupBox снабжается рамкой с надписью, объясняющей
назначение объединяемых внутри рамки элементов управления. Что же касается
панели Panel, то она представляет собой прямоугольное окно без надписи, внутри
которого можно помещать произвольные объекты. Эта панель может иметь полосы
прокрутки.
Добавление объектов в панели
Как наши флажки привязываются к панелям GroupBox и Panel?
Рассмотрим фрагменты кода приложения RadioButtonApp, отвечающие за создание
флажков, задающих цвет фона, а также за создание объединяющего их элемента
управления GroupBox.
На этапе инициализации приложения создается сам элемент управления GroupBox
(как объект класса System.Windows.Forms.GroupBox), и флажки с зависимой
фиксацией:
this.groupBox1 = new System.Windows.Forms.GroupBox();
this.radioButtonBkgRed = new System.Windows.Forms.RadioButton();
this.radioButtonBkgGreen = new System.Windows.Forms.RadioButton();
this.radioButtonBkgBlue = new System.Windows.Forms.RadioButton();
this.radioButtonBkgWhite = new System.Windows.Forms.RadioButton();
Далее флажки добавляются в элемент управления GroupBox при помощи метода
AddRange:
//
// groupBox1
//
this.groupBox1.Controls.AddRange(
new System.Windows.Forms.Control[]
{
this.radioButtonBkgWhite,
this.radioButtonBkgBlue,
this.radioButtonBkgGreen,
this.radioButtonBkgRed
});

Таким образом, флажки оказываются добавленными к панели groupBox1 класса


GroupBox. Далее программа настраивает свойства панели GroupBox, определяющие ее
расположение, размеры, текст надписи и т.д.:
this.groupBox1.Location = new System.Drawing.Point(24, 16);
this.groupBox1.Name = "groupBox1";
this.groupBox1.Size = new System.Drawing.Size(152, 136);
this.groupBox1.TabIndex = 0;
this.groupBox1.TabStop = false;
this.groupBox1.Text = "Укажите цвет фона";
Аналогичным образом создается панель panel1 класса Panel, надпись и флажки,
расположенные внутри панели:
this.panel1 = new System.Windows.Forms.Panel();
this.label1 = new System.Windows.Forms.Label();
this.radioButtonForeRed = new System.Windows.Forms.RadioButton();
this.radioButtonForeGreen = new System.Windows.Forms.RadioButton();
this.radioButtonForeBlue = new System.Windows.Forms.RadioButton();
this.radioButtonForeBlack = new System.Windows.Forms.RadioButton();
Все объекты, расположенные внутри правой панели, добавляются в нее все тем
же методом AddRange:
//
// panel1
//
this.panel1.Controls.AddRange(
new System.Windows.Forms.Control[]
{
this.radioButtonForeBlack,
this.radioButtonForeBlue,
this.radioButtonForeGreen,
this.radioButtonForeRed,
this.label1
});
this.panel1.Location = new System.Drawing.Point(192, 8);
this.panel1.Name = "panel1";
this.panel1.Size = new System.Drawing.Size(152, 144);
this.panel1.TabIndex = 1;
Обратите внимание, что в панель panel1 добавляются не только флажки, но и
надпись label1 класса Label.
Обработка событий
Для каждой группы флажков мы создаем свой собственный обработчик события
CheckedChanged. Это событие возникает, когда пользователь изменяет состояние
флажка, устанавливая или снимая отметку.
Чтобы создать обработчик событий bkgChanged для группы флажков, отвечающих
за изменение цвета фона нашей надписи, выделите любой флажок в группе Укажите
цвет фона. Затем в окне Properties откройте вкладку событий, введите в поле
CheckedChanged строку bkgChanged и нажмите клавишу Enter. В результате будет
создано тело обработчика событий bkgChanged.

Рис. 6-6. Создание обработчика события CheckedChanged

Далее выделите по очереди все остальные флажки группы Укажите цвет фона, и
назначьте для них обработчик событий bkgChanged при помощи только что
упомянутого окна Properties. Таким образом, изменение состояния всех флажков
группы Укажите цвет фона будет отслеживаться единым обработчиком событий
bkgChanged.
Далее повторите эту операцию для группы флажков Укажите цвет текста,
расположенной в правой части главного окна приложения. Назначьте для них
обработчик событий foreChanged.
Ниже мы привели модифицированные исходные тексты обработчиков событий
bkgChanged и foreChanged:
private void bkgChanged(object sender, System.EventArgs e)
{
RadioButton rb = (RadioButton)sender;
switch(rb.Text)
{
case "Красный":
{
label2.BackColor = Color.LightCoral;
break;
}
case "Зеленый":
{
label2.BackColor = Color.LightGreen;
break;
}
case "Синий":
{
label2.BackColor = Color.LightBlue;
break;
}
case "Белый":
{
label2.BackColor = Color.White;
break;
}
}
}

private void foreChanged(object sender, System.EventArgs e)


{
RadioButton rb = (RadioButton)sender;

switch(rb.Text)
{
case "Красный":
{
label2.ForeColor = Color.Red;
break;
}
case "Зеленый":
{
label2.ForeColor = Color.Green;
break;
}
case "Синий":
{
label2.ForeColor = Color.Blue;
break;
}
case "Белый":
{
label2.ForeColor = Color.White;
break;
}
}
}
Эти обработчики событий работают по одинаковому алгоритму. Вначале они
сохраняют в переменной rb идентификатор флажка, состояние которого было
изменено. Затем обработчики извлекают текст надписи, расположенной возле флажка.
В зависимости от этого текста, обработчики изменяют цвет фона или текста надписи
label2, расположенной в нижней части главного окна нашего приложения.
При необходимости Ваше приложение может обрабатывать и другие события,
создаваемые флажками с зависимой фиксацией. Полный список и кроткое описание
этих событий Вы сможете просмотреть на вкладке событий окна Properties (рис. 6-6).
Обрабатывая, например, события MouseEnter и MouseLeave, Вы сможете динамически
изменять внешний вид флажка, когда над ним находится курсор мыши.
Использование изображений
В распоряжении программиста имеются средства настройки цвета фона и текста для
флажков, аналогичные средствам настройки данных атрибутов кнопок Button.
Так же как и кнопки класса Button, рассмотренные нами в начале этой главы,
флажки можно снабжать графическими изображениями, а также фоновыми
графическими изображениями.
На рис. 6-7 мы привели внешний вид измененного приложения RadioButtonApp. В
нем мы снабдили флажки, задающие цвет фона, графическими изображениями. Кроме
того, для флажка Белый мы задали фоновое графическое изображение.

Рис. 6-7. Использование изображений

Для того чтобы поместить в окно флажка графическое изображение, необходимо


отредактировать свойство Image. Сделав это, необходимо настроить свойства
ImageAlign и TextAlign, чтобы задать взаимное расположение графического
изображения и текста надписи для флажка. Фоновое графическое изображение
помещается в окно флажка при помощи свойства BackgroundImage.
Использование изображений для флажков позволяют значительно улучшить
дизайн приложения, сделав его похожим на дизайн современных Web-приложений.
Флажки с независимой фиксацией
Напомним, что флажки с независимой фиксацией работают независимо друг от друга.
При этом не имеет никакого значения, расположены они внутри панелей GroupBox и
Panel, или непосредственно в окне приложения. Эти флажки создаются на базе класса
System.Windows.Forms.CheckBox.
Как правило, флажки с независимой фиксацией используется в программе, для
того чтобы задавать какие-либо режимы, которые можно устанавливать независимо
друг от друга.
Такие флажки могут находиться во включенном или выключенном состоянии, а
также в третьем, неопределенном состоянии.
Приложение CheckBoxApp
Для демонстрации приемов работы с флажками CheckBox мы подготовили приложение
CheckBoxApp (рис. 6-8). Это приложение предназначено для ввода параметров
подключения к почтовым серверам POP3 и SMTP.
Рис. 6-8. Приложение CheckBoxApp

Как оно работает?


В группе элементов управления Сервер POP3 пользователь может ввести такие
параметры, как адрес сервера, учетная запись и пароль. Флажок с независимой
фиксацией Запомнить пароль указывает программе на необходимость запоминания
пароля, введенного пользователем, с тем, чтобы ему не приходилось вводить этот
пароль при каждом подключении к серверу.
Группа Сервер SMTP объединяет элементы управления, позволяющие задать
параметры подключения к серверу SMTP, ведающему передачей электронной почты.
В поле Адрес пользователь может ввести доменный адресе сервера SMTP. Флажок
Использовать авторизацию на сервере SMTP, который сразу после отображения окна
приложения находится в неотмеченном состоянии, позволяет включить режим
авторизации. В этом режиме для отправки почты необходимо указать дополнительный
пароль.
Если отметить флажок Использовать авторизацию на сервере SMTP, то поле ввода
пароля и флажок Запомнить пароль будут разблокированы. Это позволит пользователю
ввести пароль для сервера SMTP, а также указать режим запоминания пароля.
После того как пользователь заполнил все поля, наша программа отображает
введенные значения в окне, показанном на рис. 6-9.

Рис. 6-9. Просмотр введенных параметров

Если пользователь отметил флажки Запомнить пароль, то этот факт отмечается


словом «сохранить».
Настройка свойств флажков CheckBox
Состав свойств флажков с независимой фиксацией, создаваемых на базе класса
CheckBox, аналогичен составу свойств флажков с зависимой фиксацией RadioButton,
рассмотренных нами ранее в этой главе. Однако есть и отличия.
Свойство Appearance определяет внешний вид флажка. По умолчанию значение
этого свойства равно Normal, в результате чего флажок выглядит так, как это показано
на рис. 6-8. Если же установить значение этого свойства, равным Button, флажок будет
похож на обычную кнопку. В отмеченном состоянии флажок будет нарисован как
нажатая кнопка, а в неотмеченном — как отжатая кнопка.
Свойство CheckAlign позволяет изменять взаимное расположение флажка и
надписи к нему. Изменяя это свойство, Вы можете, например, поместить подпись не
справа, а слева от флажка.
Очень интересное свойство называется ThreeState. Если это свойство имеет
значение true, то флажок сможет принимать не два, а три состояния:
 включен;
 выключен;
 неопределенное состояние.
Флажок последовательно проходит эти состояния, когда пользователь щелкает его
мышью. Состояние флажка можно изменять и программным способом. Для этого нужно
изменить значение свойства CheckState.
Если флажок может находиться только в двух состояниях (свойство ThreeState
равно false), то это свойство может принимать значения CheckState.Checked (флажок
установлен) и CheckState.Unchecked (флажок не установлен).
В том случае, когда значение свойства ThreeState равно true и флажок может
принимать три состояния, свойство CheckState может иметь значение
CheckState.Indeterminate (неопределенное состояние).
Обработка событий
Когда мы создавали обработчики событий для флажков с зависимой фиксацией, то
предусматривали общие обработчики событий для флажков, относящихся к одной и той
же группе. Флажки с независимой фиксацией работают самостоятельно, поэтому для
каждого из них обычно предусматривают собственные обработчики событий. Но чаще
всего состояние флажков определяется при работе обработчиков событий других
элементов управления, таких, например, как кнопки.
Переключение флажка в неопределенное состояние
Вначале мы займемся флажком Использовать авторизацию на сервере SMTP.
Подготовьте для него следующий обработчик события CheckedChanged:
private void checkBox1_CheckedChanged(object sender,
System.EventArgs e)
{
CheckBox cb = (CheckBox)sender;
if(cb.Checked)
{
textBoxSMTPPassword.Enabled = true;
checkBoxStoreSMTPPassword.Enabled = true;
}
else
{
textBoxSMTPPassword.Enabled = false;
checkBoxStoreSMTPPassword.Enabled = false;

if(checkBoxStoreSMTPPassword.Checked)
{
checkBoxStoreSMTPPassword.CheckState =
CheckState.Indeterminate;
}
}
}
Когда пользователь изменяет состояние флажка Использовать авторизацию на
сервере SMTP, обработчик checkBox1_CheckedChanged определяет текущее состояние
флажка (отмечен или не отмечен).
Если флажок отмечен, обработчик события разблокирует поле ввода пароля для
сервера SMTP и флажок Запомнить пароль:
if(cb.Checked)
{
textBoxSMTPPassword.Enabled = true;
checkBoxStoreSMTPPassword.Enabled = true;
}
В противном случае эти поля блокируются:
textBoxSMTPPassword.Enabled = false;
checkBoxStoreSMTPPassword.Enabled = false;
После блокировки полей обработчик события проверяет текущее состояния
флажка Запомнить пароль. Если этот флажок отмечен, то он переводится в
неопределенное состояние:
if(checkBoxStoreSMTPPassword.Checked)
{
checkBoxStoreSMTPPassword.CheckState =
CheckState.Indeterminate;
}
В реальном приложении для обработки аналогичной ситуации нет никакой
необходимости переводить флажок в третье состояние, т.к. можно ограничиться его
блокировкой. Мы привели этот фрагмент кода только как пример способа перевода
флажка в неопределенное состояние.
Реально неопределенное состояние флажков с независимой фиксацией часто
используется в древовидных структурах диалоговых окон настройки каких-либо
параметров. Оно отмечает ветви дерева, содержащие флажки, находящиеся и в
отмеченном, и в неотмеченном состоянии. Такие окна встречаются, например, в
программах установки приложений.
Извлечение состояния флажков
В окне нашего приложения предусмотрена кнопка Сохранить (рис. 6-8),
предназначенная для просмотра информации, введенной пользователем.
Подготовьте обработчик события button1_Click для этой кнопки в следующем
виде:
private void button1_Click(object sender, System.EventArgs e)
{
string str =
"POP3 Server: " + textBoxPOP3Server.Text + "\n"+
"POP3 Username: " + textBoxAccount.Text + "\n" +
"POP3 Password: " + textBoxPOP3Password.Text;

if(checkBoxStorePOP3Password.Checked)
{
str += " (сохранить)";
}

str += "\n\nSMTP Server: " + textBoxSMTPServer.Text + "\n";

if(checkBoxUseSMTPPassword.Checked)
{
str += "SMTP Password: " + textBoxSMTPPassword.Text;
}

if(checkBoxStoreSMTPPassword.Checked &&
checkBoxStorePOP3Password.Checked)
{
str += " (сохранить)";
}

MessageBox.Show(str);
}
Информация, введенная в текстовых полях, извлекается из свойства Text. Что же
касается текущего состояния флажков с независимой фиксацией, то их состояние
можно получить из свойства Checked.
Вот, например, как мы проверяем состояние флажка Запомнить пароль:
if(checkBoxStorePOP3Password.Checked)
{
str += " (сохранить)";
}
Напомним, что если флажок может находиться в трех состояниях, то свойство
Checked может принимать следующие значения:
 CheckState.Checked
 CheckState.Unchecked
 CheckState.Indeterminate
Списки
Перед программистом часто встает задача организации списков, предназначенных для
выбора строки из некоторого заранее определенного набора строк. Например, может
потребоваться список файлов из текущего каталога, список названий цветов для
раскраски какого-либо объекта приложения, список режимов работы приложения и т.
д. Стандартные диалоговые окна, такие как Open и Save As, содержат списки файлов,
каталогов и дисковых устройств.
На панели инструментов Tools системы разработки программного обеспечения
Microsoft Visual Studio .NET имеются готовые элементы управления, с помощью которых
программист может легко добавить списки различных типов в свое приложение.
На рис. 6-10 мы показали главное окно приложения ListBoxApp, созданного нами
для демонстрации способов применения списков в оконных приложениях C#.

Рис. 6-10. Списки в окне приложения ListBoxApp

В левом верхнем углу этого окна находится список, созданный на базе класса
ListBox. С его помощью Вы можете создавать одно-колоночные и многоколоночные
списки, имеющие вертикальную (для одно-колоночных списков) и горизонтальную (для
многоколоночных списков) полосу прокрутки. Список ListBox позволяет пользователю
выбирать как один, так и несколько элементов.
Список, создаваемый на базе класса CheckedListBox, тоже допускает выбор одного
или нескольких элементов. Этот список показан в правом верхнем углу рис. 6-10 и
обозначен как CheckedListBox. Он представляет собой комбинацию списка ListBox и
флажков CheckBox, рассмотренных в предыдущем разделе этой главы.
Список класса ComboBox, показанный в левой части рис. 6-10, является
комбинацией списка и однострочного редактора текста. Поэтому для данного списка
используются свойства и события, аналогичные свойствам и событиям списка ListBox и
редактора текста класса TextBox.
Если щелкнуть мышью кнопку со стрелкой, расположенную в правой части окна
списка ComboBox, это окно раскроется (рис. 6-11).

Рис. 6-11. Раскрыт список ComboBox

Пользователь сможет выбрать нужную строку из списка или набрать ее при


помощи клавиатуры в верхней части окна ComboBox.
В нижней части главного окна нашего приложения находятся элементы
управления DomainUpDown и NumericUpDown. Первый из них позволяет выбирать
текстовую строку из списка методом последовательного перебора, а второй —
последовательно изменять значение числовой величины.
Расскажем об использовании этих элементов управления более подробно.
Список ListBox
Список класса ListBox занимает в форме окно, в котором помещается, как правило,
несколько строк. Если окно не может отобразить сразу все строки списка, то оно
снабжается полосой прокрутки. В зависимости от настройки свойств список ListBox
можно использовать для выбора одного или нескольких элементов.
Создание списка
В процессе визуального проектирования приложения для создания списка ListBox
программист перетаскивает значок этого списка, расположенный на инструментальной
панели Toolbox.
При этом в приложение будет добавлен следующий программный код:
this.listBox1 = new System.Windows.Forms.ListBox();
Здесь список создается как объект класса System.Windows.Forms.ListBox.
Сразу после создания список пуст. Редактируя свойство Items, можно добавить в
список необходимое количество строк. При этом на программном уровне добавление
строк будет выполнено при помощи метода AddRange:
this.listBox1.Items.AddRange(
new object[]
{
"Каждый",
"Охотник",
"Желает",
"Знать",
"Где",
"Сидит",
"Фазан"
}
);
Этот метод позволяет добавить в список сразу несколько строк.
Настройка свойств
Далее необходимо настроить свойства, задающие расположение списка, его имя,
размер и порядок передачи фокуса ввода при использовании клавиши табуляции:
this.listBox1.Location = new System.Drawing.Point(16, 32);
this.listBox1.Name = "listBox1";
this.listBox1.Size = new System.Drawing.Size(120, 95);
this.listBox1.TabIndex = 0;
Весь этот код добавляется в проект автоматически мастером форм, когда Вы
помещаете список внутрь формы.
Целый ряд свойств элемента управления ListBox позволяет задать внешний вид и
поведение списка.
Свойство SelectionMode определяет режим выделения элементов списка и может
принимать следующие значения:
 SelectionMode.None;
 SelectionMode.One;
 SelectionMode.MultiSimple;
 SelectionMode.MultiExtended.
Значение SelectionMode.None запрещает выделение элементов и может быть
использовано, например, для динамического блокирования списка.
Если присвоить свойству SelectionMode значение SelectionMode.One, пользователь
сможет выделить в списке только один элемент.
Значения SelectionMode.MultiSimple и SelectionMode.MultiExtended позволяют
выделять в списке сразу несколько элементов. В первом из этих случаев выделение
нескольких элементов выполняется мышью или клавишей пробела, а во втором
дополнительно можно использовать клавишу Shift и клавиши перемещения курсора.
Установив значение свойства ScrollAlwaysVisible, можно включить режим
постоянного отображения полос прокрутки вне зависимости от количества элементов,
имеющихся в списке.
При помощи свойства Sorted, присвоив ему значение true, можно включить режим
сортировки строк списка, отключенный по умолчанию.
Свойство MultiColumn позволяет расположить строки списка в несколько столбцов.
Получение списка выделенных строк
Обычно в форме, содержащий список, имеется кнопка или другой элемент управления,
с помощью которого пользователь закрывает форму и передает подготовленные с ее
помощью данные вызывающей программе. Именно на этом этапе обработчик событий
должен определить, какие строки были выделены пользователем в списке.
Вначале мы рассмотрим ситуацию, когда пользователю разрешается выделять из
списка только один элемент. В этом случае свойству SelectionMode должно быть
присвоено значение One.
В этом случае номер выделенной строки будет храниться в свойстве
SelectedIndex. Самой верхней строке списка соответствует нулевое значение. Заметим,
что если пользователь не выделил ни одной строки, в свойство SelectedIndex будет
записано отрицательное значение.
Что же касается SelectedItem, то это свойство хранит текст строки, выделенной в
списке пользователем, или пустую строку, если пользователь не выделил ни одной
строки списка.
Для работы с этими свойствами создайте обработчик событий button1_Click для
кнопки OK, расположенной в главном окне нашего приложения (рис. 6-10):
private void button1_Click(object sender, System.EventArgs e)
{
string str;
str = "Список ListBox:";
str += "\nИндекс: " + listBox1.SelectedIndex;
str += "\nЭлемент: " + listBox1.SelectedItem;
MessageBox.Show(str);
}
Этот обработчик последовательно дописывает к строке str индекс выделенной
строки списка и текст этой строки, а затем отображает результат в диалоговом окне
MessageBox (рис. 6-12).

Рис. 6-12. Просмотр выбранной строки списка ListBox

Если пользователю разрешено выбирать из списка сразу несколько элементов, то


программа может получить номера выделенных строк, анализируя свойство
SelectedIndices. Это свойство представляет собой контейнер, содержащий список
выделенных строк. Аналогично, текст выделенных строк можно извлечь из контейнера
SelectedItems:
str += "\n";
foreach (int idx in listBox1.SelectedIndices)
{
str += " " + idx;
}

str += "\n";
foreach (string s in listBox1.SelectedItems)
{
str += " " + s;
}
Добавьте приведенные выше строки в тело обработчика событий button1_Click
непосредственно перед вызовом метода MessageBox.Show. На рис. 6-13 мы показали,
как теперь будет отображаться информация о выделенных строках.

Рис. 6-13. Результат выбора нескольких строк

Заметьте, что свойства SelectedIndex и SelectedIndex при выделении нескольких


строк будут содержать информацию только о первой выделенной строке.
Список CheckedListBox
Элемент управления CheckedListBox во многом напоминает только что рассмотренный
список ListBox, но обладает несколькими особенностями.
Прежде всего, рядом с каждым элементом списка располагается флажок.
Пользователь может отмечать его состояние при помощи мышки или клавиши пробела.
По умолчанию выделение строки списка не приводит к отметке соответствующего
флажка. Однако если установить значение свойства CheckOnClick, то флажки
выделяемых строк будут автоматически отмечаться.
Заметим, что в отличие от списка ListBox, список CheckedListBox не допускает
одновременное выделение нескольких строк. Соответственно, свойство SelectionMode
может принимать значения SelectionMode.One или SelectionMode.None. В первом случае
пользователь сможет выделить одну строку, а во втором — ни одной.
Тем не менее, в списке CheckedListBox пользователь может отметить несколько
флажков, расположенных около разных строк, реализовав, таким образом,
множественное выделение.
Добавление элементов в список CheckedListBox выполняется примерно тем же
самым способом, что и в список ListBox, а именно, с применением метода AddRange:
this.checkedListBox1 = new System.Windows.Forms.CheckedListBox();

this.checkedListBox1.Items.AddRange(
new object[]
{
"Каждый",
"Охотник",
"Желает",
"Знать",
"Где",
"Сидит",
"Фазан"
}
);
Мастер форм автоматически добавляет следующий код для настройки основных
свойств списка:
this.checkedListBox1.Location = new System.Drawing.Point(160, 32);
this.checkedListBox1.Name = "checkedListBox1";
this.checkedListBox1.Size = new System.Drawing.Size(112, 94);
this.checkedListBox1.TabIndex = 1;
this.checkedListBox1.CheckOnClick = true;
Для того чтобы узнать номер выделенной строки и получить ее текст,
воспользуйтесь свойствами SelectedIndex и SelectedItem, соответственно. Что же
касается множественной отметки строк, то свойство CheckedIndices содержит список
искомых номеров, а свойство CheckedItems — список искомых текстовых строк.
Чтобы попробовать все это в действии, добавьте следующий фрагмент кода в тело
обработчика событий button1_Click непосредственно перед вызовом метода
MessageBox.Show:
str += "\n\nСписок CheckedListBox:";
str += "\nИндекс: " + checkedListBox1.SelectedIndex;
str += "\nЭлемент: " + checkedListBox1.SelectedItem;

str += "\n";
foreach (int idx in checkedListBox1.CheckedIndices)
{
str += " " + idx;
}

str += "\n";
foreach (string s in checkedListBox1.CheckedItems)
{
str += " " + s;
}
На рис. 6-14 мы показали сведения о строках, выделенных в списках ListBox и
CheckedListBox.
Рис. 6-14. Добавлена обработка списка CheckedListBox

Список ComboBox
Как мы уже говорили, элемент управления ComboBox представляет собой комбинацию
однострочного редактора текста и списка. Когда пользователь щелкает кнопку со
стрелкой, расположенную в правой части окна элемента управления ComboBox, список
раскрывается, и пользователь может выбрать из него нужную строку. Если же такой
строки нет, или если пользователь не желает ее искать, строку можно ввести
непосредственно с клавиатуры в поле редактирования.
После создания программа добавляет в список новые строки с помощью метода
AddRange:
this.comboBox1 = new System.Windows.Forms.ComboBox();

this.comboBox1.Items.AddRange(
new object[]
{
"Каждый",
"Охотник",
"Желает",
"Знать",
"Где",
"Сидит",
"Фазан"
}
);
Далее происходит настройка основных свойств списка:
this.comboBox1.Location = new System.Drawing.Point(16, 168);
this.comboBox1.Name = "comboBox1";
this.comboBox1.Size = new System.Drawing.Size(120, 21);
this.comboBox1.TabIndex = 2;
this.comboBox1.Text = "Каждый";
Возможно, для Вас будет интересно свойство MaxDropDownItems, задающее
количество строк в раскрывающемся окне списка. По умолчанию оно равно 8.
С помощью свойства Sorted можно отсортировать элементы списка по алфавиту,
для чего нужно присвоить этому свойству значение true.
Чтобы получить строку, выбранную пользователем из списка или введенную в
поле редактирования элемента управления ComboBox, необходимо обратиться к
свойству Text:
str += "\n\nСписок ComboBox: " + comboBox1.Text;

Список DomainUpDown
Элемент управления DomainUpDown, показанный в левой нижней части рис. 6-10,
позволяет последовательно выбирать текстовые строки, нажимая кнопки со стрелками,
расположенные в правой части окна списка. При этом происходит «прокрутка»
текстовых строк списка.
Кроме того, пользователь может ввести нужную ему строку непосредственно в
окне элемента управления DomainUpDown при помощи клавиатуры. В этом список
DomainUpDown напоминает только что рассмотренный элемент управления ComboBox.
Ниже мы привели фрагмент кода, создающий и наполняющий список
DomainUpDown, а также настраивающий его некоторые свойства:
private System.Windows.Forms.DomainUpDown domainUpDown1;

this.domainUpDown1.Items.Add("Каждый");
this.domainUpDown1.Items.Add("Охотник");
this.domainUpDown1.Items.Add("Желает");
this.domainUpDown1.Items.Add("Знать");
this.domainUpDown1.Items.Add("Где");
this.domainUpDown1.Items.Add("Сидит");
this.domainUpDown1.Items.Add("Фазан");
this.domainUpDown1.Location = new System.Drawing.Point(16, 232);
this.domainUpDown1.Name = "domainUpDown1";
this.domainUpDown1.TabIndex = 3;
this.domainUpDown1.Text = "Каждый";
Среди свойств списка DomainUpDown заслуживает упоминание свойство ReadOnly.
Если установить значение этого свойства, равное true, пользователь сможет выбирать
строки из списка, но ввод строки с клавиатуры будет ему недоступен.
Свойства UpDownAlign и TextAlign позволяют задать выравнивание кнопок
прокрутки списка и текста строк списка, соответственно. По умолчанию кнопки
располагаются справа, а текст выравнивается по левой границе окна списка.
Если свойство Wrap имеет значение true, то список будет свернутым в кольцо.
Таким образом, пользователь сможет многократно перебирать все его элементы в
прямом или обратном направлении.
Для того чтобы получить строку, введенную пользователем или выбранную в
списке DomainUpDown, Вам потребуется обратиться к свойству Text:
str += "\nСписок DomainpDown: " + domainUpDown1.Text;

Элемент управления NumericUpDown


Хотя, строго говоря, элемент управления NumericUpDown и не является списком в
обычном понимании этого слова, он, подобно списку DomainUpDown, позволяет
изменять цифровые значения, «прокручивая» их в окне при помощи кнопок со
стрелками.
Вот как программа создает элемент управления NumericUpDown:
this.numericUpDown1 = new System.Windows.Forms.NumericUpDown();
По умолчанию мастер форм инициализирует элемент управления NumericUpDown
следующим образом:
this.numericUpDown1.Location = new System.Drawing.Point(160, 232);
this.numericUpDown1.Name = "numericUpDown1";
this.numericUpDown1.Size = new System.Drawing.Size(112, 20);
this.numericUpDown1.TabIndex = 4;
Вы можете выполнить дополнительные настройки, отредактировав
соответствующие свойства.
Прежде всего, при помощи свойства Hexadecimal можно переключить элемент
управления NumericUpDown в режим ввода шестнадцатеричных чисел, если приравнять
этому свойству значение true.
Свойство DecimalPlaces задает количество цифр после десятичной точки.
Присвоив значению ThousandsSeparator значение true, можно включить режим
выделения разрядов тысяч.
Величина инкремента и декремента задается свойством Increment, а минимальное
и максимальное значения — при помощи свойств Minimum и Maximum, соответственно.
Для того чтобы получить текущее значение, установленное пользователем в окне
элемента управления NumericUpDown, воспользуйтесь свойством Value:
str += "\nСписок NumericUpDown: " + numericUpDown1.Value;
На рис. 6-15 мы показали результат просмотра состояния всех списков, включая
списки DomainUpDown и NumericUpDown.

Рис. 6-15. Теперь программа обрабатывает все списки

Элемент управления TrackBar


Элемент управления TrackBar представляет собой шкалу с движком, с помощью
которого пользователь может изменять (регулировать) численное значение. Это может
быть уровень громкости, баланс звуковых каналов, насыщенность отдельных
компонентов цвета, яркость и пр.
Движок элемента управления TrackBar можно передвигать мышью, клавишами
перемещения курсора, а также клавишами Home, End, PgUp и PgDn. При перемещении
движка создаются события Scroll.
Помимо движка, в окне элемента управления TrackBar есть деления. Они
отображаются в виде коротких штрихов, расположенных на равном расстоянии друг от
друга (рис. 6-16).
Вы можете выбрать горизонтальное или вертикальное расположение окна
TrackBar. Деления могут находиться с любой стороны, с обеих сторон или их может не
быть совсем.
При создании элемента управления TrackBar приложение должно определить
диапазон значений, соответствующих положению движка, шаг делений, а также шаг
изменения этих значений.
Создание элемента управления TrackBar
Для демонстрации возможностей элемента управления TrackBar мы подготовили
приложение TrackBarApp, главное окно которого показано на рис. 6-16.

Рис. 6-16. Элемент управления TrackBar

В этом окне всего два элемента управления — текстовое поле Label и элемент
управления TrackBar. Последний из них позволяет регулировать значение,
отображаемое в текстовом поле. Диапазон регулировки — от 0 до 100. При
использовании клавиш перемещения курсора значение регулируется с дискретностью
1, а при использовании клавиш PgUp и PgDn — с дискретностью 5.
При визуальном проектировании приложения Вам нужно перетащить значок
элемента управления TrackBar из инструментальной панели Microsoft Visual Studio .NET
в окно формы. При этом мастер формы создаст для Вас весь необходимый программный
код.
Элемент управления TrackBar создается как объект класса
System.Windows.Forms.TrackBar:
this.trackBar1 = new System.Windows.Forms.TrackBar();

Свойства элемента управления TrackBar


После создания элемента управления необходимо настроить его свойства, а также
подключить обработчик события trackBar1_Scroll:
this.trackBar1.Location = new System.Drawing.Point(40, 104);
this.trackBar1.Maximum = 100;
this.trackBar1.Name = "trackBar1";
this.trackBar1.Size = new System.Drawing.Size(224, 42);
this.trackBar1.TabIndex = 0;
this.trackBar1.TickFrequency = 5;
this.trackBar1.Scroll +=
new System.EventHandler(this.trackBar1_Scroll);
Этот обработчик будет получать управление при изменении положения движка в
окне элемента управления TrackBar.
Рассмотрим самые важные свойства регулятора TrackBar.
Свойство Value хранит текущее значение, непосредственно связанное с
положением движка в окне регулятора. Программа может не только читать это
свойство, но и писать в него. При этом положение движка будет изменяться
соответствующим образом.
Свойства Minimum и Maximum определяют, соответственно, минимальное и
максимальное значение, связанное с движком. По умолчанию минимальное значение
равно 1, а максимальное — 10. В нашем приложении TrackBarApp мы увеличили
максимальное значение, записав в свойство Maximum число 100.
Дискретность изменения значения при использовании клавиш перемещения
курсора задается свойством SmallChange. По умолчанию она равна 1, и мы не стали
менять это значение.
Что же касается дискретности при использовании клавиш PgUp и PgDn, то ее
можно задать с помощью свойства LargeChange. По умолчанию значение этого свойства
равно 5.
Чтобы задать количество штрихов, отображаемых на шкале элемента управления
TrackBar, нужно отредактировать свойство TickFrequency. В приложении TrackBarApp
мы присвоили этому свойству значение 5.
Изменяя свойство Orientation, можно задать горизонтальное или вертикальное
(рис. 6-17) расположение окна элемента управления TrackBar. В первом случае
свойство должно содержать значение System.Windows.Forms.Orientation.Horizontal, а во
втором — System.Windows.Forms.Orientation.Vertical.
Рис. 6-17. Вертикальное расположение элемента управления TrackBar

Свойство TickStyle задает стиль шкалы и ползунка. Вот возможные значения:


 None;
 TopLeft;
 BottomRight;
 Both.
В первом случае, при использовании значения None, штрихи не отображаются на
шкале. Остальные константы позволяют задать расположение штрихов сверху или
снизу (справа или слева) от движка, а также по обе стороны движка (значение Both).
Обработка события Scroll
Когда пользователь перемещает движок, элемент управления TrackBar создает событие
Scroll. Обработчик этого события может получить текущее значение, связанное с
движком, извлекая его из свойства Value.
В приложении TrackBarApp мы извлекаем это значение, преобразуем его в текст с
помощью метода ToString, а затем отображаем в текстовом поле:
private void trackBar1_Scroll(object sender, System.EventArgs e)
{
label1.Text = trackBar1.Value.ToString();
}

Элемент управления ProgressBar


В то время как элемент управления TrackBar предназначен для регулирования каких-
либо числовых значений, элемент управления ProgressBar поможет Вам графически
отобразить значения. Он часто применяется, например, для отображения процента
завершения какого-либо длительного процесса.
На рис. 6-18 мы показали окно приложения TrackBarApp, в которое добавили
TrackBar, перетащив значок этого элемента управления в окно формы из
инструментальной панели Toolbar.
Рис. 6-18. Добавлен элемент управления ProgressBar

В приложении TrackBarApp мы использовали элемент управления ProgressBar для


графического отображения текущего уровня мощности, установленного при помощи
ползунка.
Элемент управления ProgressBar создается при помощи конструктора как объект
класса System.Windows.Forms.ProgressBar:
this.progressBar1 = new System.Windows.Forms.ProgressBar();
Когда Вы добавляете ProgressBar в окно формы, дизайнер форм автоматически
добавляет код настройки свойств этого элемента управления:
this.progressBar1.Location = new System.Drawing.Point(24, 64);
this.progressBar1.Name = "progressBar1";
this.progressBar1.Size = new System.Drawing.Size(224, 23);
this.progressBar1.TabIndex = 2;
Рассмотрим наиболее интересные свойства элемента управления ProgressBar.
Прежде всего, Ваша программа будет использовать свойство Value,
устанавливающее текущее значение, связанное с элементом управления ProgressBar.
Именно это значение определяет размер закрашенной области окна ProgressBar.
Текущее значение должно находиться в границах, задаваемых при помощи
свойств Minimum и Maximum. Первое из них определяет минимально возможное
значение, отображаемое при помощи ProgressBar, а второе — максимально возможное
значение. По умолчанию ProgressBar отображает значения в диапазоне от 0 до 100.
При инициализации наше приложение TrackBarApp устанавливает начальную
позицию элементов управления TrackBar и ProgressBar, используя для этого свойство
Value:
trackBar1.Value = 37;
label1.Text = trackBar1.Value.ToString();
progressBar1.Value = trackBar1.Value;
Новый обработчик сообщения trackBar1_Scroll отображает численное значение
мощности в текстовом поле label1, а также устанавливает новую позицию элемента
управления progressBar1:
private void trackBar1_Scroll(object sender, System.EventArgs e)
{
label1.Text = trackBar1.Value.ToString();
progressBar1.Value = trackBar1.Value;
}
С помощью методов Increment и PeformStep программа может увеличивать
текущую позицию элемента управления ProgressBar. Эти методы удобно использовать в
цикле. Метод Increment увеличивает позицию на величину, передаваемую этому методу
через единственный параметр. Что же касается метода PeformStep, то он увеличивает
значение на шаг, заданный при помощи свойства Step.
При необходимости заблокировать ProgressBar Ваша программа может записать в
свойство Enabled значение false.

Полосы прокрутки HScrollBar и VScrollBar


Элементы управления HScrollBar и VScrollBar представляют собой обычные полосы
прокрутки, отображаемые на границах окон редактирования и просмотра приложений
Microsoft Windows. Эти элементы управления, показанные на рис. 6-19, могут также
использоваться и для регулировки числовых значений, хотя для этого лучше применять
только что описанный элемент управления TrackBar.
Рис. 6-19. Элементы управления HScrollBar и VScrollBar

Окно элемента управления HScrollBar располагается горизонтально, а элемента


управления VScrollBar — вертикально.
Надо отметить, что элементы управления HScrollBar и VScrollBar редко
используются сами по себе. Как правило, они используются в более сложных составных
элементах управления, таких, например, как редакторы текста, списки и панели класса
Panel. Когда содержимое не помещается в окне такого элемента, в его окне
автоматически создаются полосы прокрутки. Настраивая свойства составных элементов
управления соответствующим образом, можно установить режим, при котором полосы
прокрутки отображаются постоянно, не зависимо от содержимого окна просмотра или
редактирования.
Свойства элементов управления HScrollBar и VScrollBar аналогичны свойствам
регулятора TrackBar.
Свойство Value хранит текущее значение, непосредственно связанное с
положением движка в окне полосы прокрутки. При изменении значения свойства
положение движка в окне полосы прокрутки будет изменяться соответствующим
образом.
Свойства Minimum и Maximum определяют, соответственно, минимальное и
максимальное значение, связанное с движком полосы прокрутки. По умолчанию
минимальное значение равно 1, а максимальное — 100.
Дискретность изменения значения при использовании клавиш перемещения
курсора задается свойством SmallChange. По умолчанию она равна 1. Дискретность при
использовании клавиш PgUp и PgDn задается с помощью свойства LargeChange и равна
по умолчанию 10.
Календарь
В разделе «Создание модальных окон» 5 главы мы создавали диалоговое окно регистрации
программы, в котором использовали очень мощное и удобное средство работы с
датами — календарь MonthCalendar. Элементы управления MonthCalendar и
DateTimePicker пригодятся Вам в любом приложении, требующем от пользователя ввода
дат, диапазонов дат и времени.
В этом разделе мы будем показывать возможности элементов управления
MonthCalendar и DateTimePicker на примере приложения DateTimeApp. Исходный вид
главного окна этого приложения, в котором находится только один элемент управления
MonthCalendar, мы показали на рис. 6-20.
Рис. 6-20. Календарь на один месяц

Создание календаря
Самый простой способ создать календарь — это переместить его значок MonthCalendar
из панели Toolbox в окно проектируемой формы. При этом с исходный текст
приложения будет добавлен следующий код:
private System.Windows.Forms.MonthCalendar monthCalendar1;

this.monthCalendar1 = new System.Windows.Forms.MonthCalendar();



//
// monthCalendar1
//
this.monthCalendar1.Location = new System.Drawing.Point(24, 16);
this.monthCalendar1.Name = "monthCalendar1";
this.monthCalendar1.TabIndex = 1;
Как видите, календарь создается с помощью конструктора на базе класса
System.Windows.Forms.MonthCalendar.
Настройка свойств
Программный код инициализации, добавленный дизайнером форм, по умолчанию
устанавливает расположение календаря Location, его имя Name, а также порядок
сортировки TabIndex. Далее Вам нужно самостоятельно настроить свойства календаря
для получения от него необходимой функциональности.
Прежде всего, расскажем о свойствах, определяющих внешний вид календаря.
Свойства CalendarDimensions.Width и CalendarDimensions.Height задают,
соответственно, ширину и высоту календаря. По умолчанию значения этих свойств
равны 1, в результате чего календарь отображает числа только одного месяца (рис. 6-
20).
Задав значение свойства CalendarDimensions.Width равное 3, а свойства
CalendarDimensions.Height — равное 2, можно создать календарь на 6 месяцев
(рис. 6-21).
Рис. 6-21. Календарь на полгода

По умолчанию в календаре отмечена текущая дата, а в нижней части календаря


присутствует строка Сегодня: ХХ.ХХ.ХХХХ. Однако если записать значение false в
свойство ShowToday, эта строка не будет отображаться. Если при этом записать
значение false в свойство ShowTodayCircle, текущая дата никак не будет отмечена в
календаре.
Если записать в свойство TodayDateSelect значение true, программа сможет
выбирать текущую дату, записывая ее в свойство TodayDate (по умолчанию свойство
TodayDate содержит системную дату).
При необходимости можно показывать в календаре номера недель, записав
значение true в свойство ShowWeekNumbers.
С помощью календаря пользователь может указать дату или диапазон дат,
выделив этот диапазон левой клавишей мыши. Максимальный диапазон дат, который
может выделить пользователь, задается свойством MaxSelectionCount, значение
которого по умолчанию равно 7.
Если значение свойства MaxSelectionCount установить равным 1, то пользователь
сможет выбрать только один день. Если же значение этого свойства увеличить до 365,
можно будет выбрать максимальный интервал дат, равный 1 году. На рис. 6-22 мы
выделили диапазон дат, равный 4 месяцам.
Рис. 6-22. Выделен диапазон дат, равный 4 месяцам

Свойства SelectionStart и SelectionEnd содержат, соответственно, начальную и


конечную дату из диапазона дат, выделенных пользователем. Кроме того, диапазон
выделенных дат можно получить из свойства SelectionRange.
Если значение свойства MaxSelectionCount равно 1 и пользователь может
выделить только один день, то дату, соответствующую этому дню, можно извлечь из
свойства SelectionStart.
Что касается свойств SelectionStart и SelectionEnd, то в них хранится дата в виде
объектов класса DateTime. В табл. 6-1 мы перечислили некоторые свойства этого
класса.
Таблица 6-1. Свойства класса DateTime
Свойство Что содержит
Day Число
Mouhth Номер месяца
Year Год
Millisecond Миллисекунды
Second Секунды
Minute Минуты
Hour Часы
DayOfWeek Номер дня в неделе
DayOfYear Номер дня в году
Ticks Количество периодов системного таймера
TimeOfDay Время дня
Today Текущая дата
UtcNow Текущая локальная дата в терминах универсального
координированного времени (coordinated universal time,
UTC)
Ранее в разделе «Создание модальных окон» 5 главы мы уже пользовались
свойством SelectionStart, извлекая из него отдельные компоненты даты:
public string UserBirthDay
{
get
{
DateTime dt = monthCalendar1.SelectionStart;
return dt.Day + "." + dt.Month + "." + dt.Year;
}
}
Здесь мы вначале получили из свойства monthCalendar1.SelectionStart дату,
выделенную пользователем, и сохранили ее в переменной dt типа DateTime. Далее из
отдельных компонентов даты (календарного числа dt.Day, номера месяца dt.Month и
года dt.Year) была сформирована текстовая строка.
Аналогичным образом программа может обработать и содержимое свойства
SelectionEnd.
Свойство SelectionRange содержит данные класса SelectionRange. Чтобы извлечь
начальную и конечную дату, программа должна обратиться к свойствам
SelectionRange.Start и SelectionRange.End, соответственно.
Дополните приложение DateTimeApp обработчиком события button1_Click,
получающим управление по щелчку кнопки OK:
private void button1_Click(object sender, System.EventArgs e)
{
string s = "Выбран диапазон дат: ";
DateTime dtStart = monthCalendar1.SelectionRange.Start;
DateTime dtEnd = monthCalendar1.SelectionRange.End;
s += "(" +
dtStart.Day + "." + dtStart.Month + "." + dtStart.Year +
") - (" +
dtEnd.Day + "." + dtEnd.Month + "." + dtEnd.Year + ")";

MessageBox.Show(s);
}
Здесь мы извлекаем начальную и конечную дату, а затем отображаем на экране
текстовую строку с диапазоном дат, выделенным пользователем в окне календаря (рис.
6-23).

Рис. 6-23. Отображение выделенного диапазона дат

Элемент управления DateTimePicker


Если календарь MonthCalendar смотрится в окне формы слишком громоздко, для ввода
даты и времени можно использовать более компактные элементы управления класса
DateTimePicker.
Создание элемента управления DateTimePicker
Перетащите из инструментальной панели Microsoft Visual Studio .NET значки элемента
управления DateTimePicker и разместите их в окне приложения DateTimeApp, как это
показано на рис. 6-24.

Рис. 6-24. Использование элемента управления DateTimePicker

Элемент управления DateTimePicker создается с помощью конструктора как


объект класса System.Windows.Forms.DateTimePicker:
private System.Windows.Forms.DateTimePicker dateTimePicker1;

this.dateTimePicker1 = new System.Windows.Forms.DateTimePicker();

this.dateTimePicker1.Location = new System.Drawing.Point(240, 16);
this.dateTimePicker1.Name = "dateTimePicker1";
this.dateTimePicker1.Size = new System.Drawing.Size(136, 20);
this.dateTimePicker1.TabIndex = 3;

Настройка свойств
Внешний вид окна элемента управления DateTimePicker определяется свойством
Format, которое может иметь несколько значений:
 Long;
 Short;
 Time;
 Custom
Значение Long используется по умолчанию. В этом случае окно элемента
управления DateTimePicker содержит поля для ввода числа, месяца и года, а также
кнопку со стрелкой. На рис. 6-24 такой элемент управления показан справа вверху.
Пользователь может вводить компоненты даты, непосредственно редактируя их
при помощи клавиатуры, увеличивать или уменьшать эти компоненты при помощи
клавиш перемещения курсора, а также с помощью кнопки со стрелкой. В последнем
случае рядом с окном элемента управления DateTimePicker появится уже знакомый Вам
календарь (рис. 6-25).

Рис. 6-25. Ввод даты с помощью календаря

После того как пользователь выберет дату, окно календаря исчезнет. Заметим,
однако, что таким способом можно выбрать только одну дату, но не диапазон дат.
Формат Short аналогичен формату Long, но вместо названия месяца в окне
элемента управления отображается его номер. На рис. 6-24 элемент управления с
таким форматом показан вторым сверху в правом углу.
Формат Time используется для ввода времени в часах, минутах и секундах. На
рис. 6-24 этот формат имеют два оставшихся элемента управления DateTimePicker.
И, наконец, формат Custom позволяет программисту задать произвольный формат
отображения даты и времени в окне элемента управления DateTimePicker.
Чтобы отказаться от ввода даты с помощью календаря, Вы можете установить
значение свойства ShowUpDown, равным true. В этом случае вместо одной кнопки со
стрелкой в окне элемента управления DateTimePicker появятся две кнопки, с помощью
которых пользователь сможет увеличивать или уменьшать отдельные компоненты даты
и времени.
Окно DateTimePicker можно снабдить флажком, установив значение свойства
ShowCheckBox, равным true. Если этот флажок не отмечен пользователем, работа
соответствующего элемента управления DateTimePicker блокируется. Состояние флажка
можно установить или определить при помощи свойства Checked.
Свойства MinDate и MaxDate задают, соответственно, нижнюю и верхнюю границу
изменения дат. Используя эти свойства, программист может ограничить диапазон
изменения дат в окне элемента управления DateTimePicker.
Что же касается значения даты, установленной пользователем, то программа
может извлечь его из свойства Value.
Доработайте обработчик события button1_Click приложения DateTimeApp
следующим образом, добавив в него строки для извлечения и отображения даты и
времени, введенных с помощью элементов управления DateTimePicker:
private void button1_Click(object sender, System.EventArgs e)
{
string s = "Выбран диапазон дат: ";
DateTime dtStart = monthCalendar1.SelectionRange.Start;
DateTime dtEnd = monthCalendar1.SelectionRange.End;

s += "("
+ dtStart.Day + "." + dtStart.Month + "." + dtStart.Year +
"-"+
dtEnd.Day + "." + dtEnd.Month + "." + dtEnd.Year + ")";

DateTime dt1 = dateTimePicker1.Value;


DateTime dt2 = dateTimePicker2.Value;
DateTime dt3 = dateTimePicker3.Value;
DateTime dt4 = dateTimePicker4.Value;

s += "\n\nДата 1: " + dt1.Day + "." + dt1.Month + "." + dt1.Year;


s += "\nДата 2: " + dt2.Day + "." + dt2.Month + "." + dt2.Year;

s += "\n\nВремя 3: " +
dt3.Hour + ":" + dt3.Minute + ":" + dt3.Second;

s += "\nВремя 4: " +
dt4.Hour + ":" + dt4.Minute + ":" + dt4.Second;

MessageBox.Show(s);
}
Результат работы этого фрагмента нашего приложения показан на рис. 6-26.

Рис. 6-26. Отображение времени и даты

Таймер
Во многих программах требуется следить за временем или выполнять какие-либо
периодические действия. Программы MS-DOS для работы с таймером перехватывали
аппаратное прерывание таймера INT 8.
Обычные приложения Microsoft Windows не могут самостоятельно обрабатывать
прерывания таймера, поэтому для работы с ним нужно использовать другие способы.
ОС Microsoft Windows для каждого приложения позволяет создать несколько
виртуальных таймеров. Все эти таймеры работают по прерываниям одного физического
таймера.
Так как работа Microsoft Windows основана на передаче сообщений, логично было
бы предположить, что и работа виртуального таймера также основана на передаче
сообщений. И в самом деле, приложение может заказать для любого своего окна
несколько таймеров, которые будут периодически посылать в функцию окна сообщение
с кодом WM_TIMER.
В распоряжении приложений C# имеется удобный программный компонент Timer,
периодически создающий событие Tick. Обрабатывая это событие, приложения могут
выполнять все необходимые периодические действия.
К сожалению, точность таймера ОС оставляет желать лучшего. Сообщения
таймера, создающие события Tick, проходят через очередь приложения. К тому же,
другие приложения могут блокировать на некоторое время работу Вашего приложения.
Поэтому события таймера возникают в общем случае нерегулярно. Кроме того,
несмотря на возможность указания интервалов времени в миллисекундах, реальная
дискретность таймера определяется периодом прерываний, посылаемых таймером.
Нерегулярность прихода сообщений таймера не вызывает особых проблем, если
речь не идет о системах реального времени. Такие системы, основанные на ОС
Microsoft Windows, должны использовать специальные драйверы для работы с
периферийными устройствами, критичными к скорости реакции системы. Строго
говоря, ОС Microsoft Windows не предназначена для работы в системах реального
времени. Скорее, она ориентирована на работу с пользователем, когда небольшие
задержки событий во времени не имеют никакого значения. Системы реального
времени обычно создаются на базе специальных ОС реального времени, рассмотрение
которых выходит за рамки нашей книги.
Для демонстрации приемов работы с программным компонентом Timer мы
подготовили приложение TimerApp (рис. 6-27), представляющее собой простейшие
часы.

Рис. 6-27. Таймер в действии

В окно этого приложения мы поместили текстовое поле класса Label,


предназначенное для отображения текущего времени, а также кнопки Старт и Стоп. С
помощью кнопки Старт можно запускать часы, а с помощью кнопки Стоп —
останавливать их.
Создание таймера
Чтобы добавить таймер Timer в наше приложение, перетащите мышью значок таймера
из инструментальной панели в проектируемую форму приложения или щелкните этот
значок дважды левой клавишей мыши. Значок таймера с идентификатором timer1 будет
показан в нижней части окна дизайнера форм (рис. 6-28).
Рис. 6-28. Свойства таймера Timer

Таймер будет создан как объект класса System.Windows.Forms.Timer. Ниже мы


привели код, созданный дизайнером форм при добавлении таймера и после настройки
его свойств:
private System.Windows.Forms.Timer timer1;

this.timer1 = new System.Windows.Forms.Timer(this.components);

this.timer1.Enabled = true;
this.timer1.Interval = 1000;
this.timer1.Tick += new System.EventHandler(this.timer1_Tick);
Обратите внимание, что в качестве параметра конструктору передается ссылка на
контейнер, в который был добавлен таймер. В одном приложении Вы можете
использовать одновременно несколько таймеров.
Свойства таймера
По умолчанию таймер создается в заблокированном состоянии. Чтобы его
разблокировать, необходимо записать значение true в свойство Enabled:
this.timer1.Enabled = true;
Свойство Interval определяет период, с которым таймер создает события Tick. Это
время задается в миллисекундах. Ниже мы задали для таймера период времени,
равный 1 секунде:
this.timer1.Interval = 1000;

Методы таймера
Наиболее важны методы таймера Start и Stop. Первый из этих методов запускает
таймер, а второй — останавливает.
В приложении TimerApp мы вызываем эти методы при обработке событий от
кнопок Старт и Стоп, соответственно:
private void button1_Click(object sender, System.EventArgs e)
{
timer1.Start();
}

private void button2_Click(object sender, System.EventArgs e)


{
timer1.Stop();
}

Обработка события Tick


Как мы уже говорили, таймер генерирует события Tick, период которых задается
свойством Interval. Обработчик этих событий может выполнять любые действия в
соответствии с логикой работы приложения.
Наш обработчик события timer1_Tick получает текущую дату с помощью свойства
DateTime.Now, а затем отображает ее в виде текстовой строки в поле label1:
private void timer1_Tick(object sender, System.EventArgs e)
{
DateTime dt = DateTime.Now;
label1.Text = dt.Hour + ":" + dt.Minute + ":" + dt.Second;
}
Таким образом, при работающем таймере в окне текстового поля будет
отображаться текущее время.
Элемент управления TabControl
Если Ваше приложение сложное и имеет большое количество параметров, следует
хорошо подумать о том, как пользователь будет их настраивать. Часто для настройки
параметров предлагается многоуровневая система вложенных меню и нагромождение
диалоговых окон, которые вызываются друг из друга. Однако такой подход едва ли
придется по душе пользователям.
Если посмотреть, как выглядит система настройки параметров, например, в
приложении Microsoft Word, то видно, что она организована в виде блокнота,
состоящего из нескольких диалоговых окон с закладками (рис. 6-29). По-видимому, это
один из наиболее удачных вариантов, позволяющих пользователю легко найти и
выбрать нужную ему группу параметров.

Рис. 6-29. Диалоговое окно Параметры в приложении Microsoft Word

Специально для создания блокнотов с диалоговыми окнами, подобных блокноту,


показанному на рис. 6-29, предусмотрен элемент управления TabControl, значок
которого можно найти на инструментальной панели Toolbox системы разработки
Microsoft Visual Studio .NET.
Создание элемента управления TabControl
Для демонстрации способов применения блокнот TabControl мы подготовим приложение
TabControlApp, состоящее из одной формы. Чтобы создать в форме этого приложения
блокнот, перетащите значок элемента управления TabControl из только что упомянутой
инструментальной панели Toolbox в окно формы. Добавьте туда же кнопку с надписью
OK (рис. 6-30).
Рис. 6-30. Добавлен элемент управления TabControl

Добавление страниц
Пока наш блокнот отображается в виде пустого прямоугольника и не содержит ни
одной страницы. Для добавления страниц Вам нужно отредактировать свойство
TabPages. Эта операция выполняется с помощью редактора страниц блокнота,
показанного на рис. 6-31.

Рис. 6-31. Редактор страниц блокнота

Первоначально окно этого редактора будет пустым. Для добавления страниц


щелкните несколько раз кнопку Add, расположенную в левом нижнем углу окна
редактора. Удаление страниц выполняется с помощью кнопки Remove.
Редактирование свойств блокнота и страниц
После того как страницы будут добавлены, отредактируйте для каждой страницы
свойство Text. Это свойство определяет название страницы. Назовите первую страницу
SMTP, вторую POP3, а третью — IMAP. Таким образом, на первой странице мы будем
редактировать параметры доступа к почтовому серверу SMTP, предназначенному для
передачи почты, а на второй и третьей — параметры серверов POP3 и IMAP,
предназначенных для приема почты.
Если теперь оттранслировать и запустить наше приложение, на экране появится
блокнот с тремя пустыми страницами (рис. 6-32).
Рис. 6-32. Добавлены три страницы

Редактируя свойство Alignment, можно изменить расположение закладок для


страниц. Вот возможные значения этого свойства:
 Top;
 Bottom;
 Left;
 Right
По умолчанию используется значение Top, в результате чего закладки
располагаются в верхней части блокнота, как это показано на рис. 6-32. Если
приравнять свойству Alignment значение Bottom, закладки появятся в нижней части
блокнота (рис. 6-33). И, наконец, значения Left и Right предназначены для размещения
закладок слева и справа от окна блокнота, соответственно.

Рис. 6-33. Изменение расположения закладок

Заметим, что после добавления страниц в блокнот Вы можете задавать по


отдельности свойства блокнота и свойства отдельных страниц блокнота. Чтобы задать
свойства блокнота, необходимо выделить весь блокнот, щелкнув левой клавишей мыши
в области закладок. Для выделения одной из страниц блокнота откройте ее с помощью
закладок, а затем щелкните левой клавишей мыши окно страницы блокнота.
Если выделить окно страницы, а затем изменить свойство BorderStyle этого окна
на Fixed3D, то страница будет выделена объемной рамкой. На рис. 6-34 мы показали
оформленную таким образом страницу SMTP, на которую также были добавлены
некоторые элементы управления. Экспериментируя с приложением TabControlApp,
попробуйте также использовать стиль, задаваемый константой FixedSingle.
Рис. 6-34. Изменение стиля страниц

Заметим, что использование элемента управления TabControl не требует от Вас


написания какого-либо дополнительного программного кода. Создавая диалоговое окно
настройки параметров, подобное окну, показанному на рис. 6-29, Вам достаточно
разместить все необходимые элементы управления на соответствующих страницах
блокнота, а затем создать для этих элементов управления обработчики событий. При
этом Вы можете использовать приемы, описанные нами в 5 главе нашей книги с
названием «Диалоговые окна».
Элемент управления ToolTip
Для каждого элемента управления, расположенного в форме, можно создать
всплывающую подсказку. Такая подсказка будет появляться в отдельном окне, если
навести курсор мыши на элемент управления и задержать его там на некоторое время.
На рис. 6-35 мы показали такую всплывающую подсказку около текстового поля,
предназначенного для ввода доменного имени сервера SMTP.

Рис. 6-35. Всплывающая подсказка ToolTip

Добавление всплывающих подсказок


Чтобы снабдить элементы управления Вашего приложения всплывающими
подсказками, Вам не придется написать ни одной строчки кода. Процедура добавления
подсказок может быть выполнена исключительно средствами дизайнера форм системы
Microsoft Visual Studio .NET.
Прежде всего, перетащите в окно формы из инструментальной панели Toolbox
значок программного компонента ToolTip. Значок этого компонента toolTip1 появится в
нижней части окна дизайнера формы (рис. 6-36).
Рис. 6-36. Добавлен программный компонент ToolTip

Теперь выделите левой клавишей мыши поле редактирования доменного имени


сервера SMTP, как это показано на только что упомянутом рисунке. Далее в окне
редактирования свойств Properties (рис. 6-37) введите текст подсказки в поле ToolTip
on toolTip1.

Рис. 6-37. Добавление всплывающей подсказки

Выполните эту процедуру для всех элементов управления формы, которые нужно
снабдить всплывающими подсказками. После того как вы оттранслируете приложение,
подсказки начнут работать.
Настройка параметров всплывающих подсказок
Редактируя свойства компонента toolTip1, Вы сможете настроить некоторые параметры
работы всплывающих подсказок.
Чтобы отредактировать эти свойства, выделите компонент toolTip1 левой
клавишей мыши, а затем обратитесь к окну Properties (рис. 6-38).
Рис. 6-38. Редактирование свойств компонента toolTip1

Записывая в свойство Active значение false, программа при необходимости может


полностью отключать систему всплывающих подсказок. Для повторного включения в
это свойство нужно записать значение true.
Свойства AutomaticDelay и AutoPopDelay задают, соответственно, задержку при
появлении всплывающей подсказки и время, в течение которого эта подсказка остается
видимой.
Время, в течение которого пользователь должен удерживать курсор над
элементом управления для появления подсказки, задается свойством InitialDelay.
Свойство ReshowDelay задает задержку при отображении подсказки при перемещении
курсора мыши от одного элемента управления к другому.
Заметим, что для того чтобы по своему поведению Ваше приложение не сильно
отличалось от других, Вы можете использовать значения свойств программного
компонента toolTip1, принятые по умолчанию.
И, наконец, если задать значение свойства ShowAlways, равным true, окна
подсказок будут видны даже в том случае, если окно формы становится не активным.
Программный код
Рассмотрим программный код, обеспечивающий функционирование всплывающих
подсказок.
Программный компонент toolTip1 создается при помощи конструктора как объект
класса System.Windows.Forms.ToolTip:
this.toolTip1 = new System.Windows.Forms.ToolTip(this.components);
В качестве параметра конструктору передается ссылка на компонент формы, для
элементов управления которой будут создаваться всплывающие подсказки.
Если программист изменил свойства компонента toolTip1, заданные по умолчанию,
дизайнер форм добавляет соответствующий код, например:
//
// toolTip1
//
this.toolTip1.AutomaticDelay = 300;
И, наконец, подключение того или иного текста подсказки к конкретному
элементу управления выполняется с помощью метода toolTip1.SetToolTip, как это
показано ниже:
//
// textBox6
//
this.textBox6.Location = new System.Drawing.Point(200, 96);
this.textBox6.Name = "textBox6";
this.textBox6.TabIndex = 9;
this.textBox6.Text = "";
this.toolTip1.SetToolTip(this.textBox6,
"Введите свой идентификатор");
В качестве первого параметра методу toolTip1.SetToolTip передается ссылка на
элемент управления, а в качестве второго — текст подсказки для этого элемента
управления.

Визуальное проектирование
приложений C#
А.В. Фролов, Г.В. Фролов
ГЛАВА 7. МНОГООКОННЫЙ ПОЛЬЗОВАТЕЛЬСКИЙ ИНТЕРФЕЙС
ИСПОЛЬЗОВАНИЕ ФРЕЙМОВ
Создание главного окна приложения
Добавление элемента управления TreeView
Добавление вертикального разделителя
Добавление панели Panel
Добавление элемента управления ListView
Добавление горизонтального разделителя
Добавление окна RichTextBox
ЭЛЕМЕНТ УПРАВЛЕНИЯ TREEVIEW
Инициализация дерева TreeView
Получение списка дисков
Получение списка подкаталогов
Метод DriveTreeInit
Метод GetDirs
Обработчик события BeforeExpand
Добавление значков к узлам дерева
Создание списка изображений
Подключение списка изображений к дереву
Изменения в исходном тексте программы
Добавление флажков к узлам дерева
Редактирование текста узлов дерева
СПИСОК LISTVIEW
Создание и настройка списка ListView
Режимы отображения
Создание и настройка столбцов таблицы
Сортировка содержимого списка
Подключение значков к списку
Наполнение списка ListView
Алгоритм наполнения списка
Обработчик события AfterSelect
Отображение содержимого текстовых файлов
ПРИЛОЖЕНИЯ MDI
Меню Windows
Системное меню MDI-окна
Создание главного окна MDI-приложения
Создание MDI-окон
Шаблон MDI-окна
Программный код для создания MDI-окна
Упорядочивание MDI-окон
Передача данных через буфер Clipboard
Копирование данных в буфер Clipboard
Вставка данных из буфера Clipboard
Глава 7. Многооконный пользовательский
интерфейс
Предыдущие главы нашей книги были посвящены созданию однооконных приложений,
напоминающих по своему пользовательскому интерфейсу программу Microsoft Notepad.
В этой главе мы расскажем Вам о проектировании приложений с многооконным
пользовательским интерфейсом. Это приложения с фреймами (панелями), а также
приложения с многооконным интерфейсом документов (Multiple-document interface,
MDI).
В качестве примеров автономных приложений Microsoft Windows, реализующих
многооконный интерфейс с фреймами можно привести Microsoft Explorer (рис. 7-1) и
почтовую программу Microsoft Outlook Express, а также систему разработки Microsoft
Visual Studio .NET.

Рис. 7-1. Главное окно программы Windows Explorer

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


имеется специальный элемент управления — разделитель. Передвигая разделитель
мышью, пользователь может регулировать размеры упомянутых выше окон.
Технология фреймов очень часто используется и для создания Web-сайтов,
имеющих сложную иерархическую структуру. На рис. 7-2 мы показали
информационный сайт http://info.datarecovery.ru службы восстановления данных
DataRecovery.Ru, в котором используются фреймы.
Рис. 7-2. Сайт информационного центра с фреймами

В левом фрейме отображается довольно развесистое дерево навигации,


открывающее доступ к десяткам книг и статей, общий объем которых превышает
несколько тысяч страниц. Это статьи, посвященные вопросам восстановления данных,
антивирусной защиты и другим аспектам компьютерных технологий, полное собрание
наших книг серий «Библиотека системного программиста» и «Персональный
компьютер. Шаг за шагом», вышедших в издательстве «Диалог-МИФИ»
(http://www.bitex.ru/~dialog), а также сведения о других наших книгах и работах.
Правый фрейм сайта предназначен для просмотра содержимого статей и книг, а
верхний — для отображения логотипа службы восстановления данных DataRecovery.Ru.
В зависимости от экранного разрешения и личных предпочтений пользователь
может отрегулировать ширину окна просмотра навигационного дерева, передвигая
разделитель мышью.
Что же касается некогда широко распространенного многооконного интерфейса
документов MDI, то ему следует такая известная программа, как редактор
REGEDT32.EXE регистрационная база данных ОС Microsoft Windows (рис. 7-3).
В главном окне приложения MDI могут создаваться другие окна (называемые
окнами MDI), причем все они будут находиться внутри этого главного окна.
Пользователь может изменять размеры и расположение отдельных окон MDI,
минимизировать и максимизировать их.
Сейчас приложения с интерфейсом MDI вытесняются приложениями, имеющими
современный пользовательский интерфейс на базе фреймов. Тем не менее, средства
Microsoft .NET Framework позволят Вам при необходимости легко снабжать свои
приложения и тем, и другим интерфейсом.
Рис. 7-3. Главное окно программы Registry Editor

В этой главе мы также расскажем Вам об использовании элементов управления


TreeView и ListView, с помощью которых можно просматривать древовидные и
списковые структуры данных. Такие элементы управления используются, например, в
упоминавшейся выше программе Windows Explorer (рис. 7-1).
Использование фреймов
Изучение способов создания приложений с многооконным интерфейсом мы начнем с
фреймов. Для реализации фреймов нам потребуется элемент управления Splitter,
значок которого есть в инструментальной панели системы Microsoft Visual Studio .NET.
Элемент управления Splitter представляет собой разделитель, предназначенный для
регулирования размеров окна фрейма.
Специально для демонстрации способов создания приложений с фреймами мы
вместе с Вами подготовим приложение FramesApp, действующее аналогично программе
Microsoft Explorer. На примере этого приложения мы также изучим элементы
управления TreeView и ListView.
Создание главного окна приложения
Создайте приложение FramesApp типа Windows Application, пользуясь мастером
проектов (точно так же, как мы это делали ранее).
Добавьте в форму главного окна приложения меню и строку состояния. В меню
File создайте строку Exit, предназначенную для завершения работы приложения (рис.
7-4).
Рис. 7-4. Главное окно приложения FramesApp с меню и строкой состояния

Пока наше приложение имеет обычный однооконный интерфейс. Нам нужно


добавить в это окно фреймы и новые элементы управления.
Добавление элемента управления TreeView
Добавьте в окно нашего приложения элемент управления TreeView, перетащив мышью
его значок из панели инструментов Toolbox в окно формы.
Установите значение свойства Dock, равным Left. Для этого щелкните левую
кнопку в окне редактирования этого свойства (рис. 7-5).

Рис. 7-5. Настройка свойства Dock элемента управления TreeView

В результате элемент управления TreeView будет выровнен по левой границе


главного окна приложения, как это показано на рис. 7-6.
Рис. 7-6. Выравнивание элемента управления TreeView

Добавив элемент управления TreeView, Вы можете при необходимости


отрегулировать начальную ширину его окна, которую он будет иметь сразу после
запуска приложения. Впоследствии пользователь сможет менять ширину этого окна с
помощью разделителя.
В нашем приложении окно элемента управления TreeView будет использовано для
отображения списка дисков и каталогов (аналогично дереву, расположенному в левой
части главного окна приложения Windows Explorer).
Добавление вертикального разделителя
Снабдим элемент управления TreeView разделителем. Для этого перетащите значок
элемента управления Splitter из панели инструментов Toolbox в окно формы.
Разделитель будет автоматически расположен справа от окна элемента
управления TreeView (рис. 7-7).

Рис. 7-7. Добавление разделителя к элементу управления TreeView

Как и другие элементы управления, разделитель Splitter имеет свойства, которые


можно менять в процессе разработки приложения, а также динамически во время его
работы. Разделитель создает события, которые, однако, мы не будем обрабатывать.
Самое важное свойство разделителя Splitter — это свойство Dock, задающее его
расположение. По умолчанию значение этого свойства равно Left, благодаря чему
разделитель оказался «запаркован» слева от окна дерева TreeView. При необходимости
можно изменять расположение разделителя с помощью окна редактирования,
аналогичного окну, показанному на рис. 7-5.
Свойство BorderStyle позволяет задавать внешний вид разделителя и может иметь
значения None (используется по умолчанию), FixedSingle и Fixed3D. В первом случае
разделитель не имеет рамки, во втором случае используется тонкая рамка, а в
третьем — трехмерная рамка.
Добавление панели Panel
В правой части главного окна приложения мы расположим два окна, разместив между
ними разделитель. В верхнем окне будет находиться элемент управления ListView,
показывающий список файлов и каталогов, а в нижнем — элемент управления
RichTextBox, предназначенный для отображения дополнительной информации.
Для объединения окон элементов управления ListView и RichTextBox мы создадим
панель на базе элемента управления Panel. Перетащите значок этого элемента
управления из инструментальной панели Toolbox в окно нашей формы, а затем
установите значение свойства Dock этой панели равным Fill. Последнее действие можно
сделать, щелкнув кнопку, расположенную в центре окна редактирования данного
свойства.
В результате наша панель будет расположена справа от разделителя и займет всю
правую часть окна приложения (рис. 7-8).
Рис. 7-8. Добавление панели

Добавление элемента управления ListView


На следующем шаге мы добавим в окно нашего приложения элемент управления
ListView, перетащив его значок из пиктограммы Toolbox. После установки значения
свойства Dock, равным Top, и увеличения высоты окна этого элемента управления,
главное окно нашего приложения примет вид, показанный на рис. 7-9.

Рис. 7-9. Добавление элемента управления ListView

Как мы уже говорили, в окне элемента управления ListView наша программа будет
показывать список файлов и каталогов, расположенный в текущем каталоге,
выбранном с помощью дерева TreeView.
Добавление горизонтального разделителя
Теперь нашей задачей будет добавление в правую часть главного окна
горизонтального разделителя Splitter. Перетащите его пиктограмму из панели
инструментов Toolbox в окно формы, а затем установите значение свойства Dock
добавленного разделителя, равным Top.
В результате разделитель будет расположен непосредственно под окном элемента
управления ListView, как это показано на рис. 7-10.
Рис. 7-10. Добавление горизонтального разделителя

Добавление окна RichTextBox


Последнее действие — добавление элемента управления RichTextBox. Перетащите его
значок из панели Toolbox и установите значение свойства Dock, равным Fill.
Все! Теперь запускайте приложение на выполнение. На рис. 7-11 мы показали
внешний вид его окна.

Рис. 7-11. Многооконный пользовательский интерфейс с формами реализован


полностью

Таким образом, не написав ни единой строчки кода, мы создали простейшее


приложение с действующим многооконным интерфейсом пользователя. Теперь осталось
только добавить к приложению функциональность, связанную с его основным
назначением — просмотром содержимого дисков и каталогов.
Заметим, что в некоторых случаях для достижения необходимого взаимного
расположения окон Вам придется изменять расположение окон элементов управления
по оси Z (Z-order). Это можно сделать с помощью строк Bring to Front и Send to Back
контекстного меню. Это меню появляется на экране, если щелкнуть правой клавишей
мыши окно элемента управления.
Элемент управления TreeView
Как мы уже говорили, элемент управления TreeView (для простоты мы будем называть
его деревом TreeView) представляет собой очень удобное средство отображения
иерархически организованной информации. В предыдущем разделе этой главы мы
добавили дерево TreeView в окно приложения, но еще не инициализировали его,
настраивали свойств и не создавали никаких обработчиков событий.
Надо отметить, что класс TreeView достаточно сложный. Он содержит множество
методов, свойств и создает различные события. Мы будем изучать его постепенно.
Заметим, что вместе с классом TreeView мы будем использовать классы TreeNode и
TreeNodeCollection. Первый из них содержит записи узлов дерева, а второй —
контейнер с такими записями.
Инициализация дерева TreeView
Когда Вы перетаскиваете значок дерева TreeView из панели Toolbox в окно формы,
создается объект treeView1 класса System.Windows.Forms.TreeView:
private System.Windows.Forms.TreeView treeView1;
При инициализации мы будем ссылаться на переменную treeView1, хранящую
ссылку на наше дерево.
Инициализация дерева выполняется в конструкторе класса Form1 с помощью
подготовленного нами метода DriveTreeInit:
public Form1()
{
//
// Required for Windows Form Designer support
//
InitializeComponent();

//
// TODO: Add any constructor code after InitializeComponent call
//

DriveTreeInit();
}
Получение списка дисков
Ниже мы опишем исходный текст этого метода, а пока немного отвлечемся —
расскажем о классах и методах, позволяющих получить информацию о дисках и
каталогах. Более полную информацию по этому вопросу Вы найдете в [3].
Чтобы заполнить дерево, нам нужно, прежде всего, получить список логических
дисков, имеющихся в системе. Для этого воспользуйтесь статическим методом
Directory.GetLogicalDrives:
string[]drivesArray = Directory.GetLogicalDrives();

foreach(string s in drivesArray)
Console.Write("{0} ", s);
Этот метод не имеет параметров. После выполнения он возвращает ссылку на
массив текстовых строк вида «C:\» с обозначениями всех доступных логических
дисковых устройств.
Для обращения к этому методу, а также к другим методам, работающим с дисками,
каталогами и файлами, подключите пространство имен System.IO:
using System.IO;
Получение списка подкаталогов
Для того чтобы получить список всех подкаталогов заданного каталога, мы используем
метод GetDirectories класса DirectoryInfo. Ниже приведен фрагмент кода, позволяющий
записать в массив diArray информацию обо всех подкаталогах корневого каталога
диска C:
DirectoryInfo[] diArray;
string fullPath = "C:\\";

DirectoryInfo di = new DirectoryInfo(fullPath);

try
{
diArray = di.GetDirectories();
}
catch
{

}
Свойство Name элементов полученного таким способом массива будет содержать
имя файла или каталога. Мы будем использовать это свойство для заполнения дерева.
Обратите внимание, что вызов метода GetDirectories необходимо выполнять в
блоке try-catch, т.к. этот метод может вызывать исключения. Исключения возникают,
например, если методу передается строка нулевой длины, если в строке имеется
ошибка, если программа не обладает достаточными правами доступа для просмотра
содержимого каталога или если указанный путь не найден.
Метод DriveTreeInit
Теперь, когда мы рассказали о методах и классах, позволяющих получить список
дисковых устройств и список содержимого каталогов, займемся методом инициализации
дерева DriveTreeInit.
Исходный текст этой функции Вы найдете ниже:
/// <summary>
/// Инициализация окна древовидного списка дисковых устройств
/// </summary>
public void DriveTreeInit()
{
string[] drivesArray = Directory.GetLogicalDrives();

treeView1.BeginUpdate();
treeView1.Nodes.Clear();

foreach(string s in drivesArray)
{
TreeNode drive = new TreeNode(s, 0, 0);
treeView1.Nodes.Add(drive);

GetDirs(drive);
}

treeView1.EndUpdate();
}
В самом начале своей работы этот метод получает список логических дисковых
устройств, установленных в системе, и сохраняет его в массиве drivesArray.
Далее метод DriveTreeInit вызывает метод treeView1.BeginUpdate. Этот метод
временно блокирует перерисовку окна дерева до тех пор, пока не будет вызван метод
treeView1.EndUpdate. Пара этих методов используется в том случае, когда нужно
добавить, удалить или изменить большое количество элементов дерева. Если не
заблокировать перерисовку окна, на обновление дерева уйдет слишком много времени.
В классе TreeView определено свойство Nodes, хранящее все узлы дерева. Перед
тем как приступить к заполнению дерева, метод DriveTreeInit удаляет все узлы,
вызывая для этого метод treeView1.Nodes.Clear.
Заполнение дерева происходит в цикле:
foreach(string s in drivesArray)
{
TreeNode drive = new TreeNode(s, 0, 0);
treeView1.Nodes.Add(drive);

GetDirs(drive);
}
Напомним, что массив drivesArray содержит массив текстовых строк с
обозначениями логических устройств, установленных в системе. Для каждого такого
устройства в цикле создается объект класса TreeNode — узел дерева. В качестве
первого параметра конструктору передается текстовая строка названия устройства.
Остальные параметры мы рассмотрим позже.
Созданный узел добавляется в набор узлов дерева с помощью метода
treeView1.Nodes.Add. В качестве параметра этому методу передается ссылка на объект
TreeNode, т.е. на добавляемый узел.
Далее вызывается метод GetDirs, добавляющий в дерево список содержимого
корневого каталога текущего дискового устройства. Мы рассмотрим исходный текст
этого метода в следующем разделе.
Метод GetDirs
Метод GetDirs получает в качестве параметра ссылку на узел дерева, который
требуется наполнить списком имен каталогов и файлов.
Вот исходный текст этого метода:
/// <summary>
/// Получение списка каталогов
/// </summary>
public void GetDirs(TreeNode node)
{
DirectoryInfo[] diArray;

node.Nodes.Clear();

string fullPath = node.FullPath;


DirectoryInfo di = new DirectoryInfo(fullPath);

try
{
diArray = di.GetDirectories();
}
catch
{
return;
}

foreach (DirectoryInfo dirinfo in diArray)


{
TreeNode dir = new TreeNode(dirinfo.Name, 0, 0);
node.Nodes.Add(dir);
}
}
Первым делом метод GetDirs удаляет все элементы из текущего узла, вызывая для
этого метод node.Nodes.Clear.
Далее метод переписывает в переменную fullPath типа string полный путь
node.FullPath к узлу дерева. Эта строка получается объединением (конкатенацией)
текстовых строк всех родительских узлов. Если корневой узел хранит текстовую строку
обозначения логического диска, то после выполнения этой процедуры в переменной
fullPath будет храниться полный путь к файлу или каталогу.
Далее для получения содержимого каталога, на который ссылается переменная
fullPath, мы используем рассмотренный ранее метод GetDirectories. При возникновении
исключения программа просто возвращает управление, не выполняя над узлом дерева
никаких функций.
В том случае если содержимое каталога было успешно получено, оно сохраняется
в массиве diArray.
Далее содержимое массива diArray используется для заполнения узла дерева
содержимым каталога:
foreach (DirectoryInfo dirinfo in diArray)
{
TreeNode dir = new TreeNode(dirinfo.Name, 0, 0);
node.Nodes.Add(dir);
}
Здесь в цикле создаются объекты класса TreeNode, т.е. узел дерева. В качестве
первого параметра конструктору этого класса передается имя текущего элемента
(каталога или файла) обрабатываемого каталога. Эти объекты добавляются в дерево
рассмотренным ранее методом node.Nodes.Add.
Теперь если запустить нашу программу на выполнение, то в окне дерева появится
список дисковых устройств (рис. 7-12).

Рис. 7-12. В окне дерева отображается список дисковых устройств

С этим списком, однако, есть одна проблема — узлы нашего дерева не


раскрываются, сколько ни щелкай их мышью. Для ликвидации этой проблемы нам
нужно обработать одно из событий, создаваемых элементом управления TreeView, а
именно, события BeforeExpand.
Обработчик события BeforeExpand
Чтобы узлы дерева раскрывались, когда пользователь щелкает их мышью или пытается
раскрыть при помощи клавиатуры, нам нужно обрабатывать событие BeforeExpand.
Для создания обработчика этого события выделите дерево в окне дизайнера
формы, а затем откройте вкладку событий, показанную на рис. 7-13.
Рис. 7-13. Добавление обработчика события BeforeExpand

Далее в поле BeforeExpand ведите имя обработчика событий


treeView1_OnBeforeExpand.
После этого добавьте в этот обработчик событий следующий код:
private void treeView1_OnBeforeExpand(object sender,
System.Windows.Forms.TreeViewCancelEventArgs e)
{
treeView1.BeginUpdate();

foreach(TreeNode node in e.Node.Nodes)


{
GetDirs(node);
}

treeView1.EndUpdate();
}
Событие BeforeExpand возникает при попытке пользователя раскрыть узел
дерева. В этом случае наш обработчик заполняет открываемый узел при помощи
рассмотренного ранее метода GetDirs. Ссылка на узел извлекается из поля
e.Node.Nodes, передаваемого обработчику событий в качестве параметра.
После создания обработчика события BeforeExpand появится возможность
раскрывать узлы дерева (рис. 7-14).
Рис. 7-14. Теперь узлы дерева можно раскрывать

Добавление значков к узлам дерева


Вы можете улучшить внешний вид дерева, показанного на рис. 7-14, если добавить к
его узлам значки дисковых устройств и каталогов (папок). Как правило, большинство
подобных деревьев всегда «украшается» значками.
Для нашего дерева Вы можете найти подходящие значки в каталоге с названием
Program Files\Microsoft Visual Studio .NET\Common7\Graphics\icons. Выберите один
значок для диска, один для закрытой папки и один для открытой папки.
Далее скопируйте эти значки в каталог проекта приложения FramesApp и
подключите их к проекту. Чтобы подключить флажки к проекту, выберите из меню
Project системы Microsoft Visual Studio .NET строку Add Existing Item, а затем добавьте
файлы значков при помощи появившегося на экране диалогового окна.
Создание списка изображений
Чтобы использовать изображения в дереве, их необходимо объединить в список класса
ImageList. Добавьте этот список в приложение, перетащив значок компонента ImageList
в окно проектирования формы из панели Toolbox (рис. 7-15).
Рис. 7-15. Добавили элемент управления ImageList

Выделив элемент imageList1 левой клавишей мыши, отредактируйте его свойство


Images. Для редактирования будет открыто окно Image Collection Editor, показанное на
рис. 7-16.

Рис. 7-16. Добавили значки диска и папок

Воспользуйтесь кнопкой Add для добавления в список файлов изображений,


скопированных ранее в каталог нашего приложения. Изображения следует разместить
в том порядке, в каком они показаны на рис. 7-16. А именно, первым (с индексом 0)
должно идти изображение для диска, вторым (с индексом 1) — изображение закрытой
папки, и, наконец, третьим (с индексом 2) — изображение открытой папки.
Подключение списка изображений к дереву
Создав и заполнив список изображений, подключите его к дереву просмотра дисков и
каталогов. Для этого отредактируйте свойство ImageList элемента управления
treeView1, присвоив ему ссылку на список изображений imageList1 (рис. 7-17).
Рис. 7-17. Подключили список изображений к дереву

Изменения в исходном тексте программы


Теперь для отображения значков в узлах дерева нам необходимо изменить исходный
текст методов DriveTreeInit и GetDirs. Напомним, что первый из этих методов
инициализирует дерево, а второй — добавляет к узлу дерева список каталогов.
Обратите внимание на конструктор класса TreeNode, создающий узлы дерева
внутри тела цикла foreach:
public void DriveTreeInit()
{
string[] drivesArray = Directory.GetLogicalDrives();

treeView1.BeginUpdate();
treeView1.Nodes.Clear();

foreach(string s in drivesArray)
{
TreeNode drive = new TreeNode(s, 0, 0);
treeView1.Nodes.Add(drive);

GetDirs(drive);
}

treeView1.EndUpdate();
}
Этот конструктор имеет три параметра. Про первый параметр мы уже
рассказывали — он задает текст надписи для узла дерева. Теперь настало время
рассказать и про два остальных параметра.
Если к элементу управления TreeView подключен список изображений, то второй
и третий параметры конструктора класса TreeNode задают индексы изображений для
узла дерева. При этом второй параметр определяет изображение невыделенного узла
дерева, а третий — выделенного.
Что касается метода DriveTreeInit, то расположенный в нем конструктор создает
узлы, отображающий только дисковые устройства. В любом состоянии (как
выделенном, так и невыделенном) нам необходимо отображать один и тот же значок
дискового устройства, имеющий в нашем случае индекс 0. Поэтому второй и третий
параметры конструктора передают нулевые значения.
Другое дело — метод GetDirs:
public void GetDirs(TreeNode node)
{
DirectoryInfo[] diArray;
node.Nodes.Clear();
string fullPath = node.FullPath;
DirectoryInfo di = new DirectoryInfo(fullPath);

try
{
diArray = di.GetDirectories();
}
catch
{
return;
}

foreach (DirectoryInfo dirinfo in diArray)


{
TreeNode dir = new TreeNode(dirinfo.Name, 1, 2);
node.Nodes.Add(dir);
}
}
Здесь конструктору класса TreeNode, размещенному внутри оператора цикла
foreach, через второй и третий параметры мы передаем индексы значков закрытой и
открытой папки, соответственно. В результате при отображении дерева папки, которые
выделил пользователь, выделяются своим обозначением (рис. 7-18).

Рис. 7-18. Дерево со значками дисковых устройств и папок

Что же касается дисковых устройств, то все они отображаются одним и тем же


значком.
К сожалению, среди многочисленных классов библиотеки Microsoft .NET
Framework нам не удалось найти ни одного, позволяющего получить физические
параметры дисковых устройств. Поэтому мы не смогли использовать отдельные значки
для НГМД, сменных устройств памяти и устройств CD-ROM.
Тем не менее, средства Microsoft .NET Framework позволяют программам C#
обращаться к программному интерфейсу OC Win32 API, поэтому после соответствующей
доработки приложения можно будет использовать разные значки для изображения
устройств внешней памяти разного типа.
Добавление флажков к узлам дерева
Присвоив свойству CheckBoxes значение True можно снабдить узлы дерева
индивидуальными флажками (рис. 7-19).

Рис. 7-19. Флажками можно отмечать устройства и папки

Отметив все или некоторые узлы дерева флажками, можно выполнять над
соответствующими объектами групповые операции. Если дерево показывает структуру
дисков и файлов, то это могут быть такие операции, как, например, удаление,
перемещение или копирование.
Редактирование текста узлов дерева
Изменив значение свойства LabelEdit на True можно разрешить пользователям
редактировать текстовые надписи, расположенные около узлов дерева (рис. 7-20).

Рис. 7-20. Редактирование текста узлов дерева

Если дерево связано, например, с файловой системой, то такая возможность


пригодится для переименования папок.
Список ListView
В правом верхнем фрейме нашего приложения мы расположили элемент управления
ListView. Этот элемент обладает расширенными возможностями отображения списков
различных объектов, имеющих линейную структуру. В нашем случае этот элемент
управления будет использован для отображения содержимого каталогов, примерно в
таком же виде, как это показано в правой части окна программы Windows Explorer (рис.
7-1).
Создание и настройка списка ListView
Когда Вы перетаскиваете значок элемента управления ListView в окно приложения,
дизайнер форм автоматически напишет для Вас код, создающий список как объект
класса ListView. Для использования в нашем приложении необходимо настроить
некоторые свойства списка.
Режимы отображения
Прежде всего, необходимо настроить свойство View. Это свойство определяет, в каком
виде будет отображаться список. Если значение этого свойства равно Details, то мы
увидим содержимое списка в виде детализированной таблицы. Именно это значение и
потребуется нам в приложении FramesApp. Значение List задает режим отображения в
виде простого списка, а значения SmallIcon и LargeIcon — в виде списка со значками
маленького и большого размера, соответственно.
Итак, первым делом выделите список ListView в окне дизайнера форм и
установите для свойства View значение Details.
При необходимости программа сможет динамически переключать режимы
отображения, изменяя значение свойства View во время своей работы.
Создание и настройка столбцов таблицы
Так как мы будем отображать список в виде таблицы, нам необходимо создать столбцы
таблицы и определить их атрибуты. Для этого необходимо отредактировать свойство
Columns. Это делается при помощи редактора ColumnHeader Collection Editor,
показанного на рис. 7-21.

Рис. 7-21. Редактор столбцов списка

С помощью кнопки Add добавьте в таблицу три столбца, а затем настройте


свойства Text, TextAlign и Width этих столбцов.
Свойство Text задает название столбца, отображаемого в верхней части списка
ListView. Первый столбец (с индексом 0) должен называться Имя, второй — Размер, а
третий — Изменен.
В столбце Имя мы будем показывать имена файлов или каталогов. Что же
касается столбцов Размер и Изменен, то первый из них предназначен для отображения
размеров файлов, а второй — для отображения даты последнего изменения файла. При
этом считается, что размер каталога равен нулю.
Свойство TextAlign задает выравнивание заголовка столбца. По умолчанию это
свойство имеет значение Left, задающее выравнивание по левой границе. При
необходимости Вы можете выровнять текст по правой границе, указав значение Right,
или вывести заголовки по центру с помощью значения Center.
С помощью свойства Width можно указать начальную ширину столбца в пикселах.
После отображения списка пользователь сможет изменять ширину столбцов при
помощи мыши.
После того как Вы создадите столбцы и определите их атрибуты, оттранслируйте
приложение FramesApp и запустите его на выполнение. Теперь в правом верхнем
фрейме приложения появится пустой список с заголовками (рис. 7-22).

Рис. 7-22. Появились заголовки столбцов

Сортировка содержимого списка


Свойство Sorting, задающее сортировку списка, может иметь значения None, Ascending
и Descending.
В первом случае список отображает элементы в том порядке, в котором они были
добавлены. Значение Ascending задает сортировку в порядке возрастания, а значение
Descending — в порядке убывания.
Подключение значков к списку
Как мы уже говорили, элемент управления ListView может отображать содержимое
своего окна в четырех различных режимах, задаваемых при помощи свойства View. В
зависимости от выбранного режима, элементы списка могут снабжаться значками
маленького или большого размера.
В приложении FramesApp мы будем использовать 6 значков для отображения
папок и файлов различных типов (файлы исполнимых программ, файлов справочной
системы, текстовых файлов, файлов документов и пр.).
Создавая приложение, скопируйте файлы значков CLSDFOLD.BMP, DOC.BMP,
EXE.BMP, HLP.BMP, TXT.BMP и WINDOC.BMP из папки Program Files\Microsoft Visual
Studio .NET\Common7\Graphics\bitmaps\Outline\NoMask, а затем добавьте их к проекту
приложения.
Для работы с этими значками нам потребуется еще один список изображений
ImageList. Перетащите его значок их панели Toolbar в окно дизайнера форм. Новый
список будет иметь идентификатор imageList2.
Отредактируйте этот список, как показано на рис. 7-23.
Рис. 7-23. Список изображений для списка каталогов и файлов

Необходимо расположить файлы списка в следующем порядке:


 CLSDFOLD.BMP;
 DOC.BMP;
 EXE.BMP;
 HLP.BMP;
 TXT.BMP;
 WINDOC.BMP
В нашем приложении список ListView отображается только в одном режиме, а
именно, как детализированная таблица. Такая таблица снабжается значками только
маленького размера. Если же Ваше приложение будет использовать режим LargeIcon,
необходимо подготовить дополнительный список ImageList, добавив в него файлы
значков большого размера.
Подготовив список изображений imageList2, подключите его к элементу
управления ListView. Для этого присвойте свойствам SmallImageList, LargeImageList
значение imageList2.
В том случае, если список ListView будет показывать значки и маленьких, и
больших размеров, присвойте свойству SmallImageList идентификатор списка
изображений маленького размера, а свойству LargeImageList — идентификатор списка
изображений большого размера.
Наполнение списка ListView
В общем случае список ListView содержит элементы, каждый из которых имеет
дополнительные элементы более низкого уровня, которые мы будем называть
атрибутами. Сам элемент представляется текстовой строкой (и, возможно, значком) в
первом столбце списка, отображаемого в виде детализированной таблицы. Значения
остальных атрибутов отображаются в остальных столбцах таблицы (можно также
создавать неотображаемые атрибуты элементов списка, содержащие произвольные
данные).
Алгоритм наполнения списка
Алгоритм наполнения списка достаточно прост. Прежде всего, необходимо очистить
список от предыдущего содержимого, т.к. наполнение одного и того же списка может
происходить неоднократно. Эта операция выполняется с помощью метода Clear
свойства Items списка:
listView1.Items.Clear();
Далее для добавления элемента в список нужно создать новый элемент как
объект класса ListViewItem:
ListViewItem lvi = new ListViewItem("myfile.txt");
В качестве параметра конструктору передается текстовая строка имени файла.
Она отображается в первом столбце детализированной таблицы.
Далее нужно добавить атрибуты элемента. Данная операция выполняется с
помощью метода SubItems.Add:
lvi.SubItems.Add("1024");
lvi.SubItems.Add("10-05-2003");
Здесь мы добавляем к элементу два атрибута, первый из которых представляет
собой размер файла, а второй — дату его создания.
Если к элементу управления ListView добавлены списки изображений значков, то
при создании элемента списка необходимо указать индекс нужного значка. Это
делается с помощью свойства ImageIndex:
lvi.ImageIndex = 4;
После того как все атрибуты элемента определены, Вы можете добавить элемент в
список при помощи метода Add:
listView1.Items.Add(lvi);
В нашем приложении FramesApp наполнение списка будет происходить в цикле.
Обработчик события AfterSelect
Теперь нашей задачей будет наполнить функциональностью список ListView. Когда
пользователь выделяет диск или каталог в дереве просмотра TreeView, создается
событие AfterSelect. Обработчик этого события должен определить, какой диск или
какой каталог был выделен, а затем наполнить окно списка ListView именами каталогов
и файлов, расположенных на этом диске или в этом каталоге.
Создайте обработчик события AfterSelect в таком виде, как это показано ниже:
private void treeView1_OnAfterSelect(object sender,
System.Windows.Forms.TreeViewEventArgs e)
{
TreeNode selectedNode = e.Node;
fullPath = selectedNode.FullPath;

DirectoryInfo di = new DirectoryInfo(fullPath);


FileInfo[] fiArray;
DirectoryInfo[] diArray;

try
{
fiArray = di.GetFiles();
diArray = di.GetDirectories();
}
catch
{
return;
}

listView1.Items.Clear();

foreach(DirectoryInfo dirInfo in diArray)


{
ListViewItem lvi = new ListViewItem(dirInfo.Name);
lvi.SubItems.Add("0");
lvi.SubItems.Add(dirInfo.LastWriteTime.ToString());
lvi.ImageIndex = 0;

listView1.Items.Add(lvi);
}
foreach(FileInfo fileInfo in fiArray)
{
ListViewItem lvi = new ListViewItem(fileInfo.Name);
lvi.SubItems.Add(fileInfo.Length.ToString());
lvi.SubItems.Add(fileInfo.LastWriteTime.ToString());

string filenameExtension =
Path.GetExtension(fileInfo.Name).ToLower();

switch (filenameExtension)
{
case ".com":
{
lvi.ImageIndex = 2;
break;
}
case ".exe":
{
lvi.ImageIndex = 2;
break;
}
case ".hlp":
{
lvi.ImageIndex = 3;
break;
}
case ".txt":
{
lvi.ImageIndex = 4;
break;
}
case ".doc":
{
lvi.ImageIndex = 5;
break;
}
default:
{
lvi.ImageIndex = 1;
break;
}
}

listView1.Items.Add(lvi);
}
}
Расскажем о том, как работает этот обработчик событий.
Прежде всего, метод извлекает ссылку на узел дерева, выделенный
пользователем, из свойства Node параметра обработчика событий
treeView1_OnAfterSelect.
TreeNode selectedNode = e.Node;
Далее, полный путь к выделенному узлу записывается в поле fullPath класса string
(Вам необходимо создать такое поле в классе Form1):
fullPath = selectedNode.FullPath;
Так как наше дерево содержит только обозначения дисков и каталогов, то это
будет путь либо к корневому каталогу диска, либо к одному из подкаталогов.
Далее мы создаем объект класса DirectoryInfo и получаем списки всех файлов и
каталогов, располагающихся в каталоге, выделенном пользователем в дереве:
DirectoryInfo di = new DirectoryInfo(fullPath);
FileInfo[] fiArray;
DirectoryInfo[] diArray;

try
{
fiArray = di.GetFiles();
diArray = di.GetDirectories();
}
catch
{
return;
}
Для выполнения этих операций применяются методы GetFiles и GetDirectories.
Перечень файлов обработчик события сохраняет в массиве fiArray, а перечень
каталогов — в массиве diArray.
Вооружившись перечнями файлов и каталогов, мы приступим к добавлению
элементов к нашему списку ListView, очистив предварительно содержимое списка
методом Clear:
listView1.Items.Clear();
Наполнение списка именами каталогов выполняется в цикле foreach:
foreach(DirectoryInfo dirInfo in diArray)
{
ListViewItem lvi = new ListViewItem(dirInfo.Name);
lvi.SubItems.Add("0");
lvi.SubItems.Add(dirInfo.LastWriteTime.ToString());
lvi.ImageIndex = 0;

listView1.Items.Add(lvi);
}
Все выполняемые здесь действия были описаны ранее. С помощью конструктора
класса ListViewItem мы создаем элемент списка, а затем задаем значения атрибутов
этого элемента. Длина каталогов считается равной нулю, а время последнего
изменения каталога извлекается при помощи метода LastWriteTime и преобразуется в
текстовую строку методом ToString.
В свойство lvi.ImageIndex записывается нулевое значение — индекс значка в
списке изображений imageList2 с изображением закрытой папки.
После заполнения всех атрибутов элемента списка этот элемент добавляется в
список методом listView1.Items.Add.
На следующем этапе в список добавляются имена файлов, расположенных в
выделенном каталоге. Эти имена тоже добавляются в цикле
foreach(FileInfo fileInfo in fiArray)
{
ListViewItem lvi = new ListViewItem(fileInfo.Name);
lvi.SubItems.Add(fileInfo.Length.ToString());
lvi.SubItems.Add(fileInfo.LastWriteTime.ToString());

string filenameExtension =
Path.GetExtension(fileInfo.Name).ToLower();

switch (filenameExtension)
{
case ".com":
{
lvi.ImageIndex = 2;
break;
}

case ".doc":
{
lvi.ImageIndex = 5;
break;
}
default:
{
lvi.ImageIndex = 1;
break;
}
}

listView1.Items.Add(lvi);
}
Размер очередного файла мы получаем с помощью свойства Length, а дату
последнего изменения — с помощью свойства LastWriteTime (как и для каталогов).
Что же касается значков, то тут алгоритм немного сложнее. Нам нужно определить
тип текущего файла, проанализировав расширение его имени, а затем выбрать и
записать в свойство lvi.ImageIndex индекс соответствующего значка. Расширение
имени файла извлекается из полного имени методом Path.GetExtension, а затем все его
буквы преобразуется в прописные методом ToLower. Непосредственный выбор значка
выполняется при помощи оператора switch.
Подготовленный элемент списка, описывающий текущий файл, добавляется в
список методом listView1.Items.Add.
Отображение содержимого текстовых файлов
Итак, теперь наше приложение может отображать в левом фрейме дерево каталогов, а
в правом верхнем фрейме — содержимое выбранных каталогов. Остался
незадействованным только правый нижний фрейм, в котором расположено окно
текстового редактора RichTextBox.
Предусмотрев обработчик события ItemActivate к элементу управления ListView,
мы сможем отображать в окне текстового редактора содержимое текстовых файлов,
отображаемых в правом верхнем фрейме. Это событие возникает, когда пользователь
дважды щелкает строку в окне списка ListView.
Вот исходный текст нашего обработчика события ItemActivate:
private void listView1_OnItemActivate(object sender,
System.EventArgs e)
{
foreach(ListViewItem lvi in listView1.SelectedItems)
{
string ext = Path.GetExtension(lvi.Text).ToLower();
if(ext == ".txt" || ext == ".htm" || ext == ".html")
{
try
{
richTextBox1.LoadFile(Path.Combine(fullPath, lvi.Text),
RichTextBoxStreamType.PlainText);

statusBar1.Text = lvi.Text;
}
catch
{
return;
}
}
else if(ext == ".rtf")
{
try
{
richTextBox1.LoadFile(Path.Combine(fullPath, lvi.Text),
RichTextBoxStreamType.RichText);

statusBar1.Text = lvi.Text;
}
catch
{
return;
}
}
}
}
Получив управление, обработчик событий, прежде всего, должен получить список
элементов, выделенных пользователем в окне списка. Эта операция выполняется в
цикле:
foreach(ListViewItem lvi in listView1.SelectedItems)
{

}
Идентификаторы выделенных элементов хранятся в наборе
listView1.SelectedItems.
В общем случае пользователь может выделить несколько элементов, однако если
пользователь щелкнул строку дважды, то в результате окажется выделенной только эта
строка. Поэтому наш цикл выполнит только одну итерацию.
Так как редактор RichTextBox способен отображать содержимое только текстовых
файлов, нам необходимо определить тип файла, выбранного пользователем. Мы делаем
это, анализируя расширение имени файла, полученное с помощью метода
GetExtension (с преобразованием символов расширения в строчные символы):
string ext = Path.GetExtension(lvi.Text).ToLower();
Если расширение имени соответствует текстовому файлу или файлу HTML, то мы
загружаем содержимое файла в окно редактора RichTextBox, пользуясь для этого
методом LoadFile:
if(ext == ".txt" || ext == ".htm" || ext == ".html")
{
try
{
richTextBox1.LoadFile(Path.Combine(fullPath, lvi.Text),
RichTextBoxStreamType.PlainText);

statusBar1.Text = lvi.Text;
}
catch
{
return;
}
}
В качестве первого параметра методу передается полный путь к файлу,
полученный комбинированием полного пути каталога fullPath, выделенного в дереве, и
имени файла lvi.Text, выделенного в списке ListView.
Через второй параметр методу LoadFile передается тип загружаемого документа,
заданный как RichTextBoxStreamType.PlainText (т.е. текстовый документ).
Дополнительно имя документа отображается в строке состояния главного окна
приложения, для чего это имя переписывается из свойства lvi.Text в свойство
statusBar1.Text.
Обработка файлов с документами типа RTF, имеющих расширение имени rtf,
выполняется аналогично. При этом тип документа указывается методу LoadFile как
RichTextBoxStreamType.RichText.
На рис. 7-24 мы показали внешний вид главного окна нашего приложения, когда
в нижний правый фрейм загружен документ RTF.

Рис. 7-24. Просмотр документа RTF в окне нашего приложения

Приложения MDI
Многие пользователи ОС Microsoft Windows хорошо знакомы с многооконным
интерфейсом MDI (Multiple Document Interface), позволяющим в одном приложении
работать одновременно с несколькими документами или с разными представлениями
одного и того же документа.
Этот интерфейс был впервые описан в руководстве по разработке интерфейса
пользователя System Application Architecture Common User Access Advanced Interface
Design Guide (SAA/CUA), созданном IBM. Интерфейс MDI использовался в ОС Microsoft
Windows, начиная с версии 3.0, а также в графической оболочке Presentation
Manager операционной системы OS/2. Современные версии ОС Microsoft Windows также
позволяют создавать приложения с интерфейсом MDI (MDI-приложения).
В начале этой главы мы упоминали программу Registry Editor (рис. 7-3),
использующую интерфейс MDI. Этим интерфейсом был снабжен и текстовый процессор
Microsoft Word for Windows версии 2.0, способный предоставить пользователю два
различных представления одного и того же документа. В одном окне могло
отображаться, например, обычное представление документа, а в другом —
представление в режиме просмотра оглавления документа.
Окна, отображающие содержимое документов внутри главного окна MDI-
приложения, называются MDI-окнами.
Меню Windows
Любое MDI-приложение содержит меню Windows, предназначенное для управления
окнами, отображающими различные документы или различные представления одного и
того же документа.
Как правило, в меню Windows есть строки Cascade и Tile, с помощью которых
пользователь может упорядочить MDI-окна, расположив их с перекрытием (друг за
другом) или рядом друг с другом.
В этом меню могут быть и другие строки, управляющие расположением MDI-окон.
Например, если приложение допускает минимизацию MDI-окон (когда они
отображаются в виде значков), в меню Windows присутствует строка Arrange Icons. С
помощью этой строки пользователь может упорядочить расположение пиктограмм
свернутых окон.
Ниже разделительной черты в меню Windows обычно находятся строки,
обозначающие отдельные MDI-окна. Если выбрать одну из этих строк, указанное окно
будет активизировано и показано на первом плане.
Меню Windows может быть разным в различных MDI-приложениях. Однако в
любом случае это меню позволяет пользователю автоматически упорядочить
расположение MDI-окон и выбрать нужное окно из списка для активизации.
Двигая MDI-окна при помощи заголовка, Вы не сможете переместить их за
пределы главного окна приложения. В то же время MDI-окна можно делать активными,
при этом заголовок активного MDI-окна выделяется цветом.
Системное меню MDI-окна
Каждое MDI-окно имеет, как правило, системное меню и кнопки изменения размера. С
помощью системного меню пользователь может изменять размеры или перемещать MDI-
окно (строки Restore, Move, Size, Maximize, Minimize), закрывать окно (строка Close),
передавать фокус ввода от одного MDI-окна другому (строка Next Window).
Если MDI-окно сворачивается в значок (минимизируется), то этот значок
располагается в нижней части главного окна приложения.
Создание главного окна MDI-приложения
Главное окно приложения MDI и отдельные MDI-окна связывают родительские
отношения. При этом главное окно MDI-приложения является родительским для
дочерних MDI-окон. В этом разделе мы рассмотрим процедуру создания главного окна
приложения MDI средствами Microsoft Visual Studio .NET на языке C#.
Для демонстрации способов создания приложений MDI мы создадим программу
MDIApp.
Вначале необходимо создать новый проект приложения Windows Application
Project, как обычно. Далее откройте окно редактирования свойств главного окна
(формы) приложения и установите значение свойства IsMDIContainer, равным True
(рис. 7-25).
Рис. 7-25. Установка свойства IsMDIContainer

В результате главное окно приложения превратится в контейнер для дочерних


MDI-окон.
Далее, добавьте в приложение главное меню. Для этого перетащите из панели
Toolbox системы Microsoft Visual Studio .NET значок главного меню MainMenu.
Создайте меню File со строками New, Close и Exit, как это показано на рис. 7-26.

Рис. 7-26. Меню File

С помощью строки New этого меню мы будем создавать новые MDI-окна с


редактором текста на базе элемента управления RichTextBox. Строка Close
предназначена для закрытия MDI-окон, а строка Exit — для завершения работы
приложения.
Создайте также меню Edit со строками Copy и Paste, а также меню Window,
предназначенное для управления MDI-окнами и показанное на рис. 7-27.
Рис. 7-27. Меню Window

Строка Cascade предназначена для каскадного расположения открытых MDI-окон,


а строки Horizontally Tile и Vertically Tile — для размещения окон рядом друг с другом в
горизонтальном и вертикальном направлении, соответственно.
После создания меню Window Вам необходимо выделить его, а затем в окне
редактирования свойств установить значение свойства MdiList равным True (рис. 7-28).
При этом во время работы приложения в меню Window будут автоматически добавлены
строки списка открытых MDI-окон. В этом меню строки активных окон будут отмечены.

Рис. 7-28. Установка свойства MdiList

Создание MDI-окон
Как мы уже говорили, MDI-окна обычно используются для отображения различных
документов или различных представлений одного и того же документа. В нашем
приложении MDIApp MDI-окна будут содержать внутри себя элемент управления
RichTextBox, т.е. редактор текста.
Шаблон MDI-окна
Прежде всего, нужно добавить в проект приложения новую форму, которая будет
играть роль шаблона для создания дочерних MDI-окон. Для добавления формы
щелкните правой клавишей мыши в окне Solution Explorer название проекта MDIApp и
выберите в контекстном меню Add строку Add Windows Form.
После этого на экране появится диалоговое окно Add New Item, в котором нужно
выбрать значок Windows Form и затем нажать кнопку Open. В результате в окне
дизайнера форм появится новая форма Form2.
Так как это окно будет предназначено для редактирования текста, перетащите в
него из панели Toolbox значок элемента управления RichTextBox. Чтобы окно редактора
текста RichTextBox заполнило собой клиентскую часть MDI-окна, установите значение
свойства Dock равным Fill. Кроме того, проследите, чтобы свойство Anchor было равно
Top, Left. Установка значений этого свойства выполняется при помощи редактора, окно
которого показано на рис. 7-29.

Рис. 7-29. Установка свойства Anchor

Если щелкнуть левую и верхнюю кнопку окна редактирования этого свойства, а


также установить правильным образом значение свойства Dock, то будет получен
необходимый нам результат. А именно, окно элемента управления RichTextBox будет
заполнять клиентскую область MDI-окна при любом изменении размеров последнего.
Программный код для создания MDI-окна
Теперь нам нужно написать программный код, создающий MDI-окна. Этот код должен
получать управление, когда пользователь выбирает строку New из меню File.
Добавьте следующий обработчик события Click для упомянутой выше строки New
меню File:
private void menuItem2_Click(object sender, System.EventArgs e)
{
Form2 mdiChild = new Form2();
mdiChild.MdiParent = this;
mdiChild.Show();
}
Здесь мы сначала создаем новую форму как объект класса Form2, а затем
сохраняем ссылку на эту форму в переменной mdiChild. Свойство MdiParent этого окна
должно содержать ссылку на родительское окно приложения MDI, поэтому мы
записываем в него ссылку на объект класса Form1, используя ключевое свойство this.
Для того чтобы MDI-окно появилось на экране, его необходимо отобразить явным
образом при помощи метода Show.
На рис. 7-30 мы показали главное окно нашего приложения, в котором было
создано четыре MDI-окна.
Рис. 7-30. Создано четыре MDI-окна

Создав с помощью меню File несколько дочерних MDI-окон, раскройте меню


Window и убедитесь, что в нем появилось несколько новых строк (рис. 7-31).

Рис. 7-31. Новые строки в меню Window

С помощью этих строк Вы сможете выдвинуть на передний план любое нужное


Вам MDI-окно.
Упорядочивание MDI-окон
Средства управления дочерними MDI-окнами позволяют легко упорядочить эти окна
одним из трех способов.
Прежде всего, окна могут быть упорядочены с перекрытием (каскадным образом),
как это показано на рис. 7-30. Кроме этого, возможно расположение окон рядом по
горизонтали (рис. 7-32) и по вертикали (рис. 7-33).
Рис. 7-32. Упорядочивание по горизонтали (типа Horizontally Tile)

Рис. 7-33. Упорядочивание по вертикали (типа Vertically Tile)

Чтобы пользователь мог упорядочивать дочерние MDI-окна, мы предусмотрели в


меню Window строки Cascade, Horizontally Tile и Vertically Tile. Вот обработчики событий
от этих строк меню, которые Вам необходимо добавить в исходный текст приложения:
private void menuItem11_Click(object sender, System.EventArgs e)
{
this.LayoutMdi(MdiLayout.Cascade);
}

private void menuItem10_Click(object sender, System.EventArgs e)


{
this.LayoutMdi(MdiLayout.TileHorizontal);
}

private void menuItem12_Click(object sender, System.EventArgs e)


{
this.LayoutMdi(MdiLayout.TileVertical);
}
Как видите, все они вызывают метод LayoutMdi родительской формы, передавая
ему в качестве параметра одну из констант, определяющих нужный тип
упорядочивания. Это константы MdiLayout.Cascade, MdiLayout.TileHorizontal и
MdiLayout.TileVertical.
Передача данных через буфер Clipboard
В большинстве случаев для работы с документами, отображаемыми в MDI-окнах,
используется одно меню — главное меню приложений. Например, с помощью строк New
и Close меню File пользователь нашего приложения MDIApp может, соответственно,
создавать и уничтожать MDI-окна.
Теперь мы реализуем операцию обмена данных через универсальный буфер
обмена Clipboard, для которого в нашем приложении предусмотрено меню Edit. Строка
Copy этого меню должна копировать данные из активного MDI-окна в буфер Clipboard,
а строка Paste — вставлять данные из буфера Clipboard в активное MDI-окно. Активным
окном здесь называется MDI-окно, имеющее фокус ввода.
Копирование данных в буфер Clipboard
Вначале мы реализуем операцию копирования выделенного фрагмента текста из
активного MDI-окна в Clipboard. Для этого добавьте следующий обработчик событий к
строке Copy меню Edit:
private void menuItem8_Click(object sender, System.EventArgs e)
{
Form activeChild = this.ActiveMdiChild;

if(activeChild != null)
{
RichTextBox editBox = (RichTextBox)activeChild.ActiveControl;

if(editBox != null)
{
Clipboard.SetDataObject(editBox.SelectedText);
}
}
}
Здесь мы вначале определяем идентификатор активного MDI-окна, извлекая его
из свойства ActiveMdiChild родительского окна приложения MDI. Этот идентификатор
сохраняется в переменной activeChild.
Если в приложении нет активного окна, то приведенный выше обработчик
событий завершает свою работу, не выполняя никаких других действий.
Определив идентификатор активного MDI-окна, обработчик события получает
идентификатор активного элемента управления, расположенного внутри этого окна.
Для этого используется свойство activeChild.ActiveControl, хранящее ссылку на
активный элемент управления.
В нашем случае MDI-окно содержит только один элемент управления — редактор
RichTextBox. Поэтому полученную ссылку мы преобразуем явным образом к типу
RichTextBox и сохраняем в переменной editBox для дальнейшего использования.
Теперь мы получили идентификатор нашего редактора текста и готовы переписать
выделенный в его окне фрагмент текста в универсальный буфер обмена Clipboard.
Запись в Clipboard осуществляется методом Clipboard.SetDataObject.
В качестве параметра мы передаем методу Clipboard.SetDataObject выделенный
текст, извлеченный из редактора текста с помощью свойства editBox.SelectedText.
Вставка данных из буфера Clipboard
Теперь дополним приложение кодом, необходимым для вставки данных из
универсального буфера обмена Clipboard в активное MDI-окно.
Добавьте следующий обработчик события для строки Paste меню Edit:
private void menuItem9_Click(object sender, System.EventArgs e)
{
Form activeChild = this.ActiveMdiChild;
if(activeChild != null)
{
RichTextBox editBox = (RichTextBox)activeChild.ActiveControl;

if(editBox != null)
{
IDataObject data = Clipboard.GetDataObject();

if(data.GetDataPresent(DataFormats.Text))
{
editBox.SelectedText =
data.GetData(DataFormats.Text).ToString();
}
}
}
}
Как видите, этот обработчик вначале определяет идентификатор активного MDI-
окна, а затем, пользуясь этим идентификатором и свойством ActiveControl, получает
идентификатор редактора текста RichTextBox.
Далее для вставки данных из буфера Clipboard наш метод получает ссылку на
интерфейс IDataObject, вызывая для этого метод Clipboard.GetDataObject. Эта ссылка
сохраняется в переменной data.
Пользуясь полученной ссылкой на интерфейс IDataObject, наш обработчик
события определяет, имеется ли в буфере Clipboard текст, который можно было бы
вставить в редактор текста. Для этого используется метод GetDataPresent. В качестве
параметра этому методу передается идентификатор формата текстовых данных
DataFormats.Text (напомним, что в буфере Clipboard могут одновременно храниться
данные разных форматов).
Если в буфере Clipboard имеются текстовые данные, программа извлекает их при
помощи метода GetData, а затем преобразует в текстовую строку при помощи метода
ToString. Далее эта текстовая строка записывается в свойство SelectedText нашего
редактора текста, благодаря чему и происходит вставка данных из буфера Clipboard.

Визуальное проектирование
приложений C#
А.В. Фролов, Г.В. Фролов
ГЛАВА 8. ПРИЛОЖЕНИЯ С БАЗАМИ ДАННЫХ
МЕТОДЫ ДОСТУПА К СУБД
Вызов программных интерфейсов
Прямой вызов программного интерфейса СУБД
Использование программного интерфейса ODBC
Объектные интерфейсы СУБД
Интерфейс Remote Data Object
Интерфейс OLE DB
Интерфейс ActiveX Data Objects
Метод доступа ADO .NET
Многоуровневые системы
Рассоединенные системы
Распределенная обработка данных и XML
Провайдеры данных для управляемого кода
РАБОТА С ОБЪЕКТАМИ DATASET
Приложение DataSetApp
Добавление элемента управления DataSet
Настройка свойств элемента управления DataSet
Создание таблиц
Создание столбцов
Первичный ключ
ЭЛЕМЕНТ УПРАВЛЕНИЯ DATAGRID
Добавление объекта DataGrid в проект приложения DataSetApp
Настройка внешнего вида окна элемента управления DataGrid
Рамка вокруг окна
Заголовок окна
Форматирование содержимого окна
Сортировка
Запрет редактирования
Выбор цвета
Набор стилей оформления TableStyles
Программный код
Создание элемента управления DataGrid
Настройка свойств
Добавление таблицы стилей
РАБОТА С DATASET ПРИ ПОМОЩИ МЕТОДОВ И СВОЙСТВ
Добавление новой строки
Удаление строки
ПРИЛОЖЕНИЕ PHONEBOOKAPP
Создание проекта приложения
Элементы управления для работы со списком имен и фамилий
Элементы управления для работы со списком телефонов
Создание таблиц базы данных
Таблица Contacts
Таблица Phones
Связывание таблиц Contacts и Phones
Проектирование кода для управления базой данных
Добавление нового контакта
Выбор записи в списке имен и фамилий
Редактирование записей таблицы Contacts
Удаление записей таблицы Contacts
Добавление номера телефона
Удаление номера телефона
Изменение номера телефона
Обновление списка контактов
Обновление списка телефонов
ФИЛЬТР ДЛЯ МЕТОДА SELECT
Перегруженные методы Select
КРАТКОЕ ОПИСАНИЕ СИНТАКСИСА СТРОКИ ФИЛЬТРА
Значения
Операторы
Функции

Глава 8. Приложения с базами данных


Большинство деловых и офисных приложений, таких, например, как системы
бухгалтерского учета, складские системы и разного рода системы автоматизации
финансовой и хозяйственной деятельности предприятий работают с базами данных.
Иногда базы данных встраиваются даже в относительно несложные утилиты и
программы, например, электронные фотоальбомы, записные книжки и т.п. Многие Web-
приложения, рассчитанные для работы в Интернете или интрасетях компаний также
интегрируются тем или иным способом с базами данных. Ввиду важности вопроса при
рассказе о визуальном проектировании приложений C# мы не можем обойти стороной
методики интеграции таких приложений с базами данных.
В [1] мы рассматривали различные способы интеграции автономных и Web-
приложений с системой управления базами данных (СУБД) Microsoft SQL Server. Эта
СУБД очень популярна на платформе ОС Microsoft Windows, особенно при создании
промышленных информационно-справочных систем и различного рода систем
автоматизации.
Что же касается приложений C# и платформы Microsoft .NET, то для них компания
Microsoft разработала новейший метод доступа ActiveX Data Objects .NET (ADO .NET),
наилучшим образом соответствующий нуждам современных приложений. Однако
прежде чем приступить к рассказу об ADO .NET, мы сделаем краткий обзор других
методов доступа, применяемых в работе с СУБД Microsoft SQL Server, а также с СУБД
других типов.
Методы доступа к СУБД
На сегодняшний день разработаны сотни (если не больше) СУБД различных типов,
способные работать на компьютерах любой мощности — от майнфреймов до карманных
компьютеров и компьютеров, встроенных в мобильные телефоны и другие устройства.
К счастью, в этих СУБД и в операционной системе ОС Microsoft Windows предусмотрено
относительно небольшое количество универсальных методов доступа, с помощью
которых приложения могут быть интегрированы с базами данных.
Все эти методы можно разделить на две группы, первая из которых предполагает
прямой или косвенный вызов СУБД через программные интерфейсы, а второй —
использование объектных интерфейсов.
Вызов программных интерфейсов
Здесь мы рассмотрим интерфейсы двух типов. Это программные интерфейсы,
предназначенные для прямого вызова СУБД, а также универсальный интерфейс ODBC.
Прямой вызов программного интерфейса СУБД
Как правило, СУБД любого типа, предназначенная для работы на платформе Microsoft
Windows, предоставляет в распоряжение программиста интерфейс API, с помощью
которого программа может выполнять все необходимые операции с базами данных.
Физически этот интерфейс обычно реализован с помощью библиотек динамической
компоновки DLL, экспортирующих функции доступа к СУБД.
В частности, Microsoft SQL Server предоставляет разработчикам приложений
программный интерфейс DB Library —естественный интерфейс данной СУБД,
реализованный как набор функций.
Следует заметить, что прямая работа приложений с программным интерфейсом
СУБД может привести к проблемам при появлении новых версий этих СУБД. Компания
Microsoft, например, не рекомендует использовать в новых приложениях упомянутый
выше интерфейс DB Library, оставленный только для совместимости с разработанными
ранее приложениями.
Кроме того, технология прямого вызова программного интерфейса СУБД
недоступна для разработчиков Web-приложений, использующих так называемые
серверные сценарии JavaScript и VB Script (читайте об этом подробнее в [1]).
Использование программного интерфейса ODBC
Программный интерфейс ODBC, как и только что упомянутые интерфейсы прямого
вызова СУБД также выполнен в виде набора функций. Это ограничивает его
применение в Web-приложениях [1].
Однако интерфейс ODBC, созданный специально для доступа к реляционным
базам данных, универсален. Это единый интерфейс, позволяющий приложениям
работать с СУБД всех типов, для которых имеется так называемый драйвер ODBC.
Используя ODBC, программист может не заботиться о деталях внутреннего
устройства и особенностях естественного интерфейса различных СУБД, т.к. драйвер
ODBC полностью скрывает от него эти детали. В результате программы, обращающиеся
к базам данных, становятся менее зависимыми от этих баз данных. К сожалению,
отличия в реализации драйверов ODBC различных СУБД не всегда позволяют добиться
полной независимости программ от типа СУБД.
Объектные интерфейсы СУБД
По мере развития ОС Microsoft Windows и СУБД, на смену программным интерфейсам
пришли объектные интерфейсы, основанные на использовании модели компонентных
объектов Component Object Model (COM).
Объекты COM можно представить себе как набор интерфейсов, через которые
можно получить доступ к свойствам и методам объекта. Если Вы знакомы с классами,
интерфейсами и свойствами языка C#, то эти понятия Вам тоже знакомы. Хотя с
появлением платформы .NET технология COM становится устаревшей (или, как говорят,
унаследованной), до сих пор она интенсивно применяется как в самой ОС Microsoft
Windows, так и в приложениях, создаваемых для этой ОС. При необходимости
дополнительную информацию о создании объектов COM и ActiveX (также созданных на
базе COM) Вы сможете найти в [1], [5] и [6].
В этом разделе мы рассмотрим объектные интерфейсы Remote Data Object (RDO),
OLE DB и ActiveX Data Objects (ADO).
Интерфейс Remote Data Object
Объектный интерфейс RDO был разработан для упрощения доступа к серверу СУБД
Microsoft SQL Server из приложений Microsoft Visual Basic и Visual Basic for Applications.
Он реализует все возможности интерфейса ODBC, однако для его использования
приложению не требуется напрямую вызывать какие-либо программные интерфейсы
СУБД или ODBC.
Интерфейс OLE DB
Объектный интерфейс OLE DB представляет собой открытый стандарт,
предназначенный для универсального доступа приложений к базам данных. В отличие
от интерфейса ODBC и RDO, интерфейс OLE DB позволяет приложениям обращаться не
только к реляционным БД, но и к нереляционным, таким, например, как серверы
почты, базы данных для мэйнфреймов с методами доступа IMS, VSAM и т. д.
Интерфейс OLE DB состоит из трех компонентов: провайдера (provider),
потребителя (consumer) и служебного компонента, выполняющего обработку и
передачу данных.
В роли потребителя могут выступать приложения. Задача провайдера OLE DB —
реализация интерфейса OLE DB. В составе OLE DB поставляются провайдеры для
интерфейсов ODBC, для текстовых файлов и некоторые другие. Пользуясь провайдером
ODBC, потребители интерфейса OLE DB могут получить доступ к базам данных через
драйвер ODBC.
Интерфейс ActiveX Data Objects
Упомянутый выше объектный интерфейс OLE DB не реализует механизм автоматизации,
в результате чего этот метод не подходит для создания Web-приложений, основанных
на серверных сценариях JavaScript и VB Script [1].
Объектный интерфейс ActiveX Data Objects (ADO) построен на основе интерфейса
OLE DB. При этом интерфейс OLE DB обеспечивает универсальный доступ к данным с
помощью провайдеров, таких как Microsoft OLE DB Provider для ODBC (MSDASQL) или
Microsoft OLE DB Provider для SQL Server (SQLOLEDB).
Благодаря тому, что объекты ADO реализуют средства автоматизации, интерфейс
ADO доступен из приложений, составленных с применением целого спектра
инструментальных средств, таких, как серверный сценарии ASP, C++, Visual Basic,
Visual Basic for Applications, Java и т. д.
Ключевыми элементами программной модели ADO является набор объектов, с
помощью которых выполняется соединение с базами данных, выполнение команд с
параметрами, получение результата выполнения этих команд в виде переменных или
наборов записей, обработка событий и ошибок.
Вот типичный сценарий работы приложения с базой данных посредством
интерфейса ADO:
 установка соединения;
 подготовка команды и параметров;
 выполнение команды;
 обработка результатов выполнения команды;
 закрытие соединения;
 обработка ошибок
Прежде чем обращаться к базе данных, приложение должно установить
соединение с сервером базы данных. При этом требуется указать имя источника
данных Data Source Name (DSN) или информацию об источнике данных, такую как имя
драйвера, имя сервера, пароль и т.д.
После установки соединения приложение должно подготовить объект-команду,
записав в его свойства команды, необходимые для доступа к данным (например, строки
языка SQL). Приложение может передать вместе с командой параметры. Входные
параметры позволяют передавать информацию в хранимые процедуры СУБД Microsoft
SQL Server, а выходные — принимать информацию из хранимой процедуры.
Когда программа инициирует выполнение команды, она получает результат в виде
набора записей (Recordset) или через выходные параметры хранимой процедуры (если
команда запускает такую процедуру). Приложение может просмотреть все записи из
полученного набора, сохранить их в памяти или использовать каким-либо другим
способом. В частности, можно обновить полученный набор записей с целью обновления
источник данных (если это необходимо).
После того как команда выполнена, а результаты ее выполнения обработаны,
приложение должно закрыть соединение. Большое количество незакрытых соединений
может привести к чрезмерному расходованию ресурсов сервера СУБД.
В процессе подготовки параметров команды и ее выполнения могут возникать
ошибки. Приложение должно быть готово их обработать.
Метод доступа ADO .NET
Рассмотренные выше методы доступа с программными и объектными интерфейсами
больше всего подходят для создания так называемых клиент-серверных приложений.
Такие приложения обычно открывают соединение с базой данных в начале своей
работы, а закрывают — при ее завершении. Если пользователей много, то каждый из
них будет во время своей работы держать как минимум одно соединение с сервером
СУБД (даже во время обеденного перерыва, если клиентская программа запускается на
целый день). Это отнимает немало ресурсов сервера и приводит к необходимости
приобретения большого количества серверных лицензий.
Многоуровневые системы
С появлением Web-приложений, интегрированных с базами данных, получили развитие
так называемые многоуровневые системы. В этих системах клиент (в роли которого
выступает обычный браузер, такой, например, как Microsoft Internet Explorer)
обращается к СУБД не напрямую, а через Web-сервер.
Такое обращение начинается с того, что бразуер направляет запрос к Web-
серверу (например, для выборки данных из базы данных или обновления базы
данных). Далее Web-сервер действует следующим образом:
 открывает соединение с СУБД;
 выполняет запрос, обращаясь к базе данных;
 закрывает соединение с базой данных;
 отправляет результат запроса в браузер
Браузер получает результат обработки запроса в виде текстового документа HTML
и отображает его в своем окне.
Так как соединение с базой данных устанавливается только на время обработки
запроса, это позволяет экономить ресурсы сервера СУБД, а также приобретать
небольшое количество клиентских лицензий. Фактически клиентом СУБД в этом случае
выступает Web-сервер, и только для него нужны клиентские лицензии.
Рассоединенные системы
Метод доступа ADO .NET, доступный приложениям на платформе Microsoft .NET,
позволяет создавать разновидность многоуровневых систем — так называемые
рассоединенные (disconnected) системы.
Рассоединенные системы позволяют получить локально данные, извлеченные из
базы данных, выполнить их локальную обработку, а затем обновить базу данных на
сервере по результатам этой обработки.
Данные, извлеченные из сервера СУБД методом ADO .NET, сохраняются в объекте
класса DataSet. Этот объект может хранить в себе одновременно несколько таблиц
данных, в том числе связанных между собой (related tables), а также
ограничения (constraints). В частности, можно переписать в созданный локально объект
DataSet содержимое всей базы данных, расположенной на сервере, если в этом
возникнет необходимость.
Вот возможная схема взаимодействия клиента с сервером в рассоединенной
системе, реализованной с использованием метода доступа ADO .NET:
 открытие соединения с сервером СУБД;
 отправка запроса к базе данных;
 закрытие соединения;
 обработка данных, полученных в виде объекта класса DataSet;
 открытие соединения с сервером СУБД;
 обновление базы данных с использованием содержимого объекта класса
DataSet;
 закрытие соединения
Распределенная обработка данных и XML
Если нужно создать информационную систему с распределенной обработкой данных,
встает вопрос организации взаимодействия между серверами и клиентами такой
системы. Метод доступа ADO позволяет организовать такую обработку средствами COM,
однако этот способ имеет определенные недостатки. Эти недостатки проявляются в тех
случаях, когда нужно объединить узлы системы при помощи каналов Интернета.
Дело в том, что корпоративные интрасети, подключенные к Интернету, обычно
защищаются брандмауэром (firewall), открывающим доступ только для определенных
портов TCP/IP и для определенных протоколов передачи данных. Обычно открывается
только порт 80, предназначенный для работы с Web-серверами посредством протокола
HTTP, а также порты протоколов SMTP, POP3 и IMAP, с помощью которых
осуществляется передача электронной почты. Эти ограничения обычно несовместимы с
системами удаленной обработки, реализованными с использованием модели COM.
Что же касается ADO .NET, то этот метод доступа допускает представление данных
в формате XML (хорошее описание XML Вы найдете в [9]). При этом данные могут
передаваться с использованием протокола HTTP, что позволяет объединять
информационные системы каналами Интернета, даже если эти системы защищены
брандмауэрами.
Провайдеры данных для управляемого кода
Программный компонент, называемый провайдером данных (data provider) выступает в
качестве моста между приложением и источником данных. В его задачу входит
извлечение данных из источника, а также обновление источника данных.
Для приложений, содержащих управляемый код и предназначенных для
платформы Microsoft .NET, компания Microsoft разработала три провайдера данных. Это
SQL Server .NET Data Provider, OLE DB .NET Data Provider и ODBC .NET Data Provider.
Первые два из них входят в состав среды исполнения Microsoft .NET Framework, а
третий можно загрузить с Web-сайта компании Microsoft по адресу
http://msdn.microsoft.com/downloads.
Если Ваше приложение C# должно работать с сервером Microsoft SQL Server
версии 7.0 или более новой версии, максимальная производительность будет
достигнута при использовании провайдера данных SQL Server .NET Data Provider. К
сожалению, специализированных провайдеров для прямого доступа из управляемого
кода к СУБД других типов пока не существует.
Что же касается провайдера OLE DB .NET Data Provider, то он пригодится Вам для
доступа к базам данных Microsoft Access и другим СУБД, для которых реализованы
провайдеры OLE DB.
В том случае, когда единственно возможный способ интеграции приложения и
СУБД заключается в использовании драйвера ODBC, можно воспользоваться
провайдером ODBC .NET Data Provider.
Работа с объектами DataSet
Как мы уже говорили, объекты DataSet могут содержать в себе отдельные и связанные
реляционные таблицы и ограничения. Этот объект, наряду с другими объектами
нужными для работы с базами данных, определен в пространстве имен System.Data.
Заметим, что Вы можете создавать приложения с объектами DataSet, которые не
взаимодействуют ни с какими серверами баз данных. Более того, изучение ADO .NET
мы начнем именно с таких приложений, дополнив их в дальнейшем средствами
интеграции с сервером Microsoft SQL Server.
Приложение DataSetApp
Мы подготовили приложение DataSetApp специально для изучения элемента
управления DataSet, а также еще одного элемента управления с названием DataGrid.
Последний чрезвычайно удобен для отображения содержимого таблиц баз данных в
формах приложений с графическим пользовательским интерфейсом, и потому
представляет для нас особый интерес.
Объект класса DataSet позволяет создавать наборы реляционных таблиц в
оперативной памяти. Эти наборы впоследствии могут быть использованы для
инициализации или обновления баз данных, а также экспортированы в формате XML.
Добавление элемента управления DataSet
С помощью мастера проектов создайте приложение DataSetApp, как Вы это делали
раньше. Далее откройте в инструментальной панели Toolbox вкладку Data (рис. 8-1).

Рис. 8-1. Вкладка Data инструментальной панели Toolbox

Перетащите из этой вкладки в окно дизайнера форм приложения значок элемента


управления DataSet. На экране появится диалоговое окно Add Dataset, показанное на
рис. 8-2.

Рис. 8-2. Выбор типа объекта DataSet

В этом окне необходимо выбрать типизированный или не типизированный набор


данных. Типизированный набор данных создается на базе ранее определенного в
проекте набора данных, имеющего схему XML Schema (файл с расширением .xsd). Для
нашего проекта выберите не типизированный набор данных, отметив флажок Untyped
dataset, а затем щелкните кнопку OK.
В результате в проект будет добавлен элемент управления класса DataSet с
идентификатором dataSet1.
Рис. 8-3. В проект добавлен компонент DataSet

Если после выполнения этой процедуры посмотреть исходный текст нашего


приложения, то в нем появится поле dataSet1 для хранения идентификатора
компонента, а также код инициализации компонента, приведенный ниже:

private System.Data.DataSet dataSet1;



this.dataSet1 = new System.Data.DataSet();

((System.ComponentModel.ISupportInitialize)
(this.dataSet1)).BeginInit();

//
// dataSet1
//
this.dataSet1.DataSetName = "NewDataSet";
this.dataSet1.Locale = new System.Globalization.CultureInfo("ru-RU");
Как видите, объект DataSet создается с помощью конструктора класса
System.Data.DataSet, не имеющего параметров.
Далее с помощью метода ISupportInitialize.BeginInit объекту передается сигнал о
начале инициализации. Этот сигнал поможет объекту выполнить инициализацию своих
свойств наиболее эффективным способом.
На данном этапе приложение устанавливает только два свойства — имя набора
данных DataSetName и сведения о национальной культуре (локализации) Locale.
Подробнее о национальных культурах читайте в [1].
Настройка свойств элемента управления DataSet
Для настройки свойств элемента управления DataSet выделите элемент dataSet1
мышью и воспользуйтесь окном редактирования свойств Properties, показанным на рис.
8-4.
Рис. 8-4. Свойства элемента управления DataSet

Рассмотрим наиболее важные свойства элемента управления DataSet.


Свойство DataSetName задает имя элемента управления. По умолчанию оно
указано как NewDataSet, но Вы можете изменить его. Для нашего приложения,
играющего роль заготовки простейшего органайзера, измените это имя на
OrganizerDataSet.
Свойство Locale, как мы уже говорили, задает национальную культуру, для
которой создается приложение. Выбор значения для этого свойства имеет критическое
значение для правильного выполнения таких операций с базами данных, как,
например, сортировка. К счастью, в списке значений для этого свойства присутствует
большое количество национальных культур, в том числе и российская культура (рис. 8-
5).

Рис. 8-5. Выбор значения для свойства Locale

Свойство Tables определяет набор таблиц, представленных данным элементом


управления DataSet, а свойство Relations — отношения между этими таблицами (если
они определены).
Установив значение свойства EnforceConstraints, равным False, можно отменить
ограничения, заданные для таблиц набора данных DataSet.
Создание таблиц
Теперь нам нужно добавить в набор данных DataSet две таблицы. Первая таблица
будет называться Contacts. Она предназначена для хранения имен и фамилий Ваших
знакомых и партнеров. Вторая таблица с названием Phones будет хранить номера
телефонов людей, чьи имена и фамилии хранятся в таблице Contacts.
Для создания таблиц отредактируйте свойство Tables при помощи редактора
Tables Collection Editor. На рис. 8-6 мы показали окно этого редактора после создания
таблиц Contacts и Phones.
Рис. 8-6. Редактор набора таблиц

Добавить новую таблицу можно при помощи кнопки Add, а удалить — при помощи
кнопки Remove.
Каждая таблица имеет набор свойств, которые можно редактировать в правой
части окна Tables Collection Editor. В частности, свойство Columns задает столбца
таблицы, а свойство Constarints — ограничения.
В свойстве TableName таблицы контактов Contacts Вам необходимо задать
название таблицы, как это показано на рис. 8-6. Измените также имя таблицы (в
свойстве Name), заменив его именем contactsDataTable.
Заметим, что имя и название таблиц можно оставлять такими, какими их создает
по умолчанию мастер таблиц. Однако для удобства программирования целесообразно
заменить их осмысленными строками.
Рассмотрим программный код, создаваемый дизайнером форм при добавлении
таблиц.
Идентификаторы таблиц хранятся в полях класса Form1 с именами
contactsDataTable (таблица Contacts) и phonesDataTable (таблица Phones):
private System.Data.DataTable contactsDataTable;
private System.Data.DataTable phonesDataTable;
Как объекты наши две таблицы создаются при помощи конструкторов класса
System.Data.DataTable:
this.contactsDataTable = new System.Data.DataTable();
this.phonesDataTable = new System.Data.DataTable();
Далее с помощью метода ISupportInitialize.BeginInit обеим таблицам передается
сигнал о начале инициализации:
((System.ComponentModel.ISupportInitialize)
(this.contactsDataTable)).BeginInit();

((System.ComponentModel.ISupportInitialize)
(this.phonesDataTable)).BeginInit();
Созданные таблицы добавляются к элементу управления dataSet1 при помощи
метода AddRange:
this.dataSet1.Tables.AddRange(
new System.Data.DataTable[]
{
this.contactsDataTable,
this.phonesDataTable
}
);
В качестве параметра этому методу передается ссылка на массив таблиц, поэтому
таким способом можно добавить сразу несколько таблиц.
Заметим, что этот фрагмент кода, точно также как и другие фрагменты кода,
описанные в этом разделе, создаются автоматически мастером форм, поэтому Вам не
придется писать его вручную. Однако изучение кода поможет Вам глубже разобраться
в приемах работы с элементом управления DataSet.
Создание столбцов
Для добавления столбцов в таблицу, а также для редактирования свойств столбцов
открывается редактор Columns Collection Editor. На рис. 8-7 мы показали его окно после
добавления в таблицу Contacts строк id, Имя и Фамилия.

Рис. 8-7. Столбец id таблицы Contacts

Добавление столбцов в таблицу выполняется при помощи кнопки Add, а


удаление — кнопкой Remove.
Добавив три столбца, отредактируйте их свойства в правой половине окна
редактора Columns Collection Editor.
Первый столбец с именем id предназначен для хранения уникального
идентификатора записей таблицы Contacts. Запишите это имя в свойства Caption и
ColumnName. Первое из этих свойств задает заголовок столбца, используемый при
визуальном отображении, а второй — имя столбца.
Так как столбец id хранит номера, то в свойстве DataType, определяющем тип
данных столбца, необходимо выбрать тип System.Int32. Возможные значения свойства
DataType показаны на рис. 8-8. Как видите, здесь присутствуют все типы данных,
допустимые в программах C#.
Рис. 8-8. Возможные значения свойства DataType

Чтобы столбец id мог хранить уникальные идентификаторы строк, необходимо


выполнить некоторую дополнительную настройку его свойств.
Прежде всего, установите значение свойства Unique, равное True. Это обеспечит
уникальность значений, хранящихся в данном столбце. При попытке добавить в
таблицу строку с дубликатом значения столбца id в программе возникнет исключение.
Далее, программы не должны изменять явным образом значения, хранящиеся в
столбце id. С этой целью необходимо записать в свойство ReadOnly константу True,
защитив столбец от записи.
Если записать в свойство AutoIncrement константу True, при добавлении в
таблицу новых строк текущее значение, хранящееся в столбце, будет автоматически
увеличиваться на константу, записанную в свойстве AutoIncrementStep. Начальное
значение для самой первой строки задается свойством AutoIncrementSeed.
Кроме того, мы запретили хранение в ячейках данного столбца пустых значений,
установив значение свойства AllowDBNull равным False.
И, наконец, для простоты составления программ мы заменили имя столбца
dataColumn1 именем idDataColumn.
Свойство DefaultValue позволяет автоматически задавать произвольно заданные
значения ячеек для добавляемых строк.
При помощи свойства Expression можно создавать так называемые
агрегированные столбцы (aggregate column). Ячейки таких столбцов могут содержать
значения, вычисленные на базе значений из других ячеек.
Если столбец предназначен для хранения текстовых строк, то при помощи
свойства MaxLength Вы сможете ограничить максимальную длину этих строк в
символах. При этом значение –1 используется для снятия данного ограничения.
На рис. 8-9 мы показали настройку свойств строки Имя таблицы Contacts.
Рис. 8-9. Столбец Имя таблицы Contacts

Обратите внимание, что в свойстве DataType мы указали тип данных


System.String, т.к. этот столбец хранит текстовые строки.
Чтобы не допустить создания записей с пустым именем, мы установили значение
свойства AllowDBNull равным False.
Заголовок столбца, а также имя столбца, задаваемые, соответственно, свойствами
Caption и ColumnName, мы заменили строкой Имя. Идентификатор столбца указан как
fnDataColumn.
Остальные свойства столбца Имя мы оставили без изменения. Аналогичные
настройки были выполнены и для столбца Фамилия. Идентификатор этого столбца —
lnDataColumn.
Теперь мы займемся созданием таблицы Phones, хранящей номера телефонов.
Свойства этой таблицы показаны на рис. 8-10.
Рис. 8-10. Свойства таблицы Phones

Задайте имя таблицы Phones как phonesDataTable.


Далее Вам необходимо создать в таблице Phones поля id и Телефон. Первое из
этих полей будет содержать уникальный идентификатор записей таблицы, а второй —
номера телефонов.
На рис. 8-11 мы показали, как нужно настроить свойства столбца id таблицы
Phones.

Рис. 8-11. Столбец id таблицы Phones

Здесь мы задали свойства столбца аналогично тому, как это было сделано для
столбца id таблицы Contacts, рассмотренной выше.
Что же касается столбца Телефон, то настройка его свойств аналогична настройке
свойств столбцов Имя и Фамилия таблицы Contacts (рис. 8-12).
Рис. 8-12. Строка Телефон таблицы Phones

Для хранения ссылок на столбцы наших таблиц дизайнер форм создает в классе
Form1 необходимое количество полей класса System.Data.DataColumn:
private System.Data.DataColumn idDataColumn;
private System.Data.DataColumn fnDataColumn;
private System.Data.DataColumn lnDataColumn;

private System.Data.DataColumn idPhonesDataColumn;


private System.Data.DataColumn phonesDataColumn;
Обратите внимание, что поля для хранения ссылок на столбцы таблиц создаются в
одном пространстве имен. Это означает, что для всех столбцов нужно использовать
разные идентификаторы, даже если эти столбцы называются одинаково и имеют
схожее назначение.
Например, в таблицах Contacts и Phones имеются столбцы id, предназначенные
для хранения уникальных идентификаторов строк. Для того чтобы избежать конфликта
имен, мы использовали разные идентификаторы столбцов. В первом случае столбец id
имеет идентификатор idDataColumn, а во втором — idPhonesDataColumn.
Столбцы создаются при помощи конструктора класса System.Data.DataColumn, не
имеющего параметров:
this.idDataColumn = new System.Data.DataColumn();
this.fnDataColumn = new System.Data.DataColumn();
this.lnDataColumn = new System.Data.DataColumn();

this.idPhonesDataColumn = new System.Data.DataColumn();


this.phonesDataColumn = new System.Data.DataColumn();
Рассмотрим теперь процедуру настройки свойств столбцов. Для столбцов таблицы
Contacts эта настройка выполняется следующим образом:
//
// idDataColumn
//
this.idDataColumn.AllowDBNull = false;
this.idDataColumn.AutoIncrement = true;
this.idDataColumn.Caption = "id";
this.idDataColumn.ColumnName = "id";
this.idDataColumn.DataType = typeof(int);
this.idDataColumn.ReadOnly = true;
//
// fnDataColumn
//
this.fnDataColumn.AllowDBNull = false;
this.fnDataColumn.Caption = "Имя";
this.fnDataColumn.ColumnName = "Имя";
//
// lnDataColumn
//
this.lnDataColumn.AllowDBNull = false;
this.lnDataColumn.Caption = "Фамилия";
this.lnDataColumn.ColumnName = "Фамилия";
Как видите, программа задает значения соответствующих свойств при помощи
оператора присваивания. Обратите внимание, что свойство DataType, определяющее
тип данных, задается явным образом только для столбца id, в котором хранятся
числовые значения. Для других столбцов используется тип данных по умолчанию —
текстовые строки класса string.
Столбцы таблицы Phones инициализируются аналогичным образом:
//
// idPhonesDataColumn
//
this.idPhonesDataColumn.AllowDBNull = false;
this.idPhonesDataColumn.AutoIncrement = true;
this.idPhonesDataColumn.ColumnName = "id";
this.idPhonesDataColumn.DataType = typeof(int);
this.idPhonesDataColumn.ReadOnly = true;
//
// phonesDataColumn
//
this.phonesDataColumn.AllowDBNull = false;
this.phonesDataColumn.ColumnName = "Телефон";
Созданные столбцы добавляются в соответствующие таблицы при помощи метода
AddRange:
this.contactsDataTable.Columns.AddRange(
new System.Data.DataColumn[]
{
this.idDataColumn,
this.fnDataColumn,
this.lnDataColumn
}
);

this.phonesDataTable.Columns.AddRange(
new System.Data.DataColumn[]
{
this.idPhonesDataColumn,
this.phonesDataColumn
}
);
При добавлении столбцов в таблицу ссылки на эти столбцы сохраняются в
свойстве Columns таблицы.
Так как столбцы id в обеих таблицах должны хранить уникальные значения, мы
добавили ограничение System.Data.UniqueConstraint:
this.contactsDataTable.Constraints.AddRange(
new System.Data.Constraint[]
{
new System.Data.UniqueConstraint("Constraint1",
new string[]
{
"id"
}, true)
}
);

this.phonesDataTable.Constraints.AddRange(
new System.Data.Constraint[]
{
new System.Data.UniqueConstraint("Constraint1",
new string[]
{
"id"
}, true)
}
);
Набор ограничений, налагаемых на столбцы таблицы, хранятся в свойстве
Constraints.
Первичный ключ
Как правило, в каждой таблице создается так называемый первичный ключ (primary
key), однозначно идентифицирующий строки таблицы. В наших таблицах роль
первичного ключа выполняет столбец id, предусмотренный в таблицах Contacts и
Phones.
Чтобы сделать столбец таблицы первичным ключом, необходимо настроить его
атрибуты так, как мы делали это для упомянутого выше столбца id. А именно: столбец
должен содержать только уникальные непустые значения. Кроме того, необходимо
настроить свойство таблицы PrimaryKey, указав в нем поле, играющее роль первичного
ключа.
Выбор первичного ключа при редактировании свойства PrimaryKey
осуществляется при помощи набора флажков, каждый из которых соответствует одному
столбцу таблицы (рис. 8-13).
Рис. 8-13. Первичный ключ в таблице Contacts

Чтобы сделать столбец id первичным ключом, необходимо отметить флажок id.


На рис. 8-14 мы показали выбор первичного ключа для таблицы Phones.

Рис. 8-14. Первичный ключ в таблице Phones

Заметим, что первичный ключ можно создавать из нескольких столбцов таблицы.


Для этого при редактировании свойства PrimaryKey нужно отметить сразу несколько
флажков.
Ниже мы привели фрагменты программного кода, задающего для таблиц Contacts
и Phones первичные ключи:
this.contactsDataTable.PrimaryKey =
new System.Data.DataColumn[]
{
this.idDataColumn
};

this.phonesDataTable.PrimaryKey =
new System.Data.DataColumn[]
{
this.idPhonesDataColumn
};
Как видите, при этом в свойство PrimaryKey записывается ссылка на массив
идентификаторов столбцов таблицы, составляющих первичный ключ.
Элемент управления DataGrid
При создании приложений, имеющих графический пользовательский интерфейс,
отображение содержимого баз данных часто выполняют с помощью табличных отчетов.
Хорошей новостью является то, что в наборе элементов управления системы
Microsoft .NET Framework имеется очень мощный компонент с названием DataGrid,
специально предназначенный для решения этой задачи.
Работать с элементом управления DataGrid достаточно просто. Если Вы
подготовили набор таблиц в виде объекта DataSet, то для отображения содержимого
этих таблиц достаточно подключить к этому набору объект DataGrid, выполнив
необходимую настройку свойств. При этом элемент управления DataGrid автоматически
обеспечит Ваше приложение возможностью просматривать и редактировать набор
таблиц, созданных в рамках объекта DataSet. Образно говоря, элементы управления
DataGrid и DataSet как бы созданы друг для друга.
Добавление объекта DataGrid в проект приложения
DataSetApp
Созданное в предыдущем разделе приложение DataSetApp поможет нам изучить
основные возможности элемента управления DataGrid.
Перетащите значок элемента управления DataGrid из вкладки Windows Forms
инструментальной панели Toolbox в окно формы приложения DataSetApp. Далее Вам
нужно будет настроить некоторые свойства элемента управления DataGrid, пользуясь
для этого окном Properties системы Microsoft Visual Studio .NET (рис. 8-15).

Рис. 8-15. Настройка свойств элемента управления DataGrid

Прежде всего, подключите элемент управления DataGrid к набору таблиц


dataSet1, отредактировав для этого свойство DataSource (значение свойства
DataMember пока задавать не надо).
Кроме этого, задайте значение свойства CaptionText, задающего текст заголовка
таблицы, в виде строки «Контакты». Шрифт этого заголовка можно выбрать при
помощи свойства CaptionFont.
На рис. 8-16 мы показали вид главного окна приложения DataSetApp после
выполнения всех этих операций, а также после добавления полей и кнопки, с помощью
которой можно будет добавлять новые строки в таблицу Contacts.
Рис. 8-16. Элемент управления DataGrid подключен к набору данных

Если щелкнуть мышью флажок, показанный на этом рисунке в виде квадратика со


знаком плюс, то в окне появится список таблиц, созданных в наборе dataSet1. Этот
список мы показали на рис. 8-17 в раскрытом виде.

Рис. 8-17. Раскрыт список таблиц

Как видите, элементы этого списка отображены в виде ссылок, напоминающих


ссылки, располагаемые на Web-страницах. Если щелкнуть мышью, например, ссылку
Contacts, в окне элемента управления DataGrid будет показано содержимое таблицы
Contacts.
Первоначально таблица Contacts будет отображаться пустой, поэтому Вы увидите
в ней только одну строку, отмеченную звездочкой. При помощи мыши и клавиатуры Вы
сможете добавить к таблице необходимое количество строк (рис. 8-18).

Рис. 8-18. Просмотр и редактирование содержимого таблицы Contacts

Содержимое существующих строк можно редактировать прямо в таблице, но это


при условии, что соответствующий столбец не отмечен как доступный только для
чтения.
Напомним, что за это отвечает свойство столбца с именем ReadOnly. Для столбцов
id обеих наших таблиц это свойство имеет значение true, поэтому Вы не сможете
вручную изменить содержимое ячеек данных столбцов.
Далее, при добавлении новых строк в таблицу Contacts Вы сможете наблюдать,
как изменяется значение в ячейках столбца id. В соответствии с настройками свойств
этого столбца, при добавлении новых строк значение этого столбца автоматически
увеличивается на единицу.
Ненужные строки можно удалить при помощи клавиши Delete, предварительно
выделив из левой клавишей мыши. Чтобы выделить сразу несколько строк, при
выделении держите в нажатом состоянии клавишу Shift.
Обратите внимание на кнопку со стрелкой, расположенную в правой части
заголовка окна элемента управления DataGrid. С ее помощью Вы сможете вернуться
назад к просмотру списка столбцов, показанного на рис. 8-17, а затем выбрать для
просмотра таблицу Phones (рис. 8-19).

Рис. 8-19. Просмотр и редактирование содержимого таблицы Phones

Конечно, только что рассмотренный способ отображения содержимого таблиц


пригоден далеко не для всех приложений. В частности, в приложении DataSetApp нам
нужно сделать так, чтобы элемент управления DataGrid отображал содержимое только
одной таблицы Contacts, а средства просмотра списка таблиц необходимо отключить.
К счастью, эту операцию выполнить очень легко. Достаточно задать значение для
свойства DataMember, выбрав его из списка. В нашем случае для выбора необходимо
отметить в списке строку Contacts, как это показано на рис. 8-20.
Рис. 8-20. Выбор таблицы для отображения в окне элемента управления DataGrid

Как нетрудно убедиться (рис. 8-21), после подобной настройки свойства


DataMember в окне элемента управления DataGrid будет отображаться содержимое
только одной таблицы. Кроме того, из заголовка этого окна исчезнет кнопка,
предназначенная для просмотра списка таблиц.

Рис. 8-21. Теперь отображается только таблица Contacts

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


DataGrid
Многочисленные свойства элемента управления DataGrid позволяют изменять внешний
вид его окна, приводя его в соответствие с дизайном Вашего приложения.
Рамка вокруг окна
С помощью свойства BorderStyle программист может задать внешний вид рамки вокруг
окна элемента управления DataGrid.
По умолчанию используется трехмерная рамка фиксированного размера (которой
соответствует константа Fixed3D). Ее можно заменить одномерной рамкой FixedSingle
или убрать совсем, присвоив свойству BorderStyle значение None.
Заголовок окна
Как мы уже говорили, свойство CaptionText задает текст заголовка таблицы. При
необходимости Вы можете изменить шрифт заголовка, отредактировав свойство
CaptionFont.
Форматирование содержимого окна
Установив значение свойства FlatMode, равным true, можно добиться отображения
содержимого окна DataGrid в плоском виде.
Рис. 8-22. Результат изменения значения свойства FlatMode

Свойства Font и HeaderFont позволяют, соответственно, изменять шрифт,


применяемый в окне элемента управления DataGrid, и в заголовках столбцов.
Если изменить значение свойства GridLineStyle на None, столбцы и строки таблицы
не будут отделяться друг от друга линиями.
Сортировка
По умолчанию значение свойства AllowSorting равно true, что позволяет пользователю
сортировать содержимое окна, щелкая мышью заголовки столбцов. Если по логике
работы приложения необходимо запретить такую сортировку, присвойте свойству
AllowSorting значение false.
Запрет редактирования
Установив значение свойства ReadOnly, равным true, можно запретить пользователю
редактирование содержимого окна элемента управления DataGrid. Это бывает
необходимо в тех случаях, когда по логике работы приложения это окно предназначено
только для просмотра содержимого таблиц, или когда редактирование таблиц
выполняется при помощи других элементов управления или программных средств.
Выбор цвета
На рис. 8-23 мы показали многочисленные свойства, с помощью которых можно задать
цвета различных компонентов визуального интерфейса элемента управления DataGrid.
Рис. 8-23. Свойства, определяющие цвета визуального интерфейса элемента
управления DataGrid

Выделяя по очереди эти свойства в окне Properties, Вы сможете прочитать их


детальное описание. Например, свойство GridLineColor позволяет задать цвет линий,
отделяющих строки и столбцы таблицы, а свойство ForeColor задает цвет текста.
Настраивая значения свойств, отвечающих за цветовую гамму окна элемента
управления DataGrid, можно добиться совместимости с дизайном Вашего приложения.
Набор стилей оформления TableStyles
Редактируя свойство TableStyles, можно задать стили оформления таблиц,
определенных в элементе управления DataGrid. Давайте создадим разные стили
оформления для разных столбцов таблицы Contacts.
Прежде всего, отредактируйте свойство TableStyles, добавив в него один стиль
dataGridTableStyle1 с помощью редактора стилей (рис. 8-24).

Рис. 8-24. Редактирование набора стилей оформления

Как обычно, кнопка Add позволяет добавить новый стиль, а кнопка Remove —
удалить его.
После добавления стиля необходимо настроить свойства этого стиля. Для этого
воспользуйтесь списком свойств, расположенным в правой части окна редактора
стилей, показанного на рис. 8-24.
Так как мы создавали стиль dataGridTableStyle1 для таблицы Contacts, укажите
имя этой таблицы в свойстве MappingName.
При необходимости Вы можете изменить цветовую палитру элементов
отображаемой таблицы, а также скрыть заголовки столбцов и строк. Последнее
действие выполняется путем присваивания значения false свойствам
ColumnHeadersVisible и RowHeadersVisible.
Далее необходимо создать три стиля оформления для столбцов id, Имя и Фамилия.
Это делается путем редактирования свойства GridColumnStyles. Для редактирования
вызывается специальный редактор стилей столбцов, окно которого показано на рис. 8-
25.
Рис. 8-25. Стили оформления столбца id

При помощи свойства HeaderText задайте заголовок столбца. Первый столбец


должен иметь заголовок Идентификатор, второй — Имя, и третий — Фамилия.
Выбирая по очереди созданные стили, свяжите каждый из них с соответствующим
столбцом таблицы Contacts и отредактируйте свойства стиля. Для связывания стиля со
столбцом таблицы отредактируйте свойство MappingName, как это показано на рис. 8-
26.

Рис. 8-26. Привязка стиля к столбцу id

Выполните операцию привязки для всех столбцов таблицы Contacts.


Редактируя стиль столбца, Вы можете задать выравнивание текста внутри столбца
(свойство Alignment), задать заголовок столбца HeaderText и ширину столбца Width.
По умолчанию при заполнении новой строки в ней отображается текст (null).
Замените его для второго и третьего столбца на (имя) и (фамилия), соответственно. В
результате пользователь увидит подсказку, показанную на рис. 8-27. Эта подсказка
поможет ему ввести нужные данные.

Рис. 8-27. Подсказка для ввода данных

Программный код
В предыдущих разделах мы рассказали Вам о том, как можно создавать элементы
управления DataGrid визуальными средствами проектирования, предусмотренным
специально для этой цели в системе Microsoft Visual Studio .NET. Теперь настало время
рассмотреть программный код, который создается при этом дизайнером форм.
Создание элемента управления DataGrid
Элемент управления DataGrid создается при помощи конструктора класса
System.Windows.Forms.DataGrid, как это показано ниже:
private System.Windows.Forms.DataGrid dataGrid1;
this.dataGrid1 = new System.Windows.Forms.DataGrid();
Программа сохраняет идентификатор созданного элемента управления в поле
dataGrid1.
Настройка свойств
Рассмотрим программный код, который создается для настройки свойств элемента
управления DataGrid.
Свойство DataSource задает привязку элемента управления DataGrid к набору
данных DataSet, содержащему таблицы с данными:
this.dataGrid1.DataSource = this.dataSet1;
Свойство DataMember привязывает наш элемент управления к таблице Contacts:
this.dataGrid1.DataMember = "Contacts";
Следующие строки задают шрифт для заголовка и текста:
this.dataGrid1.CaptionFont =
new System.Drawing.Font("Arial", 8.25F,
System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point,
((System.Byte)(204)));

this.dataGrid1.Font =
new System.Drawing.Font("Microsoft Sans Serif", 8.25F,
System.Drawing.FontStyle.Regular,
System.Drawing.GraphicsUnit.Point, ((System.Byte)(204)));
Шрифт указывается в виде ссылки на вновь созданный объект класса
System.Drawing.Font. В качестве первого параметра конструктору этого класса
передается название шрифта, в качестве второго — размер шрифта в единицах,
передаваемый посредством четвертого параметра. В нашем случае размер шрифта
указывается в пунктах.
Третий параметр задает стиль шрифта, а пятый — номер кодовой страницы
Unicode. Значение 204 соответствует кириллической кодовой странице.
Заголовок таблицы задается при помощи свойства CaptionText:
this.dataGrid1.CaptionText = "Контакты";
Значения других свойств определяются следующим образом:
this.dataGrid1.HeaderForeColor =
System.Drawing.SystemColors.ControlText;
this.dataGrid1.Location = new System.Drawing.Point(32, 96);
this.dataGrid1.Name = "dataGrid1";
this.dataGrid1.Size = new System.Drawing.Size(288, 208);
this.dataGrid1.TabIndex = 0;
Добавление таблицы стилей
Таблица стилей создается для нашего элемента управления DataGrid как объект класса
System.Windows.Forms.DataGridTableStyle:
this.dataGridTableStyle1 =
new System.Windows.Forms.DataGridTableStyle();
В процессе инициализации приложения эта таблица стилей подключается к
элементу управления DataGrid при помощи метода AddRange:
this.dataGrid1.TableStyles.AddRange(
new System.Windows.Forms.DataGridTableStyle[]
{
this.dataGridTableStyle1
}
);
Заметим, что к одному элементу управления DataGrid можно подключить сразу
несколько таблиц стилей, по одному на каждую таблицу, хранящуюся в DataGrid.
Для каждого столбца мы создаем свой стиль, который затем подключается к
таблице стилей dataGridTableStyle1 методом AddRange:
this.dataGridTextBoxColumn1 =
new System.Windows.Forms.DataGridTextBoxColumn();

this.dataGridTextBoxColumn2 =
new System.Windows.Forms.DataGridTextBoxColumn();

this.dataGridTextBoxColumn3 =
new System.Windows.Forms.DataGridTextBoxColumn();

this.dataGridTableStyle1.GridColumnStyles.AddRange
(
new System.Windows.Forms.DataGridColumnStyle[]
{
this.dataGridTextBoxColumn1,
this.dataGridTextBoxColumn2,
this.dataGridTextBoxColumn3
}
);
Кроме того, необходимо подключить таблицу стилей к элементу управления
DataGrid, записав соответствующую ссылку в поле DataGrid таблицы стилей:
this.dataGridTableStyle1.DataGrid = this.dataGrid1;
Стили оформления каждого столбца также задаются в процессе инициализации
приложения. Ниже мы привели соответствующий фрагмент кода:
//
// dataGridTextBoxColumn2
//
this.dataGridTextBoxColumn2.Format = "";
this.dataGridTextBoxColumn2.FormatInfo = null;
this.dataGridTextBoxColumn2.HeaderText = "Имя";
this.dataGridTextBoxColumn2.MappingName = "Имя";
this.dataGridTextBoxColumn2.NullText = "(имя)";
this.dataGridTextBoxColumn2.Width = 75;
//
// dataGridTextBoxColumn3
//
this.dataGridTextBoxColumn3.Format = "";
this.dataGridTextBoxColumn3.FormatInfo = null;
this.dataGridTextBoxColumn3.HeaderText = "Фамилия";
this.dataGridTextBoxColumn3.MappingName = "Фамилия";
this.dataGridTextBoxColumn3.NullText = "(фамилия)";
this.dataGridTextBoxColumn3.Width = 75;
//
// dataGridTextBoxColumn1
//
this.dataGridTextBoxColumn1.Format = "";
this.dataGridTextBoxColumn1.FormatInfo = null;
this.dataGridTextBoxColumn1.HeaderText = "Идентификатор";
this.dataGridTextBoxColumn1.MappingName = "id";
this.dataGridTextBoxColumn1.ReadOnly = true;
this.dataGridTextBoxColumn1.Width = 75;
Как видите, программная реализация нашего несложного приложения,
позволяющего отображать и редактировать одну реляционную таблицу, получается
довольно громоздкой.
К счастью, дизайнер форм выполняет за нас всю черновую работу по созданию и
сопровождению программного кода, отвечающего за инициализацию и настройку
свойств элементов управления DataGrid и DataSet. В этом и заключается одно из
преимуществ использования такого инструментального средства, как Microsoft Visual
Studio .NET.
Работа с DataSet при помощи методов и свойств
Как мы уже говорили, элемент управления DataGrid позволяет вводить новые данные в
таблицы набора DataSet. Однако операции добавления и извлечения данных из набора
DataSet можно выполнять и с помощью специально предназначенных для этого методов
и свойств.
Добавление новой строки
Для того чтобы добавить новую строку в таблицу набора данных DataSet, Вы вначале
должны создать эту строку как объект класса DataRow, а затем добавить ее в набор
строк таблицы при помощи метода DataTable.Rows.Add.
Ниже мы показали фрагмент кода, добавленного в конструктор класса Form1, и
предназначенного для вставки в таблицу Contacts двух новых строк:
public Form1()
{
//
// Required for Windows Form Designer support
//
InitializeComponent();

//
// TODO: Add any constructor code after InitializeComponent call
//

DataRow row = contactsDataTable.NewRow();


row["Имя"] = "Иван";
row["Фамилия"] = "Петров";
contactsDataTable.Rows.Add(row);

DataRow row1 = contactsDataTable.NewRow();


row1["Имя"] = "Петр";
row1["Фамилия"] = "Сидоров";
contactsDataTable.Rows.Add(row1);
}
Здесь мы вначале создаем новую строку в таблице, пользуясь для этого методом
NewRow.
Далее мы записываем значения в ячейки строки, пользуясь для этого названиями
столбцов. После этого готовая строка добавляется в таблицу.
В результате после инициализации приложения в таблице Contacts появятся две
строки (рис. 8-28).

Рис. 8-28. Две строки были добавлены в момент инициализации приложения

Теперь давайте задействуем поля ввода Имя и Фамилия, а также кнопку Добавить,
расположенные в верхней части окна нашего приложения (рис. 8-28).
Создайте следующий обработчик события Click для кнопки Добавить:
private void button1_Click(object sender, System.EventArgs e)
{
DataRow row = contactsDataTable.NewRow();
row["Имя"] = textBoxFName.Text;
row["Фамилия"] = textBoxLName.Text;
contactsDataTable.Rows.Add(row);
}
Здесь мы предполагаем, что идентификатор поля Имя хранится в textBoxFName, а
поля Фамилия — в textBoxLName.
Удаление строки
Вы можете удалять выделенные строки таблицы при помощи клавиши Delete. Однако
есть и другая возможность — использование метода DataTable.Rows.Delete.
Добавьте в главное окно нашего приложения кнопку Удалить (рис. 8-29). Эта
кнопка будет удалять запись, выделенную в окне элемента управления DataGrid.
Рис. 8-29. Добавлена кнопка для удаления выделенной строки

Добавьте для этой кнопки следующий обработчик событий:


private void button2_Click(object sender, System.EventArgs e)
{
try
{
contactsDataTable.Rows[dataGrid1.CurrentCell.RowNumber].Delete();
contactsDataTable.AcceptChanges();
}
catch(Exception ex)
{
}
}
Здесь мы используем свойство DataGrid.CurrentCell.RowNumber для получения
номера строки, выделенной пользователем в таблице. Кстати, если пользователь
выделил столбец, то его номер можно получить при помощи свойства
DataGrid.CurrentCell.ColumnNumber.
Что же касается метода AcceptChanges, то его применение требует некоторых
пояснений.
Дело в том, что при удалении строки методом DataTable.Rows.Delete строка
фактически не удаляется, а только отмечается для удаления. Ее еще можно
восстановить методом DataTable.Rows.RejectChanges. И только после вызова метода
AcceptChanges удаление становится окончательным.
Во время выполнения всех этих операций возможно возникновение исключения
DeletedRowInaccessibleException. Это исключение возникает в том случае, если
предпринимается попытка удаления строки, которая уже была удалена ранее. Поэтому
в нашей программе мы предусмотрели обработчик исключения, игнорирующий попытки
повторного удаления строк.
Приложение PhoneBookApp
До сих пор мы работали с набором данных DataSet при помощи элемента управления
DataGrid. Однако есть ряд приложений, в которых использование элемента DataGrid
для представления данных невозможно или нецелесообразно. Большинство
приложений, например, вводят информацию в базу данных при помощи форм с полями
различного типа, а не с помощью таблиц.
Приложение PhoneBookApp, о котором мы будем рассказывать в этой главе,
представляет собой простейший телефонный справочник, хранящий имена и фамилии
людей, а также номера их телефонов. При этом считается, что каждый человек может
иметь несколько номеров телефонов.
Окно приложения PhoneBookApp показано на рис. 8-30.

Рис. 8-30. Окно приложения PhoneBookApp

В верхней части этого окна расположены элементы управления, предназначенные


для просмотра и редактирования списка имен и фамилий, а в нижней — для просмотра
и редактирования номеров телефонов.
Создание проекта приложения
Создайте новое приложение с именем PhoneBookApp, пользуясь мастером проектов.
Затем добавьте в его окно необходимые элементы управления.
Элементы управления для работы со списком имен и фамилий
В верхнюю часть окна нужно поместить два поля ввода текстовой информации с
метками Имя и Фамилия. Первое из них должно иметь идентификатор fnTextBox, а
второе — lnTextBox.
Для просмотра списка имен и фамилий, хранящихся в нашей базе данных,
добавьте в окно приложения элемент управления ListView с идентификатором listView1.
Отредактировав свойство Columns, добавьте в него три столбца, как это показано на
рис. 8-31.
Рис. 8-31. Столбцы элемента управления listView1

Для первого столбца задайте в свойстве Text заголовок Имя, для второго —
Фамилия, и, наконец, для третьего — id.
Назначение столбцов Имя и Фамилия очевидно — в них будут отображаться имена
и фамилии людей, чьи телефонные номера хранятся в базе данных. Что же касается
столбца id, то он предназначен для отображения уникальных идентификаторов строк в
таблице Contacts, хранящей имена и фамилии.
Задайте идентификаторы столбцов Имя и Фамилия равными, соответственно,
fnColumnHeader и lnColumnHeader.
Кроме этого, переключите элемент управления ListView в режим отображения
детализированной таблицы. Для этого задайте свойству View значение Details.
Операции добавления, изменения и редактирования имен и фамилий мы будем
выполнять при помощи кнопок Добавить, Изменить и Удалить. Перетащите значки этих
кнопок из инструментальной панели Microsoft Visual Studio .NET и расположите их
справа от списка просмотра имен и фамилий.
Элементы управления для работы со списком телефонов
В нижней части окна приложения PhoneBookApp необходимо расположить элементы
управления, предназначенные для просмотра и редактирования списка телефонов
человека, имя которого выделено в верхнем списке.
Это поле редактирования текстовой информации с меткой Телефон, поле класса
Label для просмотра имени и фамилии человека, телефоны которого в настоящий
момент просматриваются или редактируются, список для просмотра телефонов, а также
кнопки Добавить, Изменить и Удалить. С помощью кнопок пользователь сможет
отредактировать список телефонов.
Поле редактирования номера телефона Телефон должно иметь идентификатор
phoneTextBox. Задайте этот идентификатор, отредактировав соответствующим образом
свойство Name.
Что касается поля Label для просмотра имени и фамилии человека, список
телефонов которого просматривается или редактируется, то оно должно иметь
идентификатор currentContactLabel. Настройте для него свойство BorderStyle, выбрав
значение Fixed3D, а также свойство TextAlign, задав выравнивание MiddleLeft. Вы
можете также изменить шрифт, которым будет отображаться имя и фамилия, при
помощи свойства Font.
Далее Вам необходимо добавить в окно приложения еще один элемент
управления ListView, предназначенный для отображения списка телефонов. Этот
элемент управления должен иметь идентификатор listView2.
Переключите список listView2 в режим отображения детализированной таблицы,
назначив свойству View значение Details, а также добавьте три столбца с названиями
Телефон, id и idContacts (рис. 8-32).

Рис. 8-32. Столбцы элемента управления listView2

Столбец Телефон предназначен для отображения номеров телефона человека,


имя которого выделено в верхнем списке. Что же касается столбцов id и idContacts, то
первый из них предназначен для отображения уникальных идентификаторов записей в
таблице Phones, хранящей номера телефонов, а второй — для хранения внешних
ключей таблицы Contacts. Обе эти таблицы мы рассмотрим ниже в этой главе.
Создание таблиц базы данных
База данных нашего телефонного справочника будет напоминать базу данных, которую
мы рассматривали ранее при описании приложения DataSetApp. Она тоже будет
состоять из двух таблиц Contacts и Phones, однако теперь эти таблицы будут связаны
между собой.
Для того чтобы создать базу данных, перетащите из инструментальной панели
Toolbox системы Microsoft Visual Studio .NET в окно дизайнера форм значок элемента
управления DataSet. Соответствующий компонент будет иметь идентификатор dataSet1.
Далее Вам нужно будет добавить в базу данных две таблицы, отредактировав для
этого свойство Tables.
Таблица Contacts
Таблица Contacts, как мы уже говорили, предназначена для хранения имен и фамилий
людей, чьи телефоны мы собираемся записывать в нашу базу данных. Эта таблица
должна иметь идентификатор contactsDataTable (рис. 8-33).
Рис. 8-33. Свойства таблицы Contacts

Создайте в таблице Contacts три столбца id, Имя и Фамилия, как это показано на
рис. 8-34.

Рис. 8-34. Столбцы таблицы Contacts

Столбец id предназначен для хранения уникальных числовых идентификаторов


строк таблицы. О настройке его свойств мы рассказывали при изучении приложения
DataSetApp. Столбцы Имя и Фамилия предназначены для хранения текстовых строк и
не имеют никаких особенностей.
Создав таблицу Contacts, отметьте столбец id как ключевой столбец,
отредактировав соответствующим образом свойство таблицы PrimaryKey.
Таблица Phones
В таблице Phones мы будем хранить номера телефонов. Эта таблица должна иметь
идентификатор phonesDataTable.
Создав таблицу, добавьте к ней три столбца id, idContacts и Телефон (рис. 8-35).

Рис. 8-35. Столбцы таблицы Phones

Столбец id должен играть роль первичного ключа (такого же, как и в таблице
Contacts). Настройка свойств этого столбца показана на рис. 8-35.
Столбец Телефон хранит телефонные номера в виде обычных текстовых строк и
не имеет никаких особенностей.
Что же касается столбца idContacts, то он хранит так называемые внешние ключи
(foreign keys) таблицы Contacts. Таким образом, каждая строка таблицы Phones с
номером телефона содержит идентификатор строки таблицы Contacts с именем и
фамилией владельца этого телефона.
Очевидно, что если у человека имеется несколько телефонных номеров, то в
таблице Phones для него будет создано несколько строк с одинаковыми значениями в
ячейках столбца idContacts.
На рис. 8-36 мы показали настройку свойств для столбца idContacts.
Рис. 8-36. Свойства столбца idContacts таблицы Phones

Как видно из этого рисунка, столбец должен хранить целые числа типа
System.Int32. За это отвечает свойство DataType.
Мы не разрешаем хранение в столбце idContacts пустых значений, т.к. если в базе
данных есть телефон, то он обязательно должен кому-нибудь принадлежать. Поэтому
мы приравниваем значение свойства AllowDBNull константе false.
Связывание таблиц Contacts и Phones
Элемент управления DataSet имеет свойство Relations, специально предназначенное
для создания отношений между таблицами, входящими в набор данных.
Отредактируйте свойство Relations. Когда редактор отношений запускается в
первый раз для набора данных, в котором еще нет отношений, на экране появляется
диалоговое окно Relation, показанное на рис. 8-37.
Рис. 8-37. Создание нового отношения

В поле Name Вы можете задать имя для создаваемого отношения. В приложении


PhoneBookApp мы оставили имя отношения, оставленное по умолчанию.
При создании нового отношения Вам нужно выбрать родительскую таблицу в
списке Parent table и дочернюю таблицу в списке Child table. В нашем случае роль
родительской таблицы будет играть таблица Contacts, а в роли дочерней — таблица
Phones.
В списке Columns нужно указать связываемые поля. Мы привязываем столбец id
родительской таблицы Contacts (выбирая его в списке Key Columns) к полю idContacts
дочерней таблицы Phones (выбирая его в списке Foreign Key Columns).
Список Update rule задает режим автоматического внесения изменений в
дочернюю таблицу при изменении записей родительской таблицы.
Если в данном списке выбрано значение Cascade, то при удалении или
обновлении строк родительской таблицы происходит обновление соответствующих
строк дочерней таблицы. Мы выбрали именно этот режим для приложения
PhoneBookApp.
Значения SetDefault и SetNull используются в том случае, когда при изменении
строк родительской таблицы необходимо записать в соответствующие строки дочерней
таблицы значения по умолчанию или пустые значения, соответственно.
Если указать значение None, то изменения в родительской таблице не будут
отражаться на дочерней таблице.
Список Delete rule задает режим удаления записей дочерней таблицы при
удалении записи родительской таблицы.
И, наконец, список Accept/Reject rule определяет, как нужно изменять записи
дочерней таблицы, когда новые записи родительской таблицы добавляются или
отвергаются.
Когда хотя бы одно отношение создано, то попытка редактирования свойства
Relations приводит к появлению на экране редактора набора отношений Relations
Collection Editor, показанного на рис. 8-38.
Рис. 8-38. Просмотр и редактирование списка отношений

Кнопки Add и Remove позволяют, соответственно, добавить новые отношения или


удалить существующие. Для того чтобы изменить параметры отношения, его нужно
выделить в списке Members, а затем щелкнуть кнопку Edit.
Проектирование кода для управления базой данных
В этом разделе нашей книги мы добавим в наше приложение PhoneBookApp
программный код, обеспечивающий всю необходимую функциональность.
Добавление нового контакта
Добавление нового контакта происходит тогда, когда пользователь щелкает кнопку
Добавить, расположенную в верхней части главного окна приложения (рис. 8-30).
Предварительно пользователь должен ввести имя и фамилию человека в полях Имя и
Фамилия, соответственно.
Вам необходимо подготовить для только что упомянутой кнопки Добавить
следующий программный код:
private void button1_Click(object sender, System.EventArgs e)
{
if(fnTextBox.Text != "" && lnTextBox.Text != "")
{
DataRow row = contactsDataTable.NewRow();
row["Имя"] = fnTextBox.Text;
row["Фамилия"] = lnTextBox.Text;
contactsDataTable.Rows.Add(row);

listUpdate();
phonesUpdate();
}
}
Здесь мы вначале убеждаемся в том, что пользователь ввел как имя, так и
фамилию. Для этого мы сравниваем содержимое свойства Text полей редактирования
fnTextBox и lnTextBox с пустой строкой. Если какое-либо поле пустое, обработчик
события завершает свою работу без выполнения других дополнительных действий.
В том случае, когда пользователь ввел все необходимые данные, обработчик
события создает новую строку в таблице Contacts:
DataRow row = contactsDataTable.NewRow();
Для этого используется метод DataTable.NewRow.
На следующем этапе введенные строки имени и фамилии записываются в
соответствующие столбцы строки:
row["Имя"] = fnTextBox.Text;
row["Фамилия"] = lnTextBox.Text;
Подготовленная таким способом строка добавляется в таблицу:
contactsDataTable.Rows.Add(row);
Далее наш обработчик события последовательно вызывает два метода с именами
listUpdate и phonesUpdate. Первый из них отображает текущее содержимое таблицы
Contacts в окне верхнего списка, а второй — список телефонов человека, имя которого
было выбрано в верхнем списке. Мы вернемся к детальному описанию этих функций
немного позже.
Выбор записи в списке имен и фамилий
Для отслеживания контактов, выбираемых пользователем в верхнем списке, мы
предусмотрели обработчик события listView1_SelectedIndexChanged:
private void listView1_SelectedIndexChanged(object sender,
System.EventArgs e)
{
try
{
int currentRowIndex = listView1.SelectedItems[0].Index;

DataRow row = contactsDataTable.Rows[currentRowIndex];

fnTextBox.Text = row["Имя"].ToString();
lnTextBox.Text = row["Фамилия"].ToString();
currentContactId = (int)row["id"];

currentContactLabel.Text = fnTextBox.Text + " " +


lnTextBox.Text;
}
catch(Exception ex)
{
}

phonesUpdate();
}
Когда пользователь выделяет новую строку в списке, этот обработчик вначале
определяет индекс строки, которая стала выделенной в результате выполнения этой
операции:
int currentRowIndex = listView1.SelectedItems[0].Index;
Далее он извлекает соответствующую строку из таблицы Contacts, а также
получает значения столбцов извлеченной строки:
fnTextBox.Text = row["Имя"].ToString();
lnTextBox.Text = row["Фамилия"].ToString();
currentContactId = (int)row["id"];
Имя и фамилия при этом записывается в поля редактирования fnTextBox и
lnTextBox для того чтобы пользователь смог просмотреть их и при необходимости
отредактировать.
Что же касается идентификатора строки id, то он сохраняется в поле
currentContactId класса Form1. Создайте это поле следующим образом:
private int currentContactId;
Дополнительно обработчик события listView1_SelectedIndexChanged записывает
имя и фамилию текущего контакта в поле, расположенное непосредственно над
списком телефонов (чтобы пользователю было ясно, чьи телефоны отображаются в
этом списке):
currentContactLabel.Text = fnTextBox.Text + " "
+ lnTextBox.Text;
Перед тем как завершить свою работу, обработчик события вызывает метод
phonesUpdate для обновления списков телефонов выделенного контакта.
Редактирование записей таблицы Contacts
Для того чтобы отредактировать имя или фамилию человека, нужно выделить его имя в
верхнем списке, содержащем записи таблицы Contacts. После этого только что
рассмотренный обработчик события listView1_SelectedIndexChanged запишет имя и
фамилию в текстовые поля редактирования Имя и Фамилия, соответственно.
Отредактировав имя или фамилию, пользователь должен щелкнуть кнопку
Изменить. Ниже мы привели исходный текст обработчика событий для этой кнопки,
изменяющего содержимое ячеек соответствующей строки в таблице Contacts:
private void button2_Click(object sender, System.EventArgs e)
{
if(fnTextBox.Text != "" && lnTextBox.Text != "")
{
string filter = "id='" + currentContactId.ToString() + "'";
DataRow[] contactsRows = contactsDataTable.Select(filter);

contactsRows[0].BeginEdit();
contactsRows[0]["Имя"] = fnTextBox.Text;
contactsRows[0]["Фамилия"] = lnTextBox.Text;
contactsRows[0].EndEdit();

contactsDataTable.AcceptChanges();

listUpdate();
}
}
Получив управление, наш обработчик убеждается в том, что пользователь задал
не непустую строку имени или фамилии. Если это так, обработчик начинает процедуру
обновления таблицы Contacts.
Для этого он вначале делает выборку из таблицы Contacts с помощью
специального фильтра и метода DataTable.Select. Текстовая строка фильтра, задающая
условие выборки, передается методу DataTable.Select в качестве параметра. Она
определяет условие отбора строк из таблицы. Таким образом, метод DataTable.Select
напоминает по своему назначению оператор SELECT языка запросов SQL, хотя,
конечно, он не обладает всей мощностью этого оператора.
Метод Select возвращает записи в виде массива строк класса DataRow, которые
можно обрабатывать в цикле или индивидуально с указанием индекса.
В нашем случае строка фильтра имеет следующий вид:
id=’<содержимое поля currentContactId>’
Напомним, что поле currentContactId класса Form1 содержит текущий
идентификатор строки таблицы Contacts, выделенной пользователем в списке имен и
фамилий. Это поле обновляется обработчиком событий listView1_SelectedIndexChanged,
когда пользователь выделяет новую строку в упомянутом списке.
Так как нам нужно обновить имя и фамилию человека, чье имя выделено в
списке, то из таблицы Contacts требуется выбрать такие строки, содержимое столбца id
которых равно текущему значению поля currentContactId. В общем случае таких строк
может быть несколько. Мы, однако, будем изменять только первую строку.
Вот как формируется и применяется строка фильтра в нашем приложении:
string filter = "id='" + currentContactId.ToString() + "'";
DataRow[] contactsRows = contactsDataTable.Select(filter);
Обратите внимание, что значение, которому должно быть равно содержимое
столбца id, заключено в одинарные кавычки.
После выполнения метода Select мы получим набор ссылок на строки
contactsRows, удовлетворяющих нашему условию. Чтобы обновить содержимое одной
выбранной строки, записав в нее новое имя и фамилию, мы используем следующий
фрагмент кода:
contactsRows[0].BeginEdit();
contactsRows[0]["Имя"] = fnTextBox.Text;
contactsRows[0]["Фамилия"] = lnTextBox.Text;
contactsRows[0].EndEdit();
Перед началом внесения изменений в ячейки строки мы вызываем метод
BeginEdit, который отключает механизм проверки целостности для связанных таблиц.
Когда все изменения внесены, этот механизм вновь включается при помощи метода
EndEdit.
Далее мы вызываем метод AcceptChanges, который вносит сделанные изменения в
таблицу Contacts, и обновляем содержимое списка контактов при помощи метода
listUpdate:
contactsDataTable.AcceptChanges();
listUpdate();
Удаление записей таблицы Contacts
Для того чтобы удалить строку, выделенную пользователем в списке контактов, нужно
воспользоваться кнопкой Удалить. Ниже мы привели исходный текст метода
button3_Click, выполняющего обработку событий от этой кнопки:
private void button3_Click(object sender, System.EventArgs e)
{
try
{
string filter = "idContacts='" +
listView1.SelectedItems[0].Tag.ToString() + "'";

DataRow[] phones = phonesDataTable.Select(filter);

for(int i = 0; i < phones.Length; i++)


{
if(phones.Length != 0)
{
phones[i].Delete();
}
}
phonesDataTable.AcceptChanges();

contactsDataTable.Rows[
listView1.SelectedItems[0].Index].Delete();

contactsDataTable.AcceptChanges();
}
catch(Exception ex)
{
}

listUpdate();
phonesUpdate();
}
Собственно удаление записи, соответствующей выделенной строке, выполняет
следующий фрагмент кода:
contactsDataTable.Rows[listView1.SelectedItems[0].Index].Delete();
contactsDataTable.AcceptChanges();
Так как таблицы Contacts и Phones связаны между собой, то перед удалением
записи из таблицы Contacts нужно удалить все строки таблицы Phones, связанные с
ней. Т.е. если мы удаляем запись об имени и фамилии человека, то перед этим нужно
удалить все телефоны этого человека.
При связывании таблиц Contacts и Phones мы выбрали в списке Update rule
значение Cascade, задающее режим автоматического внесения изменений в дочернюю
таблицу при изменении записей родительской таблицы(рис. 8-37). Поэтому при
удалении строк родительской таблицы происходит обновление соответствующих строк
дочерней таблицы.
Тем не менее, для примера мы приводим фрагмент кода, который удаляет все
строки таблицы Phones, связанные с удаляемой строкой таблицы Contacts:
string filter = "idContacts='" +
listView1.SelectedItems[0].Tag.ToString() + "'";

DataRow[] phones = phonesDataTable.Select(filter);

for(int i = 0; i < phones.Length; i++)


{
if(phones.Length != 0)
{
phones[i].Delete();
}
}

phonesDataTable.AcceptChanges();
Как видите, мы здесь вначале с помощью фильтра отбираем нужные записи из
таблицы Phones, а потом удаляем их при помощи известного Вам метода Delete. Метод
AcceptChanges завершает процедуру удаления.
Добавление номера телефона
Выделив в верхнем списке имя человека, пользователь может добавить для него один
или несколько телефонов. Эта операция выполняется при помощи кнопки Добавить,
расположенной возле списка телефонов.
Вот обработчик событий для этой кнопки:
private void button4_Click(object sender, System.EventArgs e)
{
if(phoneTextBox.Text != "")
{
try
{
DataRow rowPhone = phonesDataTable.NewRow();
rowPhone["idContacts"] = currentContactId;
rowPhone["Телефон"] = phoneTextBox.Text;
phonesDataTable.Rows.Add(rowPhone);
}
catch (Exception ex)
{
}

phonesUpdate();
}
}
Здесь мы вначале проверяем, что поле нового телефона phoneTextBox не пустое.
Если это так, то мы создаем в таблице Phones новую строку как объект класса DataRow,
вызывая для этого метод phonesDataTable.NewRow.
Номер добавляемого телефона мы сохраняем в столбце Телефон. Что же касается
столбца idContacts, то мы записываем в него идентификатор строки таблицы Contact.
Это та самая строка, которая содержит имя, выбранное пользователем в списке
контактов.
Заполненная строка добавляется в таблицу Phones методом
phonesDataTable.Rows.Add.
И, наконец, после добавления строки наш обработчик событий обновляет
содержимое списка телефонов в окне программы методом phonesUpdate.
Удаление номера телефона
Для удаления номера телефона предназначена кнопка Удалить, расположенная возле
списка телефонов.
Вот обработчик событий для этой кнопки:
private void button6_Click(object sender, System.EventArgs e)
{
try
{
string filter =
"id='" + listView2.SelectedItems[0].Tag.ToString() + "'";

DataRow[] phones = phonesDataTable.Select(filter);

for(int i = 0; i < phones.Length; i++)


{
if(phones.Length != 0)
{
phones[i].Delete();
}
}
}
catch (Exception ex)
{
}

phonesDataTable.AcceptChanges();
phonesUpdate();
}
Прежде всего, наш обработчик определяет идентификатор строки таблицы
Phones, подлежащей удалению. Напомним, что этот идентификатор хранится в столбце
id упомянутой таблицы. При формировании списка listView2 идентификаторы
соответствующих строк сохраняются в свойствах Tag элементов этого списка.
Чтобы стереть строку, мы создаем фильтр filter, при помощи которого мы сможет
получить первую строку из списка выделенных строк. Далее при помощи метода
phonesDataTable.Select мы выделяем нужную строку и затем удаляем ее методом Delete.
Окончательные изменения в таблицу Phones вносятся методом
phonesDataTable.AcceptChanges, после чего методом phonesUpdate выполняется
обновление списка телефонов.
Изменение номера телефона
Для изменения номера телефона используется кнопка Изменить, расположенная справа
от списка телефонов. исходный текст обработчика событий для этой кнопки
представлен ниже:
private void button5_Click(object sender, System.EventArgs e)
{
if(phoneTextBox.Text != "")
{
try
{
string filter = "id='" +
listView2.SelectedItems[0].Tag.ToString() + "'";
DataRow[] phones = phonesDataTable.Select(filter);

phones[0].BeginEdit();
phones[0]["Телефон"] = phoneTextBox.Text;
phones[0].EndEdit();
}
catch (Exception ex)
{
}

phonesDataTable.AcceptChanges();
phonesUpdate();
}
}
Если строка нового номера телефона phoneTextBox не пуста, то обработчик
события button5_Click выделяет с помощью фильтра нужную строку в таблице Phones и
затем изменяет содержимое ячейки в столбце Телефон.
Данная операция выполняется аналогично операции изменения содержимого
строк таблицы Contacts, описанной ранее в разделе «Редактирование записей таблицы
Contacts».
Обновление списка контактов
При описании исходных текстов приложения PhoneBookApp мы неоднократно
упоминали метод listUpdate, обновляющий содержимое списка listView1 из таблицы
Contacts. Вот его исходный текст:
public void listUpdate()
{
listView1.Items.Clear();

foreach(DataRow row in contactsDataTable.Rows)


{
ListViewItem lvi = new ListViewItem(row["Имя"].ToString());
lvi.SubItems.Add(row["Фамилия"].ToString());
lvi.SubItems.Add(row["id"].ToString());
lvi.Tag = row["id"].ToString();
listView1.Items.Add(lvi);
}
}
Перед обновлением метод listUpdate очищает список, вызывая для этого метод
listView1.Items.Clear.
Далее в цикле извлекаются все строки из таблицы Contacts. Внутри тела цикла
каждая строка таблицы сохраняется в переменной row класса DataRow.
После этого мы создаем новый элемент списка lvi класса ListViewItem, записывая
в него имя человека. Фамилия, а также идентификатор записи добавляются в элемент
списка при помощи метода lvi.SubItems.Add.
Кроме этого, для каждого элемента списка мы сохраняем в свойстве Tag
идентификатор строки таблицы, взятый из столбца id. Этот столбец является
первичным ключом таблицы Contacts. Таким образом, каждый элемент списка listView1
хранит всю информацию соответствующей ему строки таблицы Contacts.
Для добавления строки в список мы вызываем метод listView1.Items.Add.
Обновление списка телефонов
Список телефонов listView2 отображает номера только одного человека, имя которого
было выделено пользователем в списке контактов listView1. Для решения этой задачи
мы создали в классе Form1 метод phonesUpdate:
public void phonesUpdate()
{
listView2.Items.Clear();
string filter =
"idContacts='" + currentContactId.ToString() + "'";

DataRow[] phones = phonesDataTable.Select(filter);

if(phones.Length != 0)
{
for(int i = 0; i < phones.Length; i++)
{
ListViewItem lvi =
new ListViewItem(phones[i]["Телефон"].ToString());

lvi.SubItems.Add(phones[i]["id"].ToString());
lvi.SubItems.Add(phones[i]["idContacts"].ToString());
lvi.Tag = phones[i]["id"];
listView2.Items.Add(lvi);
}
}
}
Прежде всего, список телефонов очищается методом listView2.Items.Clear.
Далее наш обработчик должен отобрать в таблице Phones строки,
соответствующие телефонам выделенного контакта. Идентификатор этого контакта в
таблице Contacts хранится в поле currentContactId.
Отбор строк выполняется с помощью фильтра и метода Select, как это показано
ниже:
string filter = "idContacts='" + currentContactId.ToString() + "'";
DataRow[] phones = phonesDataTable.Select(filter);
Если была найдена хотя бы одна строка, запускается цикл добавления записей в
список listView2. Соответствующая процедура добавления записей в список аналогична
процедуре, использованной в только что описанном методе listUpdate.
Обратите внимание — мы сохраняем в свойстве Tag элементов списка
идентификаторы строк таблицы Phones (т.е. содержимое первичного ключа из столбца
id).
Фильтр для метода Select
В классе DataTable имеется несколько перегруженных вариантов метода Select, с
помощью которых можно отбирать строки из таблицы в соответствии с различными
критериями.
Перегруженные методы Select
Простейший из них не имеет параметров и возвращает все записи таблицы в порядке,
определяемым первичным ключом. Если в таблице нет первичного ключа, то записи
будут извлечены в порядке их добавления.
Если методу Select передается один параметр, то он задает текстовую строку
фильтра, содержащую условие отбора записей. Подробнее правила составления такого
фильтра мы рассмотрим ниже в этом разделе.
Вариант метода Select с двумя параметрами позволяет дополнительно задать
столбец, по которому нужно выполнять сортировку, а также порядок сортировки
(прямой или обратный).
Пусть, например, для Интернет-магазина, торгующего книгами, мы создали
таблицу покупок Orders. В этой таблице есть столбец seriesID, хранящий
идентификатор серии книг (таких как «Шаг за шагом», «Самоучитель» и т.д.), а также
столбец Price, в котором хранится стоимость книг.
Следующий фрагмент кода отберет книги, у которых идентификатор серии равен
567, отсортировав его в обратном порядке по стоимости:
DataTable booksTable = DataSet1.Tables["Orders"];

string strExpr = "seriesID='567'";


string strSort = "Price DESC";
DataRow[] foundRows = booksTable.Select(strExpr, strSort);
Если не указывать ключевое слово DESC, будет выполнена сортировка в прямом
порядке.
Существует и еще один перегруженный вариант метода Select с тремя
параметрами. Первый из этих параметров задает фильтр, второй — столбец и порядок
сортировки, а третий — состояние отбираемых строк таблицы.
Строки таблицы могут находиться в одном из нескольких состояний, описанных в
перечислении DataViewRowState (табл. 8-1).
Таблица 8-1. Перечисление DataViewRowState
Константа Состояние строки таблицы
Added Новая строка
CurrentRows Текущая неизмененная, новая или измененная строка
Deleted Удаленная строка
ModifiedCurrent Текущая версия модифицированной строки
ModifiedOriginal Исходная версия модифицированной строки
None Нет состояния
OriginalRows Исходная или удаленная строка
Unchanged Неизмененная строка

Краткое описание синтаксиса строки фильтра


В этом разделе мы приведем краткое описание синтаксиса строки фильтра. Более
подробную информацию Вы найдете в справочной документации.
Выражение для строки фильтра включает в себя имя столбца, оператор
отношения и значение. Как мы уже говорили, строка значений должна быть заключена
в одинарные кавычки.
Если в имени столбца используются специальные символы, их нужно снабжать
префиксом в виде обратного слеша, например: \t (символ табуляции), \n (новая
строка), \r (перевод каретки).
Вот еще список специальных символов:
~()#\/=><+-*%&|^'"[]
Когда такие символы используются в именах столбцов, то имя столбца необходимо
заключать в квадратные скобки.
Значения
Текстовые значения в операторах отношения необходимо заключать в одинарные
кавычки.
Числовые значения указываются без кавычек. Что же касается дат, то их нужно
заключать в пару символов #, например:
"ChangeDate > #3/30/62#"
При использовании такого условия будут отобраны строки, содержащие в столбце
ChangeDate даты после 30 марта 1962 года.
Операторы
При составлении условия допускается использовать логические операторы AND, OR и
NOT. Например:
(AuthorName = 'Иванов' OR BookName = 'Кобол') AND BookPrice >= 20
Для сравнения числовых величин можно использовать следующие операторы:
 <
 >
 <=
 >=
 <>
 =
Последние два оператора из этого списка подходят для работы с текстовыми
строками.
Оператор IN позволяет проверить содержимое столбца на принадлежность к
заданному списку значений.
С помощью оператора LIKE и символов шаблона * и % можно отбирать строки
аналогично тому, как это делается в языке Transact-SQL (имеется в виду параметр LIKE
оператора SELECT).
Числовые величины можно складывать, вычитать, умножать и делить, а также
находить остаток от деления (выполнять деление по модулю). Для этого используется
тот же набор операторов, что и в языке С#:
 + (сложение)
 - (вычитание)
 * (умножение)
 / (деление)
 % (деление по модулю)
Когда оператор + используется с текстовыми строками, он выполняет их слияние
(конкатенацию).
Функции
Дополнительно в фильтрах можно использовать функции, имена которых перечислены
в табл. 8-2.
Таблица 8-2. Функции для фильтра
Имя Описание
CONVERT Преобразование типов данных в один из типов,
предусмотренных на платформе Microsoft .NET Framework
LEN Определение длины текстовой строки
ISNULL Проверка значения на равенство пустому значению
IIF Возврат одного из значений в зависимости от результата
вычисления заданного логического выражения
SUBSTRING Извлечение подстроки из текстовой строки
Таким образом, с помощью метода Select можно выполнять довольно сложные
запросы к таблицам набора данных DataSet.
Конечно, возможности метода Select ограничены по сравнению с языком SQL.
Однако не следует забывать, что набор данных DataSet дается Вам бесплатно вместе со
средой исполнения Microsoft .NET Framework. Создавая объекты DataSet в своих
приложениях, Вы можете интегрировать их с базами данных без необходимости
установки на компьютере сервера SQL или таких систем SQL, как Microsoft Data
Engine (MSDE).
Тем не менее, многие приложения должны быть интегрированы с «настоящими»
СУБД, такими, например, как Microsoft SQL Server. В следующей главе нашей книги мы
рассмотрим методики интеграции приложений C# с сервером Microsoft SQL Server.

Визуальное проектирование
приложений C#
А.В. Фролов, Г.В. Фролов
ГЛАВА 9. ИНТЕГРАЦИЯ С MS SQL SERVER
ПРИЛОЖЕНИЕ SQLTESTAPP
Создание базы данных
Создание таблицы Contacts
Идентификатор для подключения к базе данных
Создание проекта приложения
Добавление адаптера данных SqlDataAdapter
Программный код для адаптера и соединения
Создание локального набора DataSet
Редактирование содержимого набора DataSet
Загрузка набора данных
Сохранение отредактированного набора данных
ПАРОЛЬНЫЙ ДОСТУП К СИСТЕМЕ
Приложение LoginApp
Таблица Users
Создание проекта приложения
Соединение с базой данных
Запросы SQL
Адаптер SqlDataAdapter
Элемент управления DataGrid
Подключение пользователя
Обновление таблицы Users
ХРАНЕНИЕ ДЕРЕВА В БАЗЕ ДАННЫХ
Приложение ArticlesApp
База данных Articles
Таблица Tree
Таблица Documents
Хранимая процедура sp_InsertDocument
Хранимая процедура sp_ InsertNode
Хранимая процедура sp_ UpdateDocument
Создание проекта приложения ArticlesApp
Соединение с базой данных
Добавление адаптера SqlDataAdapter
Создание набора данных DataSet
Добавление контекстного меню
СОЗДАНИЕ УЗЛА ДЕРЕВА
Метод AddNode
Диалоговое окно для ввода данных добавляемого узла
Открытие и закрытие соединения с базой данных
Использование хранимых процедур
Параметры хранимых процедур
Запуск хранимой процедуры
Получение значений выходных параметров
Добавление текста документа
Диалоговая форма редактирования документа
Свойство Title
Свойство Weight
Свойство Document
Построение дерева
Метод UpdateTree
Метод CreateNodes
РЕДАКТИРОВАНИЕ УЗЛА ДЕРЕВА
Обработчик событий меню Edit
Извлечение идентификатора редактируемого узла
Извлечение данных редактируемого узла дерева
Получение текст редактируемой статьи
Извлечение заголовка и веса сортировки
Обновление информации узла в базе данных
УДАЛЕНИЕ УЗЛА ДЕРЕВА
ОТСЛЕЖИВАНИЕ ПЕРЕМЕЩЕНИЙ ПО ДЕРЕВУ

Глава 9. Интеграция с MS SQL Server


В предыдущей главе нашей книги мы научились работать с компонентами DataSet и
DataGtid, с использованием которых можно относительно легко создавать базы данных
и отображать их содержимое в формах приложений C#.
Теперь нашей задачей будет научиться интегрировать приложения с сервером
Microsoft SQL Server, используя как технологию рассоединенной (disconnected)
обработки, так и классическую клиент-серверную технологию. Мы познакомимся с
программными компонентами, предусмотренными в Microsoft .NET Framework
специально для решения этой задачи. Это так называемые управляемые
поставщики (managed providers), реализованные в виде ряда интерфейсов. Мы уже
рассказывали о них в разделе «Провайдеры данных для управляемого кода» 8 главы.
В этой главе мы будем работать с провайдером SQL, который обеспечивает
прямой доступ с максимальной производительностью к серверу Microsoft SQL Server.
Приложение SQLTestApp
Материал этой главы мы будем изучать на нескольких примерах приложений, первое из
которых называется SQLTestApp. В этом приложении мы привяжем локальный набор
DataSet к базе данных, расположенной на сервере Microsoft SQL Server. Отображение и
редактирование таблицы Contacts, созданной в рамках локального набора DataSet,
будет осуществляться с помощью рассмотренного в предыдущей главе элемента
управления DataGrid.
Для загрузки содержимого таблицы Contacts из базы данных Microsoft SQL Server,
а также для сохранения изменений, сделанных в этой таблице локально, в окне
приложения мы создадим кнопки Загрузить и Обновить (рис. 9-1).

Рис. 9-1. Окно приложения SQLTestApp

Создание базы данных


Прежде всего, необходимо создать базу данных на сервере Microsoft SQL Server. Мы
полагаем, что Вы предварительно установили сервер Microsoft SQL Server версии 2000
или 7.0 на тот же самый компьютер, где находится Ваше приложение, или на другой
компьютер Вашей локальной сети.
Описание процедуры установки и настройки сервера Microsoft SQL Server выходит
за рамки нашей книги, однако на эту тему уже издано немало литературы. Тем не
менее, мы подробно расскажем о выполнении всех шагов, необходимых для создания
баз данных наших приложений.
Итак, запустите программу SQL Server Enterprise Manager (рис. 9-2), которая
входит в состав клиентских утилит сервера Microsoft SQL Server. Эти утилиты могут
устанавливаться как вместе с сервером SQL, так и на отдельный компьютер.
Рис. 9-2. Утилита SQL Server Enterprise Manager

Последовательно раскройте в левой части окна утилиты SQL Server Enterprise


Manager каталоги Microsoft SQL Servers и SQL Server Group, а затем каталоги,
расположенные на Вашем сервере SQL. На рис. 9-2 SQL Server называется DR4.
Далее раскройте папку с базами данных Databases. В ней имеется несколько
системных баз данных, а также базы данных, поставляемые вместе с сервером в
качестве примера.
Щелкните правой клавишей мыши папку Databases, а затем выберите из
контекстного меню строку New Database. На экране появится диалоговое окно,
показанное на рис. 9-3.
Рис. 9-3. Создание базы данных PhoneBook

Введите в поле Name строку PhoneBook (название создаваемой базы данных), а


затем щелкните кнопку OK. В результате будет создана новая база данных. Раскройте
ее папку, как это показано на рис. 9-4.
Рис. 9-4. Содержимое базы данных PhoneBook

Создание таблицы Contacts


На следующем шаге нам нужно будет создать таблицу Contacts, аналогичную той, что
была использована в предыдущей главе для телефонной книги.
Чтобы создать таблицу, щелкните правой клавишей мыши строку Tables в
раскрытой папке PhoneBook (рис. 9-4), а затем выберите из контекстного меню строку
New Table. На экране появится редактор структуры таблицы, показанный на рис. 9-5.

Рис. 9-5. Редактирование структуры таблицы Contacts

Введите в столбце редактора Column Name имена столбцов таблицы Contacts.


Столбец id будет использован для хранения уникальных идентификаторов строк, а в
столбцах FirstName и LastName будут храниться, соответственно, имена и фамилии
людей.
Столбец редактора Data Type определяет тип данных, хранящихся в столбце
таблицы. Первый столбец id должен иметь числовой тип int, а два других — текстовый
тип с переменной длиной varchar.
В столбце Length укажите размер полей, как это показано на рис. 9-5.
Далее Вам нужно снять отметку с флажков Allow Nulls, так как столбцы нашей
таблицы не могут содержать пустых значений.
На следующем этапе необходимо сделать столбец id первичным ключом в
создаваемой таблице Contacts. С этой целью щелкните соответствующую строку в окне
редактора правой клавишей мыши и затем выберите из контекстного меню строку Set
Primary Key. Около строки параметров id появится изображение маленького ключа
(рис. 9-6).
Рис. 9-6. Настройка параметров столбца id

Далее, выберите из списка Identity для столбца id значение Yes (как это показано
на рис. 9-6). В результате столбец id будет содержать уникальные идентификаторы
строк таблицы.
Кроме того, измените значение параметра Identity Seed, задающее начальное
значение столбца id, с 1 на 0. Именно такое значение используется по умолчанию для
идентификации строк таблиц, хранящихся в наборе данных DataSet.
Отредактировав параметры таблицы, сохраните ее структуру, щелкнув кнопку с
изображением дискеты. Эта кнопка находится в левом верхнем углу окна редактора. В
результате на экране появится диалоговое окно Choose Name, где Вам нужно будет
указать имя таблицы (рис. 9-7). Наша таблица должна называться Contacts.

Рис. 9-7. Сохранение структуры таблицы

Нажмите здесь кнопку OK, а затем закройте окно редактора структуры таблицы.
Ниже мы привели сценарий SQL, который создает нужную нам таблицу Contacts
при помощи команд SQL:
CREATE TABLE [dbo].[Contacts] (
[id] [int] IDENTITY (1, 1) NOT NULL ,
[FirstName] [varchar] (50) COLLATE Cyrillic_General_CI_AS NULL ,
[LastName] [varchar] (50) COLLATE Cyrillic_General_CI_AS NULL
) ON [PRIMARY]
GO

ALTER TABLE [dbo].[Contacts] WITH NOCHECK ADD


CONSTRAINT [PK_Contacts] PRIMARY KEY CLUSTERED
(
[id]
) ON [PRIMARY]
GO
Вы можете использовать этот сценарий для автоматического создания таблицы в
базе данных при помощи утилиты Query Analyzer, входящей в состав клиентских утилит
сервера Microsoft SQL Server.
Идентификатор для подключения к базе данных
Сервер Microsoft SQL Server содержит развитые средства разграничения доступа
пользователей к базам данных, которые мы настоятельно рекомендуем Вам изучить.
Для наших примеров мы создадим идентификатор, при помощи которого
приложения будут подключаться к базе данных PhoneBook.
В окне просмотра содержимого базы данных PhoneBook раскройте список
пользователей базы данных Users, показанный на рис. 9-8.

Рис. 9-8. Пользователи базы данных PhoneBook

По умолчанию в этом списке есть только один пользователь с идентификатором


dbo. Это владелец базы данных (Data Base Owner), который ее создал. Если Вы
создавали базу данных, подключившись к серверу с правами администратора, то
только администратор будет иметь доступ к базе данных.
Для добавления нового пользователя щелкните строку Users правой клавишей
мыши, а затем выберите из контекстного меню строку New Database User. Вы увидите
на экране диалоговое окно, показанное на рис. 9-9.
Рис. 9-9. Добавление нового пользователя

Выберите в списке Login name, задающем идентификатор пользователя, строку


<new> для создания нового идентификатора. Вы увидите диалоговое окно создания
нового идентификатора, показанное на рис. 9-10.

Рис. 9-10. Добавление нового идентификатора

Введите в поле Name строку c_sharp_app. Этот идентификатор будут использовать


наши приложения для подключения к базе данных PhoneBook.
Далее Вам необходимо отметить флажок SQL Server Authentication, чтобы
использовать сервер SQL для аутентификации (проверки) данного идентификатора.
Введите пароль доступа в поле Password.
На последнем шаге выберите в списке Database базу данных PhoneBook, а в
списке Language — строку Russian. После аутентификации пользователь (т.е. наше
приложение) будет по умолчанию работать с базой данных PhoneBook, используя
русские национальные настройки.
Далее раскройте вкладку Database Access (рис. 9-11) для настройки прав доступа
пользователя c_sharp_app к базам данных.

Рис. 9-11. Настройка доступа к базе данных

Отметьте флажок базы данных PhoneBook, разрешив доступ пользователя


c_sharp_app к этой базе данных, а затем щелкните кнопку OK.
После этого у Вас будет еще раз запрошен пароль, введенный ранее. Используйте
здесь тот же самый пароль, что вы указывали в поле Password диалогового окна,
показанного на рис. 9-10. Далее закройте окно создания нового пользователя.
Если Вы все сделали правильно, в списке пользователей базы данных появится
новая строка (рис. 9-12).
Рис. 9-12. Добавлен новый пользователь

На следующем этапе Вы должны задать права доступа пользователя c_sharp_app


к таблице Contacts. Для этого щелкните имя таблицы в списке таблиц базы данных
PhoneBook (рис. 9-13), а затем выберите из контекстного меню строку Properties.

Рис. 9-13. Таблицы нашей базы данных

На экране появится диалоговое окно свойств таблицы Contacts, в которой Вам


нужно будет щелкнуть кнопку Permissions. На экране появится окно с вкладкой
Permissions, где Вы сможете задать права доступа пользователя по выполнению
отдельных операций с таблицей Contacts (рис. 9-14).

Рис. 9-14. Настройка прав доступа к таблице Contacts

Отметьте в этом окне флажки SELECT, INSERT, UPDATE и DELETE, разрешив


пользователю c_sharp_app выполнение операций извлечения, вставки обновления и
удаления строк, соответственно.
Создание проекта приложения
После создания базы данных PhoneBook Вам нужно создать новое приложение с именем
SQLTestApp, пользуясь для этого мастером проектов.
Добавление адаптера данных SqlDataAdapter
Чтобы обеспечить связь между таблицей локального набора данных DataSet и базой
данных, размещенной на сервере Microsoft SQL Server, нам нужно добавить в проект
приложения программный компонент адаптера данных SqlDataAdapter.
Методы Fill и Update, предусмотренные в классе SqlDataAdapter, выполняют,
соответственно, прямую и обратную передачу данных между набором DataSet и
таблицей, хранящейся в базе данных сервера SQL.
При этом метод Fill наполняет набор DataSet данными из сервера SQL, а метод
Update обновляет базу сервера SQL данными из локального набора DataSet. Для
передачи данных используются соответствующие строки, составленные на языке
Transact-SQL.
Когда компонент SqlDataAdapter используется для доступа к серверу Microsoft SQL
Server, то вместе с ним применяются компоненты SqlConnection и SqlCommand. Первый
из них обеспечивает подключение к серверу SQL, а второй — выполнение команд SQL.
В компоненте SqlDataAdapter имеются также свойства SelectCommand,
InsertCommand, DeleteCommand, UpdateCommand и TableMappings, нужные в процессе
загрузки и обновления данных.
Нам нужно добавить в наше приложение адаптер SqlDataAdapter, перетащив его
значок в окно формы из вкладки Data инструментальной панели Toolbox системы
разработки приложений Microsoft visual Studio .NET (рис. 9-15).

Рис. 9-15. Значок адаптера SqlDataAdapter

Как только Вы это сделаете, на экране появится первое окно мастера


конфигурирования адаптера данных (рис. 9-16).
Рис. 9-16. Мастер конфигурирования адаптера данных

Щелкните в этом окне кнопку Next для продолжения работы мастера


конфигурирования.
В следующем диалоговом окне Вам нужно будет создать соединение с источником
данных (рис. 9-17).

Рис. 9-17. Создание соединения с источником данных

Чтобы создать соединение, щелкните кнопку New Connection. На экране появится


диалоговое окно Data Link Properties, открытое на вкладке Connection (рис. 9-18).
Рис. 9-18. Выбор параметров соединения с источником данных

В списке Select or enter a server name Вы должны указать сервер SQL, к которому
будет подключаться Ваше приложение. Если это тот сервер работает на том же
компьютере, что и приложение, укажите здесь строку localhost.
Далее, отметьте флажок Use a specific user name and password, после чего введите
в поле User name строку c_sharp_app. Задайте также в поле Password пароль
пользователя c_sharp_app.
Чтобы позволить приложению сохранить пароль, отметьте флажок Allow saving
password. Избегайте установки этого флажка, если к безопасности предъявляются
повышенные требования.
Далее отметьте флажок Select the database on the server и выберите в списке,
расположенном под этим флажком, нашу базу данных PhoneBook.
На этом создание соединения с источником данных завершено. Обязательно
проверьте его работоспособность, щелкнув кнопку Test Connection. Если все
нормально, Вы получите сообщение с текстом Test connection succeed.
Проверив соединение, щелкните кнопку OK. На экране появится диалоговое окно
SQL Server Login (рис. 9-19).

Рис. 9-19. Подключение к серверу SQL

В поле Password этого окна Вам нужно ввести пароль пользователя c_sharp_app, а
затем щелкнуть кнопку OK. Теперь в списке соединений появится название нового,
только что созданного Вами соединения (рис. 9-20).
Рис. 9-20. Выбрано созданное и проверенное соединение

Щелкните кнопку Next для перехода к следующему окну мастера соединений.


На экране появится диалоговое окно с флажками, в котором Вам нужно будет
определить тип запроса, т.е. способ, которым будет пользоваться адаптер
SqlDataAdapter для извлечения данных из сервера SQL (рис. 9-21).
Рис. 9-21. Выбор типа запроса

Для извлечения данных Вы можете использовать запрос, составленный на языке


SQL, новую или существующую хранимую процедуру сервера Microsoft SQL Server. В
нашем случае мы воспользуемся простой командой SELECT языка SQL, поэтому
отметьте флажок Use SQL statements, а затем щелкните кнопку Next.
На экране появиться очередное окно мастера, показанное на рис. 9-22. Это окно
позволит Вам ввести текст сценария SQL для извлечения данных.

Рис. 9-22. Окно ввода сценария SQL

Введите в этом окне следующую строку:


SELECT id, FirstName, LastName FROM Contacts
Щелкнув кнопку Query Builder, Вы сможете создать строку запроса SQL при
помощи мастера сценариев SQL, главное окно которого показано на рис. 9-23.
Рис. 9-23. Мастер сценариев SQL

Нужного нам результата можно добиться, просто отметив флажки всех строк
таблицы Contacts.
Щелкнув кнопку Next в окне мастера, показанном на рис. 9-22, Вы перейдете к
финальному окну, показанному на рис. 9-24.

Рис. 9-24. Адаптер создан

Для завершения процедуры создания адаптера щелкните в нем кнопку Finish.


Программный код для адаптера и соединения
Рассмотрим программный код, добавленный мастером в проект в результате всех наших
действий.
Прежде всего, обратите внимание, что теперь к проекту был автоматически
добавлен программный компонент sqlConnection1. Этот компонент обеспечивает
соединение с источником данных.
Мастер создал в классе Form1 несколько полей для хранения ссылок на объекты
адаптера sqlDataAdapter1, соединения sqlConnection1, а также команд SQL,
предназначенных для выборки (sqlSelectCommand1), вставки (sqlInsertCommand1),
обновления (sqlUpdateCommand1) и удаления (sqlDeleteCommand1) строк таблицы:
private System.Data.SqlClient.SqlDataAdapter sqlDataAdapter1;
private System.Data.SqlClient.SqlCommand sqlSelectCommand1;
private System.Data.SqlClient.SqlCommand sqlInsertCommand1;
private System.Data.SqlClient.SqlCommand sqlUpdateCommand1;
private System.Data.SqlClient.SqlCommand sqlDeleteCommand1;
private System.Data.SqlClient.SqlConnection sqlConnection1;
Инициализация всех этих компонентов осуществляется методом
InitializeComponent, исходный текст которого по умолчанию скрыт от разработчика.
Давайте раскроем эти «секреты».
Прежде всего, в процессе инициализации метод InitializeComponent создает
перечисленные выше объекты при помощи соответствующих конструкторов:
this.sqlDataAdapter1 = new System.Data.SqlClient.SqlDataAdapter();
this.sqlSelectCommand1 = new System.Data.SqlClient.SqlCommand();
this.sqlInsertCommand1 = new System.Data.SqlClient.SqlCommand();
this.sqlUpdateCommand1 = new System.Data.SqlClient.SqlCommand();
this.sqlDeleteCommand1 = new System.Data.SqlClient.SqlCommand();
this.sqlConnection1 = new System.Data.SqlClient.SqlConnection();
Далее выполняется настройка свойств адаптера sqlDataAdapter1.
Программа подключает к адаптеру объекты, при помощи которых выполняются
операции удаления, вставки, выборки и обновления данных:
this.sqlDataAdapter1.DeleteCommand = this.sqlDeleteCommand1;
this.sqlDataAdapter1.InsertCommand = this.sqlInsertCommand1;
this.sqlDataAdapter1.SelectCommand = this.sqlSelectCommand1;
this.sqlDataAdapter1.UpdateCommand = this.sqlUpdateCommand1;
После этого необходимо настроить свойство TableMappings адаптера. Это свойство
задает соответствие между таблицами источника данных (сервера SQL) и таблицами
локального набора данных DataSet.
В нашем случае источник данных представляет собой таблицу Contacts. Эта
таблица отображается на таблицу набора DataSet с именем Table:
this.sqlDataAdapter1.TableMappings.AddRange(
new System.Data.Common.DataTableMapping[]
{
new System.Data.Common.DataTableMapping(
"Table", "Contacts",
new System.Data.Common.DataColumnMapping[]
{
new System.Data.Common.DataColumnMapping("id", "id"),
new System.Data.Common.DataColumnMapping("FirstName",
"FirstName"),
new System.Data.Common.DataColumnMapping("LastName",
"LastName")
}
)
}
);
При этом программа отображает каждый столбец источника данных на
одноименный столбец набора данных DataSet.
Уже после создания адаптера Вы сможете изменить отображение, отредактировав
свойство TableMappings. Это можно сделать с помощью редактора, показанного на рис.
9-25.

Рис. 9-25. Редактирование отображения таблиц

Здесь список SourceTable задает исходную таблицу локального набора данных


DataSet, а список Dataset table — таблицу источника данных.
На следующем этапе программа инициализации готовит команды для выполнения
операций с источником данных, таких как извлечение, вставка, обновление и
удаление. Вот в каком виде готовится команда извлечения данных:
//
// sqlSelectCommand1
//
this.sqlSelectCommand1.CommandText =
"SELECT id, FirstName, LastName FROM dbo.Contacts";
this.sqlSelectCommand1.Connection = this.sqlConnection1;
Текст команды, составленный на языке SQL, записывается в свойство
CommandText объекта sqlSelectCommand1. Так как команда имеет отношение к вполне
определенному соединению с источником данных, в свойство Connection записывается
ссылка на соединение sqlConnection1. Об инициализации соединения мы расскажем
чуть позже.
Аналогичным образом готовятся и остальные команды.
Вот, например, как выглядит инициализация команда вставки строк:
//
// sqlInsertCommand1
//
this.sqlInsertCommand1.CommandText =
"INSERT INTO dbo.Contacts(FirstName, LastName) VALUES (@FirstName, @LastName);
SEL" +
"ECT id, FirstName, LastName FROM dbo.Contacts WHERE (id = @@IDENTITY)";
this.sqlInsertCommand1.Connection = this.sqlConnection1;
Обратите внимание, что для вставки используются параметры @FirstName и
@LastName. Они определяют содержимое ячеек вставляемых строк.
Параметры добавляются в свойство sqlInsertCommand1.Parameters при помощи
метода Add:
this.sqlInsertCommand1.Parameters.Add(
new System.Data.SqlClient.SqlParameter("@FirstName",
System.Data.SqlDbType.VarChar, 50, "FirstName"));

this.sqlInsertCommand1.Parameters.Add(
new System.Data.SqlClient.SqlParameter("@LastName",
System.Data.SqlDbType.VarChar, 50, "LastName"));
При этом каждый параметр команды SQL создается в виде объекта класса
System.Data.SqlClient.SqlParameter.
В качестве первого параметра (извиняемся за тавтологию) конструктору
передается имя параметра команды SQL, снабженное префиксом @. Второй параметр
задает тип данных, третий — размер данных, и, наконец, четвертый — имя
соответствующего столбца в источнике данных. Заметим, что в классе
System.Data.SqlClient.SqlParameter есть еще несколько перегруженных конструкторов,
описание которых Вы найдете в документации.
Команда обновления данных также имеет параметры:
//
// sqlUpdateCommand1
//
this.sqlUpdateCommand1.CommandText =
"UPDATE dbo.Contacts SET FirstName = @FirstName, LastName = @LastName WHERE (id
="+
"@Original_id) AND (FirstName = @Original_FirstName) AND (LastName = @Original_La"
+
"stName); SELECT id, FirstName, LastName FROM dbo.Contacts WHERE (id = @id)";

this.sqlUpdateCommand1.Connection = this.sqlConnection1;

this.sqlUpdateCommand1.Parameters.Add(
new System.Data.SqlClient.SqlParameter("@FirstName",
System.Data.SqlDbType.VarChar, 50, "FirstName"));

this.sqlUpdateCommand1.Parameters.Add(
new System.Data.SqlClient.SqlParameter("@LastName",
System.Data.SqlDbType.VarChar, 50, "LastName"));

this.sqlUpdateCommand1.Parameters.Add(
new System.Data.SqlClient.SqlParameter("@id",
System.Data.SqlDbType.Int, 4, "id"));

this.sqlUpdateCommand1.Parameters.Add(
new System.Data.SqlClient.SqlParameter("@Original_id",
System.Data.SqlDbType.Int, 4,
System.Data.ParameterDirection.Input, false, ((System.Byte)(0)),
((System.Byte)(0)), "id", System.Data.DataRowVersion.Original,
null));

this.sqlUpdateCommand1.Parameters.Add(
new System.Data.SqlClient.SqlParameter("@Original_FirstName",
System.Data.SqlDbType.VarChar, 50,
System.Data.ParameterDirection.Input, false, ((System.Byte)(0)),
((System.Byte)(0)), "FirstName",
System.Data.DataRowVersion.Original, null));

this.sqlUpdateCommand1.Parameters.Add(
new System.Data.SqlClient.SqlParameter("@Original_LastName",
System.Data.SqlDbType.VarChar, 50,
System.Data.ParameterDirection.Input, false, ((System.Byte)(0)),
((System.Byte)(0)), "LastName",
System.Data.DataRowVersion.Original, null));
Как видите, для команды обновления требуется больше параметров. Наряду с
параметрами, задающими новые значения для ячеек обновляемой строки @FirstName,
@LastName и @id, необходимо передать исходные значения упомянутых ячеек
посредством параметров @Original_FirstName, @Original_LastName и @Original_id.
Для передачи параметров с исходными значениями используется перегруженный
вариант конструктора класса System.Data.SqlClient.SqlParameter, имеющий 10
параметров.
В частности, четвертый параметр задает направление передачи данных как
System.Data.ParameterDirection.Input. Таким образом, здесь добавляются не выходные,
а входные параметры. После выполнения команды эти параметры будут содержать
исходные значения ячеек, какими они были до выполнения команды.
Команда удаления также имеет входные параметры:
//
// sqlDeleteCommand1
//
this.sqlDeleteCommand1.CommandText =
"DELETE FROM dbo.Contacts WHERE (id = @Original_id) AND (FirstName = @Original_Fir"
+
"stName) AND (LastName = @Original_LastName)";

this.sqlDeleteCommand1.Connection = this.sqlConnection1;

this.sqlDeleteCommand1.Parameters.Add(
new System.Data.SqlClient.SqlParameter("@Original_id",
System.Data.SqlDbType.Int, 4,
System.Data.ParameterDirection.Input, false, ((System.Byte)(0)),
((System.Byte)(0)), "id", System.Data.DataRowVersion.Original,
null));

this.sqlDeleteCommand1.Parameters.Add(
new System.Data.SqlClient.SqlParameter("@Original_FirstName",
System.Data.SqlDbType.VarChar, 50,
System.Data.ParameterDirection.Input, false, ((System.Byte)(0)),
((System.Byte)(0)), "FirstName",
System.Data.DataRowVersion.Original, null));

this.sqlDeleteCommand1.Parameters.Add(
new System.Data.SqlClient.SqlParameter("@Original_LastName",
System.Data.SqlDbType.VarChar, 50,
System.Data.ParameterDirection.Input, false, ((System.Byte)(0)),
((System.Byte)(0)), "LastName",
System.Data.DataRowVersion.Original, null));
Через эти параметры команда передает исходные значения ячеек удаляемой
строки.
Команды и параметры удобно редактировать не в исходном тексте приложения, а
при помощи соответствующих графических средств.
Для редактирования параметров, например, команды обновления данных,
раскройте список этих параметров в окне свойств объекта sqlDataAdapter1, как это
показано на рис. 9-26.

Рис. 9-26. Редактирование параметров команды обновления данных

Для изменения команды SQL, выполняющей обновление данных, отредактируйте


здесь свойство CommandText. При этом на экране появится редактор запросов Query
Builder, окно которого показано на рис. 9-27.

Рис. 9-27. Редактор запросов Query Builder

С помощью этого редактора можно внести все необходимые изменения в текст


команды SQL.
Для изменения параметров команды SQL отредактируйте свойство Parameters.
При этом на экране появится редактор параметров, показанный на рис. 9-28.
Рис. 9-28. Редактор параметров команды SQL

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


параметров соединения с источником данных:
//
// sqlConnection1
//
this.sqlConnection1.ConnectionString =
"data source=localhost;initial catalog=PhoneBook;
password=12345;persist security i" +
"nfo=True;user id=c_sharp_app;workstation id=DR4;
packet size=4096";
Как видите, в свойство ConnectionString объекта sqlConnection1 записывается
довольно длинная строка, содержащая значения параметров соединения. Каждый
такой параметр задается при помощи строк имени и значения, объединенных
оператором присваивания. Отдельные параметры в этой строке разделены символом
точка с запятой.
Назначения некоторых параметров строки соединения мы привели в табл. 9-1.
Таблица 9-1. Параметры строки соединения
Имя Описание
data source Этот параметр может также задаваться как Server,
Address, Addr или Network Address.
Он задает имя или сетевой адрес узла сети, на котором
работает сервер SQL
initial catalog Может задаваться как Database.
Этот параметр определяет имя базы данных, с которой
осуществляется соединение
password Может задаваться как Pwd.
Задает пароль для подключения к базе данных.
persist security info Когда значение этого параметра равно false, парольная
информация не включается в параметры соединения
user id Идентификатор для подключения к серверу SQL
workstation id Локальное имя рабочей станции, подключающейся к
серверу SQL
packet size Размер пакетов данных (в байтах), используемых для
обмена данными с сервером SQL
Application Name Имя приложения или провайдера данных .Net SqlClient
Data Provider
AttachDBFilename Может задаваться как extended properties или Initial File
Name.
Полный путь и имя файла базы данных. При
использовании этого параметра имя базы данных должно
быть задано с использованием параметра Database
Connect Timeout Может задаваться как Connection Timeout.
Продолжительность периода ожидания ответа от сервера
перед выдачей сообщения об ошибке
Connection Lifetime Время жизни соединения
Connection Reset Режим автоматического завершения соединения
Current Language Текущий национальный язык для сервера SQL
Network Library Может задаваться как Net.
Этот параметр задает сетевую библиотеку, используемую
для установки соединения с сервером SQL. Здесь можно
указывать значения dbnmpntw (Named Pipes), dbmsrpcn
(Multiprotocol), dbmsadsn (Apple Talk), dbmsgnet (VIA),
dbmsipcn (Shared Memory), dbmsspxn (IPX/SPX), и
dbmssocn (TCP/IP)
Создание локального набора DataSet
Вернемся к нашему приложению SQLTestApp. На данный момент мы создали базу
данных и проект приложения, а также добавили в проект адаптер данных и настроили
соединение с источником данных. Теперь нашей задачей будет создание локального
набора данных DataSet, отображаемого на базу данных, размещенную на сервере SQL.
Для этого щелкните правой клавишей мыши форму Вашего приложения и
выберите из контекстного меню строку Generate DataSet. На экране появится
одноименное диалоговое окно, показанное на рис. 9-29.
Рис. 9-29. Создание набора данных DataSet

Вам нужно отметить флажок New. При необходимости Вы также можете изменить
имя набора данных в поле, расположенном справа от этого флажка.
Убедитесь также, что в списке Choose which table(s) to add to the dataset,
определяющим таблицы, добавляемые в набор данных, выбрана таблица Contacts.
Отметьте также флажок Add this dataset to the designer.
После того как Вы щелкните кнопку OK, будет создан и добавлен в проект
приложения набор данных dataSet11.
Помимо этого к проекту приложения будет добавлена схема набора данных в виде
файла DataSet1.xsd. Этот файл содержит описание таблицы на языке XML.
Если попытаться открыть файл DataSet1.xsd для редактирования, то в окне
Microsoft Visual Studio .NET эта схема появится в виде таблицы (рис. 9-30).

Рис. 9-30. Редактирование схемы набора данных в визуальном режиме

Ячейки этой таблицы поддаются редактированию. На вкладке XML (рис. 9-30) Вы


сможете отредактировать файл схемы:
<?xml version="1.0" standalone="yes" ?>
<xs:schema id="DataSet1" targetNamespace="http://www.tempuri.org/DataSet1.xsd"
xmlns:mstns="http://www.tempuri.org/DataSet1.xsd"
xmlns="http://www.tempuri.org/DataSet1.xsd"
xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-
com:xml-msdata" attributeFormDefault="qualified" elementFormDefault="qualified">
<xs:element name="DataSet1" msdata:IsDataSet="true" msdata:Locale="ru-RU">
<xs:complexType>
<xs:choice maxOccurs="unbounded">
<xs:element name="Contacts">
<xs:complexType>
<xs:sequence>
<xs:element name="id" msdata:ReadOnly="true"
msdata:AutoIncrement="true" type="xs:int" />
<xs:element name="FirstName" type="xs:string" />
<xs:element name="LastName" type="xs:string" />
</xs:sequence>
</xs:complexType>
</xs:element>
</xs:choice>
</xs:complexType>
<xs:unique name="Constraint1" msdata:PrimaryKey="true">
<xs:selector xpath=".//mstns:Contacts" />
<xs:field xpath="mstns:id" />
</xs:unique>
</xs:element>
</xs:schema>
Детальное описание содержимого этого файла выходит за рамки нашей книги.
Заметим только, что у Вас нет необходимости редактировать этот файл вручную, т.к.
можно воспользоваться визуальным редактором, показанным на рис. 9-30.
Редактирование содержимого набора DataSet
Теперь нам нужно предусмотреть в нашем приложении средство просмотра и
редактирования созданного на предыдущем этапе набора данных DataSet. Проще всего
это сделать при помощи элемента управления DataGrid, о котором мы подробно
рассказывали в предыдущей главе.
Итак, перетащите мышью элемент управления DataGrid из панели Toolbox и
расположите его внутри формы приложения.
Далее Вам будет нужно отредактировать свойства DataGrid, чтобы привязать его к
набору данных DataSet.
Прежде всего, установите значение свойства DataSource, равным dataSet11.
Далее установите значение свойства DataMember, определяющим имя таблицы,
равным Contacts. Для этого выберите таблицу Contacts в окне, появляющемся на
экране при редактировании свойства DataMember.
Загрузка набора данных
Добавьте в форму приложения кнопку с надписью Загрузить, а затем создайте для нее
следующий обработчик событий:
private void button1_Click(object sender, System.EventArgs e)
{
try
{
dataSet11.Clear();
sqlDataAdapter1.Fill(dataSet11);
}
catch(Exception ex)
{
MessageBox.Show(ex.Message, "Ошибка");
}
}
Первым делом этот обработчик событий очищает элемент управления dataSet11,
удаляя из него все строки.
Далее он загружает данных из источника данных в локальный набор данных
dataSet11 с помощью адаптера sqlDataAdapter1 и метода Fill.
Так как при работе с сервером SQL могут возникать различные ошибки,
приводящие к возникновению исключений, мы предусмотрели обработчик таких
исключений. Этот обработчик выводит на экран текст сообщения об ошибке.
Сохранение отредактированного набора данных
Для того чтобы пользователь нашего приложения мог сохранять содержимое
локального набора данных DataSet в базе данных, расположенной на сервере,
добавьте в форму приложения кнопку с надписью Обновить.
Создайте для этой кнопки следующий обработчик событий:
private void button2_Click(object sender, System.EventArgs e)
{
try
{
sqlDataAdapter1.Update(dataSet11);
}
catch(Exception ex)
{
MessageBox.Show(ex.Message, "Ошибка");
}
}
Он сохраняет содержимое набора данных dataSet11 в источнике данных при
помощи адаптера sqlDataAdapter1 и метода Update.
Теперь Вы можете испытать приложение в работе (рис. 9-31).

Рис. 9-31. Приложение SQLTestApp в действии

Если при попытке загрузить или сохранить данные появляются сообщения об


ошибке, Вам надо проанализировать их текст. Ошибки могут возникать из-за того, что
Вы неточно следовали нашим рекомендациям по созданию приложения или из-за
ошибок, допущенных при установке или настройке севера SQL. В частности, проверьте
идентификатор и пароль, использованные для подключения к базе данных, а также
настройку прав доступа к базе данных и таблице Contacts.
Парольный доступ к системе
Как правило, коммерческие приложения с базами данных содержат в себе те или иные
средства разграничения доступа. Чаще всего эти средства основаны на использовании
идентификаторов и паролей. Чтобы получить доступ к системе, пользователь должен
ввести свой идентификатор и пароль.
В базе данных приложения может быть предусмотрена специальная таблица,
хранящая идентификаторы (логины) и пароли (или, что лучше, хэш-функции паролей).
Системный администратор наделяется правами редактирования таблицы
идентификаторов и паролей, а остальные пользователи не имеют к ней прямого
доступа.
Приложение LoginApp
В этом разделе мы создадим приложение LoginApp, предназначенное для создания и
редактирования таблицы идентификаторов и паролей пользователей. Сама таблица с
названием Users при этом хранится на сервере SQL. Для доступа к ней мы используем
адаптер SqlDataAdapter, рассмотренный в предыдущих разделах этой главы, а также
класс SqlDataReader.
При этом адаптер SqlDataAdapter мы будем использовать для получения и
обновления содержимого таблицы Users, а класс SqlDataReader — для проверки
идентификатора и пароля пользователя.
Внешний вид окна приложения LoginApp показан на рис. 9-32.
Рис. 9-32. Приложение LoginApp

В верхней части окна находятся поля, предназначенные для ввода


идентификатора и пароля, а также кнопка Войти. Для того чтобы получить доступ к
редактированию таблицы Users, пользователь (играющий роль администратора) должен
ввести свой идентификатор и пароль, а затем нажать кнопку Войти.
При самом первом подключении к системе идентификатор и пароль вводить не
надо. При этом наше приложение автоматически создаст в таблице Users строку
администратора с идентификатором admin и паролем password, отображая на экране
сообщение об этом событии (рис. 9-33). Данную парольную информацию можно будет
затем использовать для первого подключения к системе, изменив ее в дальнейшем.

Рис. 9-33. Эта учетная запись создается автоматически

Если в таблице Users уже есть хотя бы одна запись, необходимо ввести
правильный идентификатор и пароль. В этом случае содержимое таблицы Users будет
отображено в окне приложения с помощью адаптера SqlDataAdapter и элемента
управления DataGrid.
Помимо идентификаторов и паролей пользователей таблица Users хранит код
доступа каждого пользователя. В нашем приложении пользователям с кодом доступа 80
разрешается модифицировать таблицу Users, а всем остальным пользователям —
только просматривать содержимое этой таблицы.
Для сохранения изменений в таблице Users, расположенной на сервере SQL, мы
предусмотрели кнопку Обновить. Эта кнопка, однако, будет разблокирована только в
том случае, если код доступа пользователя равен 80.
Таблица Users
Прежде чем приступить к созданию приложения LoginApp Вам нужно будет создать в
базе данных PhoneBook (с которой мы работали раньше) новую таблицу Users.
В таблице Users нужно предусмотреть столбцы id, Login, Password и assess. На
рис. 9-34 мы показали таблицу Users в окне дизайнера таблиц утилиты SQL Server
Enterprise Manager.
Рис. 9-34. Таблица Users

Столбец id предназначен для хранения первичного ключа. Столбцы Login и


Password хранят, соответственно, идентификатор пользователя и пароль. Что же
касается столбца access, то он хранит код доступа в виде целого числа. Если этот код
равен 80, пользователю разрешается модифицировать таблицу Users, а если нет —
только просматривать.
Ниже мы привели сценарий SQL, с помощью которого можно создать таблицу
Users и определить для нее первичный ключ:
CREATE TABLE [dbo].[Users] (
[id] [int] IDENTITY (0, 1) NOT NULL ,
[Login] [varchar] (50) COLLATE Cyrillic_General_CI_AS NOT NULL ,
[Password] [varchar] (50) COLLATE Cyrillic_General_CI_AS NOT NULL ,
[access] [int] NOT NULL
) ON [PRIMARY]
GO

ALTER TABLE [dbo].[Users] WITH NOCHECK ADD


CONSTRAINT [PK_Users] PRIMARY KEY CLUSTERED
(
[id]
) ON [PRIMARY]
GO

Создание проекта приложения


Создайте приложения LoginApp при помощи мастера проектов, а затем добавьте в его
главное окно поля ввода идентификатора пользователя и пароля, элемент управления
DadaGrid, а также кнопки Войти и Обновить.
Расположение этих элементов управления в окне приложения показано на рис. 9-
32.
Соединение с базой данных
Затем откройте вкладку Data инструментальной панели Toolbox системы Microsoft Visual
Studio .NET и перетащите из нее в окно приложения значок компонента SqlConnection,
обеспечивающего соединение с базой данных. Настраивая свойство ConnectionString,
созданного при этом объекта sqlConnection1, Вы можете создать новое соединение, как
это было описано ранее в этой главе, или использовать соединение, созданное ранее
для приложения SQLTestApp.
Запросы SQL
Далее перетащите два раза в окно приложения компонент SqlCommand. В результате
будет создано два объекта, предназначенных для выполнения команд SQL.
Первый из них с идентификатором sqlCommand1 предназначен для выборки
информации из таблицы Users. Ниже мы показали строку команды SQL, которую нужно
записать в свойство CommandText:
SELECT id, login, password, access FROM dbo.Users
Объект sqlCommand2 содержит команду с параметрами, предназначенную для
поиска в таблице Users строки пользователя с заданным идентификатором и паролем:
SELECT id, login, password, access FROM dbo.Users
WHERE (login = @login) AND (password = @password)
Вы можете подготовить эту команду с помощью мастера Query Builder (рис. 9-35).

Рис. 9-35. Подготовка команды для объекта sqlCommand2

Окно этого мастера появится на экране при редактировании свойства


CommandText объекта sqlCommand2.
Здесь Вам нужно в окне Users отметить флажки всех столбцов таблицы.
Кроме того, в столбце Criteria задайте два условия для полей Login и Password:
= @login
= @password
Когда текст запроса SQL будет готов, щелкните кнопку OK чтобы закрыть окно
мастера Query Builder.
Так как текст команды SQL ссылается на параметры @login и @password, нам
нужно определить эти параметры, отредактировав свойство Parameters объекта
sqlCommand2. Эта операция выполняется с помощью редактора SqlParameter Collection
Editor (рис. 9-36).
Рис. 9-36. Свойства параметра @login

Каждый параметр имеет набор свойств. Эти свойства можно редактировать в


правой части окна редактора.
Установите свойства параметра @login, как это показано на рис. 9-36. Установку
свойства параметра @password мы показали на рис. 9-37.

Рис. 9-37. Свойства параметра @password

Как видите, отличия есть только в свойствах SourceColumn и ParameterName,


задающих столбец таблицы и название параметра, соответственно.
Адаптер SqlDataAdapter
Для редактирования содержимого таблицы Users в приложении LoginApp мы будем
использовать те же самую технологию, что и в ранее рассмотренном приложении
SQLTestApp, основанную на использовании адаптера SqlDataAdapter в паре с элементом
управления DataGrid.
Итак, перетащите значок компонента SqlDataAdapter в окно приложения
LoginApp. При этом в приложении будет создан объект sqlDataAdapter1.
Во втором окне мастера адаптеров выберите то же самое соединение, что было
использовано для объекта sqlConnection1. Настройте свойства адаптера аналогично
тому, как Вы это делали для приложения SQLTestApp (рис. 9-38), но укажите в
командах SQL таблицу Users.
Рис. 9-38. Настройка свойств адаптера sqlDataAdapter1

Кроме того, создайте набор данных DataSet, аналогичный набору данных из


приложения SQLTestApp, в котором будет храниться содержимое таблицы Users.
Отображение таблиц, задаваемое свойством TableMappings объекта sqlDataAdapter1,
должно быть таким, как показано на рис. 9-39.

Рис. 9-39. Настройка отображения таблиц

Ссылка на созданный набор данных класса DataSet будет храниться в поле


dataSet11.
Элемент управления DataGrid
Как мы уже говорили, элемент управления DataGrid будет использоваться в нашем
приложении для просмотра и редактирования содержимого набора данных dataSet11. В
этом наборе данных будет находиться содержимое таблицы Users, скопированной с
сервера SQL.
Настраивая свойства элемента управления DataGrid, укажите в свойстве
DataSource ссылку на объект dataSet11, а в свойстве DataMember — таблицу Users.
Для того чтобы снабдить отображаемую таблицу заголовком, задайте текст
заголовка в свойстве CaptionText. Напомним, что шрифт заголовка можно изменить,
редактируя свойство CaptionFont.
По умолчанию элемент управления DataGrid будет отображать все столбцы набора
данных dataSet11, причем имена соответствующих столбцов набора данных будут
использованы в качестве заголовков столбцов отображаемой таблицы.
Для того чтобы скрыть столбец id, который не содержит никакой информации,
интересной для пользователя приложения, создайте стиль таблицы, отредактировав
свойство TableStyles. Добавьте один стиль, как это показано на рис. 9-40.

Рис. 9-40. Добавление стиля таблицы

Укажите в свойстве MappingName имя таблицы Users.


Далее Вам нужно создать три стиля для каждого столбца таблицы, отредактировав
свойство GridColumnStyles (рис. 9-41).

Рис. 9-41. Стиль для столбца идентификаторов пользователя

Здесь Вы должны указать привязку каждого столбца при помощи свойства


MappingName, а также задать заголовок столбца, отредактировав свойство HeaderText.
В результате получится таблица со столбцами Идентификатор, Пароль и Доступ (рис. 9-
32).
Подключение пользователя
Согласно логике работы приложения LoginApp, когда пользователь щелкает кнопку
Войти, выполняется проверка идентификатора и пароля пользователя. Если
предъявленная парольная информация отсутствует в таблице Users, а также, если
идентификатор и пароль не соответствуют друг другу, приложение выводит сообщение
на экран с текстом «Доступ запрещен».
Когда идентификатор и пароль указан правильно, приложение проверяет код
доступа пользователя. Как мы уже говорили, пользователю с кодом доступа 80
разрешается редактирование таблицы Users, а пользователям с другими кодами
доступа — только просмотр этой таблицы.
Чтобы реализовать такое поведение, создайте для кнопки Войти следующий
обработчик событий:
private void button1_Click(object sender, System.EventArgs e)
{
SqlDataReader myReader;

try
{
sqlConnection1.Open();
myReader = sqlCommand1.ExecuteReader();

if(!myReader.Read())
{
sqlConnection1.Close();
myReader.Close();
CreateAdminLogin();
}
else
{
myReader.Close();

sqlCommand2.Parameters["@login"].Value = LoginTextBox.Text;
sqlCommand2.Parameters["@password"].Value =
passwordTextBox.Text;

myReader = sqlCommand2.ExecuteReader();

if(!myReader.Read())
{
MessageBox.Show("Доступ запрещен", "Ошибка");
myReader.Close();
sqlConnection1.Close();
return;
}

if(LoginTextBox.Text == myReader.GetString(1) &&


passwordTextBox.Text == myReader.GetString(2))
{
if(myReader.GetInt32(3) != 80)
{
dataGrid1.ReadOnly = true;
button2.Enabled = false;
}
else
{
dataGrid1.ReadOnly = false;
button2.Enabled = true;
}

myReader.Close();

dataSet11.Clear();
sqlDataAdapter1.Fill(dataSet11);
}
else
{
MessageBox.Show("Доступ запрещен", "Ошибка");
}
myReader.Close();
sqlConnection1.Close();
}
}
catch(Exception ex)
{
MessageBox.Show(ex.Message, "Ошибка");
}
}
Рассмотрим работу этого обработчика в деталях.
Прежде всего, заметим, что мы применили здесь класс SqlDataReader. Этот класс
очень удобен для извлечения результатов запросов SQL в режиме последовательного
чтения.
Получив управление, обработчик события button1_Click открывает соединение
sqlConnection1, о котором мы говорили раньше:
sqlConnection1.Open();
Параметры этого соединения мы задавали во время проектирования нашего
приложения.
Далее наш обработчик событий исполняет команду SQL, хранящуюся в объекте
sqlCommand1:
SqlDataReader myReader;

myReader = sqlCommand1.ExecuteReader();
В результате выполнения метода ExecuteReader создается объект класса
SqlDataReader, позволяющий последовательно прочитать все строки результата
выполнения команды SQL.
Напомним, что эта команда выбирает все строки таблицы Users, возвращая
столбцы id, login, password и access. Для выборки нужно использовать метод
SqlDataReader.Read, вызывая его в цикле.
Когда все строки, полученные в результате выполнения команды SQL, будут
получены, метод SqlDataReader.Read возвратит значение false. Мы используем это
обстоятельство в самом начале работы обработчика событий button1_Click, для того
чтобы определить, имеются ли в таблице Users какие-либо записи:
if(!myReader.Read())
{
sqlConnection1.Close();
myReader.Close();
CreateAdminLogin();
}
else
{

}
Если таблица Users пустая (как бывает при самом первом запуске приложения),
обработчик закрывает соединение методом sqlConnection1.Close, а также закрывает
объект SqlDataReader. После этого он вызывает метод CreateAdminLogin,
предназначенный для создания самой первой учетной записи администратора в
таблице Users. Исходный текст этого метода мы рассмотрим чуть позже.
Теперь мы рассмотрим ситуацию, когда в таблице Users уже имеются какие-то
записи. Вы можете добавить новые записи в эту таблицу, например, вручную с
помощью утилиты Microsoft SQL Server Enterprise Manager.
Прежде всего, в этом случае мы закрываем ненужный нам больше объект
SqlDataReader:
myReader.Close();
Теперь нам нужно выполнить команду sqlCommand2, которая выбирает из
таблицы Users учетные записи с заданным идентификатором пользователя и паролем.
Перед выполнением этой команды необходимо подготовить параметры @login и
@password, воспользовавшись для этого свойством Value контейнера
sqlCommand2.Parameters:
sqlCommand2.Parameters["@login"].Value = LoginTextBox.Text;
sqlCommand2.Parameters["@password"].Value = passwordTextBox.Text;
myReader = sqlCommand2.ExecuteReader();
В результате выполнения этого запроса будет создан объект класса
SqlDataReader. Если таблица Users не содержит ни одной записи с заданным
идентификатором пользователя и паролем, то метод myReader.Read вернет значение
false:
if(!myReader.Read())
{
MessageBox.Show("Доступ запрещен", "Ошибка");
myReader.Close();
sqlConnection1.Close();
return;
}
Это означает, что был задан неправильный идентификатор или пароль
пользователя. В этом случае наш обработчик событий выводит сообщение об отказе в
доступе, закрывает объект myReader и соединение sqlConnection1, а затем возвращает
управление.
Теперь мы рассмотрим случай, когда идентификатор и пароль был введен
правильно.
Наша программа выполняет дополнительную проверку, сравнивая данные из
ячеек строки, считанной методом myReader.Read, со значениями, введенными
пользователем в полях Идентификатор и Пароль:
if(LoginTextBox.Text == myReader.GetString(1) &&
passwordTextBox.Text == myReader.GetString(2))
{

}
else
{
MessageBox.Show("Доступ запрещен", "Ошибка");
}
myReader.Close();
sqlConnection1.Close();
Обратите внимание на то, как мы извлекаем значения ячеек прочитанной
строки — для получения текстовой строки мы вызываем метод myReader.GetString,
указывая ей в качестве параметра индекс столбца. Самый первый столбец нашей
таблицы id имеет индекс 0, второй столбец Login — индекс 1 и т.д.
В табл. 9-2 мы перечислили некоторые методы класса SqlDataReader,
предназначенные для извлечения из ячеек строки данных различных типов,
встроенных в язык программирования C#.
Таблица 9-2. Методы класса SqlDataReader для извлечения содержимого ячеек
строки стандартных типов данных
Имя метода Тип возвращаемого значения
GetBoolean bool
GetByte byte
GetChar char
GetDateTime DateTime
GetDecimal decimal
GetDouble double
GetFloat float
GetGuid Guid
GetInt16 short
GetInt32 int
Getint64 long
GetString string
Метод GetValue позволяет получить данные в естественном формате, т.е. в том
формате, в котором они хранятся в ячейке строки.
Специально для работы с серверами баз данных, такими как Microsoft SQL Server,
этот набор был расширен методами, возвращающими данные в форматах этих
серверов. Некоторые из этих методов перечислены в табл. 9-3.
Таблица 9-3. Методы класса SqlDataReader для извлечения содержимого ячеек
строки типов данных серверов SQL
Имя метода Тип Описание
возвращаемого
значения
GetSqlBinary SqlBinary Поток двоичных данных переменной
длины
GetSqlBoolean SqlBoolean Целое значение 0 или 1
GetSqlByte SqlByte Целое значение без знака размером 8
разрядов и значением от 0 до 255
GetSqlDateTime SqlDateTime Дата и время, исчисляемое в периодах
системного таймера от 1 января 1753
года
GetSqlDecimal SqlDecimal Числовое значение с фиксированной
точкой в диапазоне от -1038 -1 до 10
38 –1
GetSqlDouble SqlDouble Числовое значение с плавающей
точкой в диапазоне от -1.79E +308 до
1.79E +308
GetSqlGuid SqlGuid Глобальный уникальный
идентификатор GUID
GetSqlInt16 SqlInt16 16-разрядное целое со знаком
GetSqlInt32 SqlInt32 32-разрядное целое со знаком
GetSqlInt64 SqlInt64 64-разрядное целое со знаком
GetSqlMoney SqlMoney Денежные суммы в диапазоне от -263
(-922 337 203 685 477,5808) до 2 63 -1
(+922 337 203 685 477,5807)
GetSqlSingle SqlSingle Числовое значение с плавающей
точкой в диапазоне от -3.40E +38 до
3.40E +38
GetSqlString SqlString Поток символов переменного размера
Вернемся к нашему приложению.
Итак, обработчик события button1_Click, создаваемого кнопкой Войти, определил,
что пользователь ввел правильный идентификатор и пароль. Теперь ему нужно
получить код доступа ткущего пользователя. Он извлекает этот код из столбца access,
имеющего индекс 3, при помощи метода GetInt32:
if(myReader.GetInt32(3) != 80)
{
dataGrid1.ReadOnly = true;
button2.Enabled = false;
}
else
{
dataGrid1.ReadOnly = false;
button2.Enabled = true;
}
Если код доступа не равен 80, обработчик события приравнивает свойству
dataGrid1.ReadOnly значение true. Это запрещает редактирование строк, отображаемых
в окне элемента управления DataGrid. Кроме этого, обработчик блокирует кнопку
Обновить, приравнивая свойству button2.Enabled значение false.
В том случае, когда код доступа пользователя равен 80, обработчик события
разрешает редактирование строк и разблокирует кнопку Обновить, изменяя
соответствующим образом свойства dataGrid1.ReadOnly и button2.Enabled.
Завершив проверку прав доступа пользователя, обработчик событий закрывает
объект myReader, т.к. он больше не понадобится:
myReader.Close();
Дальнейшие действия обработчика button1_Click заключаются в очистке
содержимого набора данных dataSet11 с последующим его наполнением из таблицы
Users сервера SQL:
dataSet11.Clear();
sqlDataAdapter1.Fill(dataSet11);
Эта работа выполняется с помощью адаптера sqlDataAdapter1. Соответствующие
процедуры были подробно описаны в разделе «Приложение SQLTestApp» этой главы.
Обновление таблицы Users
Для обновления таблицы Users, отредактированной пользователем с кодом доступа 80,
мы применяем адаптер sqlDataAdapter1 и метод Update. Вот исходный текст
обработчика событий кнопки Обновить:
private void button2_Click(object sender, System.EventArgs e)
{
try
{
sqlDataAdapter1.Update(dataSet11);
}
catch(Exception ex)
{
MessageBox.Show(ex.Message, "Ошибка");
}
}
Точно такая же методика обновления применялась и для таблицы Contacts в
упомянутом ранее приложении SQLTestApp.
Хранение дерева в базе данных
Очень часто возникает задача хранения информации, организованной иерархически.
Реляционные базы данных, такие как Microsoft SQL Server, позволяют хранить
информацию в виде иерархического дерева, причем для представления такого дерева
нужна всего одна таблица.
Каждая строка таблицы, хранящей структуру дерева, соответствует одному узлу
этого дерева. При этом в таблице должно быть, как минимум, два столбца. Первый из
них должен содержать уникальные идентификаторы строки (т.е. идентификаторы
узлов), а второй — идентификатор соответствующего родительского узла. Для
корневого узла в качестве идентификатора родительского узла обычно используется
нулевое или какое-либо другое особое значение.
Для демонстрации способа хранения дерева в базе данных, а также для того
чтобы на конкретном примере изучить некоторые новые для нас методы работы с
базами данных в приложениях C#, разработали приложение ArticlesApp. Это
приложение будет подробно рассмотрено в следующем разделе.
Приложение ArticlesApp
Приложение ArticlesApp представляет собой простейшую информационную систему,
предназначенную для хранения текстовой информации. В базе данных этой системы
хранятся статьи, организованные иерархическим образом.
Главное окно приложения ArticlesApp показано на рис. 9-42.

Рис. 9-42. Главное окно приложения ArticlesApp

В левой части этого окна имеется дерево, созданное с использованием элемента


управления TreeView. Окно этого дерева отображает заголовки статей, а также так
называемые веса сортировки заголовков, указанные в круглых скобках.
Заметим, что сразу после запуска приложения, когда в базе данных нет ни одной
записи, дерево заголовков не содержит ни одного элемента. Если щелкнуть окно
дерева правой клавишей мыши, на экране появится контекстное меню со строками Add,
Delete и Edit.
Выбор строки Add приведет к тому, что на экране появится диалоговое окно,
показанное на рис. 9-43.
Рис. 9-43. Добавление новой или редактирование существующей статьи

При помощи этого окна можно добавить в базу данных новую статью, определив
для нее заголовок, тело и вес сортировки.
Если в дереве нет ни одного элемента, то при первом использовании строки Add
контекстного меню в дерево будет добавлен корневой элемент.
Для того чтобы добавить в дерево дочерний элемент, нужно вначале выделить
левой клавишей мыши заголовок родительского элемента, а потом, щелкнув этот
заголовок правой клавишей мыши, выбрать из контекстного меню строку Add.
Редактирование любого элемента выполняется аналогично. Для выполнения этой
операции нужно выделить элемент, а затем, щелкнув его правой клавишей мыши,
выбрать из контекстного меню строку Edit.
С помощью строки Delete можно удалить элемент дерева. Заметим, что программа
удаляет только элементы, не имеющие дочерних элементов. Попытки удалить элемент с
дочерними элементами наше приложение игнорирует.
Для чего нужен вес сортировки?
Мы используем его для управления порядком размещения статей, находящихся на
одном уровне иерархии. Чем этот вес меньше, тем статья располагается выше в окне
дерева. Использование какой-либо другой сортировки, например, сортировки по
алфавиту, для решения задачи упорядочивания заголовков статей неэффективно, т.к.
только тот, кто создает базу данных статей, знает, как нужно располагать заголовки.
База данных Articles
Прежде чем создавать проект приложения ArticlesApp, подготовим базу данных Articles.
В этой базе нам нужно создать две таблицы и три хранимые процедуры.
Таблица Tree
Таблица Tree предназначена для хранения структуры дерева. В ней необходимо
создать четыре столбца с именами id, parent_id, title и weight. Столбец id должен быть
первичным ключом.
Вот сценарий SQL, при помощи которого можно создать таблицу Tree:
CREATE TABLE [dbo].[Tree] (
[id] [int] IDENTITY (1, 1) NOT NULL ,
[parent_id] [int] NOT NULL ,
[title] [varchar] (50) COLLATE Cyrillic_General_CI_AS NOT NULL ,
[weight] [int] NOT NULL
) ON [PRIMARY]
GO

ALTER TABLE [dbo].[Tree] WITH NOCHECK ADD


CONSTRAINT [PK_Tree] PRIMARY KEY CLUSTERED
(
[id]
) ON [PRIMARY]
GO
Здесь столбец id хранит идентификаторы узлов дерева, а столбец parent_id —
идентификаторы родительских узлов. Таким образом, вместе с каждым узлом хранится
идентификатор его родительского узла.
Поля title и weight предназначены, соответственно, для хранения заголовка
статьи и веса сортировки, назначенного этой статье.
Таблица Documents
Мы могли бы хранить тексты документов непосредственно в таблице Tree, однако это
привело бы к неэффективному расходованию памяти.
В самом деле, при отображении дерева нам фактически нужно загрузить в память
все содержимое таблицы Tree. Однако в каждый момент времени мы просматриваем
или редактируем только одну статью, поэтому нет никакой необходимости загружать
эти данные в память вместе со структурой дерева.
Для хранения текстов статей мы создали отдельную таблицу Documents,
содержащую столбцы id, document и tree_id. Первый из этих столбцов является
ключевым:
CREATE TABLE [dbo].[Documents] (
[id] [int] IDENTITY (1, 1) NOT NULL ,
[document] [varchar] (5000) COLLATE Cyrillic_General_CI_AS NOT NULL ,
[tree_id] [int] NOT NULL
) ON [PRIMARY]
GO

ALTER TABLE [dbo].[Documents] WITH NOCHECK ADD


CONSTRAINT [PK_Documents] PRIMARY KEY CLUSTERED
(
[id]
) ON [PRIMARY]
GO
В столбце id таблицы Documents хранятся уникальные идентификаторы статей,
которые напрямую не используются в нашем приложении.
Столбец tree_id хранит идентификатор узла дерева, соответствующего данной
статье. Этот столбец является внешним ключом для таблицы Tree.
И, наконец, столбец document хранит текст самой статьи.
Хранимая процедура sp_InsertDocument
Часть работы с базой данных наше приложение будет выполнять при помощи команд
SQL, оформленных в виде объектов класса SqlCommand. Однако на примере этого
приложения мы покажем Вам как можно работать с хранимыми процедурами сервера
Microsoft SQL Server.
Хранимая процедура sp_InsertDocument предназначена для добавления нового
документа в таблицу Documents:
CREATE PROCEDURE [dbo].[sp_InsertDocument]
@tree_id AS INT,
@document AS VARCHAR(2000)
AS

INSERT INTO dbo.Documents(tree_id, document) VALUES (@tree_id, @document);


RETURN @@identity
GO
Этой процедуре необходимо передать два параметра @tree_id и @document.
Первый из этих параметров предназначен для передачи идентификатора узла, в
который добавляется статья, а второй — для передачи текста этой статьи.
Процедура возвращает идентификатор добавленной строки @@identity.
Хранимая процедура sp_ InsertNode
Хранимая процедура sp_ InsertNode вставляет новую строку в таблицу Tree, возвращая
идентификатор новой строки:
CREATE PROCEDURE [dbo].[sp_InsertNode]
@parent_id AS INT,
@title AS VARCHAR(50),
@weight AS INT
AS

INSERT INTO dbo.Tree(parent_id, title, weight) VALUES (@parent_id, @title, @weight);


RETURN @@identity
GO
Этой процедуре нужно передать через входные параметры идентификатор
родительского узла @parent_id (равный 0 для корневого узла), заголовок статьи @title
и вес сортировки @weight.
Хранимая процедура sp_ UpdateDocument
При помощи хранимой процедуры sp_UpdateDocument наше приложение обновляет
тексты статей, хранящиеся в таблице Documents:
CREATE PROCEDURE [dbo].[sp_UpdateDocument]
@tree_id AS INT,
@document AS VARCHAR(2000)
AS

UPDATE dbo.Documents SET document = @document WHERE (tree_id = @tree_id)


GO
В качестве параметра этой хранимой процедуре необходимо передать
идентификатор узла @tree_id обновляемой статьи, а также текст статьи @document.
Создание проекта приложения ArticlesApp
В окно нашего приложения нужно поместить элемент управления TreeView,
разделитель Splitter, а также редактор текста RichTextBox. Дерево TreeView должно
занимать левую часть окна, а редактор RichTextBox — правую. Соответствующие
рекомендации по настройке свойств элементов управления TreeView и RichTextBox мы
приводили в 7 главе.
Помимо только что перечисленных элементов управления в проект приложения
ArticlesApp нужно будет добавить множество других программных компонентов,
предназначенных главным образом для работы с сервером базы данных (рис. 9-44).
Рис. 9-44. Компоненты приложения ArticlesApp

Рассмотрим порядок добавления этих компонент, настройку свойств, а также


дополнительный программный код, который Вы должны добавить для наделения
нашего приложения нужной функциональностью.
Соединение с базой данных
Прежде всего, обеспечьте приложение возможностью соединения с базой данных
Articles. С этой целью добавьте программный компонент SqlConnection. Идентификатор
этого компонента будет храниться в поле sqlConnection1 класса Form1.
Для создания соединения с базой данных используйте те же приемы, что и в
предыдущих приложениях этой главы. Вот, примерно, каким должно быть значение
свойства ConnectionString объекта sqlConnection1:
data source=localhost;initial catalog=Articles;password=12345;
persist security info=True;user id=c_sharp_app;
workstation id=FROLOV;packet size=4096
Вероятно, у Вас будет другое значение идентификатора рабочей станции
workstation id, а также, возможно, идентификатор пользователя user id и пароль
password.
Добавление адаптера SqlDataAdapter
Для того чтобы приложение могло загружать содержимое таблицы Tree базы данных
Articles, хранящей структуру дерева статей, добавьте в него адаптер SqlDataAdapter.
Ссылка на адаптер будет храниться в поле sqlDataAdapter1.
Для этого адаптера необходимо использовать соединение sqlConnection1, о
котором мы говорили в предыдущем разделе.
Детально процедура добавления адаптера уже рассматривалась в этой главе,
поэтому мы не будем описывать ее заново.
Создание набора данных DataSet
После добавления адаптера создайте набор данных DataSet, воспользовавшись строкой
Generate Dataset контекстного меню. Это меню появится на экране, если щелкнуть
правой клавишей мыши область окна дизайнера форм, занимаемую значками
программных компонентов (нижняя часть окна, показанного на рис. 9-44).
В результате должен быть создан набор данных dataSet11. Кроме того, в проект
будут автоматически добавлены компоненты класса SqlCommand, предназначенные
для чтения, обновления, удаления и добавления данных в таблицу Tree. Эти команды
предназначены для совместной работы с адаптером sqlDataAdapter1, но могут
использоваться и независимо от него.
Добавление контекстного меню
До сих пор в этой книге мы не касались приемов создания и редактирования
контекстного меню. Вы можете легко добавить такое меню в приложение, перетащив
значок компонента ContextMenu из инструментальной панели Toolbox в окно элемента
управления, с которым это меню должно быть связано.
В нашем случае нужно связать контекстное меню с деревом просмотра заголовков
статей treeView1, перетащив в него упомянутый значок.
Если выделить левой клавишей мыши компонент contextMenu1 в области значков
программных компонентов дизайнера форм, то в верхней части формы появится меню
Context Menu, показанное на рис. 9-45.

Рис. 9-45. Редактирование контекстного меню

Вы сможете редактировать это меню и создавать обработчики событий для его


строк аналогично тому, как это делается и для обычного главного меню формы.
Создайте в контекстном меню строки Add, Delete и Edit. Первая из этих строк
будет использована для создания узлов дерева, вторая — для удаления этих узлов, а
третья — для редактирования информации, хранящейся в узле дерева (заголовка, веса
сортировки и текста статьи).
Создание узла дерева
Создание дерева начинается с того, что пользователь запускает приложение, щелкает
правой клавишей мыши пустое окно дерева и выбирает из контекстного меню строку
Add. В результате на экране появляется диалоговое окно, показанное на рис. 9-43, где
пользователь может ввести информацию для узла дерева.
Сейчас мы реализуем эту часть приложения, отвечающую за добавление новых
узлов в дерево.
Прежде всего, создайте обработчик событий для строки Add контекстного меню
contextMenu1, щелкнув его дважды левой клавишей мыши. Отредактируйте исходный
текст обработчика следующим образом:
private void menuItem1_Click(object sender, System.EventArgs e)
{
if(treeView1.SelectedNode != null)
{
int id = (int)treeView1.SelectedNode.Tag;
AddNode(id);
UpdateTree();
}
else
{
// Пустой список
if(treeView1.Nodes.Count == 0)
{
AddNode(0);
UpdateTree();
}
}
}
При самом первом запуске приложения и пустой базе данных в дереве treeView1
не выделено ни одного элемента, т.к. их там попросту нет. Соответственно, количество
узлов дерева treeView1.Nodes.Count равно нулю. В этом случае наше приложение
вызывает два метода:
AddNode(0);
UpdateTree();
Метод AddNode, определенный в нашем приложении, создает узел дерева. В
качестве единственного параметра этому методу нужно передать идентификатор
родительского узла. Так как в первый раз мы создаем корневой узел, то передаем
методу AddNode нулевое значение.
Что же касается метода UpdateTree, то он тоже определен в нашем приложении.
Его задачей является наполнение окна дерева treeView1 содержимым таблицы Tree
базы данных Articles. Мы вызываем этот метод всякий раз после внесения изменений в
структуру дерева (т.е. после добавления или удаления узлов дерева).
Для того чтобы содержимое дерева отображалось сразу осле запуска приложения,
мы добавили вызов метода UpdateTree в конструктор класса Form1:
public Form1()
{
//
// Required for Windows Form Designer support
//
InitializeComponent();

//
// TODO: Add any constructor code after InitializeComponent call
//
UpdateTree();
}
Методы AddNode и UpdateTree мы рассмотрим в деталях позже, а пока вернемся к
обработчику событий menuItem1_Click, созданному нами для строки Add контекстного
меню.
В том случае, если в дереве есть узлы, и пользователь выделил какой-либо узел
левой клавишей мыши или при помощи клавиатуры, наш обработчик событий
menuItem1_Click выполняет следующие действия:
if(treeView1.SelectedNode != null)
{
int id = (int)treeView1.SelectedNode.Tag;
AddNode(id);
UpdateTree();
}
Вначале он извлекает из свойства treeView1.SelectedNode.Tag идентификатор
строки таблицы Tree, соответствующий выделенному узлу. Этот идентификатор
записывается в данное свойство методом UpdateTree в процессе построения дерева.
Заметим, что данный идентификатор обозначает узел, являющийся родительским
по отношению к создаваемому узлу. Обработчик событий menuItem1_Click передает
этот идентификатор методу AddNode, а затем перерисовывает обновленное дерево
методом UpdateTree:
AddNode(id);
UpdateTree();

Метод AddNode
Создайте в классе Form1 метод AddNode. Как мы только что говорили, этот метод
предназначен для создания нового узла в дереве заголовков статей. Он не только
добавляет новый узел в окно элемента управления treeView1, но и создает все
необходимые записи в базе данных Articles.
Ниже мы привели полный исходный текст метода AddNode:
public void AddNode(int id)
{
Form2 dialog = new Form2();
if(DialogResult.Yes == dialog.ShowDialog())
{
sqlConnection1.Open();
try
{
SqlCommand cmd = new SqlCommand("sp_InsertNode",
sqlConnection1);
cmd.CommandType = CommandType.StoredProcedure;

SqlParameter param = cmd.Parameters.Add("RETURN_VALUE",


SqlDbType.Int);

param.Direction = ParameterDirection.ReturnValue;

cmd.Parameters.Add("@parent_id", SqlDbType.Int).Value = id;


cmd.Parameters.Add("@title", SqlDbType.VarChar).Value =
dialog.Title;
cmd.Parameters.Add("@weight", SqlDbType.Int).Value =
dialog.Weight;

cmd.ExecuteNonQuery();

int tree_id = (int)cmd.Parameters["RETURN_VALUE"].Value;

cmd = new SqlCommand("sp_InsertDocument", sqlConnection1);


cmd.CommandType = CommandType.StoredProcedure;

param = cmd.Parameters.Add("RETURN_VALUE", SqlDbType.Int);


param.Direction = ParameterDirection.ReturnValue;

cmd.Parameters.Add("@tree_id", SqlDbType.Int).Value = tree_id;


cmd.Parameters.Add("@document", SqlDbType.VarChar).Value =
dialog.Document;

cmd.ExecuteNonQuery();

int document_id = (int)cmd.Parameters["RETURN_VALUE"].Value;


}
catch(Exception ex)
{
MessageBox.Show(ex.Message, "Ошибка");
}
sqlConnection1.Close();
}
}
Займемся описанием этого исходного текста.
Диалоговое окно для ввода данных добавляемого узла
В самом начале работы метод AddNode отображает на экране диалоговое окно,
показанное на рис. 9-43. Вам нужно будет создать форму для данного окна, но этим мы
займемся позже.
Окно создается обычным образом с помощью конструктора. Если пользователь
закрыл диалоговое окно с помощью кнопки Сохранить, метод AddNode извлекает
данные, введенный пользователем, и добавляет их в таблицы базы данных Articles:
Form2 dialog = new Form2();
if(DialogResult.Yes == dialog.ShowDialog())
{

}
Открытие и закрытие соединения с базой данных
Для добавления данных, прежде всего, открывается соединение с базой данных:
sqlConnection1.Open();
Все дальнейшие операции выполняются в теле оператора try-catch, что позволяет
перехватывать ошибки и отображать текст сообщений об ошибках на экране:
try
{

}
catch(Exception ex)
{
MessageBox.Show(ex.Message, "Ошибка");
}

sqlConnection1.Close();
После обновления содержимого базы данных метод AddNode закрывает
соединение с базой данных.
Использование хранимых процедур
Теперь мы займемся описанием кода, добавляющего новые записи в базу данных. Этот
код интересен тем, что мы не будем встраивать в него команды SQL, а воспользуемся
механизмом хранимых процедур сервера Microsoft SQL Server.
Прежде всего, наша программа вызывает хранимую процедуру sp_InsertNode,
предназначенную для добавления новой строки в таблицу Tree, хранящую структуру
дерева. Напомним, что этой процедуре нужно передать через входные параметры
идентификатор родительского узла @parent_id, заголовок статьи @title и вес
сортировки @weight.
Вызов хранимой процедуры начинается с создания объекта класса SqlCommand:
SqlCommand cmd = new SqlCommand("sp_InsertNode", sqlConnection1);
Далее программа должна задать тип команды в свойстве CommandType в виде
константы CommandType.StoredProcedure:
cmd.CommandType = CommandType.StoredProcedure;
Это означает, что команда содержит не строку SQL, а имя хранимой процедуры.
Параметры хранимых процедур
На следующем этапе необходимо добавить параметры хранимой процедуры. Наша
хранимая процедура sp_InsertNode имеет три входных и один выходной параметр.
Через выходной параметр со специальным именем RETURN_VALUE хранимая
процедура возвращает идентификатор добавленной строки:
SqlParameter param = cmd.Parameters.Add("RETURN_VALUE",
SqlDbType.Int);
В качестве первого параметра методу Add передается имя параметра хранимой
процедуры, а в качестве второго — тип данных, соответствующих этому параметру.
Чтобы указать, что этот параметр является выходным, мы записываем константу
ParameterDirection.ReturnValue в свойство параметра с именем Direction:
param.Direction = ParameterDirection.ReturnValue;
Если этого не сделать, то по умолчанию параметр будет входным.
Вот как мы указываем входные параметры для хранимой процедуры
sp_InsertNode:
cmd.Parameters.Add("@parent_id", SqlDbType.Int).Value = id;
cmd.Parameters.Add("@title", SqlDbType.VarChar).Value = dialog.Title;
cmd.Parameters.Add("@weight", SqlDbType.Int).Value = dialog.Weight;
Обратите внимание на то, что тип числовых данных указан как SqlDbType.Int, а
тип строчных данных — как SqlDbT ype.VarChar.
Параметру хранимой процедуры @parent_id мы присваиваем значение
идентификатора родительского узла, который передается при вызове методу AddNode.
Что же касается параметров @title и @weight, то для их инициализации мы извлекаем
значения из свойств Title и Weight, определенных нами в классе Form2 диалогового
окна ввода данных узла (рис. 9-43).
Запуск хранимой процедуры
Для запуска хранимой процедуры на выполнение мы вызываем метод ExecuteNonQuery:
cmd.ExecuteNonQuery();
Если у хранимой процедуры имеются параметры (как в нашем случае), то их
необходимо подготовить. Иначе при выполнении метода ExecuteNonQuery возникнет
исключение.
Получение значений выходных параметров
После того как хранимая процедура завершит свою работу, программа может получить
значение ее выходных параметров при помощи свойства Value.
Вот как мы извлекаем значение, возвращаемое хранимой процедурой
sp_InsertNode:
int tree_id = (int)cmd.Parameters["RETURN_VALUE"].Value;
Напомним, что наша хранимая процедура возвращает идентификатор узла,
добавленного в таблицу Tree. Этот идентификатор понадобится нам в дальнейшем для
инициализации ячейки внешнего ключа таблицы Documents, ссылающейся на таблицу
Tree.
Добавление текста документа
Для добавления текста документа, извлеченного из свойства dialog.Document
диалогового окна класса Form2 мы вызываем хранимую процедуру sp_InsertDocument:
cmd = new SqlCommand("sp_InsertDocument", sqlConnection1);
cmd.CommandType = CommandType.StoredProcedure;
Эта процедура имеет один выходной параметр и два входных:
param = cmd.Parameters.Add("RETURN_VALUE", SqlDbType.Int);
param.Direction = ParameterDirection.ReturnValue;

cmd.Parameters.Add("@tree_id", SqlDbType.Int).Value = tree_id;


cmd.Parameters.Add("@document", SqlDbType.Text).Value =
dialog.Document;
Выходной параметр получает получить идентификатор новой строки в таблице
Documents. Мы извлекаем его только для примера, но в приложении не используем.
Что же касается входных параметров, то хранимой процедуре sp_InsertDocument
передается идентификатор узла @tree_id обновляемой статьи, а также текст статьи
@document.
Обратите внимание, что параметр @document имеет тип SqlDbType.Text.
Хранимая процедура sp_InsertDocument запускается при помощи метода
ExecuteNonQuery, после чего извлекается результат ее выполнения:
cmd.ExecuteNonQuery();
int document_id = (int)cmd.Parameters["RETURN_VALUE"].Value;

Диалоговая форма редактирования документа


Для того чтобы создать диалоговую форму редактирования документа, Вам нужно
добавить к проекту еще одну экранную форму Form2. Соответствующие процедуры
были подробно описаны в 5 главе нашей книги.
Для того чтобы программа могла инициализировать поля формы, а также получать
значения, введенные в ней пользователем, необходимо создать в классе Form2
свойства Title, Weight и Document. Каждое из этих свойств должно содержать методы
доступа set и get.
Свойство Title
Свойство Title должно быть определено следующим образом:
public string Title
{
get
{
return textBox1.Text;
}
set
{
textBox1.Text = value;
}
}
Предполагается, что это свойство связано с полем редактирования заголовка
статьи textBox1.
Свойство Weight
Свойство Weight предназначено для ввода и редактирования веса сортировки статьи.
вес сортировки представляет собой числовое значение. Для его редактирования мы
использовали элемент управления класса NumericUpDown.
Вот исходный текст методов доступа свойства Weight:
public int Weight
{
get
{
return (int)numericUpDown1.Value;
}
set
{
numericUpDown1.Value = value;
}
}
Свойство Document
Текст статей мы создаем и редактируем при помощи элемента управления richTextBox1.
Вот исходный текст методов доступа свойства Document, с помощью которого наша
программа может отображать исходный текст статьи, а также загружать
отредактированный вариант статьи:
public string Document
{
get
{
return richTextBox1.Text;
}
set
{
richTextBox1.Text = value;
}
}

Построение дерева
Для построения дерева заголовков статей в окне элемента управления TreeView в
нашем приложении определен метод UpdateTree, на который мы уже ссылались ранее,
а также метод CreateNodes.
Метод UpdateTree
Метод UpdateTree считывает структуру дерева из базы данных Articles и отображает ее
в окне элемента управления treeView1 класса TreeView.
Вот исходный текст этого метода:
public void UpdateTree()
{
dataSet11.Clear();
sqlDataAdapter1.Fill(dataSet11);

treeView1.Nodes.Clear();
CreateNodes(0,(TreeNode)null);
treeView1.ExpandAll();
}
Получив управление, метод UpdateTree очищает набор данных dataSet11, а затем
наполняет его из таблицы Tree базы данных Aticles, пользуясь для этого адаптером
sqlDataAdapter1 и методом Fill.
На следующем этапе метод UpdateTree удаляет все элементы из дерева treeView1.
Заполнение дерева treeView1 содержимым набора данных dataSet11
осуществляется методом CreateNodes. Далее все узлы заполненного дерева
раскрываются, для чего программа вызывает метод treeView1.ExpandAll.
Метод CreateNodes
Теперь нам нужно изучить метод CreateNodes, который создает в дереве treeView1
узел, соответствующий одной статье. Ниже мы привели исходный текст этого метода:
public void CreateNodes(int iParent, TreeNode pNode)
{
DataView dvwData = new DataView(dataSet11.Tables[0]);
dvwData.RowFilter = "[parent_id] = " + iParent;

foreach(DataRowView Row in dvwData)


{
int id = Int32.Parse(Row["id"].ToString());

if(pNode == null)
{
TreeNode zNode = treeView1.Nodes.Add(Row["title"].ToString() +
" (" + Row["weight"].ToString() + ")");
zNode.Tag = id;

CreateNodes(id, zNode);
}
else
{
if(id == iParent)
{
return;
}
TreeNode zNode = pNode.Nodes.Add(Row["title"].ToString() +
" (" + Row["weight"].ToString() + ")");

zNode.Tag = id;

CreateNodes(id, zNode);
}
}
}
Метод CreateNodes имеет два параметра.
Через первый параметр методу передается идентификатор узла, родительского по
отношению к добавляемому узлу. Если создается корневой узел, то значение этого
параметра равно нулю.
Второй параметр используется для передачи ссылки на родительский узел дерева
treeView1, который является объектом класса TreeNode. При создании корневого узла
этот параметр должен иметь значение null.
Как работает метод CreateNodes?
Прежде всего, он создает представление (view) таблицы dvwData, хранящейся в
наборе данных dataSet11. Это представление включает в себя подмножество строк
таблицы, столбец parent_id которых содержит идентификатор родительского узла,
переданного методу CreateNodes в качестве первого параметра. Иными словами, здесь
происходит отбор дочерних узлов заданного родительского узла.
Вот как создается это представление:
DataView dvwData = new DataView(dataSet11.Tables[0]);
dvwData.RowFilter = "[parent_id] = " + iParent;
В свойстве dvwData.RowFilter мы указываем нужное нам условие отбора строк в
самой первой (и единственной) таблице набора данных dataSet11.
После того как все нужные строки будут отобраны, запускается цикл по всем
строкам полученного представления:
foreach(DataRowView Row in dvwData)
{

}
Внутри этого цикла мы извлекаем идентификатор каждой строки id и сохраняем ее
в одноименной переменной. Для преобразования извлеченного значения в число мы
используем метод Int32.Parse:
int id = Int32.Parse(Row["id"].ToString());
Дальнейшие действия, выполняемые нашей программой, зависят от значения
второго параметра метода CreateNodes.
Вот как добавляется корневой узел дерева:
if(pNode == null)
{
TreeNode zNode = treeView1.Nodes.Add(Row["title"].ToString() +
" (" + Row["weight"].ToString() + ")");

zNode.Tag = id;
CreateNodes(id, zNode);
}
Здесь мы с помощью метода treeView1.Nodes.Add добавляем в дерево treeView1
новый элемент. В качестве текстовой строки, отображаемой в окне дерева, мы
используем заголовок статьи, извлеченный из столбца title текущей извлеченной
строки. К этому заголовку мы дописываем в круглых скобках значение веса
сортировки, взятого из столбца weight.
Далее наша программа записывает в свойство Tag идентификатор узла дерева,
взяв его из столбца id. Пользуясь этим свойством, приложение сможет однозначно
сопоставить элементы дерева, выделенные пользователем в окне элемента управления
treeView1, и строки таблицы Tree, хранящей информацию об узлах дерева в базе
данных Microsoft SQL Server.
И, наконец, метод CreateNodes вызывает сам себя рекурсивно для добавления в
дерево всех узлов, дочерних для только что добавленного элемента. Обратите
внимание, что теперь в качестве второго параметра этому методу передается ссылка на
только что созданный узел дерева zNode.
Теперь настало время рассмотреть действия метода CreateNodes при рекурсивном
вызове, когда значение второго параметра не равно null.
Прежде всего, в этом случае метод проверяет условие выхода из рекурсии:
if(id == iParent)
{
return;
}
Если идентификатор узла равен идентификатору родительского узла, работа
метода завершается оператором return.
В противном случае мы создаем узел дерева, записывая идентификатор узла id в
свойство zNode.Tag:
TreeNode zNode = pNode.Nodes.Add(Row["title"].ToString() +
" (" + Row["weight"].ToString() + ")");

zNode.Tag = id;
Далее следует рекурсивный вызов метода CreateNodes:
CreateNodes(id, zNode);

Редактирование узла дерева


Итак, мы рассмотрели процедуру создания узлов дерева, а также методы,
применяющиеся для отображения дерева в окне элемента управления TreeView. Теперь
нашей задачей будет добавить в приложение код, позволяющий редактировать
содержимое узла, выделенного пользователем в окне дерева.
Обработчик событий меню Edit
Когда пользователь выделяет узел дерева, а затем щелкает его правой клавишей мыши
и выбирает из контекстного меню строку Edit, управление передается обработчику
событий menuItem3_Click. Вы должны создать этот обработчик в следующем виде:
private void menuItem3_Click(object sender, System.EventArgs e)
{
// Пустой список
if(treeView1.Nodes.Count != 0)
{
int id = (int)treeView1.SelectedNode.Tag;

Form2 dialog = new Form2();

sqlConnection1.Open();

SqlDataReader myReader;
string strCmd = String.Format(
"SELECT document FROM Documents WHERE tree_id = {0}", id);
SqlCommand cmd = new SqlCommand(strCmd, sqlConnection1);
myReader = cmd.ExecuteReader();

if(myReader.Read())
{
dialog.Document = myReader.GetString(0).ToString();
}
myReader.Close();

sqlGetNodeCommand.Parameters["@id"].Value = id;
myReader = sqlGetNodeCommand.ExecuteReader();

if(myReader.Read())
{
dialog.Title = myReader.GetString(2).ToString();
dialog.Weight = myReader.GetInt32(3);

if(DialogResult.Yes == dialog.ShowDialog())
{
try
{
sqlUpdateNodeCommand.Parameters["@id"].Value = id;
sqlUpdateNodeCommand.Parameters["@title"].Value =
dialog.Title;
sqlUpdateNodeCommand.Parameters["@weight"].Value =
dialog.Weight;

myReader.Close();

sqlUpdateNodeCommand.ExecuteNonQuery();

cmd = new SqlCommand("sp_UpdateDocument",


sqlConnection1);

cmd.CommandType = CommandType.StoredProcedure;

cmd.Parameters.Add("@tree_id", SqlDbType.Int).Value = id;


cmd.Parameters.Add("@document",SqlDbType.VarChar).Value=
dialog.Document;

cmd.ExecuteNonQuery();

richTextBox1.Text = dialog.Document;
}
catch(Exception ex)
{
MessageBox.Show(ex.Message, "Ошибка");
}
}

sqlConnection1.Close();
UpdateTree();
}
}
}
Получив управление, обработчик событий menuItem3_Click, прежде всего,
проверяет, есть ли какие-либо элементы в окне дерева:
if(treeView1.Nodes.Count != 0)
{

}
Если дерево пустое, обработчик возвращает управление, не предпринимая
никаких действий.
Извлечение идентификатора редактируемого узла
В том случае, когда в дереве есть узлы, обработчик получает идентификатор
выделенного узла, извлекая его из свойства treeView1.SelectedNode.Tag:
int id = (int)treeView1.SelectedNode.Tag;
Напомним, что этот идентификатор равен содержимому столбца id строки таблицы
Tree, описывающей данный узел. Этот идентификатор записывается в свойство
treeView1.SelectedNode.Tag во время заполнения окна элемента управления treeView1.
Идентификатор id потребуется нам для извлечения из таблицы Tree информации
об узле дерева, подлежащей редактированию.
Для редактирования мы создаем форму класса Form2:
Form2 dialog = new Form2();
Это та же самая форма, которую мы использовали для создания новых узлов
дерева (рис. 9-43).
Извлечение данных редактируемого узла дерева
Далее нам нужно заполнить поля формы текущей информацией из таблиц Tree и
Document, соответствующей данному узлу с идентификатором id. С этой целью мы
открываем соединения с базой данных sqlConnection1 и создаем объект класса
SqlDataReader:
sqlConnection1.Open();
SqlDataReader myReader;
Получение текст редактируемой статьи
На первом шаге нам нужно создать команду SQL класса SqlCommand, с помощью
которой мы могли бы извлечь текст редактируемой статьи из таблицы Documents. При
этом нам необходимо найти в этой таблице такую строку, для которой в столбце tree_id
хранилось бы значение, равное идентификатору редактируемого узла дерева id.
Необходимую команду SQL мы создаем «на лету», формируя соответствующую
текстовую строку с помощью класса String.Format:
string strCmd = String.Format(
"SELECT document FROM Documents WHERE tree_id = {0}", id);

SqlCommand cmd = new SqlCommand(strCmd, sqlConnection1);


После выполнения команды методом ExecuteReader обработчик событий извлекает
текст документа методом myReader.GetString, а затем сохраняет его в свойстве
dialog.Document диалогового окна редактирования узлов дерева:
myReader = cmd.ExecuteReader();

if(myReader.Read())
{
dialog.Document = myReader.GetString(0).ToString();
}

myReader.Close();
Далее обработчик закрывает объект SqlDataReader, чтобы подготовить его к
выполнению новой команды.
Извлечение заголовка и веса сортировки
Для того чтобы извлечь заголовок редактируемой статьи и вес сортировки, мы создали
команду sqlGetNodeCommand. Ниже приведен текст команды SQL, который нужно
записать в свойство CommandText объекта sqlGetNodeCommand:
SELECT id, parent_id, title, weight FROM dbo.Tree WHERE (id = @id)
Через параметр @id команде передается идентификатор узла дерева, для
которого нужно извлечь информацию из таблицы Tree:
sqlGetNodeCommand.Parameters["@id"].Value = id;
Далее подготовленная команда запускается на выполнение с помощью метода
ExecuteReader:
myReader = sqlGetNodeCommand.ExecuteReader();
Если программа нашла в таблице Tree нужный нам узел, то заголовок статьи и вес
сортировки этого узла записывается, соответственно, в свойства dialog.Title и
dialog.Weight диалогового окна редактирования:
if(myReader.Read())
{
dialog.Title = myReader.GetString(2).ToString();
dialog.Weight = myReader.GetInt32(3);

}

Обновление информации узла в базе данных


Когда пользователь завершает редактирование данных в форме, показанной на рис. 9-
43, при помощи кнопки Сохранить, метод dialog.ShowDialog возвращает значение
DialogResult.Yes. В этом случае обработчик событий menuItem3_Click обновляет
информацию об узле в базе данных:
if(DialogResult.Yes == dialog.ShowDialog())
{
try
{
// Обновление базы данных

}
catch(Exception ex)
{
MessageBox.Show(ex.Message, "Ошибка");
}
}
Обновление выполняется с помощью команды sqlUpdateNodeCommand и хранимой
процедуры sp_UpdateDocument, исходный текст которой мы уже приводили ранее в
этой главе.
Свойство CommandText команды sqlUpdateNodeCommand должно быть
установлено следующим образом:
UPDATE dbo.Tree SET title = @title, weight = @weight WHERE (id = @id)
Команде sqlUpdateNodeCommand нужно передать три параметра:
sqlUpdateNodeCommand.Parameters["@id"].Value = id;
sqlUpdateNodeCommand.Parameters["@title"].Value = dialog.Title;
sqlUpdateNodeCommand.Parameters["@weight"].Value = dialog.Weight;
Первый из этих параметров задает идентификатор обновляемого узла, второй —
новый заголовок статьи, и третий — новый вес сортировки.
Команда выполняется обычным образом с помощью метода ExecuteNonQuery:
myReader.Close();
sqlUpdateNodeCommand.ExecuteNonQuery();
Перед выполнением этой команды мы закрываем ненужный нам больше объект
myReader.
Как мы уже говорили, текст статьи обновляется в таблице Documents при помощи
хранимой процедуры sp_UpdateDocument:
cmd = new SqlCommand("sp_UpdateDocument", sqlConnection1);
cmd.CommandType = CommandType.StoredProcedure;
Для этой хранимой процедуры необходимо задать два параметра:
cmd.Parameters.Add("@tree_id", SqlDbType.Int).Value = id;
cmd.Parameters.Add("@document",SqlDbType.VarChar).Value =
dialog.Document;
Первый из этих параметров задает идентификатор обновляемой статьи, а
второй — новый текст статьи.
После подготовки параметров хранимая процедура sp_UpdateDocument
запускается на выполнение при помощи метода ExecuteNonQuery:
cmd.ExecuteNonQuery();
С целью демонстрации различных методов обновления базы данных мы
искусственно разделили процесс обновления таблиц Tree и Documents. Первая из этих
таблиц выполняется при помощи команды SQL, созданной в виде объекта класса
SqlCommand, а вторая — с помощью хранимой процедуры sp_UpdateDocument.
В реальных приложениях имеет смысл использовать какой-то один способ,
обновляя базы данных, например, в хранимой процедуре. В качестве самостоятельного
упражнения измените хранимую процедуру sp_UpdateDocument таким образом, чтобы
она модифицировала обе таблицы. После этого уберите из приложения код,
обновляющий таблицу Tree при помощи команды sqlUpdateNodeCommand.
Сразу после обновления информации в базе данных наш обработчик событий
отображает содержимое отредактированной статьи в правой части главного окна
нашего приложения:
richTextBox1.Text = dialog.Document;
Далее этот обработчик закрывает соединение с базой данных и перерисовывает
дерево:
sqlConnection1.Close();
UpdateTree();

Удаление узла дерева


Чтобы удалить статью, пользователь должен выделить его заголовок в окне дерева,
затем щелкнуть этот заголовок правой клавишей мыши и выбрать из контекстного меню
строку Delete.
В результате этих действий управление будет передано обработчику событий
menuItem2_Click. Создайте его в следующем виде:
private void menuItem2_Click(object sender, System.EventArgs e)
{
if(treeView1.SelectedNode != null)
{
int id = (int)treeView1.SelectedNode.Tag;
DeleteNode(id);
UpdateTree();
}
}
Если в окне дерева есть узлы, выделенные пользователем (т.е. если значение
свойства treeView1.SelectedNode не равно null), то обработчик событий
menuItem2_Click получает идентификатор узла, подлежащего к удалению. Этот
идентификатор считывается из свойства treeView1.SelectedNode.Tag, куда он был
записан во время заполнения дерева.
Далее узел удаляется методом DeleteNode, определенным в нашем приложении,
после чего окно дерева обновляется при помощи метода UpdateTree.
Ниже мы привели исходный текст метода DeleteNode:
public void DeleteNode(int id)
{
SqlDataReader myReader;

sqlConnection1.Open();
try
{
sqlFindChildsCommand.Parameters["@parent_id"].Value = id;
myReader = sqlFindChildsCommand.ExecuteReader();
if(!myReader.Read())
{
myReader.Close();

sqlDeleteRowCommand1.Parameters["@id"].Value = id;
sqlDeleteRowCommand1.ExecuteNonQuery();

string strCmd = String.Format(


"DELETE FROM Documents WHERE tree_id = {0}", id);
SqlCommand cmd = new SqlCommand(strCmd, sqlConnection1);
cmd.ExecuteNonQuery();

richTextBox1.Text = "";
}
else
{
myReader.Close();
}
}
catch(Exception ex)
{
MessageBox.Show(ex.Message, "Ошибка");
}
sqlConnection1.Close();
}
Получив управление, этот метод создает объект myReader класса SqlDataReader и
открывает соединение с базой данных:
SqlDataReader myReader;
sqlConnection1.Open();
Дальнейшая работа выполняется внутри блока try-catch, в задачу которого входит
перехват и обработка исключений.
Прежде всего, необходимо убедиться, что удаляемый узел не имеет дочерних
узлов. Наше приложение позволяет удалять из дерева только такие узлы.
Для поиска всех дочерних узлов выделенного пользователем узла метод
DeleteNode использует команду sqlFindChildsCommand.
Вот текст этой команды, который нужно записать в свойство CommandText
объекта sqlFindChildsCommand:
SELECT id FROM dbo.Tree WHERE (parent_id = @parent_id)
Через параметр @parent_id мы передаем этой команде идентификатор удаляемого
узла:
sqlFindChildsCommand.Parameters["@parent_id"].Value = id;
myReader = sqlFindChildsCommand.ExecuteReader();
Если в таблице Tree есть строки, в столбце parent_id которых содержится
идентификатор удаляемого узла, значит, такой узле удалять нельзя. Вот
соответствующая проверка, выполняемая методом DeleteNode:
if(!myReader.Read())
{
// Удаление узла

}
В том случае, когда удаляемый узел не имеет дочерних узлов, нам нужно удалить
строку, описывающую этот узел, из таблицы Tree, а затем удалить соответствующую
статью из таблицы Document.
Для удаления строки из таблицы Tree мы используем команду
sqlDeleteRowCommand1, предварительно закрыв объект myReader:
myReader.Close();

sqlDeleteRowCommand1.Parameters["@id"].Value = id;
sqlDeleteRowCommand1.ExecuteNonQuery();
Свойство объекта sqlDeleteRowCommand1 должно содержать следующий текст
команды SQL:
DELETE FROM dbo.Tree WHERE (id = @id)
Идентификатор удаляемого узла передается команде sqlDeleteRowCommand1 в
качестве единственного параметра.
Для удаления текста статьи мы использовали другой подход — сформировали
исходный текст команды SQL в памяти при помощи метода String.Format:
string strCmd = String.Format(
"DELETE FROM Documents WHERE tree_id = {0}", id);

SqlCommand cmd = new SqlCommand(strCmd, sqlConnection1);


cmd.ExecuteNonQuery();
Полученная в результате этих действий команда исполняется методом
ExecuteNonQuery.
И, наконец, последнее, что делает метод DeleteNode после удаления информации
из базы данных, это очистка окна элемента управления RichEdit, отображающего
содержимое текущего документа, выделенного пользователем в окне дерева:
richTextBox1.Text = "";
О том, как текст выделенного документа попадает в это окно, мы расскажем в
следующем разделе.
Отслеживание перемещений по дереву
Последнее, что нам нужно сделать при подготовке приложения ArticlesApp, это
обеспечить просмотр содержимого статьи, заголовок которой пользователь выделил в
окне дерева. Для этого нам необходимо снабдить элемент управления treeView1
обработчиком события AfterSelect:
private void treeView1_AfterSelect(object sender,
System.Windows.Forms.TreeViewEventArgs e)
{
int id = (int)treeView1.SelectedNode.Tag;

sqlConnection1.Open();
try
{
SqlDataReader myReader;

string strCmd = String.Format(


"SELECT document FROM Documents WHERE tree_id = {0}", id);

SqlCommand cmd = new SqlCommand(strCmd, sqlConnection1);


myReader = cmd.ExecuteReader();

if(myReader.Read())
{
richTextBox1.Text = myReader.GetString(0).ToString();
}
myReader.Close();
}
catch(Exception ex)
{
MessageBox.Show(ex.Message, "Ошибка");
}

sqlConnection1.Close();
}
Этот обработчик будет получать управление всякий раз, когда пользователь
выделяет в окне дерева новый заголовок статьи.
Что делает наш обработчик события treeView1_AfterSelect?
Прежде всего, он получает идентификатор узла, выделенного пользователем,
считывая его из свойства treeView1.SelectedNode.Tag:
int id = (int)treeView1.SelectedNode.Tag;
Зная этот идентификатор, мы можем получить текст статьи из таблицы Document,
чтобы отобразить его в правой части главного окна нашего приложения.
Для того чтобы получить текст статьи, мы открываем соединение с базой данных.
Операция выполняется в блоке try-catch, после чего соединение закрывается:
sqlConnection1.Open();
try
{

}
catch(Exception ex)
{
MessageBox.Show(ex.Message, "Ошибка");
}
sqlConnection1.Close();
Чтобы извлечь текст статьи из таблицы Documents, мы формируем строку в
памяти, пользуясь методом String.Format:
string strCmd = String.Format(
"SELECT document FROM Documents WHERE tree_id = {0}", id);

SqlCommand cmd = new SqlCommand(strCmd, sqlConnection1);


Для запуска команды мы создаем объект myReader класса SqlDataReader, а затем
вызываем метод ExecuteReader:
SqlDataReader myReader;
myReader = cmd.ExecuteReader();
Результат выполнения команды записывается в свойство richTextBox1.Text, после
чего текст статьи появляется в правой части главного окна нашего приложения:
if(myReader.Read())
{
richTextBox1.Text = myReader.GetString(0).ToString();
}
Необходимое финальное действие — закрытие ненужного больше объекта
myReader:
myReader.Close();
Разумеется, для чтения текста статьи Вы можете использовать хранимые
процедуры сервера Microsoft SQL Server. Использование хранимых процедур дает
дополнительные преимущества при создании приложений с базами данных. В
частности, разработчик может настраивать права доступа к таким процедурам. Кроме
того, хранимые процедуры можно готовить и отлаживать вне контекста приложения.
Эту работу можно поручить специалисту по серверу Microsoft SQL Server, который знает
все о базах данных, но ничего о создании приложений на языке программирования C#.

Визуальное проектирование
приложений C#
А.В. Фролов, Г.В. Фролов
ГЛАВА 10. ГРАФИЧЕСКИЙ ИНТЕРФЕЙС GDI+
ОСНОВНЫЕ ПОНЯТИЯ
Независимость от аппаратуры
Контекст отображения
Класс Graphics
Приложение GraphicsApp
Отслеживание состояния клавиш мыши
Отслеживание перемещения курсора мыши
Идентификатор окна Handle и объект Graphics
Кисть для рисования
Рисование точки
Рисование в окне элемента управления
СОБЫТИЕ PAINT
Рисование в окнах приложений Microsoft Windows
Сообщение WM_PAINT
Пример обработки события Paint
Перерисовка окон элементов управления
МЕТОДЫ И СВОЙСТВА КЛАССА GRAPHICS
Рисование геометрических фигур
Линия
Набор линий
Прямоугольник
Набор прямоугольников
Многоугольник
Эллипс
Сегмент эллипса
Кривые Безье
Канонические сплайны
Замкнутый сегмент эллипса
Закрашенные фигуры
Рисование изображений
Немного о ресурсах приложения
Значки
Растровые и векторные изображения
Использование класса Image
Режим буксировки
События при буксировке
Обработка события DragOver
Обработка события DragDrop
Загрузка изображения
Рисование загруженного изображения
Рисование текста
ИНСТРУМЕНТЫ ДЛЯ РИСОВАНИЯ
Перья
Кисти
Кисть для сплошной закраски
Кисти типа HatchBrush
Кисти типа TextureBrush
Градиентные кисти
Шрифты
Классификация шрифтов
Шрифты TrueType
Шрифты OpenType
Выбор шрифта
Конструкторы класса Font
Тип шрифта FontStyle
Единицы измерения размера шрифта
Семейство шрифта FontFamily
Приложение FontApp

Глава 10. Графический интерфейс GDI+


Мы изучили многочисленные аспекты визуального создания приложений C# с
графическим интерфейсом, однако так и не познакомились непосредственно с
реализацией самого графического интерфейса библиотеки классов Microsoft .NET
Framework.
Действительно, все приложения, рассмотренные ранее в предыдущих главах этой
книги, не осуществляли непосредственный вывод графических изображений или текста
в окна приложения. Мы абстрагировались от деталей графического интерфейса,
возложив все операции по рисованию в окнах на такие элементы управления, как
текстовые поля, поля редактирования, списки, деревья и пр.
Вместе с тем рано или поздно наступает момент, когда набор стандартных
элементов управления перестает удовлетворять разработчика приложений. Иногда
возникают задачи, требующие функций и методов, позволяющих программе рисовать в
окнах приложения произвольные графические изображения или отображать текст
нестандартным способом. Типичный пример — построение всякого рода графиков и
диаграмм, а также создание анимационных изображений.
Графический интерфейс приложений C#, как и других приложений,
предназначенных для работы в рамках Microsoft .NET Framework, состоит из набора
классов. Эти классы инкапсулируют поведение объектов и инструментов,
предназначенных для рисования. Однако прежде чем приступить к описанию этих
классов, нам необходимо познакомиться с основными понятиями интерфейса
графических устройств.
Заметим, что графический интерфейс GDI+ настолько обширный, что ему можно
посвятить отдельную книгу или даже несколько книг. Поэтому в рамках одной главы мы
изложим только наиболее существенные аспекты применения GDI+. Тех из Вас, кому
будет интересно познакомиться с возможностями GDI+ глубже, мы адресуем к [10].
Основные понятия
Если Вы когда-либо создавали программы для операционной системы MS-DOS и ее
аналогов, то помните, что такие программы могут работать в текстовом или
графическом режиме.
Использование текстового режима обычно не вызывает затруднений. Программы
выводят текстовые строки на экран консоли, обращаясь к системным функциям ОС или
к функциям базовой системы ввода-вывода BIOS. Что же касается графического
режима, то его использование приводило к необходимости выполнять в программах
прямое обращение к аппаратуре видеоадаптера.
С учетом того обстоятельства, что компьютеры могут быть оснащены
видеоадаптерами самого разного типа, создание графических программ MS-DOS,
способных работать на любых компьютерах, превращалось в непростую задачу. В
самом деле, прежде чем выводить что-то на экран монитора в графическом режиме,
программа должна была определить тип видеоадаптера и его возможности,
переключить видеоадаптер в нужный видеорежим, а затем выполнять вывод
изображения с учетом особенностей этого видеорежима. Всех, кого интересуют
подробности программирования видеоадаптеров на уровне аппаратуры, мы адресуем к
нашей книге [7], посвященной этому вопросу.
Независимость от аппаратуры
При создании ОС Microsoft Windows компания Microsoft избавила программистов от
необходимости учитывать аппаратные особенности видеоадаптеров, переложив эту
задачу на драйверы видеоадаптеров. Эти драйверы создаются разработчиками
видеоадаптеров и наилучшим образом реализуют возможности аппаратуры.
Что же касается приложений, то для них в составе ОС Microsoft Windows был
предусмотрен набор системных функций, реализующих интерфейс графических
устройств (Graphics Device Interface, GDI). Применительно к приложениям Microsoft
Windows мы рассмотрели этот интерфейс в [8]. Что же касается этой книги, то здесь мы
расскажем о работе с усовершенствованным интерфейсом GDI+, доступным
приложениям Microsoft .NET Framework.
Интерфейс графических устройств GDI, как это можно предположить из названия,
предназначен для взаимодействия приложений Microsoft Windows с графическими
устройствами, такими как видеоадаптер, принтер или плоттер.
Когда приложения обращаются к GDI для выполнения операции вывода
графического изображения, они работают не с реальными (физическими) устройствами
вывода, а с логическими устройствами. Приложения Microsoft Windows не определяют
тип видеоадаптера (EGA, VGA, SVGA и т.п.), а работают с логическим видеоадаптером,
имеющим феноменальные характеристики: способность отображать практически любой
цвет, имеющим огромное разрешение и т. д.
Выполняя запрос приложения, GDI обращается к драйверу соответствующего
устройства вывода, работающему, в свою очередь, непосредственно с физическим
устройством вывода. В процессе выполнения запроса GDI (или драйвер) учитывает
ограниченные возможности видеоадаптера и его аппаратные особенности, делая
необходимые приближения.
Например, приложение может указать для цвета линии любой из примерно 16
млн. цветов, однако не всякое устройство обладает таким цветовым разрешением
(ограничения на количество одновременно отображаемых цветов присутствуют,
например, в карманных компьютерах). В зависимости от типа физического устройства,
используемого для вывода, GDI может выбрать для отображения цвет, наиболее
соответствующий запрошенному цвету, и допустимый для устройства.
Например, если устройство вывода монохромное, вместо различных цветов могут
использоваться градации серого цвета. Поэтому приложение может запросить для
вывода любой цвет, но для рисования будет использован только такой, который может
использовать данное физическое устройство.
Такая ситуация, когда приложение запрашивает у ОС Microsoft Windows одно, а
получает другое, возникает не только при работе с цветом. Приложение может
запросить для вывода шрифт, описав его характеристики. Интерфейс GDI подберет для
вывода наиболее подходящий (с его точки зрения) шрифт, соответствующий описанию,
и предоставит его приложению.
На первый взгляд, этот подход обескураживает, особенно тех программистов, кто
имеет опыт создания графических программ для MS-DOS и привык получать от системы
точно то, что требуется. Однако такой подход удобен для обеспечения аппаратной
независимости.
Составляя программы для MS-DOS, Вы работали с видеоадаптерами, указывая
конкретные цвета и загружая в его память конкретные шрифты из отдельных файлов.
Поэтому программы MS-DOS были крепко «привязаны» к аппаратуре. Для
использования новых возможностей требовалось вносить изменения в программы.
Приложения Microsoft Windows способны работать в неизменном виде на любом
оборудовании, лишь бы был соответствующий драйвер. Чем лучше используемая
аппаратура, чем большими возможностями она обладает, тем ближе будут параметры
полученного шрифта и цвета соответствовать запрошенным.
Поэтому даже если сейчас в Вашем распоряжении есть только видеоадаптер с
ограниченным количеством отображаемых цветов, при разработке приложений
Microsoft Windows Вы можете не ограничивать себя дюжиной цветов. Ваше приложение
должно быть сделано так, чтобы оно могло использовать любой цвет. Со временем,
когда у Вас появится современный видеоадаптер, окно Вашего приложения засветится
всеми цветами радуги, причем для этого не придется вносить никаких изменений в
само приложение.
Контекст отображения
С точки зрения приложений, интерфейс GDI состоит из контекста отображения и
инструментов, предназначенных для рисования.
Контекст отображения можно сравнить с листом бумаги, на котором приложение
рисует то или иное графическое изображение, а также пишет текст. Инструменты для
рисования — это перья, кисти (а также шрифты и даже целые графические
изображения), с помощью которых создается изображение.
Кроме контекста отображения и инструментов для рисования, приложениям
доступны десятки функций программного интерфейса GDI, предназначенные для
работы с контекстом отображения и инструментами. Что же касается приложений
Microsoft .NET Framework, то они реализуют возможности интерфейса GDI+ с помощью
набора соответствующих классов и интерфейсов.
В терминах ОС Microsoft Windows контекст отображения (display context)
представляет собой структуру данных, описывающую устройство отображения. В этой
структуре хранятся различные характеристики устройства и набор инструментов для
рисования, выбранный по умолчанию. Приложение может выбирать в контекст
отображения различные инструменты (например, перья различной толщины и цвета, с
различными «наконечниками»). Поэтому если Вам надо нарисовать линию красного или
зеленого цвета, перед выполнением операции следует выбрать в контекст отображения
соответствующее перо.
Заметим, что функции рисования GDI, входящие в программный интерфейс Win32
API, не имеют параметров, указывающих цвет или толщину линии. Такие параметры
хранятся в контексте отображения.
Приложение может создать контекст отображения не только для окна
приложения, но и для любого другого графического устройства вывода, например, для
принтера. В последнем случае оно может рисовать на принтере различные
изображения, используя те же функции, что и для рисования в окне приложения.
Можно создать контекст отображения для метафайла. Метафайл — это обычный
файл или файл в памяти, в котором хранятся последовательности команд интерфейса
GDI. Приложение может выполнять графический вывод в метафайл как в обычное
устройство вывода, а затем «проигрывать» метафайл на реальном устройстве вывода.
Контекст устройства в терминах ОС Microsoft Windows выступает в роли
связующего звена между приложением и драйвером устройства (рис. 10-1) и
представляет собой структуру данных размером примерно 800 байт. Эта структура
данных содержит информацию о том, как нужно выполнять операции вывода на данном
устройстве (цвет и толщину линии, тип системы координат и т. д.).

Рис. 10-1. Вывод данных через контекст устройства

Если приложение получает или создает контекст для устройства отображения,


такой контекст называется контекстом отображения (display context). Поэтому когда,
например, приложение получает контекст для отображения в одном из своих окон,
такой контекст называется контекстом отображения. Если же ему требуется выполнять
операцию вывода для устройства (для принтера или для экрана дисплея), приложение
должно получить или создать контекст устройства (device context).
Следует понимать, что контексты устройства и отображения содержат описания
одних и тех же характеристик и имеют одинаковую структуру. Название контекста
определяется только тем, относится ли контекст к окну отображения или устройству
вывода.
Класс Graphics
Концепция графического интерфейса GDI+ несколько отличается от концепции
«классического» графического интерфейса GDI, с которым привыкли иметь дело
разработчики приложений Microsoft Windows. Те из Вас, кто создавал приложения
Java с графическим интерфейсом на базе классов Abstract Window Toolkit (AWT),
найдут, что эти классы имеют много общего с классами GDI+.
Прежде всего, это касается класса Graphics, реализующего в себе как свойства
контекста отображения, так и инструменты, предназначенные для рисования в этом
контексте.
Для того чтобы приложение могло что-нибудь нарисовать в окне, оно должно,
прежде всего, получить или создать для этого окна объект класса Graphics. Далее,
пользуясь свойствами и методами этого объекта, приложение может рисовать в окне
различные фигуры или текстовые строки.
Приложение GraphicsApp
Проще всего объяснить назначение и принципы использования класса Graphics на
конкретном примере. Давайте создадим приложение GraphicsApp, в окне которого
можно рисовать мышью (рис. 10-2).

Рис. 10-2. Рисунок в окне приложения GraphicsApp

Создайте проект приложения GraphicsApp при помощи мастера проектов, а затем


установите белый цвет фона для формы этого приложения Form1. Изменить цвет фона
можно, отредактировав свойство BackColor.
Отслеживание состояния клавиш мыши
Нам нужно сделать так, чтобы пользователь мог рисовать в окне при нажатой левой
клавише мыши. Поэтому нам придется отслеживать в нашем приложении события,
связанные с нажатием и отжатием этой клавиши. Кроме того, необходимо отслеживать
перемещения курсора мыши.
Текущее состояние мыши будет храниться в поле doDraw типа bool. Вам нужно
добавить это поле в класс Form1, проинициализировав его следующим образом:
bool doDraw = false;
Создайте два обработчика событий MouseDown и MouseUp для формы Form1:
private void Form1_MouseDown(object sender,
System.Windows.Forms.MouseEventArgs e)
{
doDraw = true;
}

private void Form1_MouseUp(object sender,


System.Windows.Forms.MouseEventArgs e)
{
doDraw = false;
}
Чтобы создать эти обработчики событий, откройте окно визуального
проектирования формы, а затем на вкладке событий щелкните дважды события
MouseDown и MouseUp. Мы показали это окно на рис. 10-3, добавив обработчик
события MouseMove, необходимый для отслеживания перемещений мыши.
Рис. 10-3. Добавление обработчиков событий

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


обработчику событий Form1_MouseDown. Этот обработчик записывает в поле doDraw
значение true, отмечая таким способом тот факт, что пользователь приступил к
процедуре рисования.
Нарисовав линию, пользователь отпускает левую клавишу мыши. При этом
управление передается обработчику событий Form1_MouseUp, записывающему в поле
doDraw значение false. Это означает завершение процедуры рисования.
Отслеживание перемещения курсора мыши
Для того чтобы наделить наше приложение возможностью рисования, добавьте
обработчик события Form1_MouseMove:
private void Form1_MouseMove(object sender,
System.Windows.Forms.MouseEventArgs e)
{
if(doDraw)
{
Graphics g = Graphics.FromHwnd(this.Handle);
SolidBrush redBrush = new SolidBrush(Color.Red);
g.FillRectangle(redBrush, e.X, e.Y, 1, 1);
}
}
Этот обработчик будет получать управление при всяком перемещении курсора
мыши, причем свойства e.X и e.Y будут содержать новые координаты курсора.
Мы воспользуемся этим обстоятельством, нарисовав в месте нового расположения
курсора квадрат, с шириной стороны в один пиксел. На экране такой квадрат будет
выглядеть как точка. Рисование должно выполняться только в том случае, если в поле
doDraw хранится значение true.
Идентификатор окна Handle и объект Graphics
Как мы уже говорили, прежде чем мы сможем что-нибудь нарисовать в окне нашего
приложения, мы должны получить для этого окна объект класса Graphics. В случае
приложения GraphicsApp нам нужно получить такой объект для окна формы Form1.
Как это сделать?
Те из Вас, кто создавал приложения для ОС Microsoft Windows, знают, что каждое
окно имеет свой идентификатор (handle). Зная идентификатор окна, можно легко
получить связанный с этим окном контекст отображения.
Приложения Microsoft .NET Framework могут получить идентификатор формы или
любого другого элемента управления при помощи свойства Handle. В частности, наше
приложение получает идентификатор окна формы Form1 с помощью свойства
this.Handle.
Зная идентификатор окна, с помощью метода Graphics.FromHwnd нетрудно
получить нужный нам объект класса Graphics:
Graphics g = Graphics.FromHwnd(this.Handle);
Позже Вы узнаете и о других способах получения этого объекта.
Кисть для рисования
Для того чтобы рисовать, художнику нужна кисть. Программист, создающий
приложение GDI+, тоже нуждается в инструментах для рисования. Мы создадим
кисть как объект класса SolidBrush:
SolidBrush redBrush = new SolidBrush(Color.Red);
С помощью этой кисти можно рисовать замкнутые геометрические фигуры,
закрашенные заданным цветом. Через единственный параметр мы передаем
конструктору класса SolidBrush цвет кисти Color.Red. Таким образом, мы будем
рисовать кистью красного цвета.
Позже мы познакомимся с перьями класса Pen, а также с другими кистями.
Рисование точки
В классе Graphics имеется множество различных методов, предназначенных для
рисования самых разных геометрических фигур, таких как линии, прямоугольники,
овалы и окружности, многоугольники, кривые Безье и т.д.
Но вот чего в этом классе нет, так это метода, с помощью которого можно было бы
нарисовать одну единственную точку. Заметим, однако, что вместо точки мы можем
нарисовать закрашенный квадрат с шириной стороны, равным 1 пикселу. Эта задача
выполняется при помощи метода FillRectangle:
g.FillRectangle(redBrush, e.X, e.Y, 1, 1);
Обратите внимание на то, что метод FillRectangle вызывается для объекта g класса
Graphics, созданного нами для окна формы Form1. Поэтому квадрат будет нарисован в
окне этой формы.
В качестве первого параметра методу FillRectangle передается кисть redBrush,
которую нужно использовать для рисования. Кисть нужна и для других методов класса
Graphics, предназначенных для рисования геометрических фигур.
Второй и третий параметры метода FillRectangle задают координаты, в которых
будет нарисован квадрат.
Начало системы координат при этом находится в левом верхнем углу окна, для
которого был получен объект Graphics. В нашем случае это левый верхний угол
внутренней области окна формы Form1. Ось X в этой системе координат, принятой по
умолчанию, направлена слева направо, а ось Y — сверху вниз (рис. 10-4).
Рис. 10-4. Система координат по умолчанию

И, наконец, последние два параметра метода FillRectangle задают, соответственно,


ширину и высоту прямоугольника.
В процессе перемещения мыши, при нажатой левой клавише, происходит
многократный вызов обработчика событий Form1_MouseMove. Как результат, в окне
нашего приложения появляется рисунок, состоящий из отдельных точек (рис. 10-2).
Рисование в окне элемента управления
В приложении GraphicsApp мы получили контекст отображения (в виде объекта класса
Graphics) для всего окна формы Form1. Однако формы реальных приложений
создаются с использованием различных элементов управления, о которых мы
рассказывали в предыдущих главах нашей книги. Что же касается непосредственного
рисования, то оно чаще всего выполняется не во всем окне приложения, а только в его
некоторой части.
На рис. 10-5 мы показали главное окно приложения GraphicsApp1, внутри
которого находится элемент управления класса Panel.

Рис. 10-5. Рисование в окне элемента управления Panel

С помощью мыши пользователь может рисовать внутри окна панели Panel, однако
остальная часть главного окна приложения для рисования недоступна.
Создайте проект приложения GraphicsApp1 и перетащите из панели Toolbox
системы Microsoft Visual Studio в форму Form1 значок элемента управления Panel.
Далее создайте в классе Form1 поле doDraw и обработчики событий, создаваемых
мышью. Обработчики должны создаваться для объекта Panel, а не для формы Form1,
как это было в предыдущем приложении:
bool doDraw = false;

private void panel1_MouseDown(object sender,


System.Windows.Forms.MouseEventArgs e)
{
doDraw = true;
}

private void panel1_MouseUp(object sender,


System.Windows.Forms.MouseEventArgs e)
{
doDraw = false;
}

private void panel1_MouseMove(object sender,


System.Windows.Forms.MouseEventArgs e)
{
if(doDraw)
{
Graphics g = Graphics.FromHwnd(panel1.Handle);
SolidBrush redBrush = new SolidBrush(Color.Red);
g.FillEllipse(redBrush,e.X, e.Y, 10, 10);
}
}
Этот код почти аналогичен коду, который мы использовали в предыдущем
приложении GraphicsApp, однако в обработчиках событий имеются важные отличия.
Прежде всего, мы создали обработчики событий для панели panel1, а не для
формы Form1. В результате они будут получать управление только в том случае, когда
пользователь щелкает левую кнопку мыши и перемещает ее курсор внутри панели.
Второе важное отличие в методе panel1_MouseMove. Этот метод получает контекст
отображения не для всего окна формы, а только для окна панели panel1. С этой целью
он передает методу Graphics.FromHwnd идентификатор окна панели, извлеченный из
свойства panel1.Handle:
Graphics g = Graphics.FromHwnd(panel1.Handle);
В результате последующая операция рисования будет выполнена в системе
координат, связанной с окном панели, а не с окном формы.
Третье отличие от предыдущего приложения не соль существенно. Вместо метода
FillRectangle для рисования мы применили метод FillEllipse:
g.FillEllipse(redBrush,e.X, e.Y, 10, 10);
Назначение параметров метода FillEllipse, предназначенного для рисования
закрашенных эллипсов, аналогично назначению параметров метода FillRectangle. При
этом два последних параметра задают, соответственно, ширину и высоту
прямоугольной области, занимаемой эллипсом.
Событие Paint
Проверяя работу приложения GraphicsApp1, описанного в предыдущем разделе этой
главы, Вы наверняка заметите одну неприятную особенность — при изменении
размеров окна часть нарисованного изображения или все изображение пропадает.
Аналогичная неприятность происходит и в том случае, когда окно приложения
GraphicsApp1 перекрывается окном другого приложения.
В чем здесь проблема?
Она в том, что в приложении GraphicsApp1 не реализована техника рисования в
окне, применяемая во всех стандартных приложениях Microsoft Windows. По своей
логике способ рисования программ Microsoft Windows в корне отличается от способа, к
которому привыкли разработчики, создававшие программы для ОС MS-DOS.
Рисование в окнах приложений Microsoft Windows
Известно, что приложения Microsoft Windows не могут выводить текст или графику ни с
помощью стандартных функций библиотеки компилятора, например таких, как printf,
cprintf или putc. Не помогут и прерывания BIOS, так как приложениям Microsoft
Windows запрещено их использовать (во всяком случае, для вывода на экран). Все эти
функции ориентированы на консольный вывод в одно-единственное окно,
предоставленное в полное распоряжение программе MS-DOS.
В ОС Microsoft Windows параллельно работающие приложения должны совместно
использовать один общий экран монитора. Для этого они создают перекрывающиеся и
перемещаемые окна, в которые и выполняют вывод текста или графических
изображений. ОС Microsoft Windows берет на себя все проблемы, связанные с
возможным перекрытием или перемещением окон, так что правильно
спроектированные приложения не должны специально заботиться о восстановлении
содержимого окна после его перекрытия другим окном.
Способ, которым приложение Microsoft Windows выводит что-либо в свои окна,
коренным образом отличается от способа, используемого в программах MS-DOS.
Программа MS-DOS формирует изображение на экране «рассредоточенным»
образом, то есть в любом месте программы могут вызываться функции, которые
выводят что-либо на экран. Например, сразу после запуска программа может
нарисовать на экране диалоговую панель, а затем в любой момент времени и, что
самое главное, из любого места программы модифицировать ее.
Приложения Microsoft Windows также могут выводить в созданные ими окна текст
или графические изображения в любой момент времени и из любого места. Именно так
поступает наше приложение GraphicsApp1. Однако обычно разработчики поступают по-
другому.
Сообщение WM_PAINT
Прежде чем приступить к описанию способов рисования в окнах, применяемых
приложениями .NET Frameworks, расскажем о том, как это делают «классические»
приложения Microsoft Windows, составленные на языках программирования C или C++.
Это поможет Вам глубже разобраться в сути проблемы. При необходимости Вы найдете
более подробную информацию об этом в [4].
ОС Microsoft Windows следит за перемещением и изменением размера окон и при
необходимости извещает приложения, о том, что им следует перерисовать содержимое
окна. Для извещения в очередь приложения записывается сообщение с
идентификатором WM_PAINT. Получив такое сообщение, функция окна должна
выполнить перерисовку всего окна или его части, в зависимости от дополнительных
данных, полученных вместе с сообщением WM_PAINT. Напомним, что функция окна
выполняет обработку всех сообщений, поступающих в окно приложения Microsoft
Windows.
Для облегчения работы по отображению содержимого окна весь вывод в окно
обычно выполняют в одном месте приложения — при обработке сообщения WM_PAINT в
функции окна. Приложение должно быть сделано таким образом, чтобы в любой момент
времени при поступлении сообщения WM_PAINT функция окна могла перерисовать все
окно или любую его часть, заданную своими координатами.
Последнее нетрудно сделать, если приложение будет хранить где-нибудь в памяти
свое текущее состояние, пользуясь которым функция окна сможет перерисовать окно в
любой момент времени.
Здесь не имеется в виду, что приложение должно хранить образ окна в виде
графического изображения и восстанавливать его при необходимости, хотя это и можно
сделать. Приложение должно хранить информацию, на основании которой оно может в
любой момент времени перерисовать окно.
Например, если приложение выводит на экран дамп оперативной памяти, оно
должно хранить информацию о начальном адресе отображаемого участка памяти и
размере этого участка. При получении сообщения WM_PAINT приложение должно
определить, какой участок окна необходимо перерисовать и какому диапазону адресов
дампа памяти этот участок соответствует. Затем приложение должно заново вывести
участок дампа памяти в окно, опрашивая соответствующие адреса и выполняя
преобразование байт памяти в символьные, шестнадцатеричные или другие
используемые для вывода дампа числа.
Сообщение WM_PAINT передается функции окна, если стала видна область окна,
скрытая раньше другими окнами, если пользователь изменил размер окна или
выполнил операцию прокрутки изображения в окне. Приложение может передать
функции окна сообщение WM_PAINT явным образом, вызывая функции программного
интерфейса Win32 API, такие как UpdateWindow, InvalidateRect или InvalidateRgn.
Иногда ОС Microsoft Windows может сама восстановить содержимое окна, не
посылая сообщение WM_PAINT. Например, при перемещении курсора мыши или значка
свернутого приложения ОС восстанавливает содержимое окна. Если же ОС не может
восстановить окно, функция окна получает от ОС сообщение WM_PAINT и
перерисовывает окно самостоятельно.
Перед тем как записать сообщение WM_PAINT в очередь приложения, ОС
посылает функции окна сообщение WM_ERASEBKGND. Если функция окна не
обрабатывает сообщение WM_ERASEBKGND, передавая его функции DefWindowProc,
последняя в ответ на это сообщение закрашивает внутреннюю область окна с
использованием кисти, указанной в классе окна (при регистрации класса окна).
Поэтому, если функция окна нарисует что-либо в окне во время обработки других
сообщений, отличных от WM_PAINT, после прихода первого же сообщения WM_PAINT
нарисованное изображение будет закрашено. Заметим, что изображение, нарисованное
в окне приложения GraphicsApp1 пропадает именно по этой причине.
Что же делать в том случае, когда по логике работы приложения требуется
изменить содержимое окна не во время обработки сообщения WM_PAINT, а в любом
другом месте приложения?
В этом случае приложение должно сообщить ОС Microsoft Windows, что
необходимо перерисовать часть окна или все окно. При этом в очередь приложения
будет записано сообщение WM_PAINT, обработка которого приведет к нужному
результату. Можно указать ОС, что был изменен прямоугольный участок окна или
область произвольной формы, например эллипс или многоугольник. Для этого
предназначены функции InvalidateRect и InvalidateRgn программного интерфейса Win32
API.
Пример обработки события Paint
Для форм класса System.Windows.Forms предусмотрен удобный объектно-
ориентированный способ, позволяющий приложению при необходимости
перерисовывать окно формы в любой момент времени. Когда вся клиентская область
окна формы или часть этой области требует перерисовки, форме передается событие
Paint. Все, что требуется от программиста, это создать обработчик данного события,
наполнив его необходимой функциональностью.
Для наглядной демонстрации методики обработки события Paint мы подготовим
простейшее приложение PaintApp, рисующее в своем окне текстовую строку и
геометрические фигуры.
Прежде всего, создайте проект приложения PaintApp, пользуясь мастером
проектов системы разработки Microsoft Visual Studio .NET. Далее выделите в окне
дизайнера форму Form1 и откройте вкладку событий для этой формы, показанную на
рис. 10-6.

Рис. 10-6. Добавление обработчика события Paint

Отыщите на вкладке строку события Paint и щелкните ее дважды левой клавишей


мыши. В результате будет создан обработчик события Form1_Paint (как это видно на
рис. 10-6). Этот обработчик будет получать управление всякий раз, когда по тем или
иным причинам возникнет необходимость в перерисовке содержимого окна нашего
приложения.
Вот в каком виде будет создан обработчик события Paint:
private void Form1_Paint(object sender,
System.Windows.Forms.PaintEventArgs e)
{
}
Обработчику Form1_Paint передаются два параметра.
Через первый параметр передается ссылка на объект, вызвавший событие. В
нашем случае это будет ссылка на форму Form1.
Что же касается второго параметра, то через него передается ссылка на объект
класса PaintEventArgs. Этот объект имеет два свойства, доступных только для чтения —
Graphics и ClipRectangle.
Класс Graphics Вам уже знаком — он представляет собой контекст отображения,
необходимый для рисования текста и геометрических фигур. Мы работали с этим
классом в приложениях GraphicsApp и GraphicsApp1, рассмотренных ранее в этой главе.
Обработчик события Paint получает контекст отображения через свои параметры,
поэтому программисту не нужно определять его специальным образом.
Через свойство ClipRectangle передаются границы области, которую должен
перерисовать обработчик события Paint. Эти границы передаются в виде объекта
класса Rectangle. Свойства этого класса Left, Right, Width и Height, наряду с другими
свойствами, позволяют определить расположение и размеры области.
Заметим, что в простейших случаях обработчик события Paint может игнорировать
свойство ClipRectangle, перерисовывая содержимое окна полностью. Однако процесс
перерисовки содержимого окна можно заметно ускорить, если перерисовывать не все
окно, а только область, описанную свойством ClipRectangle. Ускорение будет особенно
заметным, если в окне нарисовано много текста и геометрических фигур.
Итак, давайте отредактируем исходный текст приложения PaintApp таким образом,
чтобы в его окне была нарисована текстовая строка, прямоугольник и эллипс.
Прежде всего, создайте в классе Form1 поле text класса string, в котором будет
храниться отображаемая текстовая строка:
public string text;
Добавьте также в конструктор класса Form1 строку инициализации упомянутого
поля text:
public Form1()
{
//
// Required for Windows Form Designer support
//
InitializeComponent();

//
// TODO: Add any constructor code after InitializeComponent call
//
text = "Обработка события Paint";
}
И, наконец, измените исходный текст обработчика события Form1_Paint
следующим образом:
private void Form1_Paint(object sender,
System.Windows.Forms.PaintEventArgs e)
{
Graphics g = e.Graphics;

g.Clear(Color.White);
g.DrawString(text, new Font("Helvetica", 15),
Brushes.Black, 0, 0);
g.DrawRectangle(new Pen(Brushes.Black,2), 10, 30, 200, 100);
g.DrawEllipse(new Pen(Brushes.Black,2), 150, 120, 100, 130);
}
Здесь в теле обработчика Form1_Paint мы определили локальную переменную g
класса Graphics, предназначенную для хранения контекста отображения. Эта
переменная инициализируется при помощи значения, полученного из свойства Graphics
первого параметра обработчика Form1_Paint:
Graphics g = e.Graphics;
Получив контекст отображения, наш обработчик события Paint может рисовать в
соответствующем окне все, что угодно.
Вначале мы закрашиваем окно белым цветом, вызывая для этого метод Clear,
определенный в классе Graphics:
g.Clear(Color.White);
Таким способом мы можем закрасить фон, цвет которого задан для формы в
свойстве BackColor.
Далее мы вызываем методы DrawString, DrawRectangle и DrawEllipse, также
определенные в классе Graphics:
g.DrawString(text, new Font("Helvetica", 15), Brushes.Black, 0, 0);
g.DrawRectangle(new Pen(Brushes.Black,2), 10, 30, 200, 100);
g.DrawEllipse(new Pen(Brushes.Black,2), 150, 120, 100, 130);
Первый из них рисует текстовую строку в верхней части окна, а два других —
прямоугольник и эллипс, соответственно (рис. 10-7).

Рис. 10-7. Окно приложения PaintApp

Запустив приложение PaintApp, Вы сможете убедиться в том, что нарисованное


изображение не пропадает при изменении размеров окна. Оно заново
перерисовывается и в том случае, если окно приложения PaintApp оказывается
временно перекрыто окном другого приложения.
Перерисовка окон элементов управления
На данном этапе у читателя может возникнуть вопрос — а почему мы заговорили об
обработке события Paint только в 10 главе нашей книги? Почему эта проблема не
вставала перед нами раньше, при создании приложений с элементами управления,
такими как кнопки, текстовые поля, списки и деревья?
Дело в том, что до сих пор мы не предпринимали никаких попыток
непосредственного рисования в окнах форм или в окнах элементов управления. Все,
что мы делали, — это размещение элементов управления в окне формы и создание
обработчиков событий, создаваемых этими элементами управления при манипуляциях
пользователя с клавиатурой и мышью.
Однако сами по себе использованные нами ранее элементы управления
выполняют обработку события Paint, перерисовывая при необходимости свои окна. И
программисту нет никакой необходимости вмешиваться в этот процесс, если только ему
не нужно наделить тот или иной элемент управления нестандартным поведением.
Обработка сообщения Paint скрыта внутри классов элементов управления, поэтому Вы
можете создавать достаточно сложные приложения, даже не зная о существовании
события Paint.
Но как только Вам потребуется что-то нарисовать или написать в окне
приложения или в окне элемента управления, тут не обойтись без обработки события
Paint, а также без знаний приемов рисования с применением контекста отображения
Graphics.
Методы и свойства класса Graphics
В предыдущих разделах этой главы Вы познакомились с некоторыми методами,
определенными в классе Graphics и предназначенными для рисования текста и
геометрических фигур. Теперь мы расскажем Вам о методах и свойствах класса
Graphics более подробно.
Рисование геометрических фигур
Имена большого количества методов, определенных в классе Graphics, начинается с
префикса Draw* и Fill*. Первые из них предназначены для рисования текста, линий и
не закрашенных фигур (таких, например, как прямоугольные рамки), а вторые — для
рисования закрашенных геометрических фигур. Мы рассмотрим применение только
самых важных из этих методов, а полную информацию Вы найдете в документации.
Линия
Метод DrawLine, как это нетрудно догадаться из его названия, рисует линию,
соединяющую две точки с заданными координатами. Ниже мы привели прототипы
различных перегруженных версий этого метода:
public void DrawLine(Pen, Point, Point);
public void DrawLine(Pen, PointF PointF;
public void DrawLine(Pen, int, int, int, int);
public void DrawLine(Pen, float, float, float, float);
Первый параметр задает инструмент для рисования линии — перо. Перья
создаются как объекты класса Pen, например:
Pen p = new Pen(Brushes.Black,2);
Здесь мы создали черное перо толщиной 2 пиксела. Создавая перо, Вы можете
выбрать его цвет, толщину и тип линии, а также другие атрибуты. Подробнее об этом
мы расскажем ниже в разделе «Инструменты для рисования».
Остальные параметры перегруженных методов DrawLine задают координаты
соединяемых точек. Эти координаты могут быть заданы как объекты класса Point и
PointF, а также в виде целых чисел и чисел с плавающей десятичной точкой.
В классах Point и PointF определены свойства X и Y, задающие, соответственно,
координаты точки по горизонтальной и вертикальной оси. При этом в классе Point эти
свойства имеют целочисленные значения, а в классе PointF — значения с плавающей
десятичной точкой.
Третий и четвертый вариант метода DrawLine позволяет задавать координаты
соединяемых точек в виде двух пар чисел. Первая пара определяет координаты первой
точки по горизонтальной и вертикальной оси, а вторая — координаты второй точки по
этим же осям. Разница между третьим и четвертым методом заключается в
использовании координат различных типов (целочисленных int и с плавающей
десятичной точкой float).
Чтобы испытать метод DrawLine в работе, создайте приложение DrawLineApp
(аналогично тому, как Вы создавали предыдущее приложение). В этом приложении
создайте следующий обработчик события Paint:
private void Form1_Paint(object sender,
System.Windows.Forms.PaintEventArgs e)
{
Graphics g=e.Graphics;
g.Clear(Color.White);
for(int i=0; i<50; i++)
{
g.DrawLine(new Pen(Brushes.Black, 1), 10, 4 * i + 20, 200,
4 * i + 20);
}
}
Здесь мы вызываем метод DrawLine в цикле, рисуя 50 горизонтальных линий
(рис. 10-8).
Рис. 10-8. Горизонтальные линии в окне приложения DrawLineApp

Набор линий
Вызвав один раз метод DrawLines, можно нарисовать сразу несколько прямых линий,
соединенных между собой. Иными словами, метод DrawLines позволяет соединить
между собой несколько точек. Координаты этих точек по горизонтальной и
вертикальной оси передаются методу через массив класса Point или PointF:
public void DrawLines(Pen, Point[]);
public void DrawLines(Pen, PointF[];
Для демонстрации возможностей метода DrawLines мы подготовили приложение
DrawLinesApp. В классе Form1 этого приложения мы создаем кисть pen для рисования
линий, а также массив точек points, которые нужно соединить линиями:
Pen pen = new Pen(Color.Black, 2);
Point[] points = new Point[50];

public Form1()
{
//
// Required for Windows Form Designer support
//
InitializeComponent();

//
// TODO: Add any constructor code after InitializeComponent call
//

for(int i=0; i < 20; i++)


{
int xPos;
if(i%2 == 0)
{
xPos=10;
}
else
{
xPos=400;
}
points[i] = new Point(xPos, 10 * i);
}
}
Обратите внимание, что координаты точек по горизонтальной оси зависят от того,
является ли значение переменной цикла i четным или нечетным.
Рисование линий выполняется за один вызов метода DrawLines во время
обработки события Paint:
private void Form1_Paint(object sender,
System.Windows.Forms.PaintEventArgs e)
{
Graphics g = e.Graphics;
g.Clear(Color.White);
g.DrawLines(pen, points);
}
На рис. 10-9 мы показали результат работы этого приложения.

Рис. 10-9. Линии в окне приложения DrawLinesApp

Как видите, внешний вид линий оставляет желать лучшего — вместо прямых
линий мы получили ломаные линии!
Можно ли избавиться от этой неприятности?
Да, можно, и для этого нужно настроить один из параметров контекста
отображения, а именно параметр SmoothingMode. Этот параметр задает режим
сглаживания при отображении линий.
Включите в исходный текст программы пространство имен
System.Drawing.Drawing2D, в котором определено перечисление SmoothingMode:
using System.Drawing.Drawing2D;
Это пространство имен будет использоваться практически во всех примерах
приложений этой главы, так что не забывайте его включать.
Далее измените обработчик Form1_Paint следующим образом:
private void Form1_Paint(object sender,
System.Windows.Forms.PaintEventArgs e)
{
Graphics g=e.Graphics;
g.Clear(Color.White);
g.SmoothingMode = SmoothingMode.HighQuality;
g.DrawLines(pen, points);
}
Константа SmoothingMode.HighQuality, определенная в перечислении
SmoothingMode, задает высококачественное сглаживание при отображении линии. Как
видно на рис. 10-10, результат получился вполне удовлетворительный.
Рис. 10-10. Сглаживание линий

В таблице 10-1 мы перечислили возможные значения перечисления


SmoothingMode.
Таблица 10-1. Значения перечисления SmoothingMode
Значение Описание
Default Режим сглаживания по умолчанию. При
использовании этой константы сглаживание не
выполняется.
None Аналогично предыдущему.
HighSpeed Сглаживание выполняется с высокой скоростью,
однако с относительно плохим качеством.
HighQuality Сглаживание с максимально возможным качеством.
AntiAlias Сглаживание в режиме antialiased. Описание этого
режима Вы найдете в книгах, посвященных
графическому редактору Adobe Photoshop.
Invalid Неправильный режим сглаживания, его
использование вызывает исключение.
В общем случае использование сглаживания повышает качество изображения,
однако приводит к уменьшению скорости рисования.
Прямоугольник
Метод DrawRectangle позволяет рисовать прямоугольники, заданные координатой
верхнего левого угла, а также шириной и высотой. В библиотеке классов .NET
Frameworks имеется три перегруженных варианта этого метода:
public void DrawRectangle(Pen, Rectangle);
public void DrawRectangle(Pen, int, int, int, int);
public void DrawRectangle(Pen, float, float, float, float);
В качестве первого параметра этим методам передается перо класса Pen.
Остальные параметры задают расположение и размеры прямоугольника.
Класс Rectangle используется для описания расположения и размеров
прямоугольной области. Свойства X и Y этого класса задают координаты верхнего
левого угла прямоугольной области, соответственно, по горизонтальной и вертикальной
оси координат. Свойства Width и Height, хранят, соответственно, ширину и высоту
прямоугольной области. В классе Rectangle определены и другие свойства, а также
методы. Подробное описание этого класса Вы найдете в документации.
Варианты метода DrawRectangle с пятью параметрами позволяют задавать
расположение и размеры прямоугольника в виде целых чисел, а также в виде числе с
плавающей десятичной точкой. Второй и третий параметр задает расположение
верхнего левого угла по горизонтальной и вертикальной оси координат,
соответственно, а четвертый и пятый — ширину и высоту прямоугольника.
Пример использования DrawRectangle мы уже приводили выше в разделе «Пример
обработки события Paint», когда рассказывали о приложении PaintApp.
Набор прямоугольников
За один вызов метода DrawRectangles программа может нарисовать сразу несколько
прямоугольников. Существует два перегруженных варианта этого метода:
public void DrawRectangles(Pen, Rectangle[]);
public void DrawRectangles(Pen, RectangleF[]);
Первый из этих методов получает в качестве второго параметра ссылку на массив
объектов класса Rectangle, описывающих размеры и расположение прямоугольных
областей.
Второй метод использует для этого объекты класса RectangleF, свойства X, Y,
Width и Height которого задают расположение и размеры прямоугольника в виде чисел
с плавающей десятичной точкой.
Для демонстрации способа применения метода DrawRectangles мы подготовили
приложение DrawRectanglesApp.
В классе Form1 этого приложения мы определили перо myPen и массив
вложенных друг в друга прямоугольников myRectsArray:
Pen myPen = new Pen(Color.Black, 2);
Rectangle[] myRectsArray =
{
new Rectangle(10, 10, 200, 200),
new Rectangle(20, 20, 180, 180),
new Rectangle(30, 30, 160, 160),
new Rectangle(40, 40, 140, 140)
};
Метод DrawRectangles вызывается в теле обработчика события Paint, исходный
текст которого приведен ниже:
private void Form1_Paint(object sender,
System.Windows.Forms.PaintEventArgs e)
{
Graphics g=e.Graphics;
g.Clear(Color.White);
g.DrawRectangles(myPen, myRectsArray);
}
В результате работы метода Form1_Paint в окне приложения появляется наш
набор прямоугольников (рис. 10-11).

Рис. 10-11. Рисование прямоугольников

Многоугольник
Метод DrawPolygon поможет Вам в тех случаях, когда нужно нарисовать многоугольник,
заданный своими вершинами.
Предусмотрено два варианта этого метода:
public void DrawPolygon(Pen, Point[]);
public void DrawPolygon(Pen, PointF[]);
В первом случае методу DrawPolygon через второй параметр передается массив
точек класса Point, в котором координаты точек заданы целыми числами, а во втором —
массив класса PointF, где координаты соединяемых точек задаются в виде числе с
плавающей десятичной точкой.
Для демонстрации возможностей метода DrawPolygon мы создали приложение
DrawPolygonApp. В нем мы определили перо myPen и массив точек myPoints:
Pen myPen = new Pen(Color.Black, 2);

Point[] myPoints =
{
new Point(10, 10),
new Point(100, 40),
new Point(50, 240),
new Point(150, 24),
new Point(100, 100),
new Point(160, 40),
new Point(220, 210)
};
Обработчик события Form1_Paint соединяет эти точки вместе, вызывая метод
DrawPolygon:
private void Form1_Paint(object sender,
System.Windows.Forms.PaintEventArgs e)
{
Graphics g=e.Graphics;
g.Clear(Color.White);
g.DrawPolygon(myPen, myPoints);
}
На рис. 10-12 мы показали результат работы нашего приложения.

Рис. 10-12. Рисование многоугольника

Эллипс
Метод DrawEllipse рисует эллипс, вписанный в прямоугольную область, расположение и
размеры которой передаются ему в качестве параметров.
Предусмотрено четыре перегруженных варианта метода DrawEllipse:
public void DrawEllipse(Pen, Rectangle);
public void DrawEllipse(Pen, RectangleF);
public void DrawEllipse(Pen, int, int, int, int);
public void DrawEllipse(Pen, float, float, float, float);
Как видите, эти методы отличаются только способом, при помощи которого
описывается расположение и размеры прямоугольной области, в которую вписан
эллипс. Вы можете задавать расположение и размеры этой области в виде
рассмотренных ранее объектов класса Rectangle, RectangleF, а также в виде целых
чисел или числе с плавающей десятичной точкой.
Пример использования метода DrawEllipse мы привели в разделе «Пример
обработки события Paint», когда рассказывали о приложении PaintApp.
Сегмент эллипса
При помощи метода DrawArc программа может нарисовать сегмент эллипса. Сегмент
задается при помощи координат прямоугольной области, в которую вписан эллипс, а
также двух углов, отсчитываемых в направлении против часовой стрелки. Первый угол
Angle1 задает расположение одного конца сегмента, а второй Angle2 — расположение
другого конца сегмента (рис. 10-13).
Предусмотрено четыре перегруженных варианта метода DrawArc:
public void DrawArc(Pen, Rectangle, float, float);
public void DrawArc(Pen, RectangleF, float, float);
public void DrawArc(Pen, int, int, int, int, int, int);
public void DrawArc(Pen, float, float, float, float, float, float);
Первый параметр метода DrawArc определяет перо, с помощью которой будет
нарисован сегмент. Последние два параметра задают углы Angle1 и Angle2 в
соответствии с рис. 10-13. Расположение и размеры прямоугольной области
передаются методу DrawArc аналогично тому, как это делается для рассмотренного
выше метода DrawEllipse.

Рис. 10-13. Углы и прямоугольник, задающие сегмент эллипса

Для рисования сегмента эллипса мы создали приложение DrawArcApp. Вся работа


по рисованию выполняется внутри обработчика события Form1_Paint:
private void Form1_Paint(object sender,
System.Windows.Forms.PaintEventArgs e)
{
Pen myPen = new Pen(Color.Black, 2);

Graphics g=e.Graphics;
g.Clear(Color.White);
g.DrawArc(myPen, 10, 10, 200, 150, 30, 270);
}
Здесь мы использовали вариант метода DrawArc, допускающий указание
расположения и размеров прямоугольной области, а также углов в виде целых чисел.
Результат работы приложения DrawArcApp показан на рис. 10-14.
Рис. 10-14. Рисование сегмента эллипса

Кривые Безье
Из институтского курса математики Вам, скорее всего, известно понятие
сплайна (spline). Сплайн представляет собой кривую линию, соединяющую между
собой несколько точек.
Кривая Безье, представляющая собой одну из разновидностей сплайна, задается
четырьмя точками. Две из них — начальная и конечная, а две другие — управляющие.
Кривая Безье проходит через начальную и конечную точки, а управляющие точки
задают изгибы кривой линии. Те из Вас, кто когда-либо работал с векторными
графическими редакторами, например, с редактором Corel Draw, знают о
существовании кривых Безье и управляющих точек.
Для рисования кривых Безье имеются два перегруженных набора методов
DrawBezier и DrawBeziers:
public void DrawBezier(Pen, Point, Point, Point, Point);
public void DrawBezier(Pen, PointF, PointF, PointF, PointF);
public void DrawBezier(Pen, float, float, float, float, float, float,
float, float);
public void DrawBeziers(Pen, Point[]);
public void DrawBeziers(Pen, PointF[]);
Во всех этих методах первый параметр задает перо, которая будет использована
для рисования. Остальные параметры задают координаты начальной, конечной и
управляющих точек.
Что касается метода DrawBeziers, то он позволяет задавать координаты точек в
виде массивов, что может быть удобно в некоторых случаях.
В приложении DrawBezierApp мы рисуем кривую Безье в обработчике события
Paint:
private void Form1_Paint(object sender,
System.Windows.Forms.PaintEventArgs e)
{
Pen myPen = new Pen(Color.Black, 2);

PointF startPt = new PointF(40.0F, 80.0F);


PointF control1Pt = new PointF(30.0F, 10.0F);
PointF control2Pt = new PointF(350.0F, 250.0 F);
PointF endPt = new PointF(400.0F, 100.0F);

PointF[] myBezierPoints =
{
startPt,
control1Pt,
control2Pt,
endPt
};

Graphics g=e.Graphics;
g.Clear(Color.White);
g.DrawBeziers(myPen, myBezierPoints);
}
Здесь мы создаем начальную и конечную точки startPt и endPt, через которые
проходит наша кривая, а также управляющие точки control1Pt и control2Pt.
Координаты всех точек передаются методу DrawBeziers через массив myBezierPoints.
Управляющие точки изгибают линию, как бы притягивая ее к себе (рис. 10-15).

Рис. 10-15. Рисование кривой Безье

Канонические сплайны
В отличие от только что рассмотренных кривых линий Безье, линии канонического или
обычного сплайна (cardinal spline) проходит через все заданные точки.
Для рисования обычных сплайнов предусмотрены методы DrawCurve и
DrawClosedCurve. Первый из этих методов рисует незамкнутую кривую линию
(открытый сплайн), а второй — замкнутую (закрытый сплайн).
В простейшем случае методам передается перо и массив соединяемых точек:
public void DrawCurve(Pen, Point[]);
public void DrawCurve(Pen, PointF[]);
public void DrawCurveClosed(Pen, Point[]);
public void DrawCurveClosed(Pen, PointF[]);
Существуют версии методов, позволяющие дополнительно задать так называемую
жесткость (tension) сплайна. Жесткость задается в виде третьего дополнительного
параметра:
public void DrawCurve(Pen, Point[], float);
public void DrawCurve(Pen, PointF[], float);
public void DrawClosedCurve(Pen, Point[], float, FillMode);
public void DrawClosedCurve(Pen, PointF[], float, FillMode);
По умолчанию значение жесткости равно 0,5. При увеличении этого параметра
увеличиваются изгибы кривой линии. При жесткости большей 1 или меньшей 0 кривая
может превратиться в петлю.
Методу DrawClosedCurve дополнительно задается параметр типа FillMode. Если
значение этого параметра равно FillMode.Alternate, при рисовании самопересекающихся
замкнутых сплайнов будут чередоваться закрашенные и не закрашенные области. Если
же значение этого параметра равно FillMode.Winding, большинство замкнутых областей
будет закрашено.
В приложении DrawClosedCurveApp мы рисуем обычный сплайн, используя точки с
теми же координатами, что и в предыдущем примере:
using System.Drawing.Drawing2D;

private void Form1_Paint(object sender,
System.Windows.Forms.PaintEventArgs e)
{
Pen myPen = new Pen(Color.Black, 2);

PointF pt1 = new PointF(40.0F, 80.0F);


PointF pt2 = new PointF(30.0F, 10.0F);
PointF pt3 = new PointF(350.0F, 250.0F);
PointF pt4 = new PointF(400.0F, 100.0F);

PointF[] myPoints =
{
pt1,
pt2,
pt3,
pt4
};

Graphics g=e.Graphics;
g.Clear(Color.White);
g.DrawClosedCurve(myPen, myPoints, (float)0.3,
FillMode.Alternate);
}
Результат показан на рис. 10-16.

Рис. 10-16. Рисование канонического сплайна

Замкнутый сегмент эллипса


Для рисования замкнутого сегмента эллипса (pie) Вы можете воспользоваться методом
DrawPie. Имеется 4 перегруженных варианта этого метода:
public void DrawPie(Pen, Rectangle, float, float);
public void DrawPie(Pen, RectangleF, float, float);
public void DrawPie(Pen, int, int, int, int, int, int);
public void DrawPie(Pen, float, float, float, float, float, float);
В качестве первого параметра методу нужно передать перо для рисования.
Последние два параметра определяют углы, ограничивающие сегмент эллипса. Эти
углы используются таким же образом, как и при рисовании незамкнутого сегмента
эллипса методом DrawArc, рассмотренным выше. Остальные параметра задают
расположение и размеры прямоугольника, в который вписывается сегмент эллипса.
Для демонстрации возможностей метода DrawPie мы подготовили приложение
DrawPieApp. Вот как выглядит обработчик события Form1_Paint этого приложения:
private void Form1_Paint(object sender,
System.Windows.Forms.PaintEventArgs e)
{
Pen myPen = new Pen(Color.Black, 2);

int xPos = 10;


int yPos = 10;
int width = 250;
int height = 150;

int startAngle = 20;


int endAngle = 75;

Graphics g=e.Graphics;
g.Clear(Color.White);

g.DrawPie(myPen, xPos, yPos, width, height,


startAngle, endAngle);

g.DrawRectangle(new Pen(Color.Black, 1), xPos, yPos,


width, height);
}
Здесь через переменные xPos, yPos, width и height передаются координаты левого
верхнего угла и размеры прямоугольной области, в которую вписывается сегмент
эллипса. Переменные startAngle и endAngle задают углы, ограничивающие сегмент.
Для большей наглядности мы рисуем не только сегмент эллипса, но и
прямоугольник, в который этот сегмент вписан. Для рисования прямоугольника мы
используем линию толщиной 1 пиксел, а для рисования сегмента — два пиксела (рис.
10-17).

Рис. 10-17. Рисование сегмента эллипса

Закрашенные фигуры
В классе Graphics определен ряд методов, предназначенных для рисования
закрашенных фигур. Имена некоторых из этих методов, имеющих префикс Fill, мы
перечислили в табл. 10-2.
Таблица 10-2. Методы для рисования закрашенных фигур
Метод Описание
Рисование закрашенного прямоугольника
FillRectangle
Рисование множества закрашенных многоугольников
FillRectangles
Рисование закрашенного многоугольника
FillPolygon
Рисование закрашенного эллипса
FillEllipse
Рисование закрашенного сегмента эллипса
FillPie
Рисование закрашенного сплайна
FillClosedCurve
Рисование закрашенной области типа Region
FillRegion
Есть два отличия методов с префиксом Fill от одноименных методов с префиксом
Draw. Прежде всего, методы с префиксом Fill рисуют закрашенные фигуры, а методы с
префиксом Draw — не закрашенные. Кроме этого, в качестве первого параметра
методам с префиксом Fill передается не перо класса Pen, а кисть класса Brush.
Пример использования метода FillEllipse мы приводили ранее в разделе
«Рисование в окне элемента управления» этой главы. Вот еще один пример:
SolidBrush redBrush = new SolidBrush(Color.Red);
g.FillEllipse(redBrush, 50, 50, 100, 110);
Здесь мы вначале создаем кисть красного цвета как объект класса SolidBrush. Эта
кисть затем передается методу FillEllipse в качестве первого параметра. Остальные
параметры метода FillEllipse задают расположение и размеры прямоугольника, в
который будет вписан эллипс.
Рисование изображений
Рассказывая в [8] о том, как устроена графическая подсистема в ОС Microsoft Windows,
и как ей пользоваться, мы отметили, что рисование графических изображений с
использованием одного только программного интерфейса Win32 API — не простая
задача. Решая ее, программисту приходится учитывать многочисленные детали,
имеющие отношение к цветовому разрешению и режимам видеоадаптера. Кроме того,
часто приходится работать напрямую с внутренними форматами файлов графических
изображений, таких как BMP, GIF, JPEG, PNG и т.д.
Графическая подсистема GDI+, реализованная на платформе Microsoft .NET
Frameworks, содержит мощные и удобные средства работы с графикой и графическими
изображениями, исключающие для программиста необходимость разбираться с
внутренними форматами файлов графических изображений, а также задумываться о
таких вещах, как палитры [8].
Однако прежде чем мы займемся рисованием, необходимо сделать маленькое
отступление и рассказать о ресурсах приложения.
Немного о ресурсах приложения
Если Вы раньше создавали приложения Microsoft Windows, то знаете, что формат
загрузочного модуля таких приложений сложнее формата загрузочного модуля
программ MS-DOS. Кроме выполняемого кода и констант в загрузочном модуле
приложения Microsoft Windows находятся дополнительные данные — ресурсы.
Что такое ресурсы?
Приложение Microsoft Windows может хранить в виде ресурсов текстовые строки,
значки, курсоры, графические изображения, меню, диалоговые окна, произвольные
массивы данных и т. д. Физически ресурсы находятся внутри exe-файла приложения.
Они могут загружаться в оперативную память автоматически при запуске приложения
или по запросу приложения (явному или неявному). Такой механизм обеспечивает
экономное использование оперативной памяти, так как все редко используемые данные
можно хранить на диске и загружать в память только при необходимости.
Например, приложение может иметь сложную систему диагностики ошибочных
ситуаций, содержащую различные диалоговые окна, массивы сообщений об ошибках и
т. п. Когда приложение работает без ошибок (что, очевидно, является нормальным
явлением) ненужные диагностические ресурсы спокойно лежат на диске, не
перегружая оперативную память. Но как только возникает ошибка, и приложение
вызовет функцию обработки ошибки, эти ресурсы будут автоматически загружены.
Ресурсы можно редактировать без повторной трансляции программного проекта.
Это означает, что Вы сможете легко перевести все сообщения и тексты меню
приложения на другой национальный язык, отредактировать графические изображения
или любые другие ресурсы, даже не имея в своем распоряжении исходные тексты.
В приложениях Microsoft .NET Framework тоже используется концепция ресурсов.
В частности, ресурсы таких приложений могут содержать значки и графические
изображения.
Если приложение должно рисовать в своих окнах значки или графические
изображения, целесообразно включить их в ресурсы приложения. В этом случае
данные из файлов, содержащих значки или изображения, будут переписаны в файл
сборки приложения. Таким образом, Вы сможете поставлять приложение как единый
загрузочный файл, не «комплектуя» его дополнительно файлами графических
изображений.
Значки
В этом разделе мы опишем приложение DrawIconApp, предназначенное для
демонстрации способов рисования значков, хранящихся в ресурсах приложения.
Создайте приложение DrawIconApp обычным образом, пользуясь мастером
проектов. Затем перепишите в каталог проекта любой файл значка, взяв его,
например, из каталога значков системы Microsoft Visual Studio .NET.
Далее включите значок в проект приложения, щелкнув название проекта правой
клавишей мыши в окне Solution Explorer и выбрав затем строку контекстного меню Add
Existing Item. В результате всех этих действий файл значка появится в списке файлов
Вашего проекта.
Щелкните этот файл левой клавишей мыши, а затем установите значение свойства
Build Action для файла значка равным Embedded Resource (рис. 10-18).

Рис. 10-18. Включение значка в ресурсы приложения

В результате выполнения этих действий файл значка (в нашем случае это был
файл FACE02.ICO) будет добавлен в ресурсы приложения.
Создайте обработчик события Form1_MouseDown, получающий управление, когда
пользователь щелкает левой клавишей мыши окно приложения:
private void Form1_MouseDown(object sender,
System.Windows.Forms.MouseEventArgs e)
{
Graphics g = Graphics.FromHwnd(this.Handle);
Icon myIcon = new Icon(GetType(),"FACE02.ICO");
g.DrawIcon(myIcon, e.X, e.Y);
}
Этот обработчик получает контекст отображения с помощью метода
Graphics.FromHwnd, а затем создает объект класса Icon, представляющий значок. Для
создания этого объекта мы использовали конструктор с двумя параметрами.
Через первый параметр конструктору класса Icon мы передаем ссылку на объект-
сборку, в котором хранится наш ресурс — значок из файла FACE02.ICO. Мы можем
получить такую ссылку при помощи метода GetType.
Что же касается второго параметра, то через него мы передаем имя ресурса,
которое в нашем случае будет совпадать с именем файла значка.
Функция DrawIcon рисует значок myIcon в точке, где был курсор мыши в момент
щелчка (рис. 10-19).

Рис. 10-19. Использование метода DrawIcon

Всего существует два перегруженных метода DrawIcon:


public void DrawIcon(Icon, Rectangle);
public void DrawIcon(Icon, int, int);
В первом варианте метода параметр типа Rectangle задает прямоугольную
область, внутри которой будет нарисован значок. При этом значок будет
масштабирован таким образом, чтобы занять собой всю заданную область.
Независимо от значка myIcon, фрагмент кода, приведенный ниже, рисует в левом
верхнем углу значок с размерами 100x100 пикселов:
g.DrawIcon(myIcon, new Rectangle(0, 0, 100, 100));
Если исходный значок меньше указанных размеров, при рисовании он будет
растянут, а если больше — сжат.
Для рисования значков можно также использовать метод DrawIconUnstretched:
public void DrawIconUnstretched(Icon, Rectangle);
Этот метод рисует значок внутри области, заданной с помощью второго
параметра. В отличие от метода DrawIcon, метод DrawIconUnstretched не растягивает и
не сжимает значок, а рисует его в точном соответствии с оригинальными размерами
значка.
Разумеется, Вам не обязательно включать значок в проект приложения и
добавлять его к ресурсам. Вы можете воспользоваться вариантом конструктора класса
Icon, допускающим указание в своем единственном параметре имени или пути к файлу
значка. Если указать только имя значка, то при запуске приложения файл значка
должен будет находиться в том же каталоге, что и exe-файл самого приложения.
Растровые и векторные изображения
В ОС Microsoft Windows используются два формата изображений — аппаратно-
зависимый DDB (Device-Dependent Bitmap) и аппаратно-независимый DIB (Device-
Independent Bitmap).
Согласно определению, данному в документации, изображение DDB есть набор
битов в оперативной памяти, который может быть отображен на устройстве вывода
(например, выведен на экран видеомонитора или распечатан на принтере). Внутренняя
структура изображения DDB жестко привязана к аппаратным особенностям устройства
вывода. Поэтому представление изображения DDB в оперативной памяти полностью
зависит от устройства вывода. Иногда такие изображения называют растровыми,
подчеркивая тот факт, что их можно рассматривать как совокупность строк растра
(горизонтальных линий развертки).
Если бы в Microsoft Windows можно было работать только с изображениями DDB,
было бы необходимо иметь отдельные наборы изображений для каждого типа
видеоадаптера и каждого видеорежима, что, очевидно, крайне неудобно.
Аппаратно-независимое изображение DIB содержит описание цвета пикселов
изображения, которое не зависит от особенностей устройства отображения. ОС
Microsoft Windows после соответствующего преобразования может нарисовать такое
изображение на любом устройстве вывода. Несмотря на некоторое замедление
процесса вывода по сравнению с выводом изображений DDB, универсальность
изображений DIB делает их весьма привлекательными для хранения изображений.
При создании обычных приложений Microsoft Windows, работающих файлами
изображений, программисту следовало учитывать, что существует множество форматов
таких файлов. Файлы изображений могут содержать таблицу цветов или цветовую
палитру, могут быть сжаты с использованием тех или иных алгоритмов. Приложение
должно также анализировать структуру графических файлов с целью обнаружения
возможных ошибок, так как отображение содержимого неправильного файла может
создать полный хаос на экране. При необходимости Вы найдете в [8] полную
информацию о форматах DDB и DIB.
Кроме растровых изображений используются так называемые векторные
изображения. Если растровые изображения содержат описание цвета каждого пиксела,
векторные изображения состоят из описаний отдельных графических объектов, из
которых состоит изображение. Это могут быть линии, окружности и т. п. Некоторые
графические редакторы, например, Corel Draw, Microsoft Draw, Microsoft Visio, для
внешнего представления изображения используют векторный формат.
Сравнивая эти форматы, отметим, что каждый из них имеет свои преимущества и
свои недостатки.
Растровые изображения, как правило, выводятся на экран быстрее, так как их
внутренняя структура аналогична (до некоторой степени) структуре видеопамяти. К
недостаткам растровых изображений можно отнести большой объем памяти,
требующийся для их хранения (которых доходит до десятков и сотен Мбайт),
невозможность масштабирования без потери качества изображения, а также сложность
выделения и изменения отдельных объектов изображения.
Векторные изображения состоят из описаний отдельных элементов, поэтому они
легко масштабируются. С помощью такого графического редактора, как, например,
Corel Draw, Вы без особого труда сможете выделить отдельный элемент изображения и
изменить его внешний вид. Следует отметить, что некоторые устройства вывода, такие
как плоттер (графопостроитель), способен работать только с векторными
изображениями, так как с помощью пера можно рисовать только линии. В
операционной системе Microsoft Windows и многих офисных приложениях широко
используется такой векторный формат изображений, как метафайл WMF (Windows
Metafile).
Как мы уже говорили, графический интерфейс GDI+, реализованный компанией
Microsoft на платформе Microsoft .NET Frameworks, значительно облегчает работу с
изображениями, как растровыми, так и векторными. В табл. 10-4 мы привели полный
список форматов графических файлов, с которыми могут работать приложения GDI+.
Эти форматы определены в виде свойств статического класса ImageFormat.
Таблица 10-4. Доступные форматы графических файлов
Свойство Описание формата
Растровые изображения DIB (Device-Independent
Bmp
Bitmap)
Расширенный формат EMF (Windows Enhanced
Emf
Metafile)
Формат для обмена файлами изображений
Exif
EXIF (Exchangeable Image File)
Формат обмена графическими изображениями
Gif
GIF (Graphics Interchange Format)
Значки ОС Microsoft Windows
Icon
Формат, созданный объединенной группой экспертов
Jpeg
по обработке фотографий JPEG (Joint Photographic
Experts Group)
Изображения DIB, создаваемые в оперативной
MemoryBmp
памяти
Формат переносимых сетевых изображений
Png
PNG (Portable Network Graphics), разработанный
группой W3C
Теговый формат файлов изображений TIFF (Tag
Tiff
Image File Format)
Метафайл Windows WMF (Windows metafile)
Wmf
Как видите, список довольно внушительный и включает в себя практически все
наиболее распространенные форматы файлов. Все эти форматы, за исключением WMF
и EMF, являются растровыми.
Использование класса Image
Для рисования изображений, загруженных из файлов с форматами, перечисленными в
табл. 10-4, поможет класс Image. В этом разделе мы создадим приложение
ImageViewApp, способное показывать содержимое графических файлов, значки
которых перетаскиваются в окно приложения из папок ОС Microsoft Windows методом
буксировки. Вы сможете убедиться, что отображение графических изображений в
программах C# действительно не вызывает никаких затруднений.
Попутно мы расскажем об использовании буксировки в приложениях
Microsoft .NET Frameworks.
Режим буксировки
Если Вы когда-либо сталкивались с необходимостью писать обычные приложения
Microsoft Windows, способные обрабатывать файлы или другие объекты, значки
которых перетаскиваются пользователем в окно приложения методом буксировки, то
знаете, насколько это непросто. Фактически при этом необходимо реализовать в своем
приложении OLE-сервер, а эта задача доступна только опытным и очень опытным
программистам, владеющим всеми тонкостями модели компонентного объекта (COM,
Component Object Model), реализованной в ОС Microsoft Windows.
К счастью, при создании приложений Microsoft .NET Framework эта задача
решается очень и очень просто. Мы покажем ее решение на примере упомянутого выше
приложения ImageViewApp, предназначенного для просмотра содержимого файлов
графических изображений. Нам необходимо сделать так, чтобы пользователи могли
перетаскивать значки изображений в окно нашего приложения из папок ОС Microsoft
Windows для просмотра.
Итак, с помощью мастера проектов создайте приложение ImageViewApp.
Для того чтобы главное окно нашего приложения могло принимать объекты,
перетаскиваемые методом буксировки, установите значение свойства AllowDrop равным
True, как это показано на рис. 10-20.
Рис. 10-20. Включение режима AllowDrop

Заметим, что данное свойство предусмотрено не только у окна формы, но и у окон


различных элементов управления, таких, например, как текстовые поля, текстовые
редакторы, списки, панели и др.
События при буксировке
Далее нам необходимо предусмотреть обработку как минимум двух событий. Это
события DragOver и DragDrop.
Событие DragOver возникает, когда пользователь при перемещении курсора над
поверхностью клиентской области окна. Обработчик этого события должен сообщить
вызывающей программе, сможет ли он принять данные, передаваемые методом
буксировки.
Событие DragDrop возникает после завершения буксировки, когда пользователь
отпускает кнопку над поверхностью окна, принимающего объект буксировки.
Обработчик этого события должен принять и обработать данные. В нашем случае
обработка будет заключаться в отображении содержимого графического файла.
Помимо этих событий возникают еще события DragEnter и DragLeave. Первое из
них позволяет отслеживать момент входа курсора мыши в область принимающего окна,
а второе — когда курсор покидает эту область.
Обработка события DragOver
Для обработки события DragOver мы подготовили метод Form1_DragOver, который Вам
нужно добавить в приложение ImageViewApp:
private void Form1_DragOver(object sender,
System.Windows.Forms.DragEventArgs e)
{
e.Effect = DragDropEffects.Copy;
}
Обработчику события DragOver передается объект класса DragEventArgs.
Анализируя свойства этого объекта, Ваша программа может принять решение о том,
сможет ли она обработать объект, который пользователь отбуксировал в окно
приложения.
Свойство KeyState объекта DragEventArgs несет в себе информацию о том, какой
клавишей мыши осуществляется буксировка, и нажаты ли при этом клавиши Shift,
Control и Alt. Это свойство представляет собой набор флажков, перечисленных в табл.
10-5.
Таблица 10-5. Флажки свойства KeyState
Бит флажка Описание
Нажата левая клавиша мыши
1
Нажата правая клавиша мыши
2
Нажата клавиша Shift
4
Нажата клавиша Control
8
Нажата средняя клавиша мыши
16
Нажата клавиша Alt
32
В свойствах X и Y объекта DragEventArgs передаются, соответственно,
горизонтальные и вертикальные координаты курсора мыши (в экранных координатах).
Анализируя эти свойства, программа может принять решение о возможности
выполнения операции буксировки в те или иные области окна.
Анализируя свойство Data объекта DragEventArgs с помощью методов GetFormats
и GetDataPresent, приложение может определить тип передаваемых ему данных.
Описание этих методов Вы найдете в документации.
Свойство AllowedEffects содержит информацию о действиях, которые программа
может предпринять с передаваемыми ей данными. Это одна из следующих констант,
описанных в табл. 10-6.
Таблица 10-6. Константы перечисления AllowedEffects
Бит флажка Описание
Приложение не может принять данные,
None
передаваемые методом буксировки
Данные должны быть скопированы в приложение,
Copy
принимающее объект методом буксировки
Данные должны быть перемещены в приложение,
Move
принимающее объект методом буксировки
Необходимо связать буксируемые данные с
Link
принимающим приложением
Разрешается операция прокрутки содержимого в
Scroll
окне принимающего приложения
Разрешаются все операции
All
После анализа содержимого перечисленных выше свойств объекта DragEventArgs
программа должна установить значение свойства Effect, разрешив или запретив
выполнение той или иной операции. При этом следует использовать константы,
перечисленные в табл. 10-6.
В приложении ImageViewApp мы разрешаем копирование буксируемых данных,
записывая в свойство Effect константу DragDropEffects.Copy:
e.Effect = DragDropEffects.Copy;
Обработка события DragDrop
Теперь займемся обработкой события DragDrop. Вот соответствующий обработчик:
Image img;

private void Form1_DragDrop(object sender,


System.Windows.Forms.DragEventArgs e)
{
string[] files = (string[])e.Data.GetData(DataFormats.FileDrop);
img = Image.FromFile(files[0]);
Invalidate();
}
Если пользователь буксирует в окно приложения файл, то формат данных,
извлеченных методом GetData из свойства Data, будет DataFormats.FileDrop. В качестве
данных будет выступать массив полных путей к файлам (пользователь может
перетаскивать сразу несколько файлов). Если же пользователь перетаскивает в окно
приложения выделенный фрагмент текста, формат данных будет
DataFormats.StringFormat.
Нас интересует только один файл, т.к. приложение ImageViewApp может
показывать за один раз только одно изображение.
Загрузка изображения
Для загрузки изображения в память мы используем очень мощный метод
Image.FromFile, передавая ему самый первый путь к файлу, извлеченный из нулевой
ячейки массива files:
Image img;

img = Image.FromFile(files[0]);
Этот метод загрузит изображение файла любого типа из набора, перечисленного в
табл. 10-4.
После того как изображение будет загружено, наше приложение сообщает о
необходимости перерисовки его окна, вызывая для этого метод Invalidate:
Invalidate();
В результате будет создано событие Paint, обработчик которого займется
рисованием загруженного изображения в окне нашей программы.
Так как при рисовании изображения мы используем масштабирование, это же
событие мы будем создавать и при изменении размеров окна нашего приложения:
private void Form1_Resize(object sender, System.EventArgs e)
{
Invalidate();
}
Изменение размеров окна приводит к генерации события Resize, так что Вам
остается только предусмотреть для него обработчик Form1_Resize.
Рисование загруженного изображения
И, наконец, вот код обработчика события Form1_Paint, на который возложена работа
по рисованию загруженного изображения:
private void Form1_Paint(object sender,
System.Windows.Forms.PaintEventArgs e)
{
if(img != null)
{
RectangleF winRect = new RectangleF(0, 0,
e.Graphics.VisibleClipBounds.Width,
e.Graphics.VisibleClipBounds.Height);

SizeF size = new SizeF(img.Width / img.HorizontalResolution,


img.Height / img.VerticalResolution);

float scale = Math.Min(winRect.Width / size.Width,


winRect.Height / size.Height);

size.Width *= scale;
size.Height *= scale;

e.Graphics.DrawImage(img,
winRect.X + (winRect.Width - size.Width) / 2,
winRect.Y + (winRect.Height - size.Height) / 2,
size.Width, size.Height);
}
}
Как видите, большая часть программного кода выполняет масштабирование, а для
рисования мы вызываем один из перегруженных методов Graphics.DrawImage.
Получив управление, метод Form1_Paint проверяет, загрузил ли пользователь
какое-либо изображение для рисования. Если загрузил, то метод Form1_Paint
определяет границы прямоугольной области, занимаемой клиентской областью окна
нашего приложения:
RectangleF winRect = new RectangleF(0, 0,
e.Graphics.VisibleClipBounds.Width,
e.Graphics.VisibleClipBounds.Height);
Именно в этой области и будет нарисовано изображение, файл которого
пользователь отбуксировал мышью.
Обычно программы, предназначенные для просмотра изображений, умеют
масштабировать их таким образом, чтобы изображение полностью помещалось в окне
приложения. Чтобы реализовать необходимую логику масштабирования, нам нужно
знать некоторые атрибуты загруженного изображения. Эти атрибуты доступны
приложению как свойства класса Image.
В частности, свойства Width и Height содержат размеры загруженного
изображения в дюймах. Чтобы пересчитать эти значения в пикселы для отображения на
экране, нам потребуются свойства HorizontalResolution и VerticalResolution. Первое из
них содержит количество пискелов в пересчет на один дюйм изображения по
горизонтали, а второе — по вертикали.
Таким образом, чтобы получить размеры изображения в пикселах, нам нужно
воспользоваться следующим выражением:
SizeF size = new SizeF(img.Width / img.HorizontalResolution,
img.Height / img.VerticalResolution);
Размеры изображения получаются в виде объекта класса SizeF, в котором
определены свойства Width и Height.
Зная размеры окна winRect.Width и winRect.Height, в котором мы будем рисовать
изображение, а также размеры изображения size.Width и size.Height, мы вычисляем
масштаб, необходимый для того, чтобы изображение полностью поместилось в окне:
float scale = Math.Min(winRect.Width / size.Width,
winRect.Height / size.Height);
Здесь с помощью метода Math.Min мы выбираем наименьшее среди отношений
ширины и высоты окна к ширине и высоте изображения.
Полученный масштаб используется для вычисления новых размеров изображения:
size.Width *= scale;
size.Height *= scale;
И, наконец, последнее действие — рисование изображения:
e.Graphics.DrawImage(img,
winRect.X + (winRect.Width - size.Width) / 2,
winRect.Y + (winRect.Height - size.Height) / 2,
size.Width, size.Height);
Через первый параметр методу Graphics.DrawImage передается изображение,
загруженное при выполнении операции буксировки. Второй и третий параметры задают
координаты верхнего левого угла прямоугольной области, в которой будет нарисовано
изображение, а четвертый и пятый — размеры этой области. При рисовании будет
выполнено масштабирование.
Расположение прямоугольной области выбирается таким образом, чтобы
изображение было отцентрировано в окне приложения при любых размерах
изображения. Результат работы приложения ImageViewApp показан на рис. 10-21.
Рис. 10-21. Просмотр изображений в окне приложения PictureViewer

Заметим, что помимо метода DrawImage, в классе Graphics определен метод


DrawImageUnscaled. Этот метод аналогичен методу DrawImage, но отличается от него
тем, что при рисовании не выполняет масштабирование изображения.
Рисование текста
Еще один важный метод, определенный в классе Graphics, это метод DrawString. С
помощью этого метода приложения могут рисовать в своих окнах текстовые строки.
Напомним, что ранее мы уже пользовались методом DrawString в приложении
PaintApp, описанном в разделе «Событие Paint» этой главы. Мы вызвали этот метод в
обработчике события Form1_Paint, как это показано ниже:
public string text;

text = "Обработка события Paint";

private void Form1_Paint(object sender,


System.Windows.Forms.PaintEventArgs e)
{
Graphics g = e.Graphics;

g.Clear(Color.White);
g.DrawString(text, new Font("Helvetica", 15),
Brushes.Black, 0, 0);

}
В качестве первого параметра методу DrawString передается текстовая строка,
которую нужно нарисовать.
Второй параметр задает шрифт. О шрифтах мы расскажем ниже, в разделе
«Инструменты для рисования». С помощью третьего параметра задается кисть, с
применением которой будет нарисован текст. И, наконец, два последних параметра
определяют координаты точки, в которой начнется рисование текста.
В классе Graphics определено несколько перегруженных вариантов метода
DrawString:
public void DrawString(string, Font, Brush, PointF);
public void DrawString(string, Font, Brush, RectangleF);
public void DrawString(string, Font, Brush, PointF, StringFormat);
public void DrawString(string, Font, Brush, RectangleF,
StringFormat);
public void DrawString(string, Font, Brush, float, float);
public void DrawString(string, Font, Brush, float, float,
StringFormat);
Параметр типа PointF задает расположение точки вывода текста. В последних двух
вариантах метода DrawString расположение этой точки задается при помощи пары
чисел формата float.
Если задан параметр типа RectangleF, то текст будет нарисован внутри области,
размеры и расположение которой заданы этим параметром. В том случае, если текст
выйдет за границы области, то он будет обрезан.
И, наконец, параметр типа StringFormat позволяет выполнить форматирование
текста. Описание этой возможности Вы найдете в документации.
Инструменты для рисования
Все методы класса Graphics, предназначенные для рисования фигур или текста,
получают через один из параметров перо класса Pen или кисть класса Brush, с
помощью которых и выполняется рисование. Метод DrawString, кроме этого, получает
еще и шрифт, применяемый для рисования текста.
В этом разделе нашей книги мы познакомимся с перечисленными выше
инструментами рисования, применяемые в системе GDI+ и доступными приложениям
C# с графическим интерфейсом.
Перья
Перья используются для рисования линий и простейших геометрических фигур и
создаются как объекты класса Pen. Вот соответствующие конструкторы:
public Pen(Color);
public Pen(Color, float);
public Pen(Brush);
public Pen(Brush, float);
Первый из этих конструкторов создает перо заданного цвета. Цвет задается при
помощи объекта класса Color. Второй конструктор позволяет дополнительно задать
толщину пера.
Третий и четвертый конструктор создают перо на основе кисти, причем в
четвертом конструкторе можно указать толщину создаваемого пера. О кистях мы
расскажем чуть позже в этой главе.
После того как перо создано, программа может определить его атрибуты при
помощи свойств класса Pen. Некоторые из этих свойств перечислены в табл. 10-7.
Таблица 10-7. Свойства пера
Свойство Описание
Выравнивание пера
Alignment
Ширина линии
Width
Кисть, используемая пером
Brush
Цвет пера
Color
Стиль пунктирных и штрих-пунктирных линий
DashStyle
Вид точек и штрихов пунктирных и штрих-
DashCup
пунктирных линий
Расстояние от начала линии до начала штриха
DashOffset
Массив шаблонов для создания произвольных
DashPattern
штрихов и пробелов штриховых и штрих-пунктирных
линий
Стиль концов линий
StartCup
EndCup
Формы концов линий
LineCap
Стиль соединения концов двух различных линий
LineJoin
Предельная толщина в области соединения
MiterLimit
остроконечных линий
Как видите, предусмотрены многочисленные возможности для создания самых
разных перьев.
Устанавливая значение свойства Color и Width, приложение может изменить,
соответственно, цвет и ширину линии, рисуемой пером.
Если надо нарисовать пунктирную или штрих-пунктирную линию, приложение
должно задать необходимое значение для свойства DashStyle. При этом допускается
изменять вид точек и тире пунктирных и штрих-пунктирных линий (свойство DashCup),
задавать расстояние от начала линии до начала штриха (свойство DashOffset) или даже
вовсе задать произвольный вид для штрихов и разделяющих эти штрихи пробелов
(свойство DashPattern).
При необходимости изменить внешний вид концов линий используйте свойства
StartCup и EndCup, задающие стиль концов линий. Свойство LineCap определяет форму
концов линий.
Если вас интересует стык между двумя различными линиями, то стиль этого стыка
задается свойством LineJoin, а предельная толщина стыка — стилем MiterLimit.
В приложении PenApp мы покажем способы изменения толщины линии, стиля
пунктирной и штрих-пунктирной линии, а также стиля концов линий. Ниже мы привели
исходный текст обработчика события Form1_Paint, рисующий линии различных типов и
стилей:
using System.Drawing.Drawing2D;

private void Form1_Paint(object sender, System.Windows.Forms.PaintEventArgs e)
{
Graphics g=e.Graphics;
g.Clear(Color.White);

int x=10;
int y=10;
Pen myPen = new Pen(Color.Black, 1);

g.DrawLine(myPen, x, y, 200, y);

y += 15;
myPen.Width = 2;
g.DrawLine(myPen, x, y, 200, y);

y += 15;
myPen.Width = 3;
g.DrawLine(myPen, x, y, 200, y);

y += 15;
myPen.Width = 5;
g.DrawLine(myPen, x, y, 200, y);

y += 15;
myPen.Width = 10;
g.DrawLine(myPen, x, y, 200, y);

y += 15;
myPen.Width = 3;
myPen.DashStyle = DashStyle.Dash;
g.DrawLine(myPen, x, y, 200, y);

y += 15;
myPen.Width = 3;
myPen.DashStyle = DashStyle.DashDot;
g.DrawLine(myPen, x, y, 200, y);

y += 15;
myPen.Width = 3;
myPen.DashStyle = DashStyle.DashDotDot;
g.DrawLine(myPen, x, y, 200, y);

y += 15;
myPen.Width = 3;
myPen.DashStyle = DashStyle.Dot;
g.DrawLine(myPen, x, y, 200, y);

y += 15;
myPen.Width = 5;
myPen.DashStyle = DashStyle.Solid;
myPen.StartCap = LineCap.ArrowAnchor;
myPen.EndCap = LineCap.DiamondAnchor;
g.DrawLine(myPen, x, y, 200, y);

y += 15;
myPen.Width = 5;
myPen.StartCap = LineCap.Round;
myPen.EndCap = LineCap.RoundAnchor;
g.DrawLine(myPen, x, y, 200, y);

y += 15;
myPen.Width = 5;
myPen.StartCap = LineCap.Square;
myPen.EndCap = LineCap.SquareAnchor;
g.DrawLine(myPen, x, y, 200, y);

y += 15;
myPen.Width = 5;
myPen.StartCap = LineCap.Triangle;
myPen.EndCap = LineCap.Flat;
g.DrawLine(myPen, x, y, 200, y);
}
В начале своей работы метод Form1_Paint получает ссылку на объект класса
Graphics, т.е. контекст отображения:
Graphics g=e.Graphics;
Используя полученный контекст отображения, метод Form1_Paint закрашивает
окно приложения белым цветом, а затем рисует черную линию толщиной 1 пиксел:
g.Clear(Color.White);

int x=10;
int y=10;
Pen myPen = new Pen(Color.Black, 1);

g.DrawLine(myPen, x, y, 200, y);


Эта техника использовалась нами ранее в приложениях, описанных в этой главе.
А вот дальше начинается новое — мы изменяем свойство Width созданного ранее
пера myPen, и снова рисуем линию с небольшим смещением по вертикали:
y += 15;
myPen.Width = 2;
g.DrawLine(myPen, x, y, 200, y);
После этого мы рисуем подобным образом еще несколько линий увеличивающейся
толщины.
На следующем этапе наша программа изменяет значение свойства DashStyle,
последовательно присваивая ему значения DashStyle.Dash, DashStyle.DashDot,
DashStyle.DashDotDot и DashStyle.Dot:
y += 15;
myPen.Width = 3;
myPen.DashStyle = DashStyle.Dash;
g.DrawLine(myPen, x, y, 200, y);

y += 15;
myPen.Width = 3;
myPen.DashStyle = DashStyle.DashDot;
g.DrawLine(myPen, x, y, 200, y);

y += 15;
myPen.Width = 3;
myPen.DashStyle = DashStyle.DashDotDot;
g.DrawLine(myPen, x, y, 200, y);

y += 15;
myPen.Width = 3;
myPen.DashStyle = DashStyle.Dot;
g.DrawLine(myPen, x, y, 200, y);
В результате приложение нарисует в своем окне четыре пунктирные и штрих-
пунктирные линии различного типа.
Финальная часть обработчика события Form1_Paint показывает возможность
изменения стиля концов линий.
Сначала мы устанавливаем значение свойства DashStyle равным DashStyle.Solid,
отменяя рисование пунктирных и штрих-пунктирных линий. Далее мы четыре раза
устанавливаем различные свойства StartCap и EndCap, снабжая линии наконечниками
различных стилей:
y += 15;
myPen.Width = 5;
myPen.DashStyle = DashStyle.Solid;
myPen.StartCap = LineCap.ArrowAnchor;
myPen.EndCap = LineCap.DiamondAnchor;
g.DrawLine(myPen, x, y, 200, y);

y += 15;
myPen.Width = 5;
myPen.StartCap = LineCap.Round;
myPen.EndCap = LineCap.RoundAnchor;
g.DrawLine(myPen, x, y, 200, y);

y += 15;
myPen.Width = 5;
myPen.StartCap = LineCap.Square;
myPen.EndCap = LineCap.SquareAnchor;
g.DrawLine(myPen, x, y, 200, y);

y += 15;
myPen.Width = 5;
myPen.StartCap = LineCap.Triangle;
myPen.EndCap = LineCap.Flat;
g.DrawLine(myPen, x, y, 200, y);
Результат выполнения этих операций представлен на рис. 10-22.

Рис. 10-22. Рисование линий различными перьями

На рис. 1.3. показаны геометрические фигуры, нарисованные с использованием


различных перьев, сплошных и пунктирных.
Разумеется, что с помощью подобных перьев можно рисовать не только прямые
линии, но и любые геометрические фигуры.
Кисти
Внутренняя область окна и замкнутых геометрических фигур может быть закрашена
при помощи кисти. В приложениях Microsoft .NET Frameworks кисти создаются на базе
классов, производных от абстрактного класса Brush. Это следующие классы:
 Brushes
 SolidBrush;
 HatchBrush;
 TextureBrush;
 LinearGradientBrush;
 PathGradientBrush
Кисть для сплошной закраски
Простейшие из кистей — это кисти Brushes и SolidBrush, предназначенные для
сплошной закраски фигур. Эти кисти создается при помощи конструктора с одним
параметром, задающим цвет в виде объекта класса Color.
В начале этой главы мы рассказывали о приложении PaintApp, в котором кисть
класса Brushes применялась для создания кисти, с помощью которой приложение
рисовало прямоугольник и эллипс. Кроме этого, кисть черного цвета создавалась и для
рисования текста:
private void Form1_Paint(object sender,
System.Windows.Forms.PaintEventArgs e)
{
Graphics g = e.Graphics;

g.Clear(Color.White);
g.DrawString(text, new Font("Helvetica", 15),
Brushes.Black, 0, 0);
g.DrawRectangle(new Pen(Brushes.Black,2), 10, 30, 200, 100);
g.DrawEllipse(new Pen(Brushes.Black,2), 150, 120, 100, 130);
}
Кисти типа HatchBrush
При помощи класса HatchBrush можно создать прямоугольную кисть заданного стиля, с
заданным цветом изображения и фона.
Для создания кистей этого типа предусмотрено два конструктора:
public HatchBrush(HatchStyle, Color);
public HatchBrush(HatchStyle, Color, Color);
Первый из этих конструкторов позволяет создать кисть заданного стиля и цвета, а
второй дополнительно позволяет указать цвет фона.
В табл. 10-8 мы перечислили различные стили кисти HatchBrush, представляющие
собой константы перечисления HatchStyle.
Таблица 10-8. Стили кисти типа HatchBrush
Константа Описание
Линии штриховки располагаются в обратном
BackwardDiagonal
направлении (от верхнего правого угла к нижнему
левому углу кисти)
Пересекающиеся горизонтальные и вертикальные
Cross
линии
Диагональные линии, идущие в направлении снизу
DarkDownwardDiagonal
вверх, и расположенные на 50% плотнее, чем при
использовании константы ForwardDiagonal (темная
штриховка)
Горизонтальные линии, которые на 50% плотнее,
DarkHorizontal
чем при использовании константы Horizontal (темная
штриховка)
Диагональные линии, плотнее на 50% чем при
DarkUpwardDiagonal
использовании константы BackwardDiagonal (темная
штриховка)
Вертикальные линии, которые на 50% плотнее, чем
DarkVertical
при использовании константы Vertical (темная
штриховка)
Штриховые диагональные линии, идущие в обратном
DashedDownwardDiagonal
направлении
Штриховые горизонтальные линии
DashedHorizontal
Штриховые диагональные линии, идущие в прямом
DashedUpwardDiagonal
направлении
Штриховые вертикальные линии
DashedVertical
Диагональная «кирпичная» штриховка
DiagonalBrick
Пересекающиеся прямые и обратные диагональные
DiagonalCross
линии
Штриховка в виде дерна
Divot
Прямые и обратные диагональные пересекающиеся
DottedDiamond
линии, состоящие из отдельных точек
Горизонтальные и вертикальные пересекающиеся
DottedGrid
линии, состоящие из отдельных точек
Прямые диагональные линии, идущие в направлении
ForwardDiagonal
от верхнего левого угла к нижнему правому углу
кисти
Горизонтальные линии
Horizontal
Горизонтальные «кирпичные» линии
HorizontalBrick
Штриховка в виде шахматной доски с крупными
LargeCheckerBoard
клетками
Штриховка в виде конфетти
LargeConfetti
Пересекающиеся горизонтальные и вертикальные
LargeGrid
линии (то же, что и Cross)
Светлая обратная диагональная штриховка
LightDownwardDiagonal
Светлая горизонтальная штриховка
LightHorizontal
Светлая прямая диагональная штриховка
LightUpwardDiagonal
Светлая вертикальная штриховка
LightVertical
То же, что и SolidDiamond
Max
То же, что и Horizonal
Min
Средняя горизонтальная штриховка
NarrowHorizontal
Средняя вертикальная штриховка
NarrowVertical
Пересекающиеся прямые и обратные диагональные
OutlinedDiamond
линии штриховки
Эти константы задают процентное соотношение
Percent05
цвета штриховки и цвета фона кисти.
Percent10
Percent20
Percent30

Percent90
Штриховка в виде пледа
Plaid
Кровельная штриховка
Shingle
Штриховка в виде шахматной доски с мелкими
SmallCheckerBoard
клетками
Штриховка в виде мелкого конфетти
SmallConfetti
Штриховка в виде мелкой сетки
SmallGrid
Штриховка в виде шахматной доски, расположенная
SolidDiamond
по диагонали
Штриховка с использованием сферических фигур
Sphere
Штриховка в виде решетки
Trellis
Вертикальные линии
Vertical
Волнообразные линии
Wave
Штриховка в виде ткани
Weave
Широкие обратные диагональные линии
WideDownwardDiagonal
Широкие прямые диагональные линии
WideUpwardDiagonal
Зигзагообразные горизонтальные линии
ZigZag
Для того чтобы продемонстрировать использование кистей класса HatchBrush, мы
подготовили приложение HatchBrushApp.
Вот исходный текст обработчика событий Form1_Paint этого приложения, в
котором выполняются существенные для нас операции:
using System.Drawing.Drawing2D;

private void Form1_Paint(object sender, System.Windows.Forms.PaintEventArgs e)


{
Graphics g=e.Graphics;
g.Clear(Color.White);

HatchBrush hb = new HatchBrush(HatchStyle.Cross, Color.Black,


Color.White);

g.FillRectangle(hb, 10, 30, 200, 100);


g.DrawRectangle(new Pen(Brushes.Black,1), 10, 30, 200, 100);

HatchBrush hb1 = new HatchBrush(HatchStyle.DottedGrid,


Color.Black, Color.White);
g.FillEllipse(hb1, 150, 120, 100, 130);
g.DrawEllipse(new Pen(Brushes.Black,1), 150, 120, 100, 130);

HatchBrush hb2 = new HatchBrush(HatchStyle.Divot, Color.Black,


Color.White);
g.FillEllipse(hb2, 60, 160, 60, 60);
g.DrawEllipse(new Pen(Brushes.Black,1), 60, 160, 60, 60);
}
Как видите, мы здесь последовательно создаем три различных кисти, а затем
используем их для закраски внутренних областей прямоугольника и эллипсов.
Результат работы приложения HatchBrushApp показан на рис. 10-23.
Рис. 10-23. Использование кистей класса HatchBrush

Кисти типа TextureBrush


Если Вас не устраивает ни одна из кистей, перечисленных в табл. 10-8, то Вы можете
создать собственную кисть на базе класса TextureBrush, в виде произвольного
изображения. Такая кисть, называемая текстурной, может иметь любой внешний вид и
любой цвет.
Для создания кисти класса TextureBrush Ваше приложение может воспользоваться
одним из следующих конструкторов:
public TextureBrush(Image);
public TextureBrush(Image, Rectangle);
public TextureBrush(Image, RectangleF);
public TextureBrush(Image, WrapMode);
public TextureBrush(Image, Rectangle, ImageAttributes);
public TextureBrush(Image, WrapMode, Rectangle);
public TextureBrush(Image, WrapMode, RectangleF);
Самому простому из этих конструкторов нужно передать изображение,
загруженное из ресурсов приложения или из внешнего файла (с помощью
рассмотренного ранее метода Image.FromFile).
Структуры Rectangle и RectangleF позволяют задать границы прямоугольной
области, ограничивающие изображение кисти.
С помощью констант перечисления WrapMode программа может задать способ
размещения текстуры по горизонтали и вертикали. Эти константы приведены в табл.
10-9.
Таблица 10-9. Константы перечисления WrapMode
Константа Описание
Текстура кисти «прикрепляется» к границе объекта
Clamp
При закраске текстура кисти повторяется по
Tile
вертикали и горизонтали
Аналогично предыдущему, но изображения в
TileFlipX
соседних столбцах заменяется зеркальным,
отражаясь по вертикали
Аналогично предыдущему, но отражение происходит
TileFlipY
по горизонтали
Отражение и по вертикали, и по горизонтали.
TileFlipXY
И, наконец, параметр ImageAttributes позволяет задать различные атрибуты
изображения, такие как количество цветов и способ рисования. Описание этого
параметра и класса ImageAttributes Вы найдете в электронной справочной
документации системы Microsoft Visual Studio .NET.
Закраску с помощью текстурной кисти мы демонстрируем в приложении
TextureBrushApp. Ниже мы привели исходный текст обработчика события Form1_Paint
этого приложения, в котором происходит все самое интересное:
private void Form1_Paint(object sender,
System.Windows.Forms.PaintEventArgs e)
{
Graphics g=e.Graphics;
g.Clear(Color.White);

Image myBrushImage = new Bitmap(GetType(),"L_RED.GIF");


TextureBrush tb = new TextureBrush(myBrushImage);

g.FillRectangle(tb, 10, 30, 200, 100);


g.DrawRectangle(new Pen(Brushes.Black,1), 10, 30, 200, 100);
}
После получения контекста отображения и очистки поверхности окна мы создаем
новое изображение класса Image, загружая его из ресурсов приложения:
Image myBrushImage = new Bitmap(GetType(),"L_RED.GIF");
Обратите внимание, что мы создаем объект класса Bitmap, а полученную в
результате ссылку присваиваем объекту класса Image. Предполагается, что перед
трансляцией приложения Вы скопировали файл текстуры L_RED.GIF в ресурсы
приложения, а также установили значение свойства Build Action для файла
изображения равным Embedded Resource.
Результат закраски прямоугольной области текстурной кистью показан на рис. 10-
24.

Рис. 10-24. Закрашивание прямоугольника кистью типа TextureBrush

Градиентные кисти
Приложениям GDI+ доступен еще один вид кистей — так называемая градиентная
кисть. Линейная градиентная кисть LinearGradientBrush позволяет задать в кисти
переход от одного цвета к другому. Кисти с множественным градиентом
PathGradientBrush позволяют задать внутри кисти область, которая будет закрашена с
использованием цветового градиента.
В нашей книге из-за недостатка места мы рассмотрим только один вариант
использования линейной градиентной кисти.
Рассмотрим обработчик события Form1_Paint приложения LinearGradientApp,
специально созданного нами для демонстрации возможностей закрашивания при
помощи линейной градиентной кисти:
private void Form1_Paint(object sender,
System.Windows.Forms.PaintEventArgs e)
{
Graphics g=e.Graphics;
g.Clear(Color.White);

Rectangle rect = new Rectangle(10, 10, 50, 50);

LinearGradientBrush gb = new LinearGradientBrush(rect,


Color.White, Color.Black, LinearGradientMode.BackwardDiagonal);

g.FillRectangle(gb, 10, 30, 200, 100);


g.DrawRectangle(new Pen(Brushes.Black,1), 10, 30, 200, 100);
}
Прямоугольная область задает пространство, в котором происходит изменение
цвета. Конструктору класса LinearGradientBrush передаются координаты этой области,
значения двух цветов, а также режим градиентного закрашивания LinearGradientMode.
Возможные значения перечисления LinearGradientMode мы привели в табл. 10-10.
Таблица 10-10. Константы перечисления LinearGradientMode
Константа Направление изменения цвета
Слева направо
Horizontal
Сверху вниз
Vertical
По диагонали из верхнего левого угла в нижний
ForwardDiagonal
правый угол
По диагонали из верхнего правого угла в нижний
BackwardDiagonal
левый угол
На рис. 10-25 мы показали пример градиентного закрашивания внутренней
области прямоугольника в приложении LinearGradientApp.

Рис. 10-25. Закрашивание прямоугольника линейной градиентной кистью типа


LinearGradientBrush

Шрифты
Для того чтобы рисовать текст, используются шрифты. ОС Microsoft Windows может
работать с растровыми, векторными и масштабируемыми шрифтами. Кроме этого,
приложения Microsoft Windows могут использовать шрифты, встроенные в устройство
вывода (обычно это принтерные шрифты).
Классификация шрифтов
Растровые шрифты содержат образы всех символов в виде растровых изображений.
При этом для каждого размера шрифта необходимо иметь свой набор символов. Кроме
того, различные устройства вывода имеют разное соотношение горизонтальных и
вертикальных размеров пиксела, что приводит к необходимости хранить отдельные
наборы образов символов не только для разных размеров шрифта, но и для разного
соотношения размеров пиксела физического устройства отображения.
Растровые шрифты плохо поддаются масштабированию, так как при этом
наклонные линии контура символа принимают зазубренный вид.
Векторные шрифты хранятся в виде набора векторов, описывающих отдельные
сегменты и линии контура символа, поэтому они легко масштабируются. Однако их
внешний вид далек от идеального. Как правило, векторные шрифты используются для
вывода текста на векторные устройства, такие, как плоттер.
В состав ОС Microsoft Windows входит не очень большое количество шрифтов,
однако при необходимости Вы можете приобрести дополнительные шрифты как
отдельно, так и в составе различного программного обеспечения. Например, вместе с
графическим редактором Corel Draw поставляются сотни различных шрифтов.
Помимо обычных шрифтов существуют символьные или декоративные шрифты,
содержащие вместо букв различные значки.
Шрифты TrueType
Масштабируемые шрифты TrueType впервые появились в Microsoft Windows версии 3.1
и сильно повлияли на рост популярности этой ОС. Шрифты TrueType поддаются
масштабированию без существенных искажений внешнего вида.
Рис. 10-26 иллюстрирует ухудшение внешнего вида растрового и векторного
шрифтов при увеличенном размере символов. Внешний вид масштабируемого шрифта
не ухудшился.
Масштабируемые шрифты TrueType не только сохраняют свое начертание при
произвольном изменении высоты букв, но и обладают другими достоинствами.
Отметим, например, возможность вывода строк текста, расположенных под любым
углом относительно горизонтальной оси. Растровые и векторные шрифты позволяют
располагать строки текста только в горизонтальном направлении, что может создать
определенные трудности, например, при необходимости надписать название улицы на
карте города.
Еще одно преимущество масштабируемых шрифтов TrueType связано с тем, что Вы
можете встроить такой шрифт непосредственно в документ.
Зачем это может понадобиться?
Дело в том, что стандартный набор шрифтов TrueType, поставляемых в составе ОС
Microsoft Windows, не всегда удовлетворяет пользователей. Поэтому они приобретают
дополнительные шрифты у независимых разработчиков. Однако использование
нестандартных шрифтов может привести к проблемам при необходимости переноса
документа из одного компьютера в другие, так как там нужного шрифта может не
оказаться. Вы, конечно, можете просто скопировать нужный шрифт и перенести его
вместе с документом, однако такая процедура может быть запрещена по условию
лицензионного соглашения с разработчиками шрифта.

Рис. 10-26. Растровый, векторный и масштабируемый шрифты

Проблему переноса документа на другой компьютер с сохранением прав


разработчиков шрифта можно решить, используя шрифты, встроенные в документ.
Пользователь может, например, подготовить документ в текстовом процессоре Microsoft
Word и встроить в него все использованные шрифты. При переносе такого документа
на другой компьютер эти шрифты можно будет использовать для просмотра и,
возможно, редактирования этого (и только этого) документа. Возможность
редактирования с использованием встроенного шрифта определяется разработчиком
шрифта.
Шрифт TrueType состоит из изображений (рисунков) отдельных символов —
глифов (glyph). Для внутреннего представления глифа в файле шрифта TrueType
используются описания контуров, причем один глиф может содержать несколько таких
контуров (рис. 10-27).

Рис. 10-27. Рисунки символов

Глифы могут иметь различный внешний вид (typeface). ОС Microsoft Windows


классифицирует шрифты по типам, или семействам (font family). Эти типы называются
Modern, Roman, Swiss, Script, Decorative.
Шрифты семейства Modern имеют одинаковую ширину букв. Шрифты семейства
Roman содержат буквы различной ширины, имеющие засечки. Семейство Swiss
отличается тем, что при переменной ширине букв они не имеют засечек. Буквы в
шрифтах семейства Script как бы написаны от руки. Семейство Decorative содержит
глифы в виде небольших картинок (значков).
В следующей таблице мы привели примеры шрифтов различных семейств.
Приложения Microsoft Windows могут заказывать шрифт, ссылаясь на название
соответствующего семейства, однако в зависимости от состава имеющихся шрифтов ОС
Microsoft Windows может предоставить приложению не тот шрифт, какой бы Вам
хотелось.
Другая важная характеристика шрифта — это размер символов. Для описания
вертикального размера символов шрифта используются несколько параметров (рис. 10-
28).

Рис. 10-28. Параметры вертикального размера шрифта

Отсчет всех размеров выполняется от так называемой базовой линии (base line)
шрифта. Для размеров используются логические единицы, которые зависят от режима
отображения, установленного в контексте устройства.
На рис. 10-28 общая высота символов отмечена как Height. Эта высота
складывается из двух компонентов — Ascent и Descent. Компонент Ascent представляет
собой высоту от базовой линии с учетом таких элементов, как тильда в букве «Й».
Компонент Descent определяет пространство, занимаемое символами ниже базовой
линии. Сумма Ascent и Descent в точности равна Height.
Величина InternalLeading определяет размер выступающих элементов символов и
может быть равна нулю.
Величина ExternalLeading определяет минимальный межстрочный интервал,
рекомендуемый разработчиком шрифта. Ваше приложение может игнорировать
межстрочный интервал, однако в этом случае строки будут соприкасаться друг с
другом, что не всегда приемлемо.
Как видите, с размерами символов здесь далеко не все так просто, как хотелось
бы!
Растровые шрифты, которые относятся к одному семейству, но имеют разные
размеры букв, хранятся в отдельных файлах. В то же время благодаря возможности
масштабирования шрифтов TrueType для них нет необходимости в отдельном хранении
глифов различных размеров.
Графический интерфейс GDI может выполнять масштабирование растровых
шрифтов, увеличивая (но не уменьшая) размер букв. Результат такого
масштабирования при большом размере букв обычно неудовлетворительный, так как
на наклонных линиях контура букв образуются зазубрины (рис. 10-26). Что же
касается GDI+, то он работает только с масштабируемыми шрифтами, к которым
относятся шрифты TrueType.
Векторные шрифты легко поддаются масштабированию, поэтому для хранения
шрифта одного семейства, но разного размера, можно использовать один файл.
Вы знаете, что шрифты могут иметь нормальное (normal), жирное (bold) или
наклонное (italic) начертание. В табл. 10-11 мы привели примеры различных
начертаний шрифтов. В табл. 10-12 Вы найдете примеры начертаний шрифтов,
доступные приложениям GDI+.
Таблица 10-11. Примеры начертаний шрифтов
Начертание Образец шрифта
AaBbCcDdEeFfGgHhIiJjKkLl АаБбВвГгДдЕеЖжЗзИиКкЛлМмНн
Normal
AaBbCcDdEeFfGgHhIiJjKkLl АаБбВвГгДдЕеЖжЗзИиКкЛлМмНн
Bold
AaBbCcDdEeFfGgHhIiJjKkLl
Italic
АаБбВвГгДдЕеЖжЗзИиКкЛлМмНнОоПпРр
Графический интерфейс GDI получает жирное и наклонное начертание растровых
шрифтов из нормального при помощи соответствующих алгоритмов утолщения и
наклона шрифта. Такие алгоритмы могут быть использованы и для масштабируемых
шрифтов TrueType, однако лучших результатов можно достигнуть при использовании
отдельных файлов шрифтов TrueType для нормального, жирного и наклонного
начертания.
Еще один часто используемый атрибут оформления строк текста —
подчеркивание. Иногда используется шрифт с перечеркнутыми буквами. GDI
выполняет подчеркивание самостоятельно, файлы шрифтов не содержат глифы с
подчеркиванием.
Шрифты OpenType
Операционная система Microsoft Windows 2000 и Microsoft Windows ХР способны
работать с шрифтами OpenType, созданными совместно компаниями Adobe и Microsoft.
Шрифты OpenType сочетают в себе достоинства шрифтов TrueType, а также шрифтов
Type1, разработанных компанией Adobe и широко применяемых в издательском деле.
На рис. 10-29 мы показали содержимое папки шрифтов Fonts, которую можно
найти в окне управляющей панели Control Panel.
Рис. 10-29. Шрифты в ОС Microsoft Windows 2000

В этой папке векторные и растровые шрифты обозначены буквой A, шрифты


TrueType — буквами TT, а шрифты OpenType — буквой O.
Чтобы просмотреть образцы текста, оформленные тем или иным шрифтом,
достаточно дважды щелкнуть название шрифта в папке Fonts. Результат такого
просмотра для шрифта Comic Sans MS показан на рис. 10-30.
Рис. 10-30. Просмотр шрифта Comic Sans MS

Щелкнув кнопку Print, расположенную в верхнем правом углу окна просмотра, Вы


сможете распечатать образец и посмотреть, как этот шрифт будет выглядеть в
бумажном документе.
Выбор шрифта
Прежде чем нарисовать текстовую строку, приложение должно выбрать шрифт, создав
объект класса Font. В приложении PaintApp мы выбирали шрифт для рисования текста
методом DrawString:
private void Form1_Paint(object sender,
System.Windows.Forms.PaintEventArgs e)
{
Graphics g = e.Graphics;

g.Clear(Color.White);
g.DrawString(text, new Font("Helvetica", 15),
Brushes.Black, 0, 0);

}
Помимо шрифта, методу DrawString необходимо передать кисть для рисования
текста, а также координаты точки, в которой этот текст должен быть нарисован.
Существуют и другие перегруженные варианты метода DrawString, причем для каждого
из них необходимо указать шрифт.
Конструкторы класса Font
В классе Font существует довольно много конструкторов, с помощью которых можно
подобрать любой нужный Вам шрифт:
public Font(string, float);
public Font(FontFamily, float);
public Font(FontFamily, float, FontStyle);
public Font(FontFamily, float, GraphicsUnit);
public Font(string, float, FontStyle);
public Font(string, float, GraphicsUnit);
public Font(FontFamily, float, FontStyle, GraphicsUnit);
public Font(string, float, FontStyle, GraphicsUnit);
public Font(FontFamily, float, FontStyle, GraphicsUnit, byte);
public Font(string, float, FontStyle, GraphicsUnit, byte);
public Font(FontFamily, float, FontStyle, GraphicsUnit, byte, bool);
public Font(string, float, FontStyle, GraphicsUnit, byte, bool);
public Font(Font, FontStyle);
Первому конструктору нужно передать название шрифта (точнее говоря, название
гарнитуры шрифта), а также высоту символов в пунктах (в одном дюйме содержится 72
пункта):
public Font(string, float);
Выбирая название гарнитуры шрифта, учитывайте, что можно указывать только
шрифты TrueType и OpenType. Если указанный Вами шрифт не установлен на
компьютере пользователя, ОС Microsoft Windows заменит его другим шрифтом,
наиболее подходящим с ее «точки зрения». Лучше всего, если программа позволит
пользователю выбирать шрифт для отображения текста из числа установленных в
системе шрифтов, тогда с отображением текста будет меньше проблем.
Последний из конструкторов позволяет создать шрифт на основе другого шрифта,
изменив его стиль FontStyle.
Конструкторы, у которых имеется параметр типа byte, позволяют задавать номер
набора символов в терминах GDI.
И, наконец, последний параметр типа bool позволяет создавать шрифты с
вертикальным расположением строк символов.
Тип шрифта FontStyle
Параметр типа FontStyle задает тип шрифта. Возможные значения констант
перечисления FontStyle и их описания мы привели в табл. 10-12.
Таблица 10-12. Константы перечисления FontStyle
Константа Описание Образец шрифта
Обычный AaBbCcDdEeFfGgHhIiJjKkLl
Regular
АаБбВвГгДдЕеЖжЗзИиКкЛлМмНн
Жирный AaBbCcDdEeFfGgHhIiJjKkLl
Bold
АаБбВвГгДдЕеЖжЗзИиКкЛлМмНн
Наклонный AaBbCcDdEeFfGgHhIiJjKkLl
Italic
АаБбВвГгДдЕеЖжЗзИиКкЛлМмНнОоПпРр
Подчеркнутый AaBbCcDdEeFfGgHhIiJjKkLl
Underline
АаБбВвГгДдЕеЖжЗзИиКкЛлМмНнОоПпРр
Перечеркнутый AaBbCcDdEeFfGgHhIiJjKkLl
Strikeout
АаБбВвГгДдЕеЖжЗзИиКкЛлМмНнОоПпРр
Единицы измерения размера шрифта
Параметр конструктора Font типа GraphicsUnit дает Вам возможность указывать
размеры шрифта не только в пунктах, но и в других единицах измерения. В табл. 10-13
мы привели константы перечисления GraphicsUnit с кратким описанием.
Таблица 10-13. Константы перечисления GraphicsUnit
Константа Описание единицы измерения
1/75 часть дюйма
Display

1/300 часть дюйма


Document

Дюйм
Inch

Миллиметр
Millimeter

Пиксел
Pixel
Пункт (1/72 дюйма)
Point

Единицы глобальных координат (world unit)


World

Семейство шрифта FontFamily


С помощью конструкторов класса Font, принимающих ссылку на объект класса
FontFamily, можно выбрать шрифт из группы шрифтов, имеющий похожий дизайн и
лишь немного отличающихся в стиле.
Вот конструкторы класса FontFamily:
public FontFamily(string);
public FontFamily(string, FontCollection);
Первый конструктор позволяет задать имя семейства, а второй — выбрать шрифт
из числа шрифтов, установленных приложением.
Приложение FontApp
Для демонстрации способов создания шрифтов класса Font с использованием
конструкторов различного типа мы создали приложение FontApp.
Вот исходный текст обработчика событий Form1_Paint этого приложения:
private void Form1_Paint(object sender,
System.Windows.Forms.PaintEventArgs e)
{
Graphics g=e.Graphics;
g.Clear(Color.White);

Font f1 = new Font("Helvetica", 10);


g.DrawString("Шрифт Helvetica", f1, Brushes.Black, 10, 10);

Font f2 = new Font(new FontFamily("Courier"), 10);

g.DrawString("Шрифт семейства Courier", f2, Brushes.Black,


10, 30);

Font f3 = new Font("Times New Roman", 10,


FontStyle.Bold | FontStyle.Strikeout);

g.DrawString("Шрифт Times New Roman, жирный, перечеркнутый",


f3, Brushes.Black, 10, 50);

Font f4 = new Font("Helvetica", 10, GraphicsUnit.Millimeter);

g.DrawString("Шрифт Helvetica (10 мм)", f4, Brushes.Black,


10, 70);
}
Сначала мы создаем шрифт, указывая его имя и размер:
Font f1 = new Font("Helvetica", 10);
Этим конструктором мы уже пользовались ранее в нашей книге.
Следующий конструктор выбирает не какой-либо конкретный шрифт, а любой
шрифт семейства Courier:
Font f2 = new Font(new FontFamily("Courier"), 10);
Такие шрифты являются моноширинными, т.е. ширина всех символов шрифта
одинаковая. Моноширинные шрифты обычно используются для оформления
программных листингов.
При создании шрифта можно задать его стиль, как мы это сделали в следующем
примере:
Font f3 = new Font("Times New Roman", 10,
FontStyle.Bold | FontStyle.Strikeout);
Здесь создается жирный перечеркнутый шрифт.
Последний конструктор создает для нас шрифт, указывая его высоту не в пунктах,
а в миллиметрах:
Font f4 = new Font("Helvetica", 10, GraphicsUnit.Millimeter);
Результат работы нашей программы Вы можете увидеть на рис. 10-31.

Рис. 10-31. Окно приложения FontApp

Визуальное проектирование
приложений C#
А.В. Фролов, Г.В. Фролов

Приложение 1. Исходные тексты приложения


SimpleNotepad
В этом приложении Вы найдете полные исходные тексты приложения SimpleNotepad,
которые мы подробно рассмотрели в 4 и 5 главах книги.
Листинг П1-1. Файл ch05\SimpleNotepad\SimpleNotepadForm.cs
using System;
using System.Drawing;
using System.Collections;
using System.ComponentModel;
using System.Windows.Forms;
using System.Data;

using System.IO;
using System.Drawing.Printing;

namespace SimpleNotepad
{
/// <summary>
/// Summary description for Form1.
/// </summary>
public class SimpleNotepadForm : System.Windows.Forms.Form
{
private System.Windows.Forms.MainMenu mainMenu1;
private System.Windows.Forms.MenuItem menuFile;
private System.Windows.Forms.MenuItem menuFileNew;
private System.Windows.Forms.MenuItem menuFileOpen;
private System.Windows.Forms.MenuItem menuFileSave;
private System.Windows.Forms.MenuItem menuFileSaveAs;
private System.Windows.Forms.MenuItem menuItem3;
private System.Windows.Forms.MenuItem menuFilePageSetup;
private System.Windows.Forms.MenuItem menuFilePrint;
private System.Windows.Forms.MenuItem menuItem6;
private System.Windows.Forms.MenuItem menuFileExit;
private System.Windows.Forms.MenuItem menuEdit;
private System.Windows.Forms.MenuItem menuEditUndo;
private System.Windows.Forms.MenuItem menuItem10;
private System.Windows.Forms.MenuItem menuEditCut;
private System.Windows.Forms.MenuItem menuEditCopy;
private System.Windows.Forms.MenuItem menuEditPaste;
private System.Windows.Forms.MenuItem menuEditDelete;
private System.Windows.Forms.MenuItem menuEditSelectAll;
private System.Windows.Forms.MenuItem menuFormat;
private System.Windows.Forms.MenuItem menuFormatFont;
private System.Windows.Forms.MenuItem menuHelp;
private System.Windows.Forms.MenuItem menuHelpAbout;
private System.Windows.Forms.OpenFileDialog openFileDialog1;
private System.Windows.Forms.RichTextBox richTextBox1;
private System.Windows.Forms.SaveFileDialog saveFileDialog1;
private System.Windows.Forms.MenuItem menuFilePrintPreview;
private System.Drawing.Printing.PrintDocument printDocument1;
private System.Windows.Forms.PageSetupDialog pageSetupDialog1;
private System.Windows.Forms.PrintPreviewDialog printPreviewDialog1;
private System.Windows.Forms.PrintDialog printDialog1;
private System.ComponentModel.IContainer components;

/// <summary>
/// StringReader для печати содержимого редактора текста
/// </summary>
private StringReader m_myReader;

/// <summary>
/// Номер текущей распечатываемой страницы документа
/// </summary>
private uint m_PrintPageNumber;
private System.Windows.Forms.MenuItem menuItem1;
private System.Windows.Forms.MenuItem menuItem2;
private System.Windows.Forms.MenuItem menuFormatFontCharacterStyle;
private System.Windows.Forms.MenuItem menuFormatFontCharacterStyleBold;
private System.Windows.Forms.MenuItem menuFormatFontCharacterStyleItalic;
private System.Windows.Forms.MenuItem menuFormatFontCharacterStyleUnderline;
private System.Windows.Forms.MenuItem menuFormatFontParagraphAlignment;
private System.Windows.Forms.MenuItem menuFormatFontParagraphAlignmentLeft;
private System.Windows.Forms.MenuItem menuFormatFontParagraphAlignmentRight;
private System.Windows.Forms.MenuItem menuFormatFontParagraphAlignmentCenter;
private System.Windows.Forms.FontDialog fontDialog1;
private System.Windows.Forms.MenuItem menuFormatColor;
private System.Windows.Forms.ColorDialog colorDialog1;
private System.Windows.Forms.MenuItem menuItem4;
private System.Windows.Forms.MenuItem menuFormatFontCharacterStyleStrikeout;
private System.Windows.Forms.ToolBar toolBar1;
private System.Windows.Forms.ImageList imageList1;
private System.Windows.Forms.ToolBarButton toolBarButton1;
private System.Windows.Forms.ToolBarButton toolBarButton2;
private System.Windows.Forms.ToolBarButton toolBarButton3;
private System.Windows.Forms.ToolBarButton toolBarButton4;
private System.Windows.Forms.ToolBarButton toolBarButton5;
private System.Windows.Forms.ToolBarButton toolBarButton6;
private System.Windows.Forms.ToolBarButton toolBarButton7;
private System.Windows.Forms.ToolBarButton toolBarButton8;
private System.Windows.Forms.ToolBarButton toolBarButton9;
private System.Windows.Forms.ToolBarButton toolBarButton10;
private System.Windows.Forms.StatusBar statusBar1;
private System.Windows.Forms.StatusBarPanel statusBarPanel1;
private System.Windows.Forms.StatusBarPanel statusBarPanel2;
private System.Windows.Forms.NotifyIcon notifyIcon1;
private System.Windows.Forms.MenuItem menuHelpRegister;

/// <summary>
/// Флаг изменения содержимого документа
/// </summary>
private bool m_DocumentChanged = false;

public SimpleNotepadForm()
{
//
// Required for Windows Form Designer support
//
InitializeComponent();

//
// TODO: Add any constructor code after InitializeComponent call
//
m_PrintPageNumber = 1;
}

/// <summary>
/// Clean up any resources being used.
/// </summary>
protected override void Dispose( bool disposing )
{
if( disposing )
{
if (components != null)
{
components.Dispose();
}
}
base.Dispose( disposing );
}

#region Windows Form Designer generated code


/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
private void InitializeComponent()
{
this.components = new System.ComponentModel.Container();
System.Resources.ResourceManager resources = new
System.Resources.ResourceManager(typeof(SimpleNotepadForm));
this.mainMenu1 = new System.Windows.Forms.MainMenu();
this.menuFile = new System.Windows.Forms.MenuItem();
this.menuFileNew = new System.Windows.Forms.MenuItem();
this.menuFileOpen = new System.Windows.Forms.MenuItem();
this.menuFileSave = new System.Windows.Forms.MenuItem();
this.menuFileSaveAs = new System.Windows.Forms.MenuItem();
this.menuItem3 = new System.Windows.Forms.MenuItem();
this.menuFilePageSetup = new System.Windows.Forms.MenuItem();
this.menuFilePrintPreview = new System.Windows.Forms.MenuItem();
this.menuFilePrint = new System.Windows.Forms.MenuItem();
this.menuItem6 = new System.Windows.Forms.MenuItem();
this.menuFileExit = new System.Windows.Forms.MenuItem();
this.menuEdit = new System.Windows.Forms.MenuItem();
this.menuEditUndo = new System.Windows.Forms.MenuItem();
this.menuItem1 = new System.Windows.Forms.MenuItem();
this.menuItem2 = new System.Windows.Forms.MenuItem();
this.menuEditCut = new System.Windows.Forms.MenuItem();
this.menuEditCopy = new System.Windows.Forms.MenuItem();
this.menuEditPaste = new System.Windows.Forms.MenuItem();
this.menuEditDelete = new System.Windows.Forms.MenuItem();
this.menuItem10 = new System.Windows.Forms.MenuItem();
this.menuEditSelectAll = new System.Windows.Forms.MenuItem();
this.menuFormat = new System.Windows.Forms.MenuItem();
this.menuFormatFont = new System.Windows.Forms.MenuItem();
this.menuFormatColor = new System.Windows.Forms.MenuItem();
this.menuItem4 = new System.Windows.Forms.MenuItem();
this.menuFormatFontCharacterStyle = new System.Windows.Forms.MenuItem();
this.menuFormatFontCharacterStyleBold = new System.Windows.Forms.MenuItem();
this.menuFormatFontCharacterStyleItalic = new System.Windows.Forms.MenuItem();
this.menuFormatFontCharacterStyleUnderline = new
System.Windows.Forms.MenuItem();
this.menuFormatFontCharacterStyleStrikeout = new
System.Windows.Forms.MenuItem();
this.menuFormatFontParagraphAlignment = new System.Windows.Forms.MenuItem();
this.menuFormatFontParagraphAlignmentLeft = new
System.Windows.Forms.MenuItem();
this.menuFormatFontParagraphAlignmentRight = new
System.Windows.Forms.MenuItem();
this.menuFormatFontParagraphAlignmentCenter = new
System.Windows.Forms.MenuItem();
this.menuHelp = new System.Windows.Forms.MenuItem();
this.menuHelpAbout = new System.Windows.Forms.MenuItem();
this.openFileDialog1 = new System.Windows.Forms.OpenFileDialog();
this.richTextBox1 = new System.Windows.Forms.RichTextBox();
this.saveFileDialog1 = new System.Windows.Forms.SaveFileDialog();
this.printDocument1 = new System.Drawing.Printing.PrintDocument();
this.pageSetupDialog1 = new System.Windows.Forms.PageSetupDialog();
this.printPreviewDialog1 = new System.Windows.Forms.PrintPreviewDialog();
this.printDialog1 = new System.Windows.Forms.PrintDialog();
this.fontDialog1 = new System.Windows.Forms.FontDialog();
this.colorDialog1 = new System.Windows.Forms.ColorDialog();
this.toolBar1 = new System.Windows.Forms.ToolBar();
this.toolBarButton1 = new System.Windows.Forms.ToolBarButton();
this.toolBarButton2 = new System.Windows.Forms.ToolBarButton();
this.toolBarButton3 = new System.Windows.Forms.ToolBarButton();
this.toolBarButton4 = new System.Windows.Forms.ToolBarButton();
this.toolBarButton5 = new System.Windows.Forms.ToolBarButton();
this.toolBarButton6 = new System.Windows.Forms.ToolBarButton();
this.toolBarButton7 = new System.Windows.Forms.ToolBarButton();
this.toolBarButton8 = new System.Windows.Forms.ToolBarButton();
this.toolBarButton9 = new System.Windows.Forms.ToolBarButton();
this.toolBarButton10 = new System.Windows.Forms.ToolBarButton();
this.imageList1 = new System.Windows.Forms.ImageList(this.components);
this.statusBar1 = new System.Windows.Forms.StatusBar();
this.statusBarPanel1 = new System.Windows.Forms.StatusBarPanel();
this.statusBarPanel2 = new System.Windows.Forms.StatusBarPanel();
this.notifyIcon1 = new System.Windows.Forms.NotifyIcon(this.components);
this.menuHelpRegister = new System.Windows.Forms.MenuItem();
((System.ComponentModel.ISupportInitialize)(this.statusBarPanel1)).BeginInit();
((System.ComponentModel.ISupportInitialize)(this.statusBarPanel2)).BeginInit();
this.SuspendLayout();
//
// mainMenu1
//
this.mainMenu1.MenuItems.AddRange(new System.Windows.Forms.MenuItem[]
{
this.menuFile,
this.menuEdit,
this.menuFormat,
this.menuHelp});
//
// menuFile
//
this.menuFile.Index = 0;
this.menuFile.MenuItems.AddRange(new System.Windows.Forms.MenuItem[] {
this.menuFileNew,
this.menuFileOpen,
this.menuFileSave,
this.menuFileSaveAs,
this.menuItem3,
this.menuFilePageSetup,
this.menuFilePrintPreview,
this.menuFilePrint,
this.menuItem6,
this.menuFileExit});
this.menuFile.Text = "&File";
this.menuFile.Select += new System.EventHandler(this.MenuSelect);
//
// menuFileNew
//
this.menuFileNew.Index = 0;
this.menuFileNew.Text = "&New";
this.menuFileNew.Click += new System.EventHandler(this.menuFileNew_Click);
this.menuFileNew.Select += new System.EventHandler(this.MenuSelect);
//
// menuFileOpen
//
this.menuFileOpen.Index = 1;
this.menuFileOpen.Text = "&Open...";
this.menuFileOpen.Click += new System.EventHandler(this.menuFileOpen_Click);
this.menuFileOpen.Select += new System.EventHandler(this.MenuSelect);
//
// menuFileSave
//
this.menuFileSave.Index = 2;
this.menuFileSave.Text = "&Save";
this.menuFileSave.Click += new System.EventHandler(this.menuFileSave_Click);
this.menuFileSave.Select += new System.EventHandler(this.MenuSelect);
//
// menuFileSaveAs
//
this.menuFileSaveAs.Index = 3;
this.menuFileSaveAs.Text = "&Save As...";
this.menuFileSaveAs.Click += new System.EventHandler(this.menuFileSaveAs_Click);
this.menuFileSaveAs.Select += new System.EventHandler(this.MenuSelect);
//
// menuItem3
//
this.menuItem3.Index = 4;
this.menuItem3.Text = "-";
//
// menuFilePageSetup
//
this.menuFilePageSetup.Index = 5;
this.menuFilePageSetup.Text = "Page Set&up...";
this.menuFilePageSetup.Click += new
System.EventHandler(this.menuFilePageSetup_Click);
this.menuFilePageSetup.Select += new System.EventHandler(this.MenuSelect);
//
// menuFilePrintPreview
//
this.menuFilePrintPreview.Index = 6;
this.menuFilePrintPreview.Text = "Print Pre&view...";
this.menuFilePrintPreview.Click += new
System.EventHandler(this.menuFilePrintPreview_Click);
this.menuFilePrintPreview.Select += new System.EventHandler(this.MenuSelect);
//
// menuFilePrint
//
this.menuFilePrint.Index = 7;
this.menuFilePrint.Text = "&Print...";
this.menuFilePrint.Click += new System.EventHandler(this.menuFilePrint_Click);
this.menuFilePrint.Select += new System.EventHandler(this.MenuSelect);
//
// menuItem6
//
this.menuItem6.Index = 8;
this.menuItem6.Text = "-";
//
// menuFileExit
//
this.menuFileExit.Index = 9;
this.menuFileExit.Text = "Exit";
this.menuFileExit.Click += new System.EventHandler(this.menuFileExit_Click);
this.menuFileExit.Select += new System.EventHandler(this.MenuSelect);
//
// menuEdit
//
this.menuEdit.Index = 1;
this.menuEdit.MenuItems.AddRange(new System.Windows.Forms.MenuItem[] {
this.menuEditUndo,
this.menuItem1,
this.menuItem2,
this.menuEditCut,
this.menuEditCopy,
this.menuEditPaste,
this.menuEditDelete,
this.menuItem10,
this.menuEditSelectAll});
this.menuEdit.Text = "&Edit";
this.menuEdit.Select += new System.EventHandler(this.MenuSelect);
//
// menuEditUndo
//
this.menuEditUndo.Index = 0;
this.menuEditUndo.Text = "&Undo";
this.menuEditUndo.Click += new System.EventHandler(this.menuEditUndo_Click);
this.menuEditUndo.Select += new System.EventHandler(this.MenuSelect);
//
// menuItem1
//
this.menuItem1.Index = 1;
this.menuItem1.Text = "&Redo";
this.menuItem1.Click += new System.EventHandler(this.menuItem1_Click);
this.menuItem1.Select += new System.EventHandler(this.MenuSelect);
//
// menuItem2
//
this.menuItem2.Index = 2;
this.menuItem2.Text = "-";
//
// menuEditCut
//
this.menuEditCut.Index = 3;
this.menuEditCut.Text = "Cu&t";
this.menuEditCut.Click += new System.EventHandler(this.menuEditCut_Click);
this.menuEditCut.Select += new System.EventHandler(this.MenuSelect);
//
// menuEditCopy
//
this.menuEditCopy.Index = 4;
this.menuEditCopy.Text = "&Copy";
this.menuEditCopy.Click += new System.EventHandler(this.menuEditCopy_Click);
this.menuEditCopy.Select += new System.EventHandler(this.MenuSelect);
//
// menuEditPaste
//
this.menuEditPaste.Index = 5;
this.menuEditPaste.Text = "&Paste";
this.menuEditPaste.Click += new System.EventHandler(this.menuEditPaste_Click);
this.menuEditPaste.Select += new System.EventHandler(this.MenuSelect);
//
// menuEditDelete
//
this.menuEditDelete.Index = 6;
this.menuEditDelete.Text = "&Delete";
this.menuEditDelete.Click += new System.EventHandler(this.menuEditDelete_Click);
this.menuEditDelete.Select += new System.EventHandler(this.MenuSelect);
//
// menuItem10
//
this.menuItem10.Index = 7;
this.menuItem10.Text = "-";
//
// menuEditSelectAll
//
this.menuEditSelectAll.Index = 8;
this.menuEditSelectAll.Text = "&Select All";
this.menuEditSelectAll.Click += new
System.EventHandler(this.menuEditSelectAll_Click);
this.menuEditSelectAll.Select += new System.EventHandler(this.MenuSelect);
//
// menuFormat
//
this.menuFormat.Index = 2;
this.menuFormat.MenuItems.AddRange(new System.Windows.Forms.MenuItem[] {
this.menuFormatFont,
this.menuFormatColor,
this.menuItem4,
this.menuFormatFontCharacterStyle,
this.menuFormatFontParagraphAlignment});
this.menuFormat.Text = "&Format";
this.menuFormat.Select += new System.EventHandler(this.MenuSelect);
//
// menuFormatFont
//
this.menuFormatFont.Index = 0;
this.menuFormatFont.Text = "&Font...";
this.menuFormatFont.Click += new System.EventHandler(this.menuFormatFont_Click);
this.menuFormatFont.Select += new System.EventHandler(this.MenuSelect);
//
// menuFormatColor
//
this.menuFormatColor.Index = 1;
this.menuFormatColor.Text = "C&olor...";
this.menuFormatColor.Click += new
System.EventHandler(this.menuFormatColor_Click);
this.menuFormatColor.Select += new System.EventHandler(this.MenuSelect);
//
// menuItem4
//
this.menuItem4.Index = 2;
this.menuItem4.Text = "-";
//
// menuFormatFontCharacterStyle
//
this.menuFormatFontCharacterStyle.Index = 3;
this.menuFormatFontCharacterStyle.MenuItems.AddRange(new
System.Windows.Forms.MenuItem[] {
this.menuFormatFontCharacterStyleBold,
this.menuFormatFontCharacterStyleItalic,
this.menuFormatFontCharacterStyleUnderline,
this.menuFormatFontCharacterStyleStrikeout});
this.menuFormatFontCharacterStyle.Text = "&Characters Style";
this.menuFormatFontCharacterStyle.Select += new
System.EventHandler(this.MenuSelect);
//
// menuFormatFontCharacterStyleBold
//
this.menuFormatFontCharacterStyleBold.Index = 0;
this.menuFormatFontCharacterStyleBold.Text = "&Bold";
this.menuFormatFontCharacterStyleBold.Click += new
System.EventHandler(this.menuFormatFontCharacterStyleBold_Click);
this.menuFormatFontCharacterStyleBold.Select += new
System.EventHandler(this.MenuSelect);
//
// menuFormatFontCharacterStyleItalic
//
this.menuFormatFontCharacterStyleItalic.Index = 1;
this.menuFormatFontCharacterStyleItalic.Text = "&Italic";
this.menuFormatFontCharacterStyleItalic.Click += new
System.EventHandler(this.menuFormatFontCharacterStyleItalic_Click);
this.menuFormatFontCharacterStyleItalic.Select += new
System.EventHandler(this.MenuSelect);
//
// menuFormatFontCharacterStyleUnderline
//
this.menuFormatFontCharacterStyleUnderline.Index = 2;
this.menuFormatFontCharacterStyleUnderline.Text = "&Underline";
this.menuFormatFontCharacterStyleUnderline.Click += new
System.EventHandler(this.menuFormatFontCharacterStyleUnderline_Click);
this.menuFormatFontCharacterStyleUnderline.Select += new
System.EventHandler(this.MenuSelect);
//
// menuFormatFontCharacterStyleStrikeout
//
this.menuFormatFontCharacterStyleStrikeout.Index = 3;
this.menuFormatFontCharacterStyleStrikeout.Text = "&Strikeout";
this.menuFormatFontCharacterStyleStrikeout.Click += new
System.EventHandler(this.menuFormatFontCharacterStyleStrikeout_Click);
this.menuFormatFontCharacterStyleStrikeout.Select += new
System.EventHandler(this.MenuSelect);
//
// menuFormatFontParagraphAlignment
//
this.menuFormatFontParagraphAlignment.Index = 4;
this.menuFormatFontParagraphAlignment.MenuItems.AddRange(new
System.Windows.Forms.MenuItem[] {
this.menuFormatFontParagraphAlignmentLeft,
this.menuFormatFontParagraphAlignmentRight,
this.menuFormatFontParagraphAlignmentCenter});
this.menuFormatFontParagraphAlignment.Text = "&Paragraph Alignment";
this.menuFormatFontParagraphAlignment.Select += new
System.EventHandler(this.MenuSelect);
//
// menuFormatFontParagraphAlignmentLeft
//
this.menuFormatFontParagraphAlignmentLeft.Index = 0;
this.menuFormatFontParagraphAlignmentLeft.Text = "&Left";
this.menuFormatFontParagraphAlignmentLeft.Click += new
System.EventHandler(this.menuFormatFontParagraphAlignmentLeft_Click);
this.menuFormatFontParagraphAlignmentLeft.Select += new
System.EventHandler(this.MenuSelect);
//
// menuFormatFontParagraphAlignmentRight
//
this.menuFormatFontParagraphAlignmentRight.Index = 1;
this.menuFormatFontParagraphAlignmentRight.Text = "&Right";
this.menuFormatFontParagraphAlignmentRight.Click += new
System.EventHandler(this.menuFormatFontParagraphAlignmentRight_Click);
this.menuFormatFontParagraphAlignmentRight.Select += new
System.EventHandler(this.MenuSelect);
//
// menuFormatFontParagraphAlignmentCenter
//
this.menuFormatFontParagraphAlignmentCenter.Index = 2;
this.menuFormatFontParagraphAlignmentCenter.Text = "&Center";
this.menuFormatFontParagraphAlignmentCenter.Click += new
System.EventHandler(this.menuFormatFontParagraphAlignmentCenter_Click);
this.menuFormatFontParagraphAlignmentCenter.Select += new
System.EventHandler(this.MenuSelect);
//
// menuHelp
//
this.menuHelp.Index = 3;
this.menuHelp.MenuItems.AddRange(new System.Windows.Forms.MenuItem[] {
this.menuHelpAbout,
this.menuHelpRegister});
this.menuHelp.Text = "&Help";
this.menuHelp.Select += new System.EventHandler(this.MenuSelect);
//
// menuHelpAbout
//
this.menuHelpAbout.Index = 0;
this.menuHelpAbout.Text = "&About...";
this.menuHelpAbout.Click += new System.EventHandler(this.menuHelpAbout_Click);
this.menuHelpAbout.Select += new System.EventHandler(this.MenuSelect);
//
// openFileDialog1
//
this.openFileDialog1.Filter = "RTF files|*.rtf|Text files|*.txt|All files|*.*";
//
// richTextBox1
//
this.richTextBox1.AcceptsTab = true;
this.richTextBox1.Dock = System.Windows.Forms.DockStyle.Fill;
this.richTextBox1.Location = new System.Drawing.Point(0, 39);
this.richTextBox1.Name = "richTextBox1";
this.richTextBox1.Size = new System.Drawing.Size(424, 140);
this.richTextBox1.TabIndex = 0;
this.richTextBox1.Text = "";
this.richTextBox1.TextChanged += new
System.EventHandler(this.richTextBox1_TextChanged);
//
// saveFileDialog1
//
this.saveFileDialog1.FileName = "doc1.rtf";
this.saveFileDialog1.Filter = "RTF file|*.rtf";
//
// printDocument1
//
this.printDocument1.DocumentName = "SimpleNotepad Document";
this.printDocument1.PrintPage += new
System.Drawing.Printing.PrintPageEventHandler(
this.PrintPageEventHandler);
//
// pageSetupDialog1
//
this.pageSetupDialog1.Document = this.printDocument1;
//
// printPreviewDialog1
//
this.printPreviewDialog1.AutoScrollMargin = new System.Drawing.Size(0, 0);
this.printPreviewDialog1.AutoScrollMinSize = new System.Drawing.Size(0, 0);
this.printPreviewDialog1.ClientSize = new System.Drawing.Size(400, 300);
this.printPreviewDialog1.Document = this.printDocument1;
this.printPreviewDialog1.Enabled = true;
this.printPreviewDialog1.Icon = ((System.Drawing.Icon)
(resources.GetObject("printPreviewDialog1.Icon")));
this.printPreviewDialog1.Location = new System.Drawing.Point(129, 54);
this.printPreviewDialog1.MaximumSize = new System.Drawing.Size(0, 0);
this.printPreviewDialog1.Name = "printPreviewDialog1";
this.printPreviewDialog1.Opacity = 1;
this.printPreviewDialog1.TransparencyKey = System.Drawing.Color.Empty;
this.printPreviewDialog1.Visible = false;
//
// printDialog1
//
this.printDialog1.Document = this.printDocument1;
//
// toolBar1
//
this.toolBar1.Appearance = System.Windows.Forms.ToolBarAppearance.Flat;
this.toolBar1.Buttons.AddRange(new System.Windows.Forms.ToolBarButton[] {
this.toolBarButton1,
this.toolBarButton2,
this.toolBarButton3,
this.toolBarButton4,
this.toolBarButton5,
this.toolBarButton6,
this.toolBarButton7,
this.toolBarButton8,
this.toolBarButton9,
this.toolBarButton10});
this.toolBar1.DropDownArrows = true;
this.toolBar1.ImageList = this.imageList1;
this.toolBar1.Name = "toolBar1";
this.toolBar1.ShowToolTips = true;
this.toolBar1.Size = new System.Drawing.Size(424, 39);
this.toolBar1.TabIndex = 1;
this.toolBar1.Wrappable = false;
this.toolBar1.ButtonClick += new
System.Windows.Forms.ToolBarButtonClickEventHandler(
this.toolBar1_ButtonClick);
//
// toolBarButton1
//
this.toolBarButton1.ImageIndex = 0;
this.toolBarButton1.Text = "New";
this.toolBarButton1.ToolTipText = "Создание нового документа";
//
// toolBarButton2
//
this.toolBarButton2.ImageIndex = 1;
this.toolBarButton2.Text = "Open";
this.toolBarButton2.ToolTipText = "Открытие существующего документа";
//
// toolBarButton3
//
this.toolBarButton3.ImageIndex = 2;
this.toolBarButton3.Text = "Save";
this.toolBarButton3.ToolTipText = "Сохранение документа";
//
// toolBarButton4
//
this.toolBarButton4.Style = System.Windows.Forms.ToolBarButtonStyle.Separator;
//
// toolBarButton5
//
this.toolBarButton5.ImageIndex = 3;
this.toolBarButton5.Text = "Cut";
this.toolBarButton5.ToolTipText = "Вырезать";
//
// toolBarButton6
//
this.toolBarButton6.ImageIndex = 4;
this.toolBarButton6.Text = "Copy";
this.toolBarButton6.ToolTipText = "Копировать";
//
// toolBarButton7
//
this.toolBarButton7.ImageIndex = 5;
this.toolBarButton7.Text = "Paste";
this.toolBarButton7.ToolTipText = "Вставить";
//
// toolBarButton8
//
this.toolBarButton8.Style = System.Windows.Forms.ToolBarButtonStyle.Separator;
//
// toolBarButton9
//
this.toolBarButton9.ImageIndex = 6;
this.toolBarButton9.Text = "Preview";
this.toolBarButton9.ToolTipText = "Предварительный просмотр";
//
// toolBarButton10
//
this.toolBarButton10.ImageIndex = 7;
this.toolBarButton10.Text = "Print";
this.toolBarButton10.ToolTipText = "Печать";
//
// imageList1
//
this.imageList1.ColorDepth = System.Windows.Forms.ColorDepth.Depth8Bit;
this.imageList1.ImageSize = new System.Drawing.Size(16, 16);
this.imageList1.ImageStream = ((System.Windows.Forms.ImageListStreamer)(
resources.GetObject("imageList1.ImageStream")));
this.imageList1.TransparentColor = System.Drawing.Color.Transparent;
//
// statusBar1
//
this.statusBar1.Location = new System.Drawing.Point(0, 179);
this.statusBar1.Name = "statusBar1";
this.statusBar1.Panels.AddRange(new System.Windows.Forms.StatusBarPanel[] {
this.statusBarPanel1,
this.statusBarPanel2});
this.statusBar1.ShowPanels = true;
this.statusBar1.Size = new System.Drawing.Size(424, 22);
this.statusBar1.TabIndex = 2;
//
// statusBarPanel1
//
this.statusBarPanel1.AutoSize =
System.Windows.Forms.StatusBarPanelAutoSize.Spring;
this.statusBarPanel1.Width = 308;
//
// notifyIcon1
//
this.notifyIcon1.Text = "notifyIcon1";
this.notifyIcon1.Visible = true;
//
// menuHelpRegister
//
this.menuHelpRegister.Index = 1;
this.menuHelpRegister.Text = "&Register...";
this.menuHelpRegister.Click += new
System.EventHandler(this.menuHelpRegister_Click);
//
// SimpleNotepadForm
//
this.AutoScaleBaseSize = new System.Drawing.Size(5, 13);
this.ClientSize = new System.Drawing.Size(424, 201);
this.Controls.AddRange(new System.Windows.Forms.Control[] {
this.richTextBox1,
this.statusBar1,
this.toolBar1});
this.Icon = ((System.Drawing.Icon)(resources.GetObject("$this.Icon")));
this.Menu = this.mainMenu1;
this.Name = "SimpleNotepadForm";
this.Text = "Редактор SimpleNotepad";
((System.ComponentModel.ISupportInitialize)(this.statusBarPanel1)).EndInit();
((System.ComponentModel.ISupportInitialize)(this.statusBarPanel2)).EndInit();
this.ResumeLayout(false);

}
#endregion

/// <summary>
/// The main entry point for the application.
/// </summary>
[STAThread]
static void Main()
{
Application.Run(new SimpleNotepadForm());
}

private void menuFileNew_Click(object sender,


System.EventArgs e)
{
MenuFileNew();
}

private void menuFileOpen_Click(object sender,


System.EventArgs e)
{
MenuFileOpen();
}

private void menuFileSave_Click(object sender,


System.EventArgs e)
{
MenuFileSaveAs();
}

private void menuFileSaveAs_Click(object sender,


System.EventArgs e)
{
MenuFileSaveAs();
}

private void menuFilePageSetup_Click(object sender,


System.EventArgs e)
{
MenuFilePageSetup();
}

private void menuFilePrintPreview_Click(object sender,


System.EventArgs e)
{
MenuFilePrintPreview();
}

private void menuFilePrint_Click(object sender,


System.EventArgs e)
{
MenuFilePrint();
}

private void menuFileExit_Click(object sender,


System.EventArgs e)
{
if(m_DocumentChanged)
{
SaveDocumentNeededForm dialog = new
SaveDocumentNeededForm();
DialogResult result = dialog.ShowDialog();

switch(result)
{
case DialogResult.Yes:
{
MenuFileSaveAs();
break;
}
case DialogResult.Cancel:
{
return;
}
}
}
this.Close();
}

private void menuEditUndo_Click(object sender,


System.EventArgs e)
{
richTextBox1.Undo();
}

private void menuItem1_Click(object sender, System.EventArgs e)


{
richTextBox1.Redo();
}

private void menuEditCut_Click(object sender,


System.EventArgs e)
{
richTextBox1.Cut();
}

private void menuEditCopy_Click(object sender,


System.EventArgs e)
{
richTextBox1.Copy();
}

private void menuEditPaste_Click(object sender,


System.EventArgs e)
{
richTextBox1.Paste();
}

private void menuEditDelete_Click(object sender,


System.EventArgs e)
{
richTextBox1.Cut();
}

private void menuEditSelectAll_Click(object sender,


System.EventArgs e)
{
richTextBox1.SelectAll();
}

private void menuFormatFont_Click(object sender,


System.EventArgs e)
{
if (fontDialog1.ShowDialog() == DialogResult.OK)
{
richTextBox1.SelectionFont = fontDialog1.Font;
}
}

private void menuFormatColor_Click(object sender,


System.EventArgs e)
{
if (colorDialog1.ShowDialog() == DialogResult.OK)
{
richTextBox1.SelectionColor = colorDialog1.Color;
}
}

private void menuFormatFontCharacterStyleBold_Click(


object sender, System.EventArgs e)
{
SetBold();
}
private void menuFormatFontCharacterStyleItalic_Click(
object sender, System.EventArgs e)
{
SetItalic();
}

private void menuFormatFontCharacterStyleUnderline_Click(


object sender, System.EventArgs e)
{
SetUnderline();
}

private void menuFormatFontCharacterStyleStrikeout_Click(


object sender, System.EventArgs e)
{
SetStrikeout();
}

private void menuFormatFontParagraphAlignmentLeft_Click(


object sender, System.EventArgs e)
{
richTextBox1.SelectionAlignment = HorizontalAlignment.Left;
}

private void menuFormatFontParagraphAlignmentRight_Click(


object sender, System.EventArgs e)
{
richTextBox1.SelectionAlignment = HorizontalAlignment.Right;
}

private void menuFormatFontParagraphAlignmentCenter_Click(


object sender, System.EventArgs e)
{
richTextBox1.SelectionAlignment = HorizontalAlignment.Center;
}

private void menuHelpAbout_Click(object sender,


System.EventArgs e)
{
HelpAboutForm dlgAbout = new HelpAboutForm();
dlgAbout.ShowDialog();
}

/// <summary>
/// Создание нового файла
/// </summary>
private void MenuFileNew()
{
if(m_DocumentChanged)
{
SaveDocumentNeededForm dialog = new
SaveDocumentNeededForm();
DialogResult result = dialog.ShowDialog();

switch(result)
{
case DialogResult.Yes:
{
MenuFileSaveAs();
break;
}
case DialogResult.Cancel:
{
return;
}
}
}

richTextBox1.Clear();
statusBarPanel2.Text = "";
m_DocumentChanged = false;
}

/// <summary>
/// Открытие существующего файла
/// </summary>
private void MenuFileOpen()
{
if(m_DocumentChanged)
{
SaveDocumentNeededForm dialog = new
SaveDocumentNeededForm();
DialogResult result = dialog.ShowDialog();

switch(result)
{
case DialogResult.Yes:
{
MenuFileSaveAs();
break;
}
case DialogResult.Cancel:
{
return;
}
}
}

if(openFileDialog1.ShowDialog() ==
System.Windows.Forms.DialogResult.OK &&
openFileDialog1.FileName.Length > 0)
{
try
{
richTextBox1.LoadFile(openFileDialog1.FileName,
RichTextBoxStreamType.RichText);
}
catch (System.ArgumentException ex)
{
richTextBox1.LoadFile(openFileDialog1.FileName,
RichTextBoxStreamType.PlainText);
}
this.Text = "Файл [" + openFileDialog1.FileName + "]";
statusBarPanel2.Text = "";
m_DocumentChanged = false;
}
}

/// <summary>
/// Сохранение документа в новом файле
/// </summary>
private void MenuFileSaveAs()
{
if(saveFileDialog1.ShowDialog() ==
System.Windows.Forms.DialogResult.OK &&
saveFileDialog1.FileName.Length > 0)
{
richTextBox1.SaveFile(saveFileDialog1.FileName);
m_DocumentChanged = false;
this.Text = "Файл [" + saveFileDialog1.FileName + "]";
statusBarPanel2.Text = "";
}
}

/// <summary>
/// Обработка события PrintPage
/// </summary>
private void PrintPageEventHandler(object sender,
System.Drawing.Printing.PrintPageEventArgs e)
{
int lineCount = 0; // счетчик строк
float linesPerPage = 0; // количество строк на одной
// странице
float yLinePosition = 0; // текущая позиция при печати по
// вертикальной оси
string currentLine = null; // текст текущей строки

// Шрифт для печати текста


Font printFont = this.richTextBox1.Font;

// Кисть для печати текста


SolidBrush printBrush = new SolidBrush(Color.Black);

// Размер отступа слева


float leftMargin = e.MarginBounds.Left;

// Размер отступа сверху


float topMargin = e.MarginBounds.Top +
3*printFont.GetHeight(e.Graphics);

// Вычисляем количество строк на одной странице с учетом


// отступа
linesPerPage = (e.MarginBounds.Height -
6*printFont.GetHeight(e.Graphics)) /
printFont.GetHeight(e.Graphics);

// Цикл печати всех строк страницы


while(lineCount < linesPerPage &&
((currentLine=m_myReader.ReadLine()) != null))
{
// Вычисляем позицию очередной распечатываемой строки
yLinePosition = topMargin + (lineCount *
printFont.GetHeight(e.Graphics));

// Печатаем очередную строку


e.Graphics.DrawString(currentLine, printFont, printBrush,
leftMargin, yLinePosition, new StringFormat());

// Переходим к следующей строке


lineCount++;
}

// Печать колонтитулов страницы

// Номер текущей страницы


string sPageNumber = "Page " + m_PrintPageNumber.ToString();

// Вычисляем размеры прямоугольной области, занимаемой верхним


// колонтитулом страницы
SizeF stringSize = new SizeF();
stringSize = e.Graphics.MeasureString(sPageNumber, printFont,
e.MarginBounds.Right - e.MarginBounds.Left);

// Печатаем номер страницы


e.Graphics.DrawString(sPageNumber, printFont, printBrush,
e.MarginBounds.Right - stringSize.Width, e.MarginBounds.Top,
new StringFormat());

// Печатаем имя файла документа


e.Graphics.DrawString(this.Text, printFont, printBrush,
e.MarginBounds.Left, e.MarginBounds.Top,
new StringFormat());

// Кисть для рисования горизонтальной линии, отделяющей


// верхний колонтитул
Pen colontitulPen = new Pen(Color.Black);
colontitulPen.Width = 2;

// Рисуем верхнюю линию


e.Graphics.DrawLine(colontitulPen,
leftMargin, e.MarginBounds.Top +
printFont.GetHeight(e.Graphics) + 3,
e.MarginBounds.Right, e.MarginBounds.Top +
printFont.GetHeight(e.Graphics) + 3);

// Рисуем линию, отделяющую нижний колонтитул документа


e.Graphics.DrawLine(colontitulPen,
leftMargin, e.MarginBounds.Bottom - 3,
e.MarginBounds.Right, e.MarginBounds.Bottom - 3);

// Печатаем текст нижнего колонтитула


e.Graphics.DrawString("SimpleNotepad, (c) Александр Фролов,
http://www.frolov.pp.ru", printFont, printBrush,
e.MarginBounds.Left, e.MarginBounds.Bottom,
new StringFormat());

// Если напечатаны не все строки документа, переходим к


// следующей странице
if(currentLine != null)
{
e.HasMorePages = true;
m_PrintPageNumber++;
}

// Иначе завершаем печать страницы


else
e.HasMorePages = false;

// Освобождаем ненужные более ресурсы


printBrush.Dispose();
colontitulPen.Dispose();
}

/// <summary>
/// Печать документа
/// </summary>
private void MenuFilePrint()
{
m_PrintPageNumber = 1;
string strText = this.richTextBox1.Text;
m_myReader = new StringReader(strText);
Margins margins = new Margins(100,50,50,50);
printDocument1.DefaultPageSettings.Margins = margins;
if (printDialog1.ShowDialog() == DialogResult.OK)
{
this.printDocument1.Print();
}
m_myReader.Close() ;
}

/// <summary>
/// Предварительный просмотр перед печатью документа
/// </summary>
private void MenuFilePrintPreview()
{
m_PrintPageNumber = 1;
string strText = this.richTextBox1.Text;
m_myReader = new StringReader(strText);
Margins margins = new Margins(100,50,50,50);
printDocument1.DefaultPageSettings.Margins = margins;
printPreviewDialog1.ShowDialog();
m_myReader.Close() ;
}

/// <summary>
/// Настройка параметров страницы
/// </summary>
private void MenuFilePageSetup()
{
pageSetupDialog1.ShowDialog();
}

private void richTextBox1_TextChanged(object sender,


System.EventArgs e)
{
m_DocumentChanged = true;
statusBarPanel2.Text = "Изменено";
}

/// <summary>
/// Установка стиля символов Bold
/// </summary>
private void SetBold()
{
if (richTextBox1.SelectionFont != null)
{
System.Drawing.Font currentFont =
richTextBox1.SelectionFont;
System.Drawing.FontStyle newFontStyle;

if (richTextBox1.SelectionFont.Bold == true)
{
newFontStyle = FontStyle.Regular;
}
else
{
newFontStyle = FontStyle.Bold;
}

richTextBox1.SelectionFont = new Font(


currentFont.FontFamily, currentFont.Size, newFontStyle);

CheckMenuFontCharacterStyle();
}
}

/// <summary>
/// Установка стиля символов Italic
/// </summary>
private void SetItalic()
{
if (richTextBox1.SelectionFont != null)
{
System.Drawing.Font currentFont =
richTextBox1.SelectionFont;
System.Drawing.FontStyle newFontStyle;
CheckMenuFontCharacterStyle();

if (richTextBox1.SelectionFont.Italic == true)
{
newFontStyle = FontStyle.Regular;
}
else
{
newFontStyle = FontStyle.Italic;
}

richTextBox1.SelectionFont = new Font(


currentFont.FontFamily, currentFont.Size, newFontStyle);

CheckMenuFontCharacterStyle();
}
}

/// <summary>
/// Установка стиля символов Underline
/// </summary>
private void SetUnderline()
{
if (richTextBox1.SelectionFont != null)
{
System.Drawing.Font currentFont =
richTextBox1.SelectionFont;
System.Drawing.FontStyle newFontStyle;
CheckMenuFontCharacterStyle();

if (richTextBox1.SelectionFont.Underline == true)
{
newFontStyle = FontStyle.Regular;
}
else
{
newFontStyle = FontStyle.Underline;
}

richTextBox1.SelectionFont = new Font(


currentFont.FontFamily, currentFont.Size, newFontStyle);

CheckMenuFontCharacterStyle();
}
}

/// <summary>
/// Установка стиля символов Strikeout
/// </summary>
private void SetStrikeout()
{
if (richTextBox1.SelectionFont != null)
{
System.Drawing.Font currentFont =
richTextBox1.SelectionFont;
System.Drawing.FontStyle newFontStyle;

if (richTextBox1.SelectionFont.Strikeout == true)
{
newFontStyle = FontStyle.Regular;
}
else
{
newFontStyle = FontStyle.Strikeout;
}

richTextBox1.SelectionFont = new Font(


currentFont.FontFamily, currentFont.Size, newFontStyle);

CheckMenuFontCharacterStyle();
}
}

/// <summary>
/// Установка отметки строк меню Font->CharacterStyle
/// </summary>
private void CheckMenuFontCharacterStyle()
{
if(richTextBox1.SelectionFont.Bold == true)
{
menuFormatFontCharacterStyleBold.Checked = true;
}
else
{
menuFormatFontCharacterStyleBold.Checked = false;
}

if(richTextBox1.SelectionFont.Italic == true)
{
menuFormatFontCharacterStyleItalic.Checked = true;
}
else
{
menuFormatFontCharacterStyleItalic.Checked = false;
}

if(richTextBox1.SelectionFont.Underline == true)
{
menuFormatFontCharacterStyleUnderline.Checked = true;
}
else
{
menuFormatFontCharacterStyleUnderline.Checked = false;
}

if(richTextBox1.SelectionFont.Strikeout == true)
{
menuFormatFontCharacterStyleStrikeout.Checked = true;
}
else
{
menuFormatFontCharacterStyleStrikeout.Checked = false;
}
}

private void toolBar1_ButtonClick(object sender,


System.Windows.Forms.ToolBarButtonClickEventArgs e)
{
switch (toolBar1.Buttons.IndexOf(e.Button))
{
case 0:
{
MenuFileNew(); break;
}
case 1:
MenuFileOpen(); break;
case 2:
MenuFileSaveAs(); break;
case 4:
richTextBox1.Cut(); break;
case 5:
richTextBox1.Copy(); break;
case 6:
richTextBox1.Paste(); break;
case 8:
MenuFilePrintPreview(); break;
case 9:
MenuFilePrint(); break;
}

}
private void MenuSelect(object sender, System.EventArgs e)
{
MenuItem mi = (MenuItem) sender;
string ms;
switch(mi.Text)
{
case "&New": ms = "Новый документ"; break;
case "&Open...": ms = "Открыть документ"; break;
case "&Save": ms = "Сохранить документ"; break;
case "&Save As...": ms = "Сохранить документ"; break;
case "Page Set&up...": ms = "Параметры страницы"; break;
case "Print Pre&view...": ms = "Предварительный просмотр";
break;
case "&Print...": ms = "Печатать документ"; break;
case "Exit": ms = "Завершение работы"; break;
case "&Undo": ms = "Отменить ввод"; break;
case "&Redo": ms = "Повторить ввод"; break;
case "Cu&t": ms = "Вырезать"; break;
case "&Copy": ms = "Скопировать"; break;
case "&Paste": ms = "Вставить"; break;
case "&Delete": ms = "Удалить"; break;
case "&Select All": ms = "Выделить все"; break;
case "&Font...": ms = "Выбрать шрифт"; break;
case "C&olor...": ms = "Выбрать цвет"; break;
case "&Bold": ms = "Жирный шрифт"; break;
case "&Italic": ms = "Наклонный шрифт"; break;
case "&Underline": ms = "Подчеркивание"; break;
case "&Strikeout": ms = "Перечеркивание"; break;
case "&Left": ms = "Выравнивание по левой границе"; break;
case "&Right": ms = "Выравнивание по правой границе";
break;
case "&Center": ms = "Центровка"; break;
case "&About...": ms = "О программе"; break;
default: ms = ""; break;
}

statusBarPanel1.Text = ms;
}

private void menuHelpRegister_Click(object sender, System.EventArgs e)


{
RegisterForm dialog = new RegisterForm();
if(DialogResult.Yes == dialog.ShowDialog())
{
string body = "Данные регистрации:";

body += " Name:" + dialog.UserName;


body += ", Email:" + dialog.UserEmail;
body += ", Level:" + dialog.UserLevel;
body += ", Gender:" + dialog.UserGender;
body += ", FavoriteOS:" + dialog.FavoriteOS;
body += ", SendNews:" + dialog.SendNews;
body += ", SendLetter:" + dialog.SendLetter;
body += ", BirthDay:" + dialog.UserBirthDay;
body += ", Comment:" + dialog.UserComment;

System.Diagnostics.Process.Start(
"mailto:alexandre@frolov.pp.ru?subject=Регистрация&body="
+ body);
}
}
}
}
Листинг П1-2. Файл ch05\SimpleNotepad\SaveDocumentNeededForm.cs
using System;
using System.Drawing;
using System.Collections;
using System.ComponentModel;
using System.Windows.Forms;

namespace SimpleNotepad
{
/// <summary>
/// Summary description for SaveDocumentNeededForm.
/// </summary>
public class SaveDocumentNeededForm : System.Windows.Forms.Form
{
private System.Windows.Forms.Button buttonYes;
private System.Windows.Forms.Button buttonNo;
private System.Windows.Forms.Button buttonCancel;
private System.Windows.Forms.PictureBox pictureBox1;
private System.Windows.Forms.Label label1;
private System.Windows.Forms.Label label2;
private System.Windows.Forms.Label label3;
/// <summary>
/// Required designer variable.
/// </summary>
private System.ComponentModel.Container components = null;

public SaveDocumentNeededForm()
{
//
// Required for Windows Form Designer support
//
InitializeComponent();
}

/// <summary>
/// Clean up any resources being used.
/// </summary>
protected override void Dispose( bool disposing )
{
if( disposing )
{
if(components != null)
{
components.Dispose();
}
}
base.Dispose( disposing );
}

#region Windows Form Designer generated code


/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
private void InitializeComponent()
{
System.Resources.ResourceManager resources = new
System.Resources.ResourceManager(typeof(SaveDocumentNeededForm));
this.buttonYes = new System.Windows.Forms.Button();
this.buttonNo = new System.Windows.Forms.Button();
this.buttonCancel = new System.Windows.Forms.Button();
this.pictureBox1 = new System.Windows.Forms.PictureBox();
this.label1 = new System.Windows.Forms.Label();
this.label2 = new System.Windows.Forms.Label();
this.label3 = new System.Windows.Forms.Label();
this.SuspendLayout();
//
// buttonYes
//
this.buttonYes.BackColor = System.Drawing.SystemColors.ControlLight;
this.buttonYes.DialogResult = System.Windows.Forms.DialogResult.Yes;
this.buttonYes.Location = new System.Drawing.Point(232, 88);
this.buttonYes.Name = "buttonYes";
this.buttonYes.TabIndex = 0;
this.buttonYes.Text = "Да";
//
// buttonNo
//
this.buttonNo.BackColor = System.Drawing.SystemColors.ControlLight;
this.buttonNo.DialogResult = System.Windows.Forms.DialogResult.No;
this.buttonNo.Location = new System.Drawing.Point(232, 120);
this.buttonNo.Name = "buttonNo";
this.buttonNo.TabIndex = 1;
this.buttonNo.Text = "Нет";
//
// buttonCancel
//
this.buttonCancel.BackColor = System.Drawing.SystemColors.ControlLight;
this.buttonCancel.DialogResult = System.Windows.Forms.DialogResult.Cancel;
this.buttonCancel.Location =
new System.Drawing.Point(232, 152);
this.buttonCancel.Name = "buttonCancel";
this.buttonCancel.TabIndex = 2;
this.buttonCancel.Text = "Отменить";
//
// pictureBox1
//
this.pictureBox1.Image = ((System.Drawing.Bitmap)
(resources.GetObject("pictureBox1.Image")));
this.pictureBox1.Location = new System.Drawing.Point(0, 24);
this.pictureBox1.Name = "pictureBox1";
this.pictureBox1.Size = new System.Drawing.Size(200, 136);
this.pictureBox1.TabIndex = 3;
this.pictureBox1.TabStop = false;
//
// label1
//
this.label1.Font = new System.Drawing.Font("Arial", 12F,
System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point, ((System.Byte)
(204)));
this.label1.Location = new System.Drawing.Point(208, 8);
this.label1.Name = "label1";
this.label1.Size = new System.Drawing.Size(104, 23);
this.label1.TabIndex = 4;
this.label1.Text = "Внимание!";
//
// label2
//
this.label2.Location = new System.Drawing.Point(208, 40);
this.label2.Name = "label2";
this.label2.Size = new System.Drawing.Size(152, 16);
this.label2.TabIndex = 5;
this.label2.Text = "Документ был изменен.";
//
// label3
//
this.label3.Location = new System.Drawing.Point(208, 56);
this.label3.Name = "label3";
this.label3.Size = new System.Drawing.Size(136, 23);
this.label3.TabIndex = 6;
this.label3.Text = "Сохранить изменения?";
//
// SaveDocumentNeededForm
//
this.AcceptButton = this.buttonYes;
this.AutoScaleBaseSize = new System.Drawing.Size(5, 13);
this.BackColor = System.Drawing.Color.FromArgb(((System.Byte)(255)),
((System.Byte)(224)), ((System.Byte)(192)));
this.CancelButton = this.buttonCancel;
this.ClientSize = new System.Drawing.Size(352, 189);
this.ControlBox = false;
this.Controls.AddRange(new System.Windows.Forms.Control[] {
this.label3,
this.label2,
this.label1,
this.pictureBox1,
this.buttonCancel,
this.buttonNo,
this.buttonYes});
this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedDialog;
this.Icon = ((System.Drawing.Icon)(resources.GetObject("$this.Icon")));
this.Name = "SaveDocumentNeededForm";
this.StartPosition = System.Windows.Forms.FormStartPosition.CenterParent;
this.Text = "Сохранение документа";
this.ResumeLayout(false);

}
#endregion
}
}
Листинг П1-3. Файл ch05\SimpleNotepad\RegisterForm.cs
using System;
using System.Drawing;
using System.Collections;
using System.ComponentModel;
using System.Windows.Forms;

namespace SimpleNotepad
{
/// <summary>
/// Summary description for RegisterForm.
/// </summary>
public class RegisterForm : System.Windows.Forms.Form
{
private System.Windows.Forms.Button button1;
private System.Windows.Forms.Button button2;
private System.Windows.Forms.TextBox textBoxName;
private System.Windows.Forms.Label label1;
private System.Windows.Forms.Label label2;
private System.Windows.Forms.Label label3;
private System.Windows.Forms.TextBox textBoxEmail;
private System.Windows.Forms.Label label5;
private System.Windows.Forms.ComboBox comboBoxLevel;
private System.Windows.Forms.RadioButton radioButton1;
private System.Windows.Forms.RadioButton radioButton2;
private System.Windows.Forms.RadioButton radioButton3;
private System.Windows.Forms.RadioButton radioButton4;
private System.Windows.Forms.RadioButton radioButton5;
private System.Windows.Forms.TextBox textBoxFavoriteOS;
private System.Windows.Forms.MonthCalendar monthCalendar1;
private System.Windows.Forms.Label label4;
private System.Windows.Forms.CheckBox checkBoxSendNews;
private System.Windows.Forms.CheckBox checkBoxSendLetter;
private System.Windows.Forms.TextBox textBoxComment;
private System.Windows.Forms.Label label6;
private System.Windows.Forms.GroupBox groupBoxGender;
private System.Windows.Forms.GroupBox groupBoxOS;
private System.Windows.Forms.ErrorProvider errorProvider1;
/// <summary>
/// Required designer variable.
/// </summary>
private System.ComponentModel.Container components = null;

public RegisterForm()
{
//
// Required for Windows Form Designer support
//
InitializeComponent();
}

/// <summary>
/// Clean up any resources being used.
/// </summary>
protected override void Dispose( bool disposing )
{
if( disposing )
{
if(components != null)
{
components.Dispose();
}
}
base.Dispose( disposing );
}

#region Windows Form Designer generated code


/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
private void InitializeComponent()
{
this.button1 = new System.Windows.Forms.Button();
this.button2 = new System.Windows.Forms.Button();
this.textBoxName = new System.Windows.Forms.TextBox();
this.label1 = new System.Windows.Forms.Label();
this.label2 = new System.Windows.Forms.Label();
this.label3 = new System.Windows.Forms.Label();
this.textBoxEmail = new System.Windows.Forms.TextBox();
this.label5 = new System.Windows.Forms.Label();
this.comboBoxLevel = new System.Windows.Forms.ComboBox();
this.radioButton1 = new System.Windows.Forms.RadioButton();
this.radioButton2 = new System.Windows.Forms.RadioButton();
this.groupBoxGender = new System.Windows.Forms.GroupBox();
this.groupBoxOS = new System.Windows.Forms.GroupBox();
this.textBoxFavoriteOS = new System.Windows.Forms.TextBox();
this.radioButton5 = new System.Windows.Forms.RadioButton();
this.radioButton4 = new System.Windows.Forms.RadioButton();
this.radioButton3 = new System.Windows.Forms.RadioButton();
this.monthCalendar1 = new
System.Windows.Forms.MonthCalendar();
this.label4 = new System.Windows.Forms.Label();
this.checkBoxSendNews = new System.Windows.Forms.CheckBox();
this.checkBoxSendLetter = new System.Windows.Forms.CheckBox();
this.textBoxComment = new System.Windows.Forms.TextBox();
this.label6 = new System.Windows.Forms.Label();
this.errorProvider1 = new
System.Windows.Forms.ErrorProvider();
this.groupBoxGender.SuspendLayout();
this.groupBoxOS.SuspendLayout();
this.SuspendLayout();
//
// button1
//
this.button1.BackColor = System.Drawing.SystemColors.Control;
this.button1.DialogResult =
System.Windows.Forms.DialogResult.Yes;
this.button1.Location = new System.Drawing.Point(117, 480);
this.button1.Name = "button1";
this.button1.Size = new System.Drawing.Size(112, 23);
this.button1.TabIndex = 0;
this.button1.Text = "Зарегистрировать";
//
// button2
//
this.button2.BackColor = System.Drawing.SystemColors.Control;
this.button2.DialogResult =
System.Windows.Forms.DialogResult.Cancel;
this.button2.Location = new System.Drawing.Point(253, 480);
this.button2.Name = "button2";
this.button2.Size = new System.Drawing.Size(112, 23);
this.button2.TabIndex = 1;
this.button2.Text = "Отменить";
//
// textBoxName
//
this.textBoxName.Location = new System.Drawing.Point(108, 56);
this.textBoxName.Name = "textBoxName";
this.textBoxName.Size = new System.Drawing.Size(121, 20);
this.textBoxName.TabIndex = 2;
this.textBoxName.Text = "";
this.textBoxName.Validating +=
new System.ComponentModel.CancelEventHandler(
this.textBoxName_Validating);
//
// label1
//
this.label1.Font = new System.Drawing.Font("Arial", 14.25F,
System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point, ((System.Byte)
(204)));
this.label1.Location = new System.Drawing.Point(29, 8);
this.label1.Name = "label1";
this.label1.Size = new System.Drawing.Size(424, 32);
this.label1.TabIndex = 3;
this.label1.Text = "Зарегистрируйте свою копию программы";
//
// label2
//
this.label2.Location = new System.Drawing.Point(48, 56);
this.label2.Name = "label2";
this.label2.Size = new System.Drawing.Size(48, 23);
this.label2.TabIndex = 4;
this.label2.Text = "Ф. И. О.";
this.label2.TextAlign = System.Drawing.ContentAlignment.TopRight;
//
// label3
//
this.label3.Location = new System.Drawing.Point(8, 88);
this.label3.Name = "label3";
this.label3.Size = new System.Drawing.Size(88, 16);
this.label3.TabIndex = 5;
this.label3.Text = "Адрес E-Mail:";
this.label3.TextAlign = System.Drawing.ContentAlignment.TopRight;
//
// textBoxEmail
//
this.textBoxEmail.Location =
new System.Drawing.Point(108, 88);
this.textBoxEmail.Name = "textBoxEmail";
this.textBoxEmail.Size = new System.Drawing.Size(121, 20);
this.textBoxEmail.TabIndex = 6;
this.textBoxEmail.Text = "";
this.textBoxEmail.Validating +=
new System.ComponentModel.CancelEventHandler(
this.textBoxEmail_Validating);
//
// label5
//
this.label5.Location = new System.Drawing.Point(8, 120);
this.label5.Name = "label5";
this.label5.Size = new System.Drawing.Size(88, 16);
this.label5.TabIndex = 9;
this.label5.Text = "Квалификация:";
this.label5.TextAlign = System.Drawing.ContentAlignment.TopRight;
//
// comboBoxLevel
//
this.comboBoxLevel.Items.AddRange(new object[] {
"Начальная",
"Средняя",
"Высокая",
"Специалист",
"Эксперт"});
this.comboBoxLevel.Location =
new System.Drawing.Point(108, 120);
this.comboBoxLevel.Name = "comboBoxLevel";
this.comboBoxLevel.Size = new System.Drawing.Size(121, 21);
this.comboBoxLevel.TabIndex = 10;
this.comboBoxLevel.Text = "Начальная";
//
// radioButton1
//
this.radioButton1.Checked = true;
this.radioButton1.Location = new System.Drawing.Point(16, 16);
this.radioButton1.Name = "radioButton1";
this.radioButton1.Size = new System.Drawing.Size(48, 24);
this.radioButton1.TabIndex = 12;
this.radioButton1.TabStop = true;
this.radioButton1.Text = "Муж.";
//
// radioButton2
//
this.radioButton2.Location = new System.Drawing.Point(64, 16);
this.radioButton2.Name = "radioButton2";
this.radioButton2.Size = new System.Drawing.Size(48, 24);
this.radioButton2.TabIndex = 13;
this.radioButton2.Text = "Жен.";
//
// groupBoxGender
//
this.groupBoxGender.Controls.AddRange(new System.Windows.Forms.Control[] {
this.radioButton2,
this.radioButton1});
this.groupBoxGender.Location =
new System.Drawing.Point(253, 56);
this.groupBoxGender.Name = "groupBoxGender";
this.groupBoxGender.Size = new System.Drawing.Size(176, 48);
this.groupBoxGender.TabIndex = 14;
this.groupBoxGender.TabStop = false;
this.groupBoxGender.Text = "Пол";
//
// groupBoxOS
//
this.groupBoxOS.Controls.AddRange(new System.Windows.Forms.Control[] {
this.textBoxFavoriteOS,
this.radioButton5,
this.radioButton4,
this.radioButton3});
this.groupBoxOS.Location = new System.Drawing.Point(253, 112);
this.groupBoxOS.Name = "groupBoxOS";
this.groupBoxOS.Size = new System.Drawing.Size(176, 144);
this.groupBoxOS.TabIndex = 15;
this.groupBoxOS.TabStop = false;
this.groupBoxOS.Text = "Любимая ОС";
//
// textBoxFavoriteOS
//
this.textBoxFavoriteOS.Enabled = false;
this.textBoxFavoriteOS.Location =
new System.Drawing.Point(40, 104);
this.textBoxFavoriteOS.Name = "textBoxFavoriteOS";
this.textBoxFavoriteOS.TabIndex = 3;
this.textBoxFavoriteOS.Text = "";
//
// radioButton5
//
this.radioButton5.Location = new System.Drawing.Point(16, 72);
this.radioButton5.Name = "radioButton5";
this.radioButton5.Size = new System.Drawing.Size(152, 24);
this.radioButton5.TabIndex = 2;
this.radioButton5.Text = "Другая (укажите, какая)";
this.radioButton5.CheckedChanged +=
new System.EventHandler(this.radioButton5_CheckedChanged);
//
// radioButton4
//
this.radioButton4.Location = new System.Drawing.Point(16, 48);
this.radioButton4.Name = "radioButton4";
this.radioButton4.Size = new System.Drawing.Size(64, 24);
this.radioButton4.TabIndex = 1;
this.radioButton4.Text = "Linux";
//
// radioButton3
//
this.radioButton3.Checked = true;
this.radioButton3.Location = new System.Drawing.Point(16, 24);
this.radioButton3.Name = "radioButton3";
this.radioButton3.Size = new System.Drawing.Size(88, 24);
this.radioButton3.TabIndex = 0;
this.radioButton3.TabStop = true;
this.radioButton3.Text = "MS Windows";
//
// monthCalendar1
//
this.monthCalendar1.Location =
new System.Drawing.Point(37, 184);
this.monthCalendar1.MaxSelectionCount = 1;
this.monthCalendar1.Name = "monthCalendar1";
this.monthCalendar1.TabIndex = 16;
//
// label4
//
this.label4.Location = new System.Drawing.Point(37, 160);
this.label4.Name = "label4";
this.label4.Size = new System.Drawing.Size(152, 16);
this.label4.TabIndex = 17;
this.label4.Text = "День Вашего рождения:";
//
// checkBoxSendNews
//
this.checkBoxSendNews.Checked = true;
this.checkBoxSendNews.CheckState = System.Windows.Forms.CheckState.Checked;
this.checkBoxSendNews.Location =
new System.Drawing.Point(253, 288);
this.checkBoxSendNews.Name = "checkBoxSendNews";
this.checkBoxSendNews.Size = new System.Drawing.Size(152, 16);
this.checkBoxSendNews.TabIndex = 18;
this.checkBoxSendNews.Text = "Подписка на новости";
//
// checkBoxSendLetter
//
this.checkBoxSendLetter.Checked = true;
this.checkBoxSendLetter.CheckState =
System.Windows.Forms.CheckState.Checked;
this.checkBoxSendLetter.Location = new
System.Drawing.Point(253, 304);
this.checkBoxSendLetter.Name = "checkBoxSendLetter";
this.checkBoxSendLetter.Size =
new System.Drawing.Size(168, 32);
this.checkBoxSendLetter.TabIndex = 19;
this.checkBoxSendLetter.Text = "Подписка на электронную газету";
//
// textBoxComment
//
this.textBoxComment.Location =
new System.Drawing.Point(77, 384);
this.textBoxComment.Multiline = true;
this.textBoxComment.Name = "textBoxComment";
this.textBoxComment.Size = new System.Drawing.Size(328, 72);
this.textBoxComment.TabIndex = 20;
this.textBoxComment.Text = "";
//
// label6
//
this.label6.Location = new System.Drawing.Point(77, 360);
this.label6.Name = "label6";
this.label6.Size = new System.Drawing.Size(208, 23);
this.label6.TabIndex = 21;
this.label6.Text = "Оставьте Ваш комментарий:";
//
// errorProvider1
//
this.errorProvider1.DataMember = null;
//
// RegisterForm
//
this.AcceptButton = this.button1;
this.AutoScaleBaseSize = new System.Drawing.Size(5, 13);
this.CancelButton = this.button2;
this.ClientSize = new System.Drawing.Size(482, 519);
this.ControlBox = false;
this.Controls.AddRange(new System.Windows.Forms.Control[] {
this.label6,
this.textBoxComment,
this.checkBoxSendLetter,
this.checkBoxSendNews,
this.label4,
this.monthCalendar1,
this.groupBoxOS,
this.comboBoxLevel,
this.label5,
this.textBoxEmail,
this.label3,
this.label2,
this.label1,
this.textBoxName,
this.button2,
this.button1,
this.groupBoxGender});
this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedDialog;
this.Name = "RegisterForm";
this.StartPosition = System.Windows.Forms.FormStartPosition.CenterParent;
this.Text = "Регистрация программы";
this.groupBoxGender.ResumeLayout(false);
this.groupBoxOS.ResumeLayout(false);
this.ResumeLayout(false);

}
#endregion

private void radioButton5_CheckedChanged(object sender, System.EventArgs e)


{
RadioButton rb = (RadioButton)sender;
if(rb.Checked)
textBoxFavoriteOS.Enabled = true;
else
textBoxFavoriteOS.Enabled = false;
}

private void textBoxName_Validating(object sender,


System.ComponentModel.CancelEventArgs e)
{
if(textBoxName.Text.Length == 0)
{
errorProvider1.SetError(textBoxName, "Не указано имя");
}
else
errorProvider1.SetError(textBoxName, "");
}

private void textBoxEmail_Validating(object sender,


System.ComponentModel.CancelEventArgs e)
{
string email = textBoxEmail.Text;

if(email.IndexOf('@') == -1 || email.IndexOf('.') == -1)


{
errorProvider1.SetError(textBoxEmail,
"Неправильный адрес E-Mail");
}
else
errorProvider1.SetError(textBoxEmail, "");
}

public string UserName


{
get
{
return textBoxName.Text;
}
}
public string UserEmail
{
get
{
return textBoxEmail.Text;
}
}

public string UserLevel


{
get
{
return comboBoxLevel.Text;
}
}

public string UserComment


{
get
{
return textBoxComment.Text;
}
}

public string UserGender


{
get
{
for(int i=0; i < groupBoxGender.Controls.Count; i++)
{
RadioButton rb = (RadioButton)groupBoxGender.Controls[i];
if(rb.Checked)
return rb.Text;
}
return "";
}
}

public string FavoriteOS


{
get
{
for(int i=0; i < groupBoxOS.Controls.Count; i++)
{
if(groupBoxOS.Controls[i] is RadioButton)
{
RadioButton rb = (RadioButton)groupBoxOS.Controls[i];

if(rb.Checked)
{
if(rb.Name != radioButton5.Name)
return rb.Text;
else
return textBoxFavoriteOS.Text;
}
}
}
return "";
}
}

public string SendNews


{
get
{
if(checkBoxSendNews.Checked)
return "Yes";
else
return "No";
}
}

public string SendLetter


{
get
{
if(checkBoxSendLetter.Checked)
return "Yes";
else
return "No";
}
}

public string UserBirthDay


{
get
{
DateTime dt = monthCalendar1.SelectionStart;
return dt.Day + "." + dt.Month + "." + dt.Year;
}
}

}
}
Листинг П1-4. Файл ch05\SimpleNotepad\HelpAboutForm.cs
using System;
using System.Drawing;
using System.Collections;
using System.ComponentModel;
using System.Windows.Forms;

namespace SimpleNotepad
{
/// <summary>
/// Summary description for HelpAboutForm.
/// </summary>
public class HelpAboutForm : System.Windows.Forms.Form
{
private System.Windows.Forms.Button button1;
private System.Windows.Forms.PictureBox pictureBox1;
private System.Windows.Forms.Label label1;
private System.Windows.Forms.Label label2;
private System.Windows.Forms.Label label3;
private System.Windows.Forms.LinkLabel linkLabel1;
private System.Windows.Forms.LinkLabel linkLabel2;
/// <summary>
/// Required designer variable.
/// </summary>
private System.ComponentModel.Container components = null;

public HelpAboutForm()
{
//
// Required for Windows Form Designer support
//
InitializeComponent();
}

/// <summary>
/// Clean up any resources being used.
/// </summary>
protected override void Dispose( bool disposing )
{
if( disposing )
{
if(components != null)
{
components.Dispose();
}
}
base.Dispose( disposing );
}

#region Windows Form Designer generated code


/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
private void InitializeComponent()
{
System.Resources.ResourceManager resources = new
System.Resources.ResourceManager(typeof(HelpAboutForm));
this.button1 = new System.Windows.Forms.Button();
this.pictureBox1 = new System.Windows.Forms.PictureBox();
this.label1 = new System.Windows.Forms.Label();
this.label2 = new System.Windows.Forms.Label();
this.label3 = new System.Windows.Forms.Label();
this.linkLabel1 = new System.Windows.Forms.LinkLabel();
this.linkLabel2 = new System.Windows.Forms.LinkLabel();
this.SuspendLayout();
//
// button1
//
this.button1.BackColor =
System.Drawing.SystemColors.ControlLight;
this.button1.DialogResult =
System.Windows.Forms.DialogResult.Cancel;
this.button1.Location = new System.Drawing.Point(92, 128);
this.button1.Name = "button1";
this.button1.TabIndex = 0;
this.button1.Text = "OK";
this.button1.Click +=
new System.EventHandler(this.button1_Click);
//
// pictureBox1
//
this.pictureBox1.Image = ((System.Drawing.Bitmap)
(resources.GetObject("pictureBox1.Image")));
this.pictureBox1.Location = new System.Drawing.Point(8, 16);
this.pictureBox1.Name = "pictureBox1";
this.pictureBox1.Size = new System.Drawing.Size(40, 64);
this.pictureBox1.TabIndex = 1;
this.pictureBox1.TabStop = false;
//
// label1
//
this.label1.Font = new System.Drawing.Font("Arial", 14.25F,
System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point, ((System.Byte)
(204)));
this.label1.Location = new System.Drawing.Point(72, 16);
this.label1.Name = "label1";
this.label1.Size = new System.Drawing.Size(160, 24);
this.label1.TabIndex = 2;
this.label1.Text = "SimpleNotepad";
//
// label2
//
this.label2.Location = new System.Drawing.Point(72, 48);
this.label2.Name = "label2";
this.label2.Size = new System.Drawing.Size(100, 16);
this.label2.TabIndex = 3;
this.label2.Text = "Version 1.0";
//
// label3
//
this.label3.Location = new System.Drawing.Point(72, 64);
this.label3.Name = "label3";
this.label3.Size = new System.Drawing.Size(144, 16);
this.label3.TabIndex = 4;
this.label3.Text = "(c) Alexandre Frolov, 2003";
//
// linkLabel1
//
this.linkLabel1.Location = new System.Drawing.Point(72, 80);
this.linkLabel1.Name = "linkLabel1";
this.linkLabel1.Size = new System.Drawing.Size(96, 16);
this.linkLabel1.TabIndex = 5;
this.linkLabel1.TabStop = true;
this.linkLabel1.Text = "www.frolov.pp.ru";
this.linkLabel1.LinkClicked += new
System.Windows.Forms.LinkLabelLinkClickedEventHandler(this.linkLabel1_LinkClicked);
//
// linkLabel2
//
this.linkLabel2.Location = new System.Drawing.Point(72, 96);
this.linkLabel2.Name = "linkLabel2";
this.linkLabel2.Size = new System.Drawing.Size(136, 16);
this.linkLabel2.TabIndex = 6;
this.linkLabel2.TabStop = true;
this.linkLabel2.Text = "alexandre@frolov.pp.ru";
this.linkLabel2.LinkClicked += new
System.Windows.Forms.LinkLabelLinkClickedEventHandler(this.linkLabel2_LinkClicked);
//
// HelpAboutForm
//
this.AcceptButton = this.button1;
this.AutoScaleBaseSize = new System.Drawing.Size(5, 13);
this.BackColor = System.Drawing.Color.Khaki;
this.CancelButton = this.button1;
this.ClientSize = new System.Drawing.Size(258, 167);
this.ControlBox = false;
this.Controls.AddRange(new System.Windows.Forms.Control[] {
this.linkLabel2,
this.linkLabel1,
this.label3,
this.label2,
this.label1,
this.pictureBox1,
this.button1});
this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedDialog;
this.Icon = ((System.Drawing.Icon)(resources.GetObject("$this.Icon")));
this.MaximizeBox = false;
this.MinimizeBox = false;
this.Name = "HelpAboutForm";
this.ShowInTaskbar = false;
this.StartPosition = System.Windows.Forms.FormStartPosition.CenterParent;
this.Text = "About SimpleNotepad";
this.ResumeLayout(false);

}
#endregion

private void button1_Click(object sender, System.EventArgs e)


{
this.Close();
}

private void linkLabel1_LinkClicked(object sender,


System.Windows.Forms.LinkLabelLinkClickedEventArgs e)
{
linkLabel1.Links[linkLabel1.Links.IndexOf(e.Link)].Visited =
true;
System.Diagnostics.Process.Start(linkLabel1.Text);
}

private void linkLabel2_LinkClicked(object sender,


System.Windows.Forms.LinkLabelLinkClickedEventArgs e)
{
linkLabel2.Links[linkLabel2.Links.IndexOf(e.Link)].Visited =
true;
System.Diagnostics.Process.Start(
"mailto:alexandre@frolov.pp.ru");
}
}
}

Визуальное проектирование
приложений C#
А.В. Фролов, Г.В. Фролов
Библиографический список
 Фролов А.В., Фролов Г.В. Создание Web-приложений: Практическое руководство. — М.:
Издательско-торговый дом «Русская Редакция», 2001.
 Фролов А.В., Фролов Г.В. Практика применения PERL, PHP, Apache и MySQL для активных Web-
сайтов. — М.: Издательско-торговый дом «Русская Редакция», 2002.
 Фролов А.В., Фролов Г.В. Язык C#. Самоучитель. — М.: «ДИАЛОГ-МИФИ», 2002.
 Фролов А.В., Фролов Г.В. Операционная система Microsoft Windows 3.1 для программиста. — М.:
«ДИАЛОГ-МИФИ», 1993. — (Библиотека системного программиста; Т. 11).
 Дейл Роджерсон. Основы COM. — М.: Издательско-торговый дом «Русская Редакция», 2000.
 Том Армстронг. ActiveX: создание Web-приложений. — К.: Издательская группа BHV, 1998.
 Фролов А.В., Фролов Г.В. Программирование видеоадаптеров CGA, EGA и VGA. — М.: «ДИАЛОГ-
МИФИ», 1992. — (Библиотека системного программиста; Т. 3).
 Фролов А.В., Фролов Г.В. Графический интерфейс GDI в Microsoft Windows. — М.: «ДИАЛОГ-
МИФИ», 1993. — (Библиотека системного программиста; Т. 14).
 Дейтел Х.М., Дейтел П. Дж. И др. Как программировать на XML. — М.: ЗАО «Издательство
БИНОМ», 2001.
 Петцольд Ч. Программирование для Microsoft Windows на C#. — М.: Издательско-торговый дом
«Русская Редакция», 2002.
 Фролов А.В., Фролов Г.В. Программирование для Windows NT. — М.: «ДИАЛОГ-МИФИ», 1996. —
(Библиотека системного программиста; Т. 26).
 Фролов А.В., Фролов Г.В. Программирование для Windows NT. — М.: «ДИАЛОГ-МИФИ», 1997. —
(Библиотека системного программиста; Т. 27).
 Найк Дилип. Стандарты и протоколы Интернета. — М.: Издательско-торговый дом «Русская
Редакция», 1999.
 Пройдаков Э.М., Теплицкий Л.А. Англо-русский толковый словарь по вычислительной технике,
Интернету и программированию. — М.: Издательско-торговый дом «Русская Редакция», 2000.

Вам также может понравиться