Основы GDI+.
Глава 1. Основные
понятия и ошибки.
Приступаю к рассказу как
рисовать в .NET.
Несколько вводных
слов.
Если вы хотите что-то
изобразить на экране - вам надо это что-то
нарисовать. Другого способа нет. Все
окна, кнопки и прочие контролы - это все
рисуется, просто код рисования
написан за вас. Но те, кто создают свои элементы
управления знают, что рисовать
приходится все - начиная от рамки и
заканчивая введенным текстом.
Есть разные
технологии рисования. Можно назвать три
основные - GDI+, DirectX, системные
функции.
Системные функции
имеют очень ограниченные возможности,
однако работают очень быстро. В случае
тех же контролов, лучше пользоваться
функциями системы для рисования
кусков стиля, эти функции работают
намного быстрее, чем GDI+, поскольку вместо
длительного прорисовывания
картинки, они копируют содержимое
напрямую в видео память.
DirectX - самая
быстрая технология, поскольку
рисует прямо в видео памяти, используя
графические чипы-ускорители. Однако
имеет ряд недостатков - количество и
объем подгружаемых библиотек и
драйверов может превышать объем
программы в несколько раз. Не говоря уже о том,
что программировать под DirectX много
сложнее, чем под GDI+.
GDI+ - самая медленная
технология. И при этом самая удобная для
программиста. В отличии от DirectX -
рисует используя основной процессор,
поэтому занимает кучу ресурсов и
времени. В отличии от системных функций
имеет огромные возможности. Говорят, что
в ближайшем будущем (в районе .NET 4.0) GDI+ тоже
будет рисовать использую графические
чипы...
Основные моменты
GDI+
Все рисование в GDI+
ведется через объект класса Graphics. Это,
можно сказать, ядро технологии. Объект
содержит функции рисования плоских
примитивов, изображений, текста,
поддерживает пространственные
преобразования, умеет проводить
сглаживание в разных режимах и пр. Объект
Graphics может быть создан от любого
контрола, включая форму, от любого
объекта Image, и еще несколькими
способами, которые вряд ли
понадобятся.
Итак, если вам надо что-то
где-то нарисовать, действуете так:
1. Создаете объект
Graphics, или получаете уже созданный, от
того объекта, на котором вам надо
рисовать.
2. Рисуете через
объект Graphics.
3. Удаляете объект
Graphics.
Graphics gr =
Graphics.FromHwnd(panel1.Handle);
gr.DrawLine(pen1, point1,
point2);
gr.FillEllipse(brush1,
0,0,100,100);
gr.DrawEllipse(pen2,
0,0,100,100);
gr.Dispose();
Если вашей программе
нужно очень много рисовать, да еще из разных
функций, вы можете сделать единый объект
Graphics, и пользоваться им от разных
функций. Впрочем, это как правило плохая
методика, гораздо лучше
использовать родные event.
Использование Graphics в
event.
Обычно это выглядит
так:
private void
panel1_Paint(object sender,
System.Windows.Forms.PaintEventArgs e) {
e.Graphics.FillEllipse(Brushes.Magenta,0,0,150,150);
}
И лучше всего, весь код
рисования закладывать в event. Вам никто не
мешает сделать его сильно
параметрическим и пр. Можете в event
поставить вызов функции собственно
рисования, и передавать объект
e.Graphics как аргумент, с модификатором
ref. Это позволит вам использовать функцию
рисования не только для рисования на
экране, но и для рисования на принтере,
если у вас будет поддержка печати, для
рисования на Image, если вы будете
сохранять изображение в файл, причем не тратя
лишних ресурсов на написания трех
одинаковых функций, или на создание
новых объектов Graphics и т.д.
Основные приемы
Рисование
динамической информации в форме,
обычно, производится двумя путями -
либо в контроле panel, либо в Image
контрола pictureBox. Конечно, вам
никто не мешает рисовать прямо в форме,
если у вас вся форма отведена под
рисование, но там свои заморочки. Мы
расcмотрим схему рисования через event
Paint в двух контролах.
Общая схема такая:
1. Функция
panel1_Paint или pictureBox1_Paint
содержит параметрический код
рисования.
2. Остальные
контролы меняют параметры
рисования.
Предположим есть две
радиокнопки - одна задает красный цвет,
другая синий. Тогда код рисования будет
выглядеть примерно так:
Для panel:
private void
panel1_Paint(object sender,
System.Windows.Forms.PaintEventArgs e) {
if
(radioButton1.Checked) {
e.Graphics.FillEllipse(Brushes.Red,0,0,150,150);
}
else {
e.Graphics.FillEllipse(Brushes.Blue,0,0,150,150);
}
}
Для pictureBox:
private void
pictureBox1_Paint(object sender,
System.Windows.Forms.PaintEventArgs e) {
Graphics gr =
Graphics.FromImage(pictureBox1.Image);
gr.Clear(Color.White);
if
(radioButton1.Checked) {
gr.FillEllipse(Brushes.Red,0,0,150,150);
}
else {
gr.FillEllipse(Brushes.Blue,0,0,150,150);
}
gr.Dispose();
}
В последнем случае -
gr.Clear(Color.White) - заполняет всю
картинку белым цветом, это нужно если надо
очистить то, что было нарисовано до
этого.
В чем разница - если
вам надо рисовать только в программе,
используйте panel, оно проще и меньше
ресурсов ест. Если ваша программа
работает с изображениями, в том числе с
файлами изображений -
загрузка/сохранение, рисуйте в
pictureBox.Image, так проще сохранять,
во-первых, и pictureBox создан для хранения
изображений, так что дешевле записать
Image в pictureBox.Image, чем вызывать в
panel1_Paint e.Graphics.DrawImage().
Но не забывайте при
загрузке формы создавать Image в
pictureBox, ибо по умолчанию,
pictureBox.Image = null;
Для радио кнопок:
private void
radioButton1_CheckedChanged(object
sender, EventArgs e) {
panel1.Refresh();
// или
pictureBox1.Refresh();
}
Основные ошибки
Идеология GDI+ почему-то у
многих вызывает кучу проблем... скорее
всего, потому что люди не понимают
принцип работы Windows.
Ошибка 1.
Пропадающее изображение - люди
рисуют в panel, или pictureBox не в event
Paint, а по нажатию кнопки, или откуда-то еще и
удивляются, почему их изображение не
перерисовывается, а пропадает,
когда форма перерисовывается (ее
свернули/развернули, убрали за
экран/вывели обратно и пр.).
Ответ 1. Так и
должно быть - когда вы рисуете по
контролу - вы рисуете на экране, когда
экран перерисовывается, он
перерисовывается с нуля, и если ваш
код рисования не включен в цепочку
перерисовывания - вашего
рисунка и не будет. Чтобы включить ваш код в
цепочку - поставьте его в event Paint нужного
контрола.
Ошибка 2. При
рисовании в контроле pictureBox,
люди рисуют по контролу, а не по Image.
Ответ 2. Когда
рисуете - получайте Graphics через
Graphics.FromImage(pictureBox.Image), a
не e.Graphics.
Ошибка 3. При
использовании pictureBox -
создается новый Image, в нем проводится
рисование, потом он вставляется в
pictureBox.
Ответ 3. На хрена
создавать новый Image, каждый раз когда вы
рисуете. Создавать новый надо только
когда вы меняете размер pictureBox.
И последнее,
несмотря не недостатки технологии
сбора мусора (Garbage Collector),
рекомендуется им пользоваться.
Во-первых, если это войдет в привычку сейчас, то
когда технология заработает
(например, в .NET 4.0), вы будете писать
правильно. Во-вторых, технология и сейчас
работает с большими объемами в
памяти, а изображения (и многие другие
объекты рисования) относятся именно
к большим объемам. Так что любой созданный вами
объект Graphics должен быть Dispose() после
окончания работы с ним, любой созданный
Image должен быть Dispose() после окончания
работы с ним. И главное, после всех Dispose() - не
забывайте вызывать GC.Collect().
Знающим английский
могу порекомендовать сайт Bob Powell (
http://www.bobpowell.net/), как
источник кучи полезной информации для
начинающих в GDI+.
Глава 2. Объект
Graphics.
Как уже говорилось,
Graphics - основной объект (и класс) для
рисования. Рассмотрим подробно, что он
умеет.
Конструкторы
У класса есть
статические функции для создания
объекта Graphics, и обратите внимание -
нет public конструктора. Поэтому
создавать объект приходиться одним из
следующих способов:
FromHdc - по
указателю на контекст устройства
(Device Context) - используется
редко.
FromHwnd - по
указателю на контрол (или форму).
Graphics gr =
Graphics.FromHwnd(panel1.Handle);
FromImage - для
рисунка (Image).
Graphics gr =
Graphics.FromImage(bitmap);
Поле
рисования
Объект Graphics
привязан к определенному полю
рисования (Clip), которое имеет привязку
к объекту, координаты и размеры. Очень
удобная вещь с точки зрения двух моментов -
во-первых, вы можете изменять область, в
которой происходит собственно
рисование, не изменяя кода
рисования. Например, вам надо кусок
существующей картинки перекрасить -
вы можете закрасить этот кусок, указав точно
все его координаты, а можете залить всю
картинку через Clear, изменив размеры и
координаты Clip так, чтобы они совпали с
нужным вам куском. И во-вторых, рисование
ведется только в пределах Clip региона, т.е.
на все, что рисуется вне его время и ресурсы не
тратятся.
В классе есть набор
функций для управления
координатами и размерами Clip:
свойство Clip -
возвращает Region в котором
ведется рисование, также позволяет
установить новый Region напрямую.
свойство ClipBounds
- возвращает прямоугольник,
описывающий регион Clip.
свойство
IsClipEmpty - показывает пуст ли
Clip.
свойство
IsVisibleClipEmpty - то же, но с
проверкой видимой части Clip. Работает
только при рисовании по контролу.
IsVisible -
проверяет, видим ли данный
прямоугольник. Очень удобно, если вы рисуете
в контроле что-то, что требует больших и
долгих расчетов, прежде чем считать -
проверьте, а показано-то оно будет.
ExcludeClip -
исключает из Clip фигуру,
переданную как аргумент.
IntersectClip -
оставляет в Clip только область
пересечения Clip и фигуры в
аргументе.
ResetClip -
устанавливает Clip равным
бесконечности.
SetClip -
устанавливает Clip из аргументов.
TranslateClip -
cмещает Clip по плоскости рисования.
Разница между Clip и
видимой частью Clip: если у вас есть panel
контрол, с включенным AutoScroll и
содержимое превышает размеры panel,
то создавая Graphics по указателю на
контрол, вы получите Clip = всей области
panel, однако видимая часть Clip - это та,
которая показывается в настоящий
момент пользователю. Но помните, что
видимая часть Clip имеет координаты и
размер контрола - т.е. в случае panel, со
скроллом, смещенным вправо до конца, видимый
клип все равно будет от 0;0 до
panel.Width;panel.Height.
Рисование и
заливка
Среди функций
объекта можно выделить две большие группы -
для рисования и для заливки.
Почти все функции,
начинающиеся со слова Draw - рисуют
заданную фигуру заданной ручкой.
Все функции,
начинающиеся со слова Fill - заливают
заданную фигуру заданной кистью.
Кисти и ручки будут
рассмотрены в следующем посте.
Список функций весьма
велик:
DrawArc - рисует
дугу
DrawLine - рисует
линию
DrawPolygon - рисует
многоугольник и т.д.
Аналогично, функции
FillArc, FillPolygon и т.д.
Дополнительное
рисование
Дополнительно к
рисованию простых форм есть следующие
функции:
DrawString - рисует
строку, и заливает ее заданной кистью.
DrawIcon и
DrawIconUnstretched -
рисует иконку (объект Icon), и рисует иконку без
изменения, соответственно.
DrawImage,
DrawImageUnscaled и
DrawImageUnscaledAndClipped
- рисует картинку (объект Image), рисует
картинку без изменений в указанной точке
и рисует картинку без изменений с
обрезкой по указанному
прямоугольнику
соответственно.
Clear -
заливает все поле рисования
указанным цветом.
CopyFromScreen -
копирует, попиксельно, изображение
на экране в указанном прямоугольнике в
указанный прямоугольник поля
рисования.
Текст
Для рисования
текста есть вспомогательные
функции:
MeasureString -
позволяет получить размеры строки,
когда она будет нарисована.
MeasureCharacterRanges -
позволяет получить размеры набора
символов, когда они будут
нарисованы.
Разница между
функциями в разном подходе к
определению допусков на свисающие
части букв, разный допуск на сглаживание и еще
чуть-чуть. Подсчет в любом случае не
идеальный, так как функции почему-то не
используют установленный параметр
типа сглаживания шрифта в системе и в
объекте Graphics.
свойство
TextRenderingHint -
определяет режим сглаживания
текста. Варианты - без сглаживания
(SingleBitPerPixel) и со
сглаживанием (AntiAlias), каждый может
быть с подстройкой свисающих частей
(GridFit) или без. Плюс есть ClearTypeGridFit
- рисует через движок ClearType. И есть
вариант SystemDefault - использовать
настройки системы.
свойство
TextContrast - определяет
контрастность текста, если в
системе включено сглаживание текста
или ClearType.
Преобразования
При рисовании
довольно часто необходимо провести
над изображением, или будущим
изображением, некие трансформации -
изменить масштаб, повернуть, может быть
подвинуть, не меняя основного кода
рисования. Для этого в GDI+ есть класс Matrix,
описывающий векторные
(координатные) трансформации для
каждой рисуемой точки. Для тех кто не знает, или уже
благополучно забыл что такое матрицы и
как с ними работают, есть функции,
которые выполняют операции над
матрицей за вас.
свойство Transform -
возвращает и позволяет задать
матрицу преобразований.
MultiplyTransform -
перемножает текущую матрицу и
матрицу в аргументе в заданном
порядке.
TranslateTransform -
передвигает рисование по
плоскости.
RotateTransform -
поворачивает рисование
относительно начала
координат.
ResetTransform -
обнуляет все трансформации.
ScaleTransform -
изменяет масштаб, по каждой оси
отдельно.
TransformPoints -
преобразует координаты
заданного массива точек из одной
системы координат в другую.
Качество
Еще есть набор свойств,
определяющих качество
рисования.
CompositingMode -
определяет как рисунки (Image) будут
рисоваться. Варианты SourceCopy -
цвет рисунка перекрывает подложку,
SourceOver - цвет рисунка
смешивается с цветом подложки, в
пропорции, определяемой альфа
компонентой цвета.
CompositingQuality -
определяет качество
преобразования рисунков (Image),
когда они рисуется один по другому. Из
вариантов реально пользоваться
стоит только HighQuality -
качественно, но медленно и
HighSpeed - быстро, но не очень
качественно.
DpiX и DpiY -
позволяют узнать dpi по обеим осям.
InterpolationMode -
определяет метод интерполяции, по
сути - сглаживание, при рисовании.
Варианты для использования в
порядке возрастания качества и
времени: NearestNeighbor,
Bilinear,
HighQualityBilinear, Bicubic,
HighQualityBicubic.
PixelOffsetMode -
определяет качество offset
пикселей, чтоб я понимал что это такое :).
Насколько я понял, это тоже параметр
сглаживания, но на уровне смешения цветов
пикселей. Для использования обычные два
варианта - HighQuality и
HighSpeed, и вариант None - никакой
обработки.
SmoothingMode -
определяет режим сглаживания линий.
Те же варианты - HighQuality,
HighSpeed и None.
Прочее
Есть еще набор
функций, которые ни к какой группе не
относяться, но иногда они нужны:
GetNearestColor -
возвращает ближайщий цвет к
аргументу в цветовом
пространстве объекта Graphics.
Save -
позволяет сохранить состояние
объекта Graphics: трансформации, Clip,
качество.
Restore -
позволяет воостановить состоянии
объект из ранее сохраненного.
И последнее - есть
еще функции для управления
метафайлами, контейнерами и еще
несколько вспомогательных. Их не будет в
дальнейших примерах, я ими не пользовался
никогда, и думаю, что если они кому нужны - эти
люди способны сами разобраться.
Глава 3.
Цвета.
Прежде чем начать
рисовать, надо бы разобраться с цветами
и их использованием. Мы будем
рассматривать только вопросы
определения и выбора цветов в
компьютерных цветовых
пространствах, и не будем касаться
биолого-художественных аспектов.
Хотя есть несколько моментов, которые
необходимо знать и помнить:
1. восприятие
цветов у каждого человека
индивидуально.
2. Каждый
монитор/принтер/и т.д. показывают один и тот
же цвет (с точки зрения цифр) по-разному. Более
того, большинство мониторов
показывают один и тот же цвет по-разному в
разных частях экрана.
Цветовые
пространства
Я думаю все знают
известный постулат - любой цвет можно
получить смешением трех, так называемых,
основных. Поправка первая: "любой цвет" - это
любой из палитры, воспринимаемой
человеческим глазом.
Устоявшееся мнение гласит, что
человеческий глаз различает всего
около 16 миллионов цветов. Существуют
многие несогласные с этим, но
большинство работает именно с 2^24
цветами, и мы будем рассматривать именно
такие пространства. Однако, надо
знать, что существуют цветовые
пространства построенные на 4
цветах (например, CMYK) и на 6 цветах. Впрочем, 4-х
цветные нашли применение только в
полиграфии, а с 6-ти цветными я
сталкивался только в теории, и не знаю где они
применяются.
Итак, трех-осевые
цветовые пространства, самое
известное из них - RGB - Red (Красный) Green
(Зеленый) Blue (Синий).
Используется в ЭЛТ мониторах, во многих
принтерах и во многих форматах файлов.
Смешивая эти три основные цвета в разных
пропорциях можно получить "любой" цвет.
Отведя по 8 бит на цвет мы получаем 2^24 = 16777216
цветов, что, как принято, описывает все
цвета, воспринимаемые
человеческим глазом. Все довольны.
Второе по
популярности пространство - HSB
(HSL/HSV) - Hue (Цветность) Saturation
(Насыщенность) Brightness (Яркость) /
Lightness (Освещенность) / Value
(Значение). Используется
практически во всех графических
редакторах. Кстати, в стандартном
диалоге выбора цвета Windows
используется именно это
пространство:
В данном случае все
разрисовано в форме квадрата, что
неверно. Hue - это параметр, обозначающий
угол на цветовом круге. Справка: цветовой круг
Гете, самый известный, при всей своей
правильности не был принят в
технологическом мире. Однако,
именно он послужил основой для создания
пространства HSB. Более правильная
форма выбора цвета в пространстве
HSB такая:

C
пространством HSB связана
наприятная проблема - параметр Hue
должен принимать значения от 0 до 360, что никак
не укладывается в нормальную
двоичную систему записи данных... Да и для
Saturation и Brightness единого
мнения нет - кто-то считает что значения
должны быть от 0 до 100, кто-то от 0 до 1, кто-то от 0 до 240...
Поэтому, несмотря на то, что работать многие
предпочитают в нем, сохранение данных
ведется в RGB, благо оба пространства
взаимоконвертируемые, хотя тут есть
несколько ловушек, об этом ниже.
Каждая ось любого
цветового пространства также
называется каналом - красный канал, или
канал красного и т.п.
Существует еще около 10-15
цветовых пространств, но среди Windows
программистов они очень мало
распространены и мы их
рассматривать не будем.
Битность цвета
Битность цвета -
параметр, определяющий сколько бит
памяти приходится на цвет каждого
пикселя. Если вдруг кто не знает: пиксель -
минимальная единица площади
экрана/растрового рисунка, т.е.
точка.
Я уже упоминал, что на
каждый из трех основных цветов выделили по 8
бит и всем стало хорошо. Однако, это
случилось не так давно, а до этого выделять по 3
байта на один пиксель было
непозволительной роскошью. История
развития примерно такая:
1. Монохромы - один
цвет. Может кто помнит, были такие мониторы,
которые показывали все
исключительно ядовито-зеленым
цветом.
2. 4-х цветные. Была
такая вещь, однако долго не прожила,
поскольку ее быстро сменили.
3. 16-ти цветные. Это уже
начало нормального цвета в
компьютере. На каждый пиксель выделялось
4 бита, они описывали один из 16 известных
компу цветов. Но, как выяснилось позже, это
тоже было временно - мониторы стали
показывать все 16 миллионов, а компы не могли
столько выдать одновременно... И тогда
придумали выход.
4. 256 цветов. На каждый
пиксель выделялся байт памяти, который
мог описать цвет. Но тогда же появилась идея палитр -
быйт памяти определял номер цвета в
палитре, а сама палитра в 256 цветов
выбиралась из полного набора.
5. Дальше все просто - с
ростом компьютерной мощности и объемов
памяти появился 16 битный цвет - 65535 цветов,
никаких палитр, и выглядит все вполне
пристойно. Потом 24 бита - 16 миллионов
цветов, с которыми сейчас все и
работают.
6. 32 и 48 бит: 48 бит я пока в
деле не видел, однако он есть, под него есть карты
и т.д. 32 бита - имеет два применения, во-первых
для 4-х цветных пространств, а во-вторых для
поддержки прозрачности, об этом чуть
ниже.
Прозрачность
Сначала прозрачность
появилась в битовой форме - т.е. пиксель или
полностью прозрачный или цветной.
Прозрачность в gif сделана именно так. Потом уже
появилась градационная прозрачность. Для
нее сделали 256 градаций, т.е. выделили
еще байт на хранение прозрачности пикселя.
Байт, хранящий прозрачность получил
название альфа. Добавление альфа
канала не создает дополнительной оси
пространства. Это не компонент цвета в
прямом смысле, - это параметр,
показывающий, в какой пропорции
надо смешивать этот цвет, с лежащим "ниже".
Среди распространенных форматов
файлов только png поддерживает
альфа-канал.
К слову сказать,
Windows до сих пор умеет нормально работать
только с битовой прозрачностью, говорят
Vista научилась работать с
градационной, но это мы посмотрим после
релиза.
Форматы цветов
Итак, совмещая
вышеописанное в разных комбинациях
мы получаем форматы цветов/цветности.
Например, один из самых сейчас
распространенных форматов - 32bppARGB:
32 бита на пиксель, цветовое
пространство RGB с поддержкой
альфа-канала. Форматов существует
множество, но все они понятны из названия, я
перечислю и опишу только те, которые
используются в .NET 2.0 и содержаться в
перечислении
System.Drawing.Imaging.PixelFormat:
Alpha - каждый
пиксель содержит только альфа-канал. 8бит.
Canonical - 32 бита,
ARGB. Значения в RGB каналах не
изменены.
DontCare - не
указывать формат.
Extended - не
используется.
Format16bppArgb1555 - 16 бит
на пиксель, 1 бит на прозрачность и по 5 бит на
цветовые каналы. Таким образом
получаем 32768 цветов и битовую
прозрачность.
Format16bppRgb555 - 16 бит. по
5 бит на цветовой канал, 1 бит не
используется. Те же 32768 цветов, но без
прозрачности.
Format16bppRgb565 - 16 бит, по
5 бит на красный и синий и 6 бит на зеленый.
Получаем 65536 цветов.
Format24bppRgb - 24 бита,
по 8 на каждый цвет. Самый
распространенный сейчас формат без
прозрачности.
Format32bppArgb - 32
бита, по 8 на каждый цвет и 8 на альфа-канал. Самый
распространенный сейчас формат с
прозрачностью.
Format32bppPArgb - То же, что
и предыдущий, но значения цветовых
каналов преобразованы в
соответствии с альфа
значением.
Format32bppRgb - 32 бита,
по 8 на канал и 8 не используются.
Format48bppRgb - 48 бит, по 16
на каждый канал.
Format64bppArgb - 64
бита, по 16 на цветовой канал, и 16 на
прозрачность.
Format64bppPArgb - то же, что
предыдущий, но значения в цветовых
канал преобразованы в
соответствии с альфа
значением.
Format1bppIndexed -
индексированные цвета, т.е.
завязанные на палитру. Палитра из двух
цветов. 1 бит на пиксель.
Format4bppIndexed - 4
бита, палитра из 16 цветов.
Format8bppIndexed - 8 бит,
палитра из 256 цветов.
Format16bppGrayScale - 65536
градаций серого.
Лично я пользовался
только 24bppRGB, 32bppARGB, 8bppIndexed и
16bppGrayScale. При создании масок в
графических редакторах часто
используют однобитовые форматы,
т.е. Alpha вполне может понадобиться.
Остальные, на мой взгляд, оставлены для
совместимости со старыми форматами
и с будущими форматами.
Использование цветов в
.NET
Для работы с
цветами есть класс Color. Он содержит набор
статических свойств, которые
определяют распространенные
цвета. Например, если вам нужен красный цвет,
проще всего получить его так:
Color красный =
Color.Red;
Для нестандартных
цветов есть функция FromArgb():
Color странныйЦвет =
Color.FromArgb(255, 120, 12, 211);
Есть еще две
статические функции для создания
цвета:
FromKnownColor -
создает цвет из списка известных цветов, т.е.
из перечисления KnownColor, в
которое входят все стандартные цвета и все
системные цвета.
FromName - создает
цвет из строки с именем цвета.
Помимо
перечисления KnowColor есть еще
перечисление SystemColors, которое
содержит только системные цвета - цвета
рамок, кнопок, области окна и пр.
У класса Color есть еще не
статические члены:
свойства A, R, G, B -
возвращают соответствующую
компоненту цвета.
свойства
IsKnownColor, IsNamedColor,
IsSystemColor - проверяют,
является ли цвет "известным", названным и
системным соответственно.
свойство Name -
возвращает имя цвета
ToKnownColor -
возвращает член перечисления
KnownColor.
GetHue,
GetSaturation, GetBrightness -
возвращают значения цвета для осей Hue,
Saturation и Brightness
пространства HSB.
свойство IsEmpty -
проверяет был ли цвет
инициализирован.
ToArgb -
возвращает Int32.
Использование
пространства HSB
Частая ситуация для
графических программ - надо подсветлить
изображение, или понизить
контрастность, или инвертировать
цветность. Такие вопросы легко решаются
через HSB пространство, однако очень
сложно решаются через RGB. Почему в
классе Color есть возможность получить
значения HSB, но нет возможность задать их - вещь
необъяснимая ни чем, кроме скудоумия
проектировщиков MS, и отсутствия у
них опыта работы с графикой.
Сделано множество
классов, позволяющих работать с цветами
в .NET нормально, т.е. используя оба
пространства - RGB и HSB. Вот ссылки на два из
них:
Для
интересующихся вопросом серьезнее,
и знающих английский, вот еще две хорошие
ссылки:
Глава 4.
Карандаши.
Наконец-то приступаем
к рисованию. Думаю, ни для кого не новость, что
основными инструментами
рисования являются карандаши и
кисти. В .net дело обстоит так же - класс Pen
описывает карандаши, классы,
порожденные от Brush - кисти.
Карандаши
Для описания
стандартных карандашей есть
перечисление Pens. Оно содержит простые
карандаши, толщиной 1, для всех
стандартных цветов.
private void
panel1_Paint(object sender,
PaintEventArgs e) {
e.Graphics.DrawRectangle(Pens.Blue,
new Rectangle(10, 50, 100, 100));
}
Весь дальнейший код
рисования буду писать без имени функции
- он всегда в event panel1_Paint.
Разумеется карандаши
позволяют гораздо больше, нежели
рисовать простые линии. Рассмотрим
свойства класса Pen:
Color - цвет, если
карандаш одноцветный.
Brush - кисть,
определяющия способ заливки линий,
нарисованнх карандашом, если он не
одноцветный.
Width - толщина
карандаша.
PenType - тип
карандаша, на деле - тип заливки.
Свойство определяется типом Brush,
переданной карандашу.
Pen p1 = new Pen(Color.Red,
5);
Pen p2 = new Pen(new
HatchBrush(HatchStyle.SolidDiamond,
Color.Yellow, Color.HotPink), 10);
Pen p3 = new Pen(new
LinearGradientBrush(new
Point(10, 70), new Point(150, 70), Color.Indigo,
Color.IndianRed), 15);
e.Graphics.DrawLine(p1, 10, 10, 150,
10);
e.Graphics.DrawLine(p2, 10, 40, 150,
40);
e.Graphics.DrawLine(p3, 10, 70, 150,
70);
DashStyle - стиль линии.
варианты: Solid - сплошная, Dash -
пунктирная, DashDot - тире-точка,
DashDotDot - тире-точка-точка, Dot - точки, и
Custom - задается пользователем.
Если выбран Custom, то для определения
стиля используются значения в
следующем свойстве.
DashPattern - рисунок
линии - массив дробных чисел описывающий
длину штришков и пропусков.
DashOffset - расстоянии от
начала линии, с которого линия
становиться не сплошной, а штришками.
DashCap - оконечности
штришков. Варинаты: Flat - обычный
(квадратный), Round - скругленный,
Triangle - треугольником.
Pen p1 = new Pen(Color.Red,
15);
p1.DashStyle =
DashStyle.DashDot;
Pen p2 = new Pen(Color.Black,
15);
p2.DashStyle =
DashStyle.DashDotDot;
p2.DashOffset = 30;
p2.DashCap =
DashCap.Round;
Pen p3 = new
Pen(Color.Magenta, 15);
p3.DashStyle =
DashStyle.Custom;
p3.DashCap =
DashCap.Triangle;
p3.DashPattern = new float[4]
{5,3,10,3};
e.Graphics.DrawLine(p1, 10, 10, 300,
10);
e.Graphics.DrawLine(p2, 10, 40, 300,
40);
e.Graphics.DrawLine(p3, 10, 70, 300,
70);
CompoundArray - массив
дробных значений для определения
нескольких параллельных линий,
значения определяют отметки границ
линий и пропусков в долях единицы, первое
значение - 0, последнее - 1.
Pen p1 = new Pen(Color.Red,
15);
p1.CompoundArray = new float[] {
0.0f, 0.3f, 0.5f, 1.0f };
e.Graphics.DrawLine(p1, 10, 10, 300,
10);
StartCap - оконечность
линии в начале. Варианты: Flat -
простой, Round - скругленный, Square -
квадратный, Triangle - треугольный,
ArrowAnchor - стрелка с якорем,
DiamondAnchor - ромб с якорем,
RoundAnchor - скругленный с якорем,
SquareAnchor - квадратный с якорем,
Custom - определяется
пользователем. Если задан Custom, то в
качестве оконечности
используется CustomStartCap.
EndCap - оконечность
линии в конце. Варианты те же. В случае
Custom - используется
CustomEndCap.
CustomStartCap - личная
оконечность линии, создается из
GraphicsPath.
CustomEndCap - личная
оконечность линии, создается из
GraphicsPath.
Pen p1 = new Pen(Color.Red,
10);
p1.StartCap =
LineCap.ArrowAnchor;
p1.EndCap =
LineCap.DiamondAnchor;
Pen p2 = new Pen(Color.Black,
10);
p2.StartCap =
LineCap.Round;
p2.EndCap =
LineCap.RoundAnchor;
Pen p3 = new
Pen(Color.Magenta, 10);
p3.StartCap =
LineCap.Square;
p3.EndCap =
LineCap.SquareAnchor;
Pen p4 = new
Pen(Color.LightGreen, 10);
p4.StartCap =
LineCap.Triangle;
p4.EndCap =
LineCap.Flat;
e.Graphics.DrawLine(p1, 10, 10, 300,
10);
e.Graphics.DrawLine(p2, 10, 40, 300,
40);
e.Graphics.DrawLine(p3, 10, 70, 300,
70);
e.Graphics.DrawLine(p4, 10, 100, 300,
100);
Как видно из рисунка,
якорь - это увеличенная часть линии,
предназначенная для предоставления
пользователю возможности
перетаскивания концов линии.
Alignment -
выравнивание карандаша. Весьма
глючная вещь. Из 5 вариантов работают
только 2 - Centered и Inset, причем Inset
имеет ряд ограничений и глюков.
Pen p1 = new Pen(Color.Red,
10);
Pen p2 = new Pen(Color.Black,
10);
p2.Alignment =
PenAlignment.Inset;
Pen p3 = new
Pen(Color.White, 1);
e.Graphics.DrawRectangle(p1, new
Rectangle(10, 50, 100, 100));
e.Graphics.DrawRectangle(p2, new
Rectangle(10, 50, 100, 100));
e.Graphics.DrawRectangle(p3, new
Rectangle(10, 50, 100, 100));
Как можно видеть - при
стандартном значении, толщина
карандаша распределяется
поровну, по обе стороны от центральной
линии, которая отрисовывается
карандашом с единичной толщиной. В
случае Inset - вся толщина помещается
внутри линии, описанной единичной
толщины карандашом. На деле, при сложных
фигурах, при значении Inset карандаш
иногда вылезает за центральную
линию.
LineJoin - -
определяет вид соединения линий.
Варианты: Bevel - срезает угол,
оставляет тупой конец; Round -
соединяет дугой; Miter - острый
конец или тупой в зависимости от
значения MiterLimit; MiterClipped -
острый угол или срезанный, в
зависимости от значения MiterLimit,
между двумя последними я разницы не
заметил.
MiterLimit -
определяет предельную толщину линии
в месте соединения двух отрезков.
Измеряется в долях единицы от
толщины линии.
Pen p1 = new Pen(Color.Red,
10);
p1.LineJoin =
LineJoin.Bevel;
e.Graphics.DrawLines(p1, new
Point[] { new Point(10, 10), new Point(100, 30), new
Point(10, 60)});
p1.LineJoin =
LineJoin.Miter;
e.Graphics.DrawLines(p1, new
Point[] { new Point(110, 10), new Point(200, 30), new
Point(110, 60) });
e.Graphics.SmoothingMode =
SmoothingMode.HighQuality;
p1.LineJoin =
LineJoin.Round;
e.Graphics.DrawLines(p1, new
Point[] { new Point(10, 70), new Point(100, 100), new
Point(10, 130) });
p1.LineJoin =
LineJoin.MiterClipped;
p1.MiterLimit = 0.5f;
e.Graphics.DrawLines(p1, new
Point[] { new Point(110, 70), new Point(200, 100), new
Point(110, 130) });
Transform - матрица
преобразований. И есть еще набор
стандартных функций для
трансформаций. Это мы рассмотрим в части,
посвященной матричным
преобразованиям.
Глава 5.
Кисти.
Теперь рассмотрим
кисти. Кистью можно заливать любую
закрытую фигуру, а также текст или то, что
рисует карандаш. В отличии от
карандашей, кистей в .net много (5), и
работать с ними приходится по разному.
Собственно сам класс Brush является
abstract, так что им самим пользоваться
нельзя. Рассмотрим 5 порожденных от него
классов, готовых к использованию.
Простые (сплошные)
кисти
Класс
SolidBrush.
Самый простой вариант
- однотонная кисть. Как и для карандашей,
существует перечисление
однотонных кистей - Brushes, описывающее
однотонные кисти всех известных цветов,
и перечисление SystemBrushes,
описывающее кисти системных цветов. Если
вам нужен нестандартный цвет - можно создать
свою кисть, передав цвет как аргумент в
конструктор.
private void
panel1_Paint(object sender,
PaintEventArgs e) {
Brush br = Brushes.Red;
e.Graphics.FillRectangle(br, 10, 10,
50, 50);
br =
SystemBrushes.ButtonHighlight;
e.Graphics.FillRectangle(br, 70, 10,
50, 50);
br = new
SolidBrush(Color.FromArgb(199, 157, 71));
e.Graphics.FillRectangle(br, 130, 10,
50, 50);
}
Дальнейший код буду писать
без имени функции - он всегда в
panel1_Paint.
Кисти со
штриховкой
Класс
HatchBrush.
Существует
перечисление HatchStyle,
содержащее 54 известных системе
штриховки. Соответственно, класс
HatchBrush позволяет создать кисть с
одной из этих 54 штриховок, и любыми цветами
фона и штриховки.
Свойство класса Graphics
RenderingOrigin позволяет
изменить положение начала
штриховки.
HatchBrush br = new
HatchBrush(HatchStyle.SolidDiamond,
Color.Red, Color.White);
e.Graphics.FillRectangle(br, 10, 10,
50, 50);
e.Graphics.RenderingOrigin = new
Point(1, 1);
e.Graphics.FillRectangle(br, 70, 10,
50, 50);
br = new
HatchBrush(HatchStyle.Sphere,
Color.White, Color.Blue);
e.Graphics.FillRectangle(br, 130, 10,
50, 50);
Текстурированная кисть
Класс
TextureBrush.
Позволяет залить форму
изображением. Изображение
представлено классом Bitmap, и может быть как
создано программой, так и загружено из
файла.
Особое свойство -
WrapMode - описывает, как именно будет
заполняться форма. Варианты: Clamp -
изображение рисуется один раз, Tile -
изображение размножается, чтобы
заполнить всю форму, TileFlipX -
изображение отражается
относительно горизонтальной оси, при
каждом следующем использовании,
TileFlipY - аналогично,
относительно вертикальной оси,
TileFlipXY - аналогично,
относительно горизонтальной,
потом вертикальной оси.
TextureBrush br = new
TextureBrush(Bitmap.FromFile("smile.png"),
WrapMode.Clamp);
e.Graphics.FillRectangle(br, 10, 10,
50, 50);
br.WrapMode =
WrapMode.Tile;
e.Graphics.FillRectangle(br, 70, 10,
50, 50);
br.WrapMode =
WrapMode.TileFlipY;
e.Graphics.FillRectangle(br, 130, 10,
50, 50);
Обратите внимание,
заливка рассчитывается от левого
верхнего угла контрола (панели, в
данном случае). Так что, если вы хотите, чтобы
смайлик влез целиком - вам надо подвинуть кисть,
для этого и придуманы координатные
преобразования. В данном случае, надо бы
дописать такую строчку после
конструктора:
br.TranslateTransform(9,10);
Линейный
градиент
Класс
LinearGradientBrush.
В общем виде
система такая - задаются точки с
координатами и цветом, и система
рассчитывает цвета для всех остальных
пикселей. Если нужен градиентный
переход от одного цвета к другому - то все
просто, прямо в конструкторе задете
две точки, два цвета и кисть готова. Вместо двух
точек можете дать прямоугольник.
Если нужен переход
между несколькими цветами - это чуть сложнее.
Задаете набор позиций и цветов в
свойстве InterpolationColors.
У этой кисти тоже есть
свойство WrapMode, аналогично
предыдущей.
Свойство Blend -
позволяет задать параметры
пропорций смешения цветов, чтобы,
например, два цвета смешивались не
равномерно, а со смещением.
Функции
SetSigmaBellShape и
SetBlendTriangularShape -
позволяют установить одну из двух схем
расчета градиента. Вторая - по
умолчанию. Аргументы функций: focus
- задает точку (в долях единицы) в
которой градиент заканчивается;
scale - задает "скорость" изменения
цвета (1 - по умолчанию).
Свойство
LinearColors - позволяет менять два
крайних цвета.
Свойство Rectangle -
позволяет получить прямоугольник, для
которого рассчитывается
градиент.
LinearGradientBrush br = new
LinearGradientBrush(new
Point(10,10), new Point(60,60), Color.Red,
Color.Blue);
e.Graphics.FillRectangle(br, 10, 10,
50, 50);
br = new
LinearGradientBrush(new
Point(70, 10), new Point(120, 60), Color.Red,
Color.Blue);
Blend blnd = new Blend();
blnd.Factors = new float[] { 1f,
0.8f, 0.0f };
blnd.Positions = new float[] {
0f, 0.6f, 1f };
br.Blend = blnd;
e.Graphics.FillRectangle(br, 70, 10,
50, 50);
br = new
LinearGradientBrush(new
Point(130, 10), new Point(180, 60), Color.Red,
Color.Blue);
br.SetSigmaBellShape(1);
e.Graphics.FillRectangle(br, 130, 10,
50, 50);
Для многоцветных
градиентов есть несколько
ограничений - Blend не работает для
InterpolationColors,
SetSigmaBellShape и
SetBlendTriangularShape -
обнуляют установленные
InterpolationColors.
LinearGradientBrush br = new
LinearGradientBrush(new
Point(10,10), new Point(60,60), Color.Red,
Color.Blue);
ColorBlend intColors =
new ColorBlend();
intColors.Positions = new
float[] { 0f, 0.5f, 1f };
intColors.Colors = new Color[] {
Color.Red, Color.Green, Color.Blue };
br.InterpolationColors =
intColors;
e.Graphics.FillRectangle(br, 10, 10,
50, 50);
br = new
LinearGradientBrush(new
Point(70, 10), new Point(120, 60), Color.Red,
Color.Blue);
intColors = new
ColorBlend();
intColors.Positions = new
float[] { 0f, 0.2f, 0.4f, 0.6f, 0.8f, 1f };
intColors.Colors = new Color[] {
Color.Red, Color.Orange, Color.Yellow,
Color.Green, Color.Cyan, Color.Blue };
br.InterpolationColors =
intColors;
e.Graphics.FillRectangle(br, 70, 10,
50, 50);
br = new
LinearGradientBrush(new
Point(130, 10), new Point(180, 60), Color.Red,
Color.Blue);
br.SetSigmaBellShape(1, 0.7f);
e.Graphics.FillRectangle(br, 130, 10,
50, 50);

Градиенты сложной
формы
Класс
PathGradientBrush.
Этот класс позволяет
создавать градиенты сложной формы. Кисть
создается по GraphicsPath, который
может быть любой формы. Однако, из опыта, не
рекомендуется подсовывать очень
сложные формы с многими разобщенными
кусками. Кисть исходит из одной
замкнутой формы.
Для создания
градиента в этом классе есть два пути -
первый, создание многоцветного
градиента, расходящегося из
центральной точки и повторяющего
очертания формы. Для этого используется
свойство InterpolationColors - для
задания массива цветов, и свойство
CenterPoint - для смещения центральной
точки, если надо.
Второй путь -
присваивание цвета каждому углу формы и
центру. Для этого используется
свойство SurroundingColors для
задания цветов в углах, и свойство
CenterColor для задания
центрального цвета.
У этой кисти также есть
свойства Blend и WrapMode и функции
SetSigmaBellShape и
SetBlendTriangularShape,
аналогичные предыдущей.
Свойство
FocusScales позволяет расширить
зону цвета в центральной точке, значения
от 0 до 1, по умолчанию - 0.
GraphicsPath gp = new
GraphicsPath();
gp.AddRectangle(new
Rectangle(10, 10, 50, 50));
PathGradientBrush br = new
PathGradientBrush(gp);
ColorBlend intColors =
new ColorBlend();
intColors.Positions = new
float[] { 0f, 0.5f, 1f };
intColors.Colors = new Color[] {
Color.Red, Color.Green, Color.Blue };
br.InterpolationColors =
intColors;
e.Graphics.FillPath(br, gp);
gp.Reset();
gp.AddRectangle(new
Rectangle(70, 10, 50, 50));
br = new
PathGradientBrush(gp);
br.SurroundColors = new Color[]
{Color.Red, Color.Green, Color.Blue, Color.Black
};
br.CenterColor =
Color.White;
br.CenterPoint = new PointF(105,
35);
e.Graphics.FillPath(br, gp);
gp.Reset();
gp.AddEllipse(130, 10, 50, 50);
br = new
PathGradientBrush(gp);
intColors = new
ColorBlend();
intColors.Positions = new
float[] { 0f, 0.2f, 0.4f, 0.6f, 0.8f, 1f };
intColors.Colors = new Color[] {
Color.Red, Color.Orange, Color.Yellow,
Color.Green, Color.Cyan, Color.Blue };
br.InterpolationColors =
intColors;
br.FocusScales = new
PointF(0.3f, 0);
e.Graphics.FillPath(br,
gp);

Глава 6.
Геометрические
элементы.
Рассмотрим те фигуры,
элементы и прочие плоские объекты,
которыми можно наполнять свое
изображение в GDI+.
Простые фигуры
Простых фигур всего
несколько, но, как показывает практика,
зачастую только они и нужны. Рассказывать о
них особо нечего - все это есть в учебниках
геометрии :). К простым фигурам
относятся Прямоугольник
(Rectangle), Эллипс (Ellipse), Сектор (Pie).
Задаются они координатами
верхнего-левого угла и шириной и
высотой. Для сектора указывается еще
угол начала и величина угла сектора. К
чуть более сложным фигурам относятся
Полигон (Polygon) и Замкнутая Кривая
(Closed Curve). Эти задаются массивом
угловых точек. Для кривой дополнительно
указывается кривизна.
Элементы
Кроме готовых фигур
есть возможность рисовать элементы фигур. Тут
тоже все просто. Прямая (Line), Кривая (Curve),
Дуга (Arc) и Кривая Безье (Bezier). Прямая
задается по двум точкам, кривая - по набору
точек и кривизне, дуга по
прямоугольнику, описывающему
эллипс, начальному углу и углу дуги,
безьера - по 4 точкам. Если вдруг кто не знает что
такое безьера - пара ссылок:
Пути
Графические пути (Graphics
Path) - по сути, представляют собой набор
закрытых фигур. В путь можно добавлять
целиком фигуры, строки (текст) или составлять
фигуру из элементов прямо в пути. Для всех этих
операций есть соответствующие
функции:
AddEllipse - добавляет
эллипс и т.п.
AddLine - добавляет
прямую и т.п.
Помните только об одном -
если вы добавляете элементы, они
добавляются к текущей фигуре. Если
вы добавляете фигуру - она закрывает
текущую фигуру и добавляется. Для
управления открытыми фигурами
есть две функции:
StartFigure -
закрывает текущую фигуру и
открывает новую. Все добавляемые
элементы дописываются к новой
фигуре.
CloseFigure -
закрывает текущую фигуру.
Обработка пути
происходит быстрее, чем рисовать каждую
фигуру отдельно. В зависимости от
сложности пути, значения
преимущества получаются разные, но
для сложных многокомпонентных
построений - раз в 10 пути быстрее.
Регионы
Region - особая фича. Это,
по сути, набор активных пикселей.
Соответственно, никаких
фигур/элементов не содержит, хотя и может
быть создан из прямоугольника или пути.
Основное использование - управление
битовой прозрачностью. Я, например,
часто использую набор регионов для сложных
многослойных рисований, чтобы
заранее определить область
рисования для каждого слоя - это экономит
ресурсы при прорисовке, и
гарантирует, что заливка одного слоя не
вылезет на другой. Обработка регионов
происходит значительно быстрее, чем
обработка путей. Совсем недавно сравнил
- прорисовка сложной картинки, состоящей
из нескольких сотен замкнутых кривых
сложной формы; сначала были созданы
кривые, потом они вводились в графический
путь, а потом либо рисовались из пути, либо
создавался регион из пути, и
заливался он. Заливка региона
занимала в, примерно, 1000 раз меньше
времени.
Текст
Рисование текста
выполняется через функцию
DrawString. В качестве аргументов
передаются строка, шрифт, кисть для заливки и
положение. Возможные варианты -
можно передавать координаты
верхнего-левого угла, а можно
передавать прямоугольник, в который
надо поместить текст, тогда текст не будет
превышать прямоугольник по ширине.
Шрифт можно, как обычно,
взять из перечисления SystemFonts, а можно
создать самим. При создании шрифта надо
указывать семейство и размер, а также
можно уточнить - в чем именно вы указали размер,
определить дополнительные стили
шрифта (курсив, например), можно указать
набор символов GDI, который надо
использовать, а также сделать шрифт
вертикальным. Самое главное тут -
семейство шрифта, его можно указать через
строковое название, а можно выбрать из
массива FontFamily.Families.
Еще одно - часто
бывает надо определить размеры
текста, когда он будет нарисован. Для
этого есть две функции в классе Graphics:
MeasureString и
MeasureCharacterRanges. Первая
вычисляет прямоугольник (структуру
SizeF) для данных строки и шрифта. При
необходимости, этой функции можно
тоже указать прямоугольник, в котором
должен быть размещен текст, тогда ширина
сохраниться, а необходимую высоту
вычислят. MeasureCharacterRanges
возвращает массив Region'ов. Расчет
занимаемого пространства
проводиться по-разному, и если ваша
программа должна быть очень точна в
размещении текста на
экране/принтере и пр., то вам придется
использовать обе функции. Но в обычных
программах нужна только первая. Она удобнее для
использования, быстрее работает, а
то, что она бывает не слишком точна - так это или почти
не заметно, или легко компенсировать. Она
если и ошибается, то только в меньшую
сторону, а значит, достаточно
предусмотреть запас пространства в
3-4 пикселя, и все будет в порядке.
Глава 7. Матричные
трансформации.
Многое из
ниженаписанного является
вольным переводом/пересказом
материалов с этого сайта -
http://www.bobpowell.net/.
Системы
координат
Если коротко - в GDI+
существует набор двухмерных
координатных систем. Ось X направлена
слева-направо, ось Y сверху-вниз.
Координатных систем три:
World
coordinate space - мировая
система координат. Та система, в
которой вы работаете. Единицы
измерения - дробные числа, мало что
означающие для всех, кроме вас :).
Page
Coordinate Space - система
координат страницы. Единицами
измерения могут являться разные
величины - пиксели, дюймы,
миллиметры и пр. Вы устанавливаете
коэффициент перехода между
числами из мировой системы и системой
координат страницы через параметр
PageScale объекта Graphics. Так ваши
числа превращаются в пиксели, дюймы и
пр.
Device
Coordinate Space - система
координат устройства. Это
недоступная программистам область,
контролируется системой и
драйверами производителей
оборудования. На этом уровне дюймы и пр.
превращаются в реальные точки - зерна
экрана, точки (капли) краски и т.д.
В сумме, набор таких
систем координат позволяет говорить
о GDI+ как о "независимой от разрешения"
системе рисования. Заданная толщина
в миллиметр будет миллиметром как на
мониторе, так и на принтере, так и на
плоттере и т.д. Ну, это в идеале.
Единицы измерения
системы координат страницы:
Pixel - пиксель, он и в
Африке пиксель. Но надо помнить, что размер
пикселя разный, на разных
устройствах. Т.е. ваш рисунок может
выглядеть по-разному на экране и на
принтере.
Millimeter -
миллиметр. Выглядит везде
одинаково.
Inch - дюйм.
Выглядит везде одинаково.
Point - поинт или
точка. 1/72 дюйма. Единица измерения
шрифта в типографиях. Выглядит везде
одинаково.
Display - 1/75 дюйма.
Когда-то - величина зерна ЭЛТ мониторов.
Выглядит везде одинаково.
Document - 1/300 дюйма.
Стандартное разрешение лазерных
принтеров. Одинаковость не
гарантируется.
World - должно быть
то же, что и пиксель. Но на практике выдает кучу
ошибок - лучше не использовать.
Матричные
трансформации
Зачем они вообще нужны
- затем, чтобы не писать вот такие вот длинные
строчки:
DrawLine(myPen,(panX+x1)*zoom,(panY+y1)*zoom,(panX+x2)*zoom,(panY+y2)*zoom);
Умножение на
коэффициент увеличения - 4 лишние
операции, да и читаемость кода резко
ухудшается.
Все управление
трансформациями ведется через
свойство Transform (класс Matrix), объекта
Graphics.
С
трансформациями все довольно просто, до
тех пор, пока трансформации простые. У класса
Graphics есть вспомогательные функции -
TranslateTransform и пр. для
проведения простых трансформаций.
TranslateTransform -
переместить.
ScaleTransform -
масштабировать.
RotateTransform -
повернуть.
ResetTransform - сбросить
все трансформации.
Все трансформации
выполняются относительно
начала координат.
У класса Matrix есть
аналогичный набор функций.
Translate -
переместить.
Scale -
масштабировать.
Rotate - повернуть
относительно начала
координат.
RotateAt - повернуть
относительно точки.
Shear - исказить
(сделать параллелограм из
прямоугольника).
Reset - сбросить
трансформации.
Все эти функции, в
варианте для класса Matrix имеют
аргумент, позволяющий задать порядок
применения трансформаций.
MatrixOrder.Append - добавить
трансформацию в конец,
MatrixOrder.Prepend - поставить
трансформацию в начало.
Если нужно что-то
посложнее, придется вспомнить, как работать с
матрицами.
Итак, в основе лежит
матрица 2х3.
m11,m12
m21,m22
dx nbsp;,dy
В дальнейшем,
матрицу буду записывать в строку -
(m11,m12,m21,m22,dx,dy).
Итак, матрица без
трансформаций выглядит так: (1,0,0,1,0,0)
Каждая точка, перед тем,
как будет нарисована, умножается на
матрицу трансформаций.
x = x1*m11+y1*m12+dx
y = x1*m21+y1*m22+dy
В общем-то, основная
суть уже сказана :). Теперь базовые матрицы,
для стандартных трансформаций:
вдвое больше -
(2,0,0,2,0,0)
вдвое меньше -
(0.5,0,0,0.5,0,0)
перенести вправо на 10 и вниз на
5 - (1,0,0,1,10,5)
матрица поворота в
общем виде - (cosA, sinA, -sinA, cosA, 0,
0).
например, поворот на 30
градусов по часовой стрелке: (0.866, 0.5, -0.5, 0.866,
0, 0)
переворот оси Y -
(1,0,0,-1,0,0)
Примеры
использования:
увеличить вдвое
private void
panel1_Paint(object sender,
PaintEventArgs e) {
e.Graphics.FillEllipse(Brushes.Blue,
20, 30, 30, 20);
e.Graphics.Transform = new
Matrix(2, 0, 0, 2, 0, 0);
e.Graphics.FillEllipse(Brushes.Blue,
20, 30, 30, 20);
}
Того же эффекта можно
достичь, используя
e.Graphics.ScaleTransform(2,2);
переворот оси Y
private void
panel1_Paint(object sender,
PaintEventArgs e) {
e.Graphics.DrawString("строка", new
Font("Times New Roman", 24), Brushes.Black, 10, 10);
e.Graphics.Transform = new
Matrix(1, 0, 0, -1, 0, 120);
e.Graphics.DrawString("строка", new
Font("Times New Roman", 24), Brushes.Black, 10, 10);
}
Обратите внимание,
переворачивая ось Y, вы
располагаете область рисования за
верхним краем своего контрола,
поэтому необходимо еще и перемещать
ее по оси Y.
Глава 8.
Графические файлы.
Еще раз напоминаю -
все рисование производится в
объекте класса Graphics, следовательно -
результат рисования остается в том
объекте, от которого сделан объект
класса Graphics.
Класс Bitmap
В общем вид этот класс
сделан для хранения растровых
изображений. Вы можете создать объект
класса Bitmap, нарисовать в нем что-то, потом это
что-то показать пользователю,
распечатать, сохранить в файл и т.д.
Реально же такой подход
редко практикуется. Если надо что-то
показать пользователю - это обычно
рисуется в контроле, так как статичные
изображения нужны редко, а
динамические проще рисовать каждый раз
сразу в контроле, чем рисовать в памяти
в Bitmap, а потом выводить этот Bitmap в
контрол. И прочее так же - обычно
используется единый код рисования,
который может подстраиваться под тип
вывода (экран, принтер, файл), а собственно
класс Bitmap используется только для
загрузки/сохранения файлов.
Долго тут
рассказывать, на мой взгляд, нечего.
Рассмотрим код решения одной, но очень
распространенной, задачи.
Требуется:
1. загрузить
изображение одного из "родных" для .NET
форматов (jpeg, png, tiff, gif, bmp)
2. изменить
изображение согласно пожеланиям
пользователя (изменить размер/наложить
копирайт)
3. сохранить в один из
родных форматов, с использованием
параметров кодирования
//параметры, вводимые
пользователем
string
input_filename = "input.jpg";
string
output_filename = "output";
ImageFormat imft =
ImageFormat.Jpeg;
Size new_image_size =
Size.Empty;
new_image_size = new Size(640,
480);
string copyright = "(c)
2006 doci.nnm.ru/selfprogrammer";
long compress = 80;
Bitmap bmp_in = null;
Bitmap bmp_out;
if
(!new_image_size.IsEmpty) { //если новый размер
введен
bmp_in =
(Bitmap)Bitmap.FromFile(input_filename); //
считываем оригинал
bmp_out = new Bitmap(bmp_in,
new_image_size); // создаем копию нужного
размера
}
else { // если менять
размер не надо
bmp_out =
(Bitmap)Bitmap.FromFile(input_filename); //
считываем оригинал
}
if
(!String.IsNullOrEmpty(copyright)) {
// рисуем
копирайт
Graphics gr =
Graphics.FromImage(bmp_out);
gr.DrawString(copyright,
SystemFonts.CaptionFont, Brushes.White,
new_image_size.Width - 220, new_image_size.Height -
30);
gr.Dispose();
}
// сохраняем
ImageCodecInfo imcodec =
null;
EncoderParameters
encparams = null;
if (imft ==
ImageFormat.Jpeg) { // если jpeg - с
параметрами
imcodec =
GetEncoderInfo("image/jpeg");
encparams = new
EncoderParameters(1);
encparams.Param[0] = new
EncoderParameter(Encoder.Quality,
compress);
bmp_out.Save(output_filename +
".jpg", imcodec, encparams);
}
else if (imft ==
ImageFormat.Png) { // если png - по
умолчанию
bmp_out.Save(output_filename,
imft);
}
if (bmp_in != null) {
bmp_in.Dispose();
}
bmp_out.Dispose();
Параметры вводимые
пользователем - надо бы получать из
интерфейса, но мне лень было тут еще кнопки
воротить.
По поводу
сохранения в разных форматах, я
рассмотрел только 2. По образу и подобию
можно добавить и другие. Помните о таких
вещах - нормальная поддержка форматов в
.NET есть только для jpeg и bmp. Для Tiff - сжатие LZW не
поддерживается, хотя заявлено, для
Png - нет поддержки качества, хотя
заявлена. Так что, приходиться сохранять
по дефолту.
В jpeg у меня
сохраняется с использованием
Encoder'a, что необязательно - в jpeg можно
сохранять так же как у меня в png, если вам не
мешает, что качество будет
выбираться само.
Для изменения
размера нужно создавать новый Bitmap,
причем если нужно хорошее качество, то
надо создавать новый Bitmap нужного
размера, создавать для него Graphics,
задавать Graphics наилучшее качество
сглаживания/интерполяции и рисовать
старый Bitmap в новом через DrawImage.
Другие изменения
можно проводить прямо над загруженным
Bitmap, не создавая нового.
класс
Metafile
Не знаю, как он должен
использоваться - со своими
мета-возможностями, но я использовал, и
видел использование, только в
качестве файла векторной графики.
Есть некоторый глюк в
системе использования метафайлов,
но общий принцип такой же - создаете
метафайл, создаете из него Graphics,
рисуете, убиваете. Отличия от растра -
при рисовании все сразу пишется в файл,
использовать save не надо.
Пример кода:
string
output_filename = "output.emf";
EmfType emftype =
EmfType.EmfPlusDual;
Bitmap tmpbmp = new Bitmap(800,
600);
tmpbmp.SetResolution(300, 300);
Graphics gr1 =
Graphics.FromImage(tmpbmp);
IntPtr hdc = gr1.GetHdc();
Metafile vector_image = new
Metafile(output_filename, hdc,
emftype);
Graphics tgr =
Graphics.FromImage(vector_image);
tgr.Clear(Color.White);
Rectangle rect1 = new
Rectangle(10, 10, 150, 150);
tgr.FillRectangle(Brushes.Blue,
rect1);
tgr.DrawRectangle(Pens.Red,
rect1);
tgr.DrawString("some text",
SystemFonts.CaptionFont, Brushes.Black, 200,
200);
tgr.Dispose();
vector_image.Dispose();
gr1.Dispose();
tmpbmp.Dispose();
emftype - тип emf файла,
может быть Emf, EmfPlus и
EmfPlusDual.
Соответственно, emf и emfPlus -
разные версии формата, EmfPlusDual
- в файл каждая запись пишется в двух форматах
сразу, и в emf и в emfPlus.
Как видите, для
создания метафайла нужен указатель на
контекст устройства (Hdc), для получения
которого и городится весь огород с
созданием временного bitmap и
созданием Graphics из него - это
позволяет описать параметры
рисования (разрешения, систему
координат) для создания hdc и передачи
его метафайлу.
После исполнения
кода можете попробовать открыть файл в
любом редакторе векторной графики и
посмотреть что получилось. Некоторые
известные моменты: Corel Draw версии до 12
открывает emf в сгруппированном
виде - не забудьте сначала дать ungroup,
потом уже смотреть по-объектно. Тот же Corel до
последней версии неправильно
считывает единицы измерения
текста - в нем текст всегда отображается
очень-очень мелко и со смещенными
координатами.
Глава 9.
Примеры.
Первый пример
Создаем форму с
панелькой, на которой рисуем всякое
разное (многокомпонентный путь и текст в
рамке), подключаем автоскроллинг и
масштабирование: левая кнопка -
приблизить, правая - отдалить.
Конструктор формы, в нем
задаем путь, который будем рисовать в
первой панели и создаем матрицу
масштабирования.
public Form1() {
InitializeComponent();
gp = new
GraphicsPath();
gp.StartFigure();
gp.AddLine(5, 5, 50, 50);
gp.AddBezier(55, 55, 65, 35, 75, 35,
85, 55);
gp.CloseFigure();
gp.AddEllipse(5, 100, 70, 70);
gp.AddString("(c) 2006
doci.nnm.ru/selfprogrammer",
FontFamily.GenericSansSerif, 0, 12,
new Point(5, 180),
StringFormat.GenericDefault);
zoomMatrix = new Matrix(); //
матрица масштаба
}
Глобальные переменные
- путь, матрица масштаба,
коэффициент
масштабирования.
private GraphicsPath
gp;
private Matrix
zoomMatrix;
private float _zoom =
2;
private float zoom {
get { return _zoom; }
set {
_zoom = value;
zoomMatrix.Reset();
zoomMatrix.Scale(value,
value);
RectangleF rect =
gp.GetBounds(zoomMatrix); //
получаем размеры пути после
изменения масштаба
panel1.AutoScrollMinSize = new
Size((int)rect.Width, (int)rect.Height); //
устанавливаем область скроллинга
panel1.Invalidate(); //
обновляем панельку
}
}
private void
panel1_Paint(object sender,
PaintEventArgs e) {
e.Graphics.Transform =
zoomMatrix; // устанавливаем
текущий масштаб
e.Graphics.TranslateTransform(panel1.AutoScrollPosition.X,
panel1.AutoScrollPosition.Y); // и
текущее смещение
e.Graphics.FillPath(Brushes.Aqua,
gp); // рисуем путь
Font ft = new Font("Times New
Roman", 16); // создаем шрифт
SizeF sz =
e.Graphics.MeasureString("Текст в рамке", ft); //
замеряем строчку текста
e.Graphics.DrawString("Текст в
рамке", ft, Brushes.Navy, 5, 200); // рисуем текст
e.Graphics.DrawRectangle(Pens.Navy,
5, 200, sz.Width, sz.Height); // рисуем рамку
}
Осталось только
изменение масштаба на кнопку мыши
поставить:
private void
panel1_MouseUp(object sender,
MouseEventArgs e) {
if (e.Button ==
MouseButtons.Left) {
zoom += 0.2f;
}
else if (e.Button ==
MouseButtons.Right) {
zoom -= 0.2f;
}
}
Готово - можете
проверять. Должно получиться что-то
вроде такого:
Второй пример
Нарисуем прямоугольник
со скругленными углами, напишем
что-нибудь по одной стороне и пусть он у нас
сворачивается-разворачивается
по клику.
Сначала
нарисуем:
private void
panel2_Paint(object sender,
PaintEventArgs e) {
e.Graphics.FillRectangle(Brushes.Violet,
e.ClipRectangle);
int X = 5, Y = 5, height = 150,
radius = 10;
int width =
roundRectWidth;
GraphicsPath gp2 = new
GraphicsPath();
gp2.AddLine(X + radius, Y,
X + width - (radius * 2), Y);
gp2.AddArc(X + width -
(radius * 2), Y, radius * 2, radius * 2, 270,
90);
gp2.AddLine(X + width, Y +
radius, X + width, Y + height - (radius * 2));
gp2.AddArc(X + width -
(radius * 2), Y + height - (radius * 2),
radius * 2, radius * 2, 0, 90);
gp2.AddLine(X + width -
(radius * 2), Y + height, X + radius, Y +
height);
gp2.AddArc(X, Y + height -
(radius * 2), radius * 2, radius * 2, 90,
90);
gp2.AddLine(X, Y + height -
(radius * 2), X, Y + radius);
gp2.AddArc(X, Y, radius *
2, radius * 2, 180, 90);
gp2.CloseFigure();
e.Graphics.FillPath(Brushes.Aqua,
gp2);
e.Graphics.DrawPath(Pens.Navy,
gp2);
gp2.Dispose();
StringFormat sf = new
StringFormat(StringFormatFlags.DirectionVertical);
sf.Alignment =
StringAlignment.Center;
sf.LineAlignment =
StringAlignment.Center;
e.Graphics.DrawString("Sample", new
Font("Times New Roman", 18,
FontStyle.Underline,
GraphicsUnit.Pixel), Brushes.Black, new
RectangleF(5, 7, 20, 150), sf);
e.Graphics.DrawLine(Pens.Navy, 25, 5,
25, 155);
}
Код рисования
таких прямоугольников я честно(с)тырил с
Bob Powell.
Добавим эти две строчки в
конструктор формы:
roundRectWidth = 150;
roundRectDir =
true;
И
соответственные глобальные
переменные объявим:
private int
roundRectWidth;
private bool
roundRectDir;
Теперь зарядим
анимацию:
Добавим в форму
компонент timer, установим ему
interval=20 и пропишем такую функцию на
tick:
private void
timer1_Tick(object sender, EventArgs e) {
if
(roundRectDir) {
roundRectWidth -= 2;
if
(roundRectWidth ‹= 27) { timer1.Stop();
roundRectDir = false;
}
}
else {
roundRectWidth += 2;
if
(roundRectWidth ›= 150) {
timer1.Stop();
roundRectDir = true;
}
}
panel2.Invalidate(new
Rectangle(roundRectWidth-3, 0, 11,
160));
}
Думаю, все ясно - если
одно направление - уменьшаем ширину
прямоугольника, пока не кончится, тогда
выключаем таймер и меняем
направление. Если другое направление -
увеличиваем ширину, пока не
вырастет, как надо, тогда
останавливаем таймер и меняем
направление.
Последняя строчка -
перерисовка панели. Если вызвать
Invalidate без аргументов, будет
перерисовываться вся панель, и будет она
постоянно мерцать. Чтобы не мерцала -
перерисовываем только
изменившуюся часть - прямоугольник
изменений. Все равно мерцать будет, но меньше
и ресурсов меньше тратиться... а по уму - надо
проверять, что из рисуемого попадает в
область обновления, а что нет... А чтобы
вообще не мерцало - это надо свои
контролы писать и ставить там
DoubleBuffered, рисовать все
самому, убирать
paintBackground и пр.
И последнее - запуск
таймера:
private void
panel2_MouseClick(object sender,
MouseEventArgs e) {
if (!timer1.Enabled)
{
timer1.Start();
}
}
Можете проверять.
Должно выглядеть примерно так:

Приложения.
Примеры программ для
решения каких-то конкретных задач. Код с
комментариями. К каждому примеру
существует файл с архивом проекта для MS
Visual Studio 2005, скачать можно здесь:
http://www.robinland.com/csharp-basis/samples.zip.
1. Самодельный
MD5-хешер.
Итак, для начала
создаем окно. Такое примерно:
Как это делать думаю
пояснять не надо. Добавьте еще
OpenFileDialog в него, в котором
задайте в свойствах Filter = "Все файлы
(*.*)|*.*||".
Теперь описываем все
кнопки последовательно (в дизайнере
дважды щелкаем на кнопку - создается
функция обработки нажатия):
Открыть файл:
private void
button1_Click(object sender, EventArgs e) {
if
(openFileDialog1.ShowDialog(this) ==
DialogResult.OK) { // открываем окно
"Открыть файл" и если в нем нажали ОК
textBox1.Text =
openFileDialog1.FileName; // пишем имя
выбранного файла в первом текстовом
поле
CalculateHash(); // считаем
хеш. Функцию напишем позже
if
(System.IO.File.Exists(textBox1.Text + ".md5")) { //
проверяем есть ли файл
‹имя_файла_с_расширением›.md5
CompareHash(textBox1.Text + ".md5",
false); // проверяем хеш. Функцию
напишем позже
}
else if
(System.IO.File.Exists(Path.GetDirectoryName(textBox1.Text)
+ "\" +
Path.GetFileNameWithoutExtension(textBox1.Text)
+ ".md5")) { // проверяем есть ли файл ‹имя_файла›.md5
CompareHash(Path.GetDirectoryName(textBox1.Text)
+ "\" +
Path.GetFileNameWithoutExtension(textBox1.Text)
+ ".md5", false); // сверяем хеш
}
}
}
Сохранить файл:
private void
button2_Click(object sender, EventArgs e) {
if
(System.IO.File.Exists(textBox1.Text)
textBox2.Text != "") { // если файл выбран и хеш для
него посчитан
StreamWriter sw = new
StreamWriter(Path.GetDirectoryName(textBox1.Text)
+ "\" +
Path.GetFileNameWithoutExtension(textBox1.Text)
+ ".md5", false); // открываем файл с
перезаписью с тем же именем, но с
расширением md5
sw.WriteLine(textBox2.Text + " *" +
Path.GetFileName(textBox1.Text)); // пишем в
файл хеш и имя файла
sw.Close();
}
}
Открыть файл с
хешем:
private void
button3_Click(object sender, EventArgs e) {
if
(openFileDialog1.ShowDialog(this) ==
DialogResult.OK) { // открываем диалог
"Открыть файл" и если в нем нажали ОК
CompareHash(openFileDialog1.FileName,
true); // сверяем хеш
}
}
Теперь пишем основные
функции:
private void
CalculateHash() { // посчитать хеш
if
(System.IO.File.Exists(textBox1.Text)) { // если файл
существует
FileStream fs =
System.IO.File.Open(textBox1.Text, FileMode.Open,
FileAccess.Read, FileShare.Read);
//открываем файл
byte[] hash = new byte[16]; //
резервируем место под хеш
System.Security.Cryptography.MD5CryptoServiceProvider
md5 = new
System.Security.Cryptography.MD5CryptoServiceProvider();
// создаем объект управления md5
hash =
md5.ComputeHash(fs); // считаем хеш
textBox2.Text =
HashToString(hash); // пишем хеш в
текстовое поле, преобразуя его через
нашу функцию.
}
else {
MessageBox.Show("Файл не
существует или не выбран.", "Ошибка"); //
если файл не существует
return;
}
}
private void
CompareHash(string filename, bool
tryToFindFile) { // сравнить хеши из файла
filename и с варинтом - искать файл для
которого хеш был посчитан или нет
StreamReader sr = new
StreamReader(filename); //
открываем файл
string md5str =
sr.ReadLine(); // считываем строку
sr.Close(); //
закрываем файл
if (md5str.IndexOf("*") == -1 ||
md5str.IndexOf(" ") == -1) { // если строка не
соотвествует формату, т.е. не
содержит пробел или звездочку
MessageBox.Show("Файл
неправильного формата.", "Ошибка");
//показать ошибку
return; // вернуться,
проверять нечего
}
string fileToCheck =
md5str.Substring(md5str.IndexOf("*") + 1); //
выдираем из считанной строки имя файла
string hashToCheck =
md5str.Substring(0, md5str.IndexOf(" ")); //
получаем хеш из строки
textBox3.Text =
hashToCheck.ToUpper(); // пишем хеш в третье
текстовое поле
fileToCheck =
Path.GetDirectoryName(filename) +
"\" + fileToCheck; // корректируем имя
файла, предписывая к нему путь
if (tryToFindFile
System.IO.File.Exists(fileToCheck)) { // если
искать файл с которого хеш посчитан нужно и
файл существует
textBox1.Text =
fileToCheck; // пишем имя файла в первое
текстовое поле
CalculateHash(); // считаем
хеш
}
if (textBox2.Text ==
textBox3.Text) { // сверяем написанное во
втором и третьем текстовых полях, если
совпало
textBox3.BackColor =
System.Drawing.Color.LightGreen; //
подсвечиваем зеленым третье
текстовое поле
}
else { // если не
совпало
textBox3.BackColor =
System.Drawing.Color.Red; // подсвечиваем
красным
}
}
private string
HashToString(byte[] hash) { // функция
преобразования хеша в строку
string ret = ""; // создаем
пустую строку
for (int i = 0; i ‹ hash.Length; i++)
{ // для каждого байта в массиве хеша
ret += String.Format("{0:X1}",
hash[i]); // перевести его в
шестнадцатиричную строковую
запись
}
return ret; // вернуть
полученную строку
}
В общем-то программа
готова, но она явно не дотягивает до
удобной. Хотя можно открыть любой файл, тут же
получить MD5 хеш и тут же сверить с контрольным
файлом, если он лежит тут же и так же
называется. Можно открыть md5 файл и
автоматически получить сравнение,
если файл, который надо проверить лежит тут
же.
Но все равно как-то
бедно... Начинаем
украшательства.
Включаем
перетаскивание (в дизайнере
выбираем textbox1 потом textbox3, в
окошке свойств дважды щелкаем на DragEnter и
DragDrop):
private void
textBox1_DragEnter(object sender,
DragEventArgs e) { // когда курсор над
текстовым полем
if
(e.Data.GetDataPresent("FileNameW")) {
// если тащимый объект содержит
информацию об имени файла
e.Effect =
DragDropEffects.Link; // сменить курсор на
"Создать ярлык"
}
}
private void
textBox1_DragDrop(object sender,
DragEventArgs e) { // когда что-то бросили на
поле
textBox1.Text =
((string[])e.Data.GetData("FileNameW"))[0]; //
получить имя файла и записать в первое
текстовое поле
CalculateHash(); //
посчитать хеш
if
(System.IO.File.Exists(textBox1.Text + ".md5")) { //
проверить на наличие файла md5
CompareHash(textBox1.Text + ".md5",
false);
}
else if
(System.IO.File.Exists(Path.GetDirectoryName(textBox1.Text)
+ "\" +
Path.GetFileNameWithoutExtension(textBox1.Text)
+ ".md5")) {
CompareHash(Path.GetDirectoryName(textBox1.Text)
+ "\" +
Path.GetFileNameWithoutExtension(textBox1.Text)
+ ".md5", false);
}
}
private void
textBox3_DragEnter(object sender,
DragEventArgs e) { // когда курсор над
текстовым полем
if
(e.Data.GetDataPresent("FileNameW")) {
// если тащимый объект содержит
информацию об имени файла
e.Effect =
DragDropEffects.Link; // сменить курсор на
"Создать ярлык"
}
}
private void
textBox3_DragDrop(object sender,
DragEventArgs e) { // когда что-то бросили на
поле
CompareHash(((string[])e.Data.GetData("FileNameW"))[0],
true); // сравнить хеш из файла уроненого
объекта и проверить на оригинальный
файл
}
Теперь включаем кнопки
вноса в реестр и удаления из реестра:
Тут нас поджидает
проблема - пункт контекстного меню
Послать (Send To) - это папка с ярлыками, а не ключ
реестра. А ярлыки создаются сложно
- только с помощью дополнительной
библиотеки, на которую надо добавить
ссылку (Reference). Например, на файл
C:\Windows\system32\wshom.ocx.
private void
button4_Click(object sender, EventArgs e) { // внести в
реестр
RegistryKey rk =
Registry.ClassesRoot; //
открываем ветвь реестра
HKEY_CLASSES_ROOT
rk =
rk.CreateSubKey(".md5\\shell\\Open\\command"); //
создаем (или открываем, если уже есть) ветвь
.md5\\shell\\Open\\command
rk.SetValue("", """ +
Application.ExecutablePath + "" %1"); //
пишем в ключ (По умолчанию) путь к запущенной
программе и указатель аргумента
rk.Close(); //
закрываем ключ
IWshShell ws = new
IWshRuntimeLibrary.WshShell(); //
создаем объект библиотеки Shell
IWshShortcut sc =
(IWshShortcut)ws.CreateShortcut(System.Environment.GetFolderPath(System.Environment.SpecialFolder.SendTo)
+ "\" + "md5hasher.lnk"); //создаем файл md5hasher.lnk в
папке SendTo текущего
пользователя
sc.TargetPath =
Application.ExecutablePath; //
прописываем путь к запускному
файлу
sc.WindowStyle = 1; // тип окна
- нормальный
sc.WorkingDirectory =
Path.GetDirectoryName(Application.ExecutablePath);
// рабочая директория
sc.IconLocation =
Application.ExecutablePath + ", 0"; //
иконка
sc.Save(); //
сохраняем файл
}
private void
button5_Click(object sender, EventArgs e) { // убрать
из реестра
RegistryKey rk =
Registry.ClassesRoot; //
открываем ветвь реестра
HKEY_CLASSES_ROOT
rk.DeleteSubKeyTree(".md5"); //
удаляем ключ и все подчиненные
rk.Close(); //
закрываем ключ
if
(System.IO.File.Exists(System.Environment.GetFolderPath(System.Environment.SpecialFolder.SendTo)
+ "\" + "md5hasher.lnk")) { // если файл никто не
удалил
System.IO.File.Delete(System.Environment.GetFolderPath(System.Environment.SpecialFolder.SendTo)
+ "\" + "md5hasher.lnk"); // удаляем его
}
}
И для обработки
аргумента в коммандной строке
добавлем такую функцию (дважды щелкаем в
дизайнере на пустом месте формы):
private void
Form1_Load(object sender, EventArgs e) { // при
загрузке окна
if
(Environment.GetCommandLineArgs().Length
› 1) { // если есть аргументы командной
строки
if
(Path.GetExtension(Environment.GetCommandLineArgs()[1])
== ".md5") { // если первый аргумент
оканчивается на md5
CompareHash(Environment.GetCommandLineArgs()[1],
true); // сравнить хеш, с поиском
оригинального файла
}
else { // если нет
textBox1.Text =
Environment.GetCommandLineArgs()[1];
// внести первый аргумент в первое
текстовое поле
CalculateHash(); //
посчитать хеш
}
}
}
Не забывайте
вставлять новые ссылки на namespace. Под
конец работы список using выглядел так:
using System;
using
System.ComponentModel;
using
System.Windows.Forms;
using System.IO;
using
Microsoft.Win32;
using
IWshRuntimeLibrary;
Программа не проверяет
запущена ли она уже, так что всегда
запускается новая. Да и
перетаскивание работает только над
текстовыми окнами. Но вы ведь, как автор, об
этом знаете и не ошибетесь?
Оставшийся недостаток
- программа работает только с одним
файлом. Т.е. нельзя получить md5 хеш нескольких
файлов или целого каталога сразу. Надо
считать их отдельно.
Каталог проекта для MS
Visual Studio 2005 с примером находится
в архиве примеров, подкаталог
md5hasher.
2. Самодельная
головоломка.
попробуем сделать простую
головоломку. А чтобы она не была
интересна только нам - попробуем ее
украсить.
Итак, идея
головоломки - поле 6х6. На нем
расположены фишки 6 цветов и 6 форм - итого
36 фишек. Расположены случайно, в
начале игры. Задача - расположить их
так, чтобы в рядах располагались фишки
одинакового цвета и формы. Либо по
горизонтали один цвет, по вертикали -
одна форма, либо наоборот. Вроде этого:
Фишки можно менять
местами с соседними по вертикали,
горизонтали и диагонали, если
соседняя того же цвета или той же формы. Вроде все
просто. Своеобразные пятнашки
получаются.
Приступим. Создаем
окно. Нам понадобятся кнопки Новой игры,
Настройки и Выхода. Остальное
пространство под игровое поле.
Поскольку рисовка простая - используем
движок GDI+, без подключения OpenGL или DirectX.
Рисовать будем на элементе panel.
Сначала задаем нужные
нам поля...
#region fields
internal
GameOptions options; //настройки
игры, класс создадим позднее
internal Fishka[]
fishki; // собственно фишки, класс
создадим позднее
internal bool IsGame;
// флаг что игра идет
internal SolidBrush[]
brushes; // кисти для закраски фишек
internal bool eog; // флаг
конца игры
private Graphics panelGr;
// объект, через который будем рисовать
#endregion
У элемента Panel есть
свой объект класса Graphics, но его уж очень неудобно
использовать. Нам понадобится доступ к
графике не только из события Paint, но и из
других функций, так что проще создать свой
Graphics объект и приписать его к Panel. Это не
лучший путь, но в нашем случае он подходит лучшим
образом, на мой взгляд.
Дальше проводим
инициализацию созданных полей и
включаем кнопки.
private void
Form1_Load(object sender, EventArgs e) {
options = new
GameOptions(); // создаем объект
настроек с значениями по умолчанию
fishki = new Fishka[36]; //
создаем фишки
brushes = new
SolidBrush[6]; // создаем простые кисти
InitBrushes(); //
инициализируем их
//инициализируем фишки
for (byte i = 0; i ‹ 6; i++) {
for (byte j = 0; j ‹ 6; j++) {
fishki[i * 6 + j] = new
Fishka(j, i, i, j); // создаем фишки с
параметрами.
}
}
panelGr =
Graphics.FromHwnd(panel1.Handle); //
создаем объект рисования для panel1
NewGame(); //
Инициализируем игру
}
#region buttons
private void
button1_Click(object sender, EventArgs e) { // кнопка
Новая игра
NewGame();
}
private void
button3_Click(object sender, EventArgs e) { // Кнопка
Настройка
SetOptions();
}
private void
button2_Click(object sender, EventArgs e) { // Кнопка
Выход
Close();
}
#endregion
Теперь создаем все то, что
мы уже упомянули: