- •Книга вышла в издательстве Кудиц-Образ Оглавление
- •Глава 1. Принципы работы приложений Windows. 2
- •Глава 1. Принципы работы приложений Windows
- •Глава 2. Работа с Microsoft Visual Studio .Net.. 2
- •Глава 2. Работа с Microsoft Visual Studio .Net
- •Глава 3. Формы в приложениях c#. 2
- •Глава 3. Формы в приложениях c#
- •Глава 4. Создание редактора текста.. 2
- •Глава 4. Создание редактора текста
- •Глава 5. Диалоговые окна.. 2
- •Глава 5. Диалоговые окна
- •Глава 6. Использование элементов управления.. 2
- •Глава 6. Использование элементов управления
- •Глава 7. Многооконный пользовательский интерфейс.. 2
- •Глава 7. Многооконный пользовательский интерфейс
- •Глава 8. Приложения с базами данных.. 3
- •Глава 8. Приложения с базами данных
- •Глава 9. Интеграция с ms sql Server.. 2
- •Глава 9. Интеграция с ms sql Server
Глава 9. Интеграция с ms sql Server.. 2
Приложение SQLTestApp. 2
Создание базы данных. 2
Создание таблицы Contacts. 4
Идентификатор для подключения к базе данных. 6
Создание проекта приложения. 10
Добавление адаптера данных SqlDataAdapter 10
Программный код для адаптера и соединения. 16
Создание локального набора DataSet 22
Редактирование содержимого набора DataSet 24
Загрузка набора данных. 24
Сохранение отредактированного набора данных. 25
Парольный доступ к системе. 25
Приложение LoginApp. 26
Таблица Users. 27
Создание проекта приложения. 27
Соединение с базой данных. 27
Запросы SQL. 28
Адаптер SqlDataAdapter 29
Элемент управления DataGrid. 30
Подключение пользователя. 32
Обновление таблицы Users. 36
Хранение дерева в базе данных.. 36
Приложение ArticlesApp. 36
База данных Articles. 37
Таблица Tree. 38
Таблица Documents. 38
Хранимая процедура sp_InsertDocument 38
Хранимая процедура sp_ InsertNode. 39
Хранимая процедура sp_ UpdateDocument 39
Создание проекта приложения ArticlesApp. 39
Соединение с базой данных. 40
Добавление адаптера SqlDataAdapter. 40
Создание набора данных DataSet 41
Добавление контекстного меню.. 41
Создание узла дерева.. 41
Метод AddNode. 43
Диалоговое окно для ввода данных добавляемого узла. 43
Открытие и закрытие соединения с базой данных. 44
Использование хранимых процедур. 44
Параметры хранимых процедур. 44
Запуск хранимой процедуры.. 45
Получение значений выходных параметров. 45
Добавление текста документа. 45
Диалоговая форма редактирования документа. 45
Свойство Title. 45
Свойство Weight 46
Свойство Document 46
Построение дерева. 46
Метод UpdateTree. 46
Метод CreateNodes. 47
Редактирование узла дерева.. 48
Обработчик событий меню Edit 49
Извлечение идентификатора редактируемого узла. 50
Извлечение данных редактируемого узла дерева. 50
Получение текст редактируемой статьи. 50
Извлечение заголовка и веса сортировки. 51
Обновление информации узла в базе данных. 51
Удаление узла дерева.. 52
Отслеживание перемещений по дереву.. 54
Глава 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+. 2
Основные понятия.. 2
Независимость от аппаратуры.. 2
Контекст отображения. 3
Класс Graphics. 4
Приложение GraphicsApp. 4
Отслеживание состояния клавиш мыши. 5
Отслеживание перемещения курсора мыши. 5
Идентификатор окна Handle и объект Graphics. 6
Кисть для рисования. 6
Рисование точки. 6
Рисование в окне элемента управления. 7
Событие Paint.. 8
Рисование в окнах приложений Microsoft Windows. 8
Сообщение WM_PAINT. 9
Пример обработки события Paint 10
Перерисовка окон элементов управления. 12
Методы и свойства класса Graphics. 12
Рисование геометрических фигур. 12
Линия. 12
Набор линий. 13
Прямоугольник. 16
Набор прямоугольников. 16
Многоугольник. 17
Эллипс. 18
Сегмент эллипса. 18
Кривые Безье. 19
Канонические сплайны.. 20
Замкнутый сегмент эллипса. 22
Закрашенные фигуры.. 23
Рисование изображений. 23
Немного о ресурсах приложения. 24
Значки. 24
Растровые и векторные изображения. 26
Использование класса Image. 27
Режим буксировки. 27
События при буксировке. 28
Обработка события DragOver 28
Обработка события DragDrop. 29
Загрузка изображения. 29
Рисование загруженного изображения. 30
Рисование текста. 32
Инструменты для рисования.. 32
Перья. 33
Кисти. 36
Кисть для сплошной закраски. 37
Кисти типа HatchBrush. 37
Кисти типа TextureBrush. 40
Градиентные кисти. 41
Шрифты.. 42
Классификация шрифтов. 42
Шрифты TrueType. 42
Шрифты OpenType. 44
Выбор шрифта. 46
Конструкторы класса Font 46
Тип шрифта FontStyle. 47
Единицы измерения размера шрифта. 47
Семейство шрифта FontFamily. 48
Приложение FontApp. 48
Глава 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 |
Рисование закрашенного сплайна |
FillRegion |
Рисование закрашенной области типа Region |
Есть два отличия методов с префиксом 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. Доступные форматы графических файлов
Свойство |
Описание формата |
Bmp |
Растровые изображения DIB (Device-Independent Bitmap) |
Emf |
Расширенный формат EMF (Windows Enhanced Metafile) |
Exif |
Формат для обмена файлами изображений EXIF (Exchangeable Image File) |
Gif |
Формат обмена графическими изображениями GIF (Graphics Interchange Format) |
Icon |
Значки ОС Microsoft Windows |
Jpeg |
Формат, созданный объединенной группой экспертов по обработке фотографий JPEG (Joint Photographic Experts Group) |
MemoryBmp |
Изображения DIB, создаваемые в оперативной памяти |
Png |
Формат переносимых сетевых изображений PNG (Portable Network Graphics), разработанный группой W3C |
Tiff |
Теговый формат файлов изображений TIFF (Tag Image File Format) |
Wmf |
Метафайл Windows WMF (Windows metafile) |
Как видите, список довольно внушительный и включает в себя практически все наиболее распространенные форматы файлов. Все эти форматы, за исключением 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 |
Нажата правая клавиша мыши |
4 |
Нажата клавиша Shift |
8 |
Нажата клавиша Control |
16 |
Нажата средняя клавиша мыши |
32 |
Нажата клавиша Alt |
В свойствах 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 (темная штриховка) |
DarkHorizontal |
Горизонтальные линии, которые на 50% плотнее, чем при использовании константы Horizontal (темная штриховка) |
DarkUpwardDiagonal |
Диагональные линии, плотнее на 50% чем при использовании константы BackwardDiagonal (темная штриховка) |
DarkVertical |
Вертикальные линии, которые на 50% плотнее, чем при использовании константы Vertical (темная штриховка) |
DashedDownwardDiagonal |
Штриховые диагональные линии, идущие в обратном направлении |
DashedHorizontal |
Штриховые горизонтальные линии |
DashedUpwardDiagonal |
Штриховые диагональные линии, идущие в прямом направлении |
DashedVertical |
Штриховые вертикальные линии |
DiagonalBrick |
Диагональная «кирпичная» штриховка |
DiagonalCross |
Пересекающиеся прямые и обратные диагональные линии |
Divot |
Штриховка в виде дерна |
DottedDiamond |
Прямые и обратные диагональные пересекающиеся линии, состоящие из отдельных точек |
DottedGrid |
Горизонтальные и вертикальные пересекающиеся линии, состоящие из отдельных точек |
ForwardDiagonal |
Прямые диагональные линии, идущие в направлении от верхнего левого угла к нижнему правому углу кисти |
Horizontal |
Горизонтальные линии |
HorizontalBrick |
Горизонтальные «кирпичные» линии |
LargeCheckerBoard |
Штриховка в виде шахматной доски с крупными клетками |
LargeConfetti |
Штриховка в виде конфетти |
LargeGrid |
Пересекающиеся горизонтальные и вертикальные линии (то же, что и Cross) |
LightDownwardDiagonal |
Светлая обратная диагональная штриховка |
LightHorizontal |
Светлая горизонтальная штриховка |
LightUpwardDiagonal |
Светлая прямая диагональная штриховка |
LightVertical |
Светлая вертикальная штриховка |
Max |
То же, что и SolidDiamond |
Min |
То же, что и Horizonal |
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. Примеры начертаний шрифтов
Начертание |
Образец шрифта |
Normal |
AaBbCcDdEeFfGgHhIiJjKkLl АаБбВвГгДдЕеЖжЗзИиКкЛлМмНн |
Bold |
AaBbCcDdEeFfGgHhIiJjKkLl АаБбВвГгДдЕеЖжЗзИиКкЛлМмНн |
Italic |
AaBbCcDdEeFfGgHhIiJjKkLl АаБбВвГгДдЕеЖжЗзИиКкЛлМмНнОоПпРр |
Графический интерфейс 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
Константа |
Описание |
Образец шрифта |
Regular |
Обычный |
AaBbCcDdEeFfGgHhIiJjKkLl АаБбВвГгДдЕеЖжЗзИиКкЛлМмНн |
Bold |
Жирный |
AaBbCcDdEeFfGgHhIiJjKkLl АаБбВвГгДдЕеЖжЗзИиКкЛлМмНн |
Italic |
Наклонный |
AaBbCcDdEeFfGgHhIiJjKkLl АаБбВвГгДдЕеЖжЗзИиКкЛлМмНнОоПпРр |
Underline |
Подчеркнутый |
AaBbCcDdEeFfGgHhIiJjKkLl АаБбВвГгДдЕеЖжЗзИиКкЛлМмНнОоПпРр |
Strikeout |
Перечеркнутый |
|
Единицы измерения размера шрифта
Параметр конструктора Font типа GraphicsUnit дает Вам возможность указывать размеры шрифта не только в пунктах, но и в других единицах измерения. В табл. 10-13 мы привели константы перечисления GraphicsUnit с кратким описанием.
Таблица 10-13. Константы перечисления GraphicsUnit
Константа |
Описание единицы измерения |
Display |
1/75 часть дюйма |
Document |
1/300 часть дюйма |
Inch |
Дюйм |
Millimeter |
Миллиметр |
Pixel |
Пиксел |
Point |
Пункт (1/72 дюйма) |
World |
Единицы глобальных координат (world unit) |
Семейство шрифта 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.
