Графический интерфейс GDI+ (язык C#)

ivt
0

Статья про методы и средства разработки графических приложений. Глава из книжки Фролова.






Визуальное проектирование приложений C#


А.В. Фролов, Г.В. Фролов

ГЛАВА 10. ГРАФИЧЕСКИЙ ИНТЕРФЕЙС GDI+

ОСНОВНЫЕ ПОНЯТИЯ

Независимость от аппаратуры

Контекст отображения

Класс Graphics

Приложение GraphicsApp

Отслеживание состояния клавиш мыши

Отслеживание перемещения курсора мыши

Идентификатор окна Handle и объект Graphics

Кисть для рисования

Рисование точки

Рисование в окне элемента управления

СОБЫТИЕ PAINT

Рисование в окнах приложений Microsoft Windows

Сообщение WM_PAINT

Пример обработки события Paint

Перерисовка окон элементов управления

МЕТОДЫ И СВОЙСТВА КЛАССА GRAPHICS

Рисование геометрических фигур

Линия

Набор линий

Прямоугольник

Набор прямоугольников

Многоугольник

Эллипс

Сегмент эллипса

Кривые Безье

Канонические сплайны

Замкнутый сегмент эллипса

Закрашенные фигуры

Рисование изображений

Немного о ресурсах приложения

Значки

Растровые и векторные изображения

Использование класса Image

Режим буксировки

События при буксировке

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

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

Загрузка изображения

Рисование загруженного изображения

Рисование текста

ИНСТРУМЕНТЫ ДЛЯ РИСОВАНИЯ

Перья

Кисти

Кисть для сплошной закраски

Кисти типа HatchBrush

Кисти типа TextureBrush

Градиентные кисти

Шрифты

Классификация шрифтов

Шрифты TrueType

Шрифты OpenType

Выбор шрифта

Конструкторы класса Font

Тип шрифта FontStyle

Единицы измерения размера шрифта

Семейство шрифта FontFamily

Приложение FontApp

 

Глава 10. Графический интерфейс GDI+

Мы изучили многочисленные аспекты визуального создания приложений C# с графическим интерфейсом, однако так и не познакомились непосредственно с реализацией самого графического интерфейса библиотеки классов Microsoft .NET Framework.

Действительно, все приложения, рассмотренные ранее в предыдущих главах этой книги, не осуществляли непосредственный вывод графических изображений или текста в окна приложения. Мы абстрагировались от деталей графического интерфейса, возложив все операции по рисованию в окнах на такие элементы управления, как текстовые поля, поля редактирования, списки, деревья и пр.

Вместе с тем рано или поздно наступает момент, когда набор стандартных элементов управления перестает удовлетворять разработчика приложений. Иногда возникают задачи, требующие функций и методов, позволяющих программе рисовать в окнах приложения произвольные графические изображения или отображать текст нестандартным способом. Типичный пример — построение всякого рода графиков и диаграмм, а также создание анимационных изображений.

Графический интерфейс приложений C#, как и других приложений, предназначенных для работы в рамках Microsoft .NET Framework, состоит из набора классов. Эти классы инкапсулируют поведение объектов и инструментов, предназначенных для рисования. Однако прежде чем приступить к описанию этих классов, нам необходимо познакомиться с основными понятиями интерфейса графических устройств.

Заметим, что графический интерфейс GDI+ настолько обширный, что ему можно посвятить отдельную книгу или даже несколько книг. Поэтому в рамках одной главы мы изложим только наиболее существенные аспекты применения GDI+. Тех из Вас, кому будет интересно познакомиться с возможностями GDI+ глубже, мы адресуем к [10].

Основные понятия

Если Вы когда-либо создавали программы для операционной системы MS-DOS и ее аналогов, то помните, что такие программы могут работать в текстовом или графическом режиме.

Использование текстового режима обычно не вызывает затруднений. Программы выводят текстовые строки на экран консоли, обращаясь к системным функциям ОС или к функциям базовой системы ввода-вывода BIOS. Что же касается графического режима, то его использование приводило к необходимости выполнять в программах прямое обращение к аппаратуре видеоадаптера.

С учетом того обстоятельства, что компьютеры могут быть оснащены видеоадаптерами самого разного типа, создание графических программ MS-DOS, способных работать на любых компьютерах, превращалось в непростую задачу. В самом деле, прежде чем выводить что-то на экран монитора в графическом режиме, программа должна была определить тип видеоадаптера и его возможности, переключить видеоадаптер в нужный видеорежим, а затем выполнять вывод изображения с учетом особенностей этого видеорежима. Всех, кого интересуют подробности программирования видеоадаптеров на уровне аппаратуры, мы адресуем к нашей книге [7], посвященной этому вопросу.

Независимость от аппаратуры

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

Что же касается приложений, то для них в составе ОС Microsoft Windows был предусмотрен набор системных функций, реализующих интерфейс графических устройств (Graphics Device InterfaceGDI). Применительно к приложениям 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(redBrushe.Xe.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(redBrushe.Xe.Y, 1, 1);

Обратите внимание на то, что метод FillRectangle вызывается для объекта g класса Graphics, созданного нами для окна формы Form1. Поэтому квадрат будет нарисован в окне этой формы.

В качестве первого параметра методу FillRectangle передается кисть redBrush, которую нужно использовать для рисования. Кисть нужна и для других методов класса Graphics, предназначенных для рисования геометрических фигур.

Второй и третий параметры метода FillRectangle задают координаты, в которых будет нарисован квадрат.

Начало системы координат при этом находится в левом верхнем углу окна, для которого был получен объект Graphics. В нашем случае это левый верхний угол внутренней области окна формы Form1. Ось X в этой системе координат, принятой по умолчанию, направлена слева направо, а ось — сверху вниз (рис. 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.Xe.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 не могут выводить текст или графику ни с помощью стандартных функций библиотеки компилятора, например таких, как printfcprintf или 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, такие как UpdateWindowInvalidateRect или 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. Свойства этого класса LeftRightWidth и 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.

Далее мы вызываем методы DrawStringDrawRectangle и 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(PenPointPoint);
public void DrawLine(PenPointF PointF;
public void DrawLine(Penintintintint);
public void DrawLine(Penfloatfloatfloatfloat);

Первый параметр задает инструмент для рисования линии — перо. Перья создаются как объекты класса 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(PenRectangle[]);
public void DrawRectangles(PenRectangleF[]);

Первый из этих методов получает в качестве второго параметра ссылку на массив объектов класса Rectangle, описывающих размеры и расположение прямоугольных областей.

Второй метод использует для этого объекты класса RectangleF, свойства XYWidth и 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);

Как видите, эти методы отличаются только способом, при помощи которого описывается расположение и размеры прямоугольной области, в которую вписан эллипс. Вы можете задавать расположение и размеры этой области в виде рассмотренных ранее объектов класса RectangleRectangleF, а также в виде целых чисел или числе с плавающей десятичной точкой.

Пример использования метода 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(myPenmyBezierPoints);
}

Здесь мы создаем начальную и конечную точки 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(PenPoint[], float);
public void DrawCurve(PenPointF[], 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(myPenmyPoints, (float)0.3,
     FillMode.Alternate);
}

Результат показан на рис. 10-16.

Рис. 10-16. Рисование канонического сплайна

Замкнутый сегмент эллипса

Для рисования замкнутого сегмента эллипса (pie) Вы можете воспользоваться методом DrawPie. Имеется 4 перегруженных варианта этого метода:

public void DrawPie(PenRectanglefloatfloat);
public void DrawPie(PenRectangleFfloatfloat);
public void DrawPie(Penintintintintintint);
public void DrawPie(Penfloatfloatfloatfloatfloatfloat);

В качестве первого параметра методу нужно передать перо для рисования. Последние два параметра определяют углы, ограничивающие сегмент эллипса. Эти углы используются таким же образом, как и при рисовании незамкнутого сегмента эллипса методом 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(myPenxPosyPoswidthheight,
     startAngleendAngle);

  g.DrawRectangle(new Pen(Color.Black, 1), xPosyPos,
     widthheight);
}

Здесь через переменные xPosyPoswidth и 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 — не простая задача. Решая ее, программисту приходится учитывать многочисленные детали, имеющие отношение к цветовому разрешению и режимам видеоадаптера. Кроме того, часто приходится работать напрямую с внутренними форматами файлов графических изображений, таких как BMPGIFJPEGPNG и т.д.

Графическая подсистема 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(myIcone.Xe.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-сервер, а эта задача доступна только опытным и очень опытным программистам, владеющим всеми тонкостями модели компонентного объекта (COMComponent 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 несет в себе информацию о том, какой клавишей мыши осуществляется буксировка, и нажаты ли при этом клавиши ShiftControl и 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.Widthsize.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.Widthsize.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,  xy, 200, y);

  y += 15; 
  myPen.Width = 2;
  g.DrawLine(myPen,  xy, 200, y);

  y += 15; 
  myPen.Width = 3;
  g.DrawLine(myPen,  xy, 200, y);

  y += 15; 
  myPen.Width = 5;
  g.DrawLine(myPen,  xy, 200, y);

  y += 15; 
  myPen.Width = 10;
  g.DrawLine(myPen,  xy, 200, y);

  y += 15; 
  myPen.Width = 3;
  myPen.DashStyle  = DashStyle.Dash;
  g.DrawLine(myPen,  xy, 200, y);

  y += 15; 
  myPen.Width = 3;
  myPen.DashStyle  = DashStyle.DashDot;
  g.DrawLine(myPen,  xy, 200, y);

  y += 15; 
  myPen.Width = 3;
  myPen.DashStyle  = DashStyle.DashDotDot;
  g.DrawLine(myPen,  xy, 200, y);

  y += 15; 
  myPen.Width = 3;
  myPen.DashStyle  = DashStyle.Dot;
  g.DrawLine(myPen,  xy, 200, y);

  y += 15; 
  myPen.Width = 5;
  myPen.DashStyle  = DashStyle.Solid;
  myPen.StartCap   = LineCap.ArrowAnchor;
  myPen.EndCap  = LineCap.DiamondAnchor;
  g.DrawLine(myPen,  xy, 200, y);

  y += 15; 
  myPen.Width = 5;
  myPen.StartCap   = LineCap.Round;
  myPen.EndCap  = LineCap.RoundAnchor;
  g.DrawLine(myPen,  xy, 200, y);

  y += 15; 
  myPen.Width = 5;
  myPen.StartCap   = LineCap.Square;
  myPen.EndCap  = LineCap.SquareAnchor;
  g.DrawLine(myPen,  xy, 200, y);

  y += 15; 
  myPen.Width = 5;
  myPen.StartCap   = LineCap.Triangle;
  myPen.EndCap  = LineCap.Flat;
  g.DrawLine(myPen,  xy, 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.DashDashStyle.DashDotDashStyle.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,  xy, 200, y);

y += 15; 
myPen.Width = 5;
myPen.StartCap   = LineCap.Round;
myPen.EndCap  = LineCap.RoundAnchor;
g.DrawLine(myPen,  xy, 200, y);

y += 15; 
myPen.Width = 5;
myPen.StartCap   = LineCap.Square;
myPen.EndCap  = LineCap.SquareAnchor;
g.DrawLine(myPen,  xy, 200, y);

y += 15; 
myPen.Width = 5;
myPen.StartCap   = LineCap.Triangle;
myPen.EndCap  = LineCap.Flat;
g.DrawLine(myPen,  xy, 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

Percent
90

Эти константы задают процентное соотношение цвета штриховки и цвета фона кисти.

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.CrossColor.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.DivotColor.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). Эти типы называются ModernRomanSwissScriptDecorative.

Шрифты семейства 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(stringfloat);

Выбирая название гарнитуры шрифта, учитывайте, что можно указывать только шрифты 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

Перечеркнутый

AaBbCcDdEeFfGgHhIiJjKkLl АаБбВвГгДдЕеЖжЗзИиКкЛлМмНнОоПпРр

Единицы измерения размера шрифта

Параметр конструктора 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



Tags

Дописати коментар

0Коментарі
* Please Don't Spam Here. All the Comments are Reviewed by Admin.
Дописати коментар (0)

#buttons=(Accept !) #days=(20)

Our website uses cookies to enhance your experience. Learn More
Accept !