Основы 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
Теперь создаем все то, что
мы уже упомянули:
internal class
GameOptions // класс настроек игры
{
internal Color[]
fishkaColors; // массив выбранных цветов
для фишек
internal
GameOptions() { // конструктор с
значениями по умолчанию
fishkaColors = new
Color[6];
fishkaColors[0] =
Color.Red;
fishkaColors[1] =
Color.LightGreen;
fishkaColors[2] =
Color.Blue;
fishkaColors[3] =
Color.Cyan;
fishkaColors[4] =
Color.Magenta;
fishkaColors[5] =
Color.Orange;
}
}
internal class Fishka //
класс фишек
{
internal byte shape; //
номер формы фишки
internal byte color; //
номер цвета фишки
internal byte col; //
столбец
internal byte row; // ряд
internal Fishka() { //
конструктор с значениями по
умолчанию
shape = 0;
color = 0;
col = 0;
row = 0;
}
internal Fishka(byte
inShape, byte inColor, byte inCol, byte
inRow) { // конструктор
shape = inShape;
color = inColor;
col = inCol;
row = inRow;
}
}
Теперь функции:
internal void
InitBrushes() { // инициализация
кистей
for (int i = 0; i ‹ 6; i++) {
brushes[i] = new
SolidBrush(options.fishkaColors[i]); //
создаем кисть с заданным цветом
}
GC.Collect(); // убираем
мусор из памяти.
}
internal void
NewGame() { // инициализация игры
//ставим фишки на
случайные места
Random rnd = new Random();
byte tmpCol, tmpRow;
int tmpInt;
for (int i = 0; i ‹ 36; i++) { //
проводим 36 случайных замен мест
tmpCol =
fishki[i].col;
tmpRow =
fishki[i].row;
tmpInt = rnd.Next(0, 35);
fishki[i].col =
fishki[tmpInt].col;
fishki[i].row =
fishki[tmpInt].row;
fishki[tmpInt].col = tmpCol;
fishki[tmpInt].row = tmpRow;
}
eog = false; //
снимаем флаг окончания игры
IsGame = true; // игра
началась
panel1.Refresh(); //
перерисовываем поле
}
internal void
SetOptions() { //задание настроек
IsGame = false; // игру на
паузу
Form2
settingsForm = new Form2(this); //
открываем окно настроек
settingsForm.StartPosition =
FormStartPosition.CenterParent;
// открывать нужно по центру
основного окна
if
(settingsForm.ShowDialog(this) ==
DialogResult.OK) { // открываем диалог
настроек и если нажали ОК
InitBrushes(); //
переинициализируем кисти
}
IsGame = true; // снимаем
игру с паузы
panel1.Refresh(); //
перерисовываем поле
}
private void
panel1_Paint(object sender,
PaintEventArgs e) { // функция
рисования поля
if (!IsGame) { return; } //
если нет игры - не рисовать
for (byte i = 0; i ‹ 36; i++) {
DrawFishka(i); // рисуем
фишки
}
}
Теперь собственно
рисуем фишки:
internal void
DrawFishka(byte i) {
int fWidth = 42; // ширина
фишки
int fHeight = 42; //
высота фишки
int x = 4 + fishki[i].col *
50; // начало фишки по Х
int y = 4 + fishki[i].row *
50; // начало вишки по Y
panelGr.FillRectangle(SystemBrushes.Control,
x - 1, y - 1, fWidth + 2, fHeight + 2); // закрашиваем
поле фишки цветом фона
switch
(fishki[i].shape) { // рисуем фишки разной
формы
case 0:
panelGr.FillRectangle(brushes[fishki[i].color],
x, y, fWidth, fHeight); // квадрат
break;
case 1:
panelGr.FillEllipse(brushes[fishki[i].color],
x, y, fWidth, fHeight); // круг
break;
case 2:
panelGr.FillPie(brushes[fishki[i].color],
x - fWidth/2, y, fWidth*2, fHeight*2, -120, 60); // сектор
break;
case 3:
panelGr.FillPolygon(brushes[fishki[i].color],
new Point[] { new Point(x, y + fHeight), new Point(x +
fWidth, y + fHeight), new Point(x + fWidth/2, y) }); //
треугольник вверх
break;
case 4:
panelGr.FillPolygon(brushes[fishki[i].color],
new Point[] { new Point(x + fWidth, y + fHeight), new
Point(x + fWidth, y), new Point(x, y + fHeight/2) }); //
треугольник влево
break;
case 5:
panelGr.FillPolygon(brushes[fishki[i].color],
new Point[] { new Point(x, y + fHeight), new Point(x, y),
new Point(x + fWidth, y + fHeight/2) }); // треугольник
вправо
break;
}
}
На этом моменте поле
рисуется, но не управляется.
Приступаем к созданию
управления. Нам надо, чтобы можно было
мышкой выбирать фишку - значит нужно куда
записывать выбранную. Дописываем в
регион fields такую запись:
internal byte
curSelected; // индекс выбранной
фишки
А в функцию
инициализации (Form1_Load) такую:
curSelected = 255; //
никакая не выбрана
Теперь описываем
функцию клика мышки, но покольку нам нужны
точные координаты клика, возьмем
событие MouseUp:
private void
panel1_MouseUp(object sender,
MouseEventArgs e) {
if (eog || !IsGame) {
return; } // если игра окончена или на паузе - не
обрабатывать
byte f =
GetFishkaFromCoord(e.X, e.Y); //
получить индекс фишки по координатам
клика
if (f != curSelected)
{ // если кликнули не на уже выбранную
if (f == 255) { // если
кликнули вне поля
curSelected = f; // никакая
не выбрана
return;
}
if (curSelected ==
255) { curSelected = f; } // если никто не был
выбран, говорим что была выбрана она же
else if
(CheckAndSwitch(curSelected, f)) {
return; } // проверяем надо ли фишки менять
местами, если да - возвращаемся
curSelected = f; // выбрана
новая
}
else if (f != 255) { // если
кликнули на ту же фишку
curSelected = 255; //
никакая не выбрана
}
}
internal byte
GetFishkaFromCoord(int x, int y) { //
получить индекс фишки по
координатам
byte col = (byte)(x / 50); //
получить столбец
byte row = (byte)(y / 50); //
получить ряд
for (byte i = 0; i ‹ 36; i++) {
//последовательно проверить, кто здесь
стоит
if (fishki[i].col == col
fishki[i].row == row) {
return i;
}
}
return 255; // если никто -
вернуть никто
}
Последовательные перебор -
плохое решение, но если в массиве 36
объектов, то на современных компах сойдет.
Лучше конечно создать парный массив мест, с
постоянно поддерживаемой
индексацией какая фишка стоит в i
месте.
Теперь функция смены
фишек:
internal bool
CheckAndSwitch(byte f1, byte f2) {
//проверяем соседние ли
фишки
if (Math.Abs(fishki[f1].col
- fishki[f2].col) ‹= 1 Math.Abs(fishki[f1].row -
fishki[f2].row) ‹= 1) {
//проверяем совпадает
цвет или форма
if (fishki[f1].color
== fishki[f2].color || fishki[f1].shape ==
fishki[f2].shape) {
//меняем местами
byte tc =
fishki[f1].col;
byte tr =
fishki[f1].row;
fishki[f1].col =
fishki[f2].col;
fishki[f1].row =
fishki[f2].row;
fishki[f2].col = tc;
fishki[f2].row = tr;
//снимаем выбранность
curSelected = 255;
//перерисовываем
DrawFishka(f1);
DrawFishka(f2);
//проверяем не окончена ли
игра
eog = true;
// смотрим на формы по
горизонтали и цвета по вертикали
for (int i = 0; i ‹ 6; i++) {
for (int j = 0; (j ‹ 5 eog); j++)
{
if (j != 5 fishki[i * 6 +
j].row != fishki[i * 6 + j + 1].row) {
eog = false;
}
if (i != 5 fishki[i * 6 +
j].col != fishki[(i + 1) * 6 + j].col) {
eog = false;
}
}
}
if (eog) { // если ошибок
нет
DrawEOG(); // рисуем
конец игры
}
else { // если ошибки
были - проверяем наоборот
eog = true;
for (int i = 0; i ‹ 6; i++) {
for (int j = 0; (j ‹ 5 eog); j++)
{
if (j != 5 fishki[i * 6 +
j].col != fishki[i * 6 + j + 1].col) {
eog = false;
}
if (i != 5 fishki[i * 6 +
j].row != fishki[(i + 1) * 6 + j].row) {
eog = false;
}
}
}
if (eog) { // ошибок нет
DrawEOG(); // конец
игры
}
}
return true; // фишки
меняли местами
}
}
return false; // фишки не
меняли местами
}
Осталось последнее -
нарисовать конец игры:
internal void
DrawEOG() {
panelGr.DrawString("Собрано", new
Font("Times New Roman", 80, GraphicsUnit.Pixel),
Brushes.Black, 0, 100); // рисуем слово Собрано
поверх поля
}
В общем и целом - игра
работает. Другое дело, что она не красивая и
не удобная. Настройку цветов мы
предусмотрели, но пока не сделали. Да и
играть, когда не видно какая фишка выбрана -
тяжело. Попробуем немного украсить
игру.
Во-первых введем
индикацию над какой фишкой находится
мышка и будем отрисовывать выбранную
фишку, и ту над которой мышка.
Добавим индекс
"под-мышечной" фишки:
#region fields
internal byte
curHovered;
//Form1_Load
curHovered =
255;
Для контроля
опишем функцию движения мышки по полю:
private void
panel1_MouseMove(object sender,
MouseEventArgs e) {
if (eog || !IsGame) {
return; } // если игра окончена или на паузе - не
реагировать
byte f =
GetFishkaFromCoord(e.X, e.Y); //
получить фишку по координатам
if (f == 255
curHovered != 255) { // если мышка вне поля, но
какая-то фишка была отмечена - снимаем
отметку
curHovered = 255;
return;
}
if (f != curHovered) {
// если мышка перешла на другую фишку,
меняем отметку
curHovered = f;
}
}
Теперь надо
отрисовывать выбранность фишек и их
реакцию на мышку. А заодно, хорошо бы и
рисовать их получше.
Изменим функции
реакций на мышку так, чтобы фишки
перерисовывались:
#region mouse events
private void
panel1_MouseMove(object sender,
MouseEventArgs e) { // движение мышки по
полю
if (eog || !IsGame) {
return; }
byte f =
GetFishkaFromCoord(e.X, e.Y);
if (f == 255
curHovered != 255) {
f = curHovered; //
запоминаем какая была
отмечена
curHovered = 255; //
снимаем отметку
DrawFishka(f); //
перерисовываем фишку, которая
была отмечена, чтобы снять отметку
return;
}
if (f != curHovered)
{
if (curHovered == 255)
{ curHovered = f; }
byte old = curHovered;
// запоминаем какая была
отмечана
curHovered = f; // меняем
отметку
DrawFishka(old); //
перерисовываем струю
DrawFishka(curHovered); // и
новую
}
}
private void
panel1_MouseUp(object sender,
MouseEventArgs e) { // клик мышки
if (eog || !IsGame) {
return; }
byte f =
GetFishkaFromCoord(e.X, e.Y);
if (f != curSelected)
{
if (f == 255) {
f = curSelected; //
запоминаем, какая была выбрана
curSelected = 255; //
снимаем выбранность
DrawFishka(f); //
перерисовываем старую, чтобы снять
выделение
return;
}
if (curSelected ==
255) { curSelected = f; }
else if
(CheckAndSwitch(curSelected, f)) {
return; }
byte old =
curSelected; // запоминаем, какая
была выбрана
curSelected = f; // меняем
отметку
DrawFishka(old); //
перерисовываем старую
DrawFishka(curSelected); // и
новую
}
else if (f != 255) {
f = curSelected; //
запоминаем какая была выбрана
curSelected = 255; //
снимаем выделение
DrawFishka(f); //
перерисовываем старую
}
}
#endregion
Ну и наконец
рисование... добавим градиентную
заливку к фишкам. А чтобы было проще менять
способы заливки - введем создание кистей
в функцию рисования. Нерационально,
но при столь малом рисовании - почти не
заметно. Итак функция InitBrush, все ее
упоминания и поле brushes не нужны. А
функцию рисования сделаем
такой:
internal void
DrawFishka(byte i) {
int fWidth = 42;
int fHeight = 42;
int x = 4 + fishki[i].col *
50;
int y = 4 + fishki[i].row *
50;
panelGr.FillRectangle(SystemBrushes.Control,
x - 1, y - 1, fWidth + 2, fHeight + 2);
LinearGradientBrush br; //
декларируем кисть с градиентом
if (curSelected == i)
{ // если рисуемая фишка - выбрана
br = new
LinearGradientBrush(new
Point(x,y), new Point(x+fWidth, y+fHeight), Color.Gray,
options.fishkaColors[fishki[i].color]);
// создаем градиент от серого к цвету
фишки
}
else if (curHovered ==
i) { // если рисуемая фишка под мышкой
br = new
LinearGradientBrush(new
Point(x, y), new Point(x + fWidth, y + fHeight),
options.fishkaColors[fishki[i].color],
Color.Gray); // создаем градиент от цвета
фишки к серому
}
else { // если это просто
фишка
br = new
LinearGradientBrush(new
Point(x, y), new Point(x + fWidth, y + fHeight),
options.fishkaColors[fishki[i].color],
Color.White); // градиент от цвета фишки к
белому
}
switch
(fishki[i].shape) {
case 0:
panelGr.FillRectangle(br, x, y,
fWidth, fHeight);
break;
case 1:
panelGr.FillEllipse(br, x, y, fWidth,
fHeight);
break;
case 2:
panelGr.FillPie(br, x - fWidth/2, y,
fWidth*2, fHeight*2, -120, 60);
break;
case 3:
panelGr.FillPolygon(br, new
Point[] { new Point(x, y + fHeight), new Point(x + fWidth, y
+ fHeight), new Point(x + fWidth/2, y) });
break;
case 4:
panelGr.FillPolygon(br, new
Point[] { new Point(x + fWidth, y + fHeight), new Point(x +
fWidth, y), new Point(x, y + fHeight/2) });
break;
case 5:
panelGr.FillPolygon(br, new
Point[] { new Point(x, y + fHeight), new Point(x, y), new
Point(x + fWidth, y + fHeight/2) });
break;
}
}
Осталось сделать выбор
цветов.
Не забудьте
добавить ColorDialog, указать
AcceptButton = button7 для формы.
Инициализируем форму:
private Form1 pf;
internal Form2(Form1 inFrm) { //
конструктор с указанием
родительской формы
InitializeComponent();
pf = inFrm; // задаем
указатель на родительскую форму
button1.BackColor =
pf.options.fishkaColors[0]; // задаем
цвета для кнопок
button2.BackColor =
pf.options.fishkaColors[1];
button3.BackColor =
pf.options.fishkaColors[2];
button4.BackColor =
pf.options.fishkaColors[3];
button5.BackColor =
pf.options.fishkaColors[4];
button6.BackColor =
pf.options.fishkaColors[5];
}
Дальше есть два пути.
Первый обычный: для каждой кнопки пишем такую
функцию:
private void
button1_Click(object sender, EventArgs e) {
colorDialog1.Color =
button1.BackColor; // устанавливаем
цвет фишки в диалоге выбора цвета
if
(colorDialog1.ShowDialog(this) ==
DialogResult.OK) { // открываем диалог,
и если в нем нажали ОК
button1.BackColor =
colorDialog1.Color; //
устанавливаем выбранный цвет на
кнопку
pf.options.fishkaColors[0] =
colorDialog1.Color; // и на фишку
}
}
И так 6 раз. Занудно, но
просто. Второй путь - задать для каждой кнопки
параметр Tag = номеру цвета. Т.е. 0, 1, 2, 3, 4, 5 в
порядке создания кнопок. Приписать к
каждой один и тот же обработчик события
нажатия, например button_Click. Это
делается в файле Form2.Designer.cs в
регионе Windows Form Designer generated
code таким образом:
this.button1.Click += new
System.EventHandler(this.button_Click);
И так для каждой из шести
кнопок.
И описать этот самый
обработчик так:
private void
button_Click(object sender, EventArgs e) {
int idx =
Int32.Parse(((Button)sender).Tag as string); //
определяем к какой фишке относится
кнопка
colorDialog1.Color =
((Button)sender).BackColor; //
устанавливаем цвет фишки в диалоге
if
(colorDialog1.ShowDialog(this) ==
DialogResult.OK) { // открываем
диалог
((Button)sender).BackColor =
colorDialog1.Color; //
устанавливаем выбранный цвет на
кнопку
pf.options.fishkaColors[idx] =
colorDialog1.Color; // и на фишку
}
}
Так получается
одна функция, но чуть больше другой
работы.
Вот теперь можно
играть. :)
Желающие могут
поэкспериментировать с формами
фишек. Мне просто было откровенно лень
выписывать всякие тороиды,
пятиугольники и прочее. Гораздо
интереснее эксперименты с
градиентными заливками. Те, кому
интересно - посмотрите класс Blend, в
котором задаются распределение
цветов для множественного
градиента.
Каталог проекта для MS
Visual Studio 2005 с примером находится
в архиве примеров, подкаталог
puzzle.
3. Самодельный изменятель размера
картинок.
На многих
форумах/блогах/сайтах есть ограничения
на загружаемые картинки типа таких: "не
больше чем 1024 пикселей по стороне и не больше
чем 1 Мб".
Постановка задачи -
надо создать программку для массового
уменьшения размеров и объема картинок по
заданным параметрам. Будем исходить из
того, что картинки с которыми
предстоит работать, находятся в
формате jpeg или png, впрочем gif, bmp и tiff - тоже
подходят. Если формат другой - то придется
подключать библиотеку работы с этим
форматом, а в худшем случае, писать свою.
Перечисленные 5 форматов
поддерживаются .NET, так что проблем не
будет.
Нам
понадобится: возможность
добавления/удаления
файлов/каталогов в список обработки,
задание параметров (размера и объема),
выбор варианта сохранения и места
сохранения. Приступаем - создаем
окно, в котором предусматриваем все, что
только что перечислили.
Добавляем
openFileDialog,
folderBrowserDialog и заполняем
comboBox1 значениями JPEG, PNG, TIFF, BMP, GIF. Для
openFileDialog описываем свойство
Filter так - "Графические файлы (*.jpg, *.png, *.tiff,
*.gif, *.bmp)|*.jpg;*.jpe;*.jpeg;*.png;*.tif;*.tiff;*.bmp;*.gif||", а
свойство MultiSelect = true. Для comboBox1
устанавливаем свойство
DropDownStyle = DropDownList. Для
listBox1 устанавливаем свойство
HorizontalScrollbar = true
Первое
приближение
Описываем функцию
кнопки Добавить файлы:
private void
button1_Click(object sender, EventArgs e) {
if
(openFileDialog1.ShowDialog(this) ==
DialogResult.OK) { // открыть диалог
выбора файлов
listBox1.Items.AddRange(openFileDialog1.FileNames);
// заполняем listbox именами
выбранных файлов
}
}
Запускаем прогу и смотрим
что получилось. После выбора файлов
видим, что в listbox они пишутся с полным
путем... не очень удобно читать. Как бы сделать так,
чтобы писались только имена файлов, но
запоминались они полностью?
Создаем класс, который
будет содержать полный путь и только имя файла, и
возвращать в listbox только короткое
имя:
internal class
FileListItem
{
internal string
fullFilename; // полное имя файла
internal string
shortFilename; // короткое имя
файла
internal
FileListItem() { // конструктор
fullFilename = "";
shortFilename = "";
}
internal
FileListItem(string inFull, string inShort) {
// конструктор со всеми
параметрами
fullFilename = inFull;
shortFilename =
inShort;
}
internal
FileListItem(string inFull) { //
конструктор только с полным именем
fullFilename = inFull;
shortFilename =
Path.GetFileName(inFull);
}
public override string
ToString() { // функция, к которой
обращается listbox при заполнении
списка
return
shortFilename;
}
}
Теперь изменим
функцию обработки нажатия кнопки на
такую:
for (int i = 0; i ‹
openFileDialog1.FileNames.Length; i++) { //
для каждого из выбранных файлов
listBox1.Items.Add(new
FileListItem(openFileDialog1.FileNames[i]));
// добавить в listbox созданный объект
FileListItem
}
Запускаем и смотрим, что
получилось. Вроде нормально.
Дальше пишем функцию
кнопки Добавить каталог:
private void
button2_Click(object sender, EventArgs e) {
if
(folderBrowserDialog1.ShowDialog(this)
== DialogResult.OK) { // открываем
диалог выбора каталога
DirectoryInfo di = new
DirectoryInfo(folderBrowserDialog1.SelectedPath);
// создаем объект информации о выбранном
каталоге
foreach (FileInfo fi
in di.GetFiles("*.jpg")) { // для каждого файла
jpg
listBox1.Items.Add(new
FileListItem(fi.FullName)); // добавить
имя файла в список
}
//аналогичный цикл для
каждого расширения
}
}
Можно использовать
цикл для каждого файла и внутри цикла
проверять расширение файла, напрямую или
через Regex:
foreach (FileInfo fi
in di.GetFiles("*.*")) { // для каждого файла в
каталоге
if
(fi.Extension.Equals(".jpg") ||
fi.Extension.Equals(".jpe") ||
fi.Extension.Equals(".jpeg") ||
fi.Extension.Equals(".png") ||
fi.Extension.Equals(".tif") ||
fi.Extension.Equals(".tiff") ||
fi.Extension.Equals(".gif") ||
fi.Extension.Equals(".bmp")) { // если
расширение нам подходит
listBox1.Items.Add(new
FileListItem(fi.FullName)); // добавить
в список
}
}
Запускаем, проверям.
Работает, но программа не
просматривает подкаталоги. Надо
поправить. Для этого:
выносим код добавления
файлов в отдельную функцию:
internal void
AddFilesFromDir(string dir) {
DirectoryInfo di = new
DirectoryInfo(dir);
foreach (FileInfo fi
in di.GetFiles("*.*")) {
if
(fi.Extension.Equals(".jpg") ||
fi.Extension.Equals(".jpe") ||
fi.Extension.Equals(".jpeg") ||
fi.Extension.Equals(".png") ||
fi.Extension.Equals(".tif") ||
fi.Extension.Equals(".tiff") ||
fi.Extension.Equals(".gif") ||
fi.Extension.Equals(".bmp")) {
listBox1.Items.Add(new
FileListItem(fi.FullName));
}
}
}
меняем
функцию обработки кнопки Добавить каталог:
if
(folderBrowserDialog1.ShowDialog(this)
== DialogResult.OK) {
AddFilesFromDir(folderBrowserDialog1.SelectedPath);
}
и добавляем в
функцию добавления файлов
рекурсивный код, в конец функции:
foreach
(DirectoryInfo di2 in
di.GetDirectories()) { // для каждого
подкаталога в каталоге di
AddFilesFromDir(di2.FullName); //
добавить файлы из него
}
Запускаем, проверяем.
Вроде работает. Неудобно каждый раз в
окне выбора каталога прыгать по
длинному дереву... надо бы поправить... В
функцию обработки кнопки Добавить
каталог добавим первой строчкой:
if
(folderBrowserDialog1.SelectedPath
== "") {
folderBrowserDialog1.SelectedPath =
@"C:\"; }
Ну, путь пишете тот,
который вам удобен.
Пишем функцию
обработки кнопки Убрать
выделенное:
private void
button3_Click(object sender, EventArgs e) {
if
(listBox1.SelectedIndex != -1) { // если
что-то выделенно
listBox1.Items.RemoveAt(listBox1.SelectedIndex);
// убрать выделенный элемент
}
}
Дальше описываем
кнопку Выбрать для каталога
сохранения:
private void
button4_Click(object sender, EventArgs e) {
if
(folderBrowserDialog1.ShowDialog(this)
== DialogResult.OK) { // открываем
диалог выбора каталога
textBox4.Text =
folderBrowserDialog1.SelectedPath;
// записываем выбранный путь в
текстовое поле
}
}
Запускаем, проверям.
Замечаем, что в выпадающем окошке Формат
сохранения ничего не выбирается
само... непорядок. Создадим
обработчик события Form1_Load и впишем
туда такой код:
comboBox1.SelectedIndex = 0; //
выбрать первый в списке элемент
Осталось самое главное -
написать основные функции. Поскольку в
форме хватает параметров, вводимых
пользователем, лучше создать отдельную
функцию проверки правильности
введенных данных. Естественно,
функция преобразования картинки
тоже должна быть отдельной. Таким
образом, обработчик кнопки Уменьшить
будет примерно таким:
private void
button5_Click(object sender, EventArgs e) {
if (!CheckInput()) {
return; }
DoJob();
}
Теперь описываем
проверку введенных данных. Что надо
проверять: наличие файлов проверять не
надо, это будет делать функция, которая
будет с ними работать, потому что все
выбранные файлы существовали на
момент выбора, а значит они могут исчезнуть
только по ошибке, которая может произойти
когда-угодно, и проверять
работоспособность файла надо прямо
перед (или во время) обращения к нему. Надо
проверить введенные числа - чтобы
состояли только из цифр, и не были слишком
большими/маленькими. А еще надо
проверить каталог сохранения - если его
нет, надо создать. Заодно, создадим
глобальные переменные, в которых будем
хранить введенные данные.
internal int width;
//ширина
internal int height;
//высота
internal int size; // объем
в байтах
internal string resdir; //
каталог для результатов
internal int
filesErrorCount; // счетчик ошибок
internal
System.Drawing.Imaging.ImageFormat
imageFormat; // выбранный формат файлов
internal string imageExt;
// расширение выбранного
формата
internal bool
CheckInput() {
try { //
попробовать
width =
Int32.Parse(textBox1.Text); // превратить
введеное в текстовом поле в целое
число
if (width ‹= 1) { return
false; } // если ширина слишком маленькая -
вернуть ошибку
}
catch { // если что-то не
получилось
return false; // вернуть
ошибку
}
try {
height =
Int32.Parse(textBox2.Text);
if (height ‹= 1) { return
false; } // если высота слишком маленькая -
вернуть ошибку
}
catch {
return false;
}
try {
size =
Int32.Parse(textBox3.Text);
size *= 1024; //
переводим в байты
if (size == 0) { return
false; } // если объем равен 0 - вернуть
ошибку
}
catch {
return false;
}
if (textBox4.Text.Length ==
0) { return false; } // если путь не введен -
ошибка
resdir =
textBox4.Text;
if (resdir[resdir.Length -
1] != '') { resdir += ""; } // уточняем наличие
слеша на конце пути
if
(!Directory.Exists(resdir)) { // если каталог
не существует
try { //
попробовать
resdir =
Directory.CreateDirectory(resdir).FullName;
// создать каталог, записать полный путь в
resdir и добавить слеш
}
catch {
return false;
}
}
switch
(comboBox1.SelectedIndex) { // в
зависимости от того, что выбрано в
окошке Формат
case 0:
imageFormat =
System.Drawing.Imaging.ImageFormat.Jpeg; //
установить формат файлов
imageExt = ".jpg"; // и
расширение
break;
case 1:
imageFormat =
System.Drawing.Imaging.ImageFormat.Png;
imageExt = ".png";
break;
case 2:
imageFormat =
System.Drawing.Imaging.ImageFormat.Tiff;
imageExt = ".tif";
break;
case 3:
imageFormat =
System.Drawing.Imaging.ImageFormat.Bmp;
imageExt = ".bmp";
break;
case 4:
imageFormat =
System.Drawing.Imaging.ImageFormat.Gif;
imageExt = ".gif";
break;
}
return true;
}
Желающие могут перед
каждым return false вставить сообщение об
ошибке, либо просто:
MessageBox.Show(this,
"Неправильно введена ширина",
"Ошибка");
либо сделать отдельную
функцию показа ошибок.
Переходим к главному -
функции уменьшения. Итак, функция должна
открыть файл, посмотреть попадает ли он под
введенные параметры. Если да - просто
скопировать его, если нет - то
пересохранить его. Причем, сначала надо
подогнать размеры, определив какая из
сторон больше выступает за разрешенное
значение, попробовать сохранить,
посмотреть объем - и последовательно
пропорционально уменьшать размер, пока
объем не станет нужным. Желающим
экспериментировать со степенью
сжатия для изменеия объема, что более
разумно во многих случаях, придется
пойти чуть другим путем - при сохранении
картинки передавать функции Save
объект класса EncoderParameters с
установленной степенью сжатия. Сделаем
одну функцию управляющей, а
проверку и все операции вынесем в
отдельный функции.
internal void DoJob()
{
filesErrorCount = 0;
int imageCheck = 0;
for (int i = 0; i ‹
listBox1.Items.Count; i++) { // для каждого
элемента списка
imageCheck =
IsImageOk(((FileListItem)listBox1.Items[i]).fullFilename);
// проверяем файл
if (imageCheck == 0) { //
если ошибок нет
File.Copy(((FileListItem)listBox1.Items[i]).fullFilename,
resdir +
((FileListItem)listBox1.Items[i]).shortFilename);
// скопировать файл
continue;
}
else if (imageCheck == 4) {
continue; } // если ошибка открытия
файла - пропустить
else if (imageCheck == 1) {
// если размер и формат в порядке
SmallerImage(((FileListItem)listBox1.Items[i]).fullFilename,
false); // уменьшить объем
}
else if (imageCheck == 2) {
// если размер больше
ResizeImage(((FileListItem)listBox1.Items[i]).fullFilename);
// уменьшить картинку
}
else if (imageCheck == 3) {
// если размер в порядке, но формат другой
ConvertImage(((FileListItem)listBox1.Items[i]).fullFilename);
// изменить формат
}
}
// сообщить сколько
файлов прошло, сколько выдали ошибку
MessageBox.Show(this,
String.Format("Успешно преобразовано {0}
файлов.nПроизошли ошибки при работе с {1}
файлами.",
listBox1.Items.Count-filesErrorCount,
filesErrorCount), "Ошибка");
}
internal int
IsImageOk(string fn) { // проверка файлов
Bitmap bmp;
try { //
попробовать
bmp = new Bitmap(fn); //
создать картинку из файла
}
catch { // если не
получилось
filesErrorCount++; //
посчитать ошибку
return 4; // вернуть ошибку
открытия
}
if (bmp.Width › width ||
bmp.Height › height) { return 2; } // если размер
больше, вернуть 2
bmp.Dispose(); // убрать
картинку из памяти
if
(!Path.GetExtension(fn).ToLower().Equals(imageExt))
{ return 3; } // если расширение не то,
которое надо, вернуть 3
FileInfo fi = new
FileInfo(fn); // создать объект
информации о файле
if (fi.Length › size) {
return 1; } // если размер файла больше, вернуть
1
return 0; // ошибок нет
}
internal void
ResizeImage(string fn) {
int newImageWidth
= 0;
int
newImageHeight = 0;
Bitmap bmp;
try {
bmp = new Bitmap(fn);
}
catch {
filesErrorCount++;
return;
}
if ((double)width /
bmp.Width ‹ (double)height / bmp.Height) { { // если
ширина больше выступает за границы, чем
высота
newImageWidth = width; //
ширину на максимум
newImageHeight =
(int)(bmp.Height * ((double)width / bmp.Width)); //
высоту пропорционально
}
else { // если
наоборот, высота больше выступает
newImageHeight = height; //
высоту на максимум
newImageWidth = (int)(bmp.Width *
((double)height / bmp.Height)); // ширину
пропорционально
}
bmp = new Bitmap(bmp,
newImageWidth, newImageHeight); //
создать измененное изображение
try { //
попробовать
bmp.Save(resdir +
Path.GetFileNameWithoutExtension(fn)
+ imageExt, imageFormat); // сохранить в
нужном формате
}
catch {
filesErrorCount++;
return;
}
finally { // при любом
раскладе
bmp.Dispose(); //
уничтожить объект картинки
}
FileInfo fi = new
FileInfo(resdir +
Path.GetFileNameWithoutExtension(fn)
+ imageExt); // информация о новом
файле
if (fi.Length › size) { //
если объем больше
SmallerImage(resdir +
Path.GetFileNameWithoutExtension(fn)
+ imageExt, true); // уменьшить объем
}
}
В следующей
функции вроде все должно быть понятно, все
уже было откомментировано.
internal void
ConvertImage(string fn) {
Bitmap bmp;
try {
bmp = new Bitmap(fn);
}
catch {
filesErrorCount++;
return;
}
try {
bmp.Save(resdir +
Path.GetFileNameWithoutExtension(fn)
+ imageExt, imageFormat);
}
catch {
filesErrorCount++;
return;
}
finally {
bmp.Dispose();
}
FileInfo fi = new
FileInfo(resdir +
Path.GetFileNameWithoutExtension(fn)
+ imageExt);
if (fi.Length › size) {
SmallerImage(resdir +
Path.GetFileNameWithoutExtension(fn)
+ imageExt, true);
}
}
Уменьшаем объем. Я пошел
самым простым путем - пустил в цикл уменьшение на
5% по размеру, пока объем не станет нужным.
Входящий параметр
inplace - показывает что меняем -
старый файл с копированием, или новый в
своем месте.
internal void
SmallerImage(string fn, bool inplace)
{
bool done = false; // флаг
правильности объема
string resfn; // имя
результирующего файла
if (inplace) { resfn = fn; } //
если на месте - результирующее имя равно
исходному
else { resfn = resdir +
Path.GetFileNameWithoutExtension(fn)
+ imageExt; } // если нет - создаем
результирующее
Bitmap bmp, bmp2;
while (!done) { // пока
размер не подойдет
try {
bmp = new Bitmap(fn);
}
catch {
filesErrorCount++;
return;
}
bmp2 = new Bitmap(bmp,
(int)(bmp.Width * 0.95), (int)(bmp.Height * 0.95)); // создаем
новую картинку на 5% меньше
bmp.Dispose(); //
уничтожаем старую картинку
try {//
попробовать
if (inplace) { // если на
месте
File.Delete(resfn); // удалить
файл, на место которого будем сейчас
писать
}
bmp2.Save(resfn,
imageFormat); // сохраняем новую
картинку
}
catch {
filesErrorCount++;
return;
}
finally {
bmp2.Dispose(); // в любом
случае - уничтожаем новую картинку
}
FileInfo fi = new
FileInfo(resfn); // информация о новом
файле
if (fi.Length ‹= size) { //
если размер подходит
done = true; // ставим
флаг
}
fn = resfn; // имя файла
теперь точно равно результирующему
inplace = true; // и
работаем дальше на месте
GC.Collect(); // очищаем
память от мусора.
}
}
Запускаем. Проверяем.
Вроде все работает... вот только понять это
сложно - кнопку нажали и прога висит, пока не
закончит. Надо бы по-аккуратнее сделать,
прогресс индикатор, например.
Второе приближение
- или безопасная мультипоточность
Добавим в форму
маленький прогресс индикатор и компонент
BackgroundWorker, который
появился только в .NET 2.0. Это такой
специальный работник, который сам себе
тихо работает в фоновом режиме, не
мешает основной программе
выполняться и только сообщает о своем
прогрессе, да ждет отмены. Прикольная фича,
раньше приходилось то же самое писать
лапками, или копировать из проекта в
проект, а теперь удобнее стало и безопаснее.
:)
Для компонента
backgroundWorker1 определим
все три доступных события:
private void
backgroundWorker1_DoWork(object
sender, DoWorkEventArgs e) { // запуск
работника
DoJob(sender); // наша
главная функция работы, как аргумент
передаем ей указатель на работника
}
private void
backgroundWorker1_ProgressChanged(object
sender, ProgressChangedEventArgs e) { //
изменение прогресса
progressBar1.Value =
e.ProgressPercentage; // установить
значение прогресс индикатора
}
private void
backgroundWorker1_RunWorkerCompleted(object
sender,
RunWorkerCompletedEventArgs e) { //
завершение работы
UseWaitCursor = false; //
отменить курсор ожидания (часики)
progressBar1.Value = 0; // снять
прогресс
button5.Enabled = true; //
включаем кнопку Уменьшить
}
Поправим нашу функцию
работы, и обработчик события
нажатия кнопки Уменьшить:
private void
button5_Click(object sender, EventArgs e) {
if (!CheckInput()) {
return; }
UseWaitCursor = true; //
включаем курсор часики
button5.Enabled = false; //
выключаем кнопку уменьшить - чтоб не было
всяких двойных кликов
backgroundWorker1.RunWorkerAsync();
// запускаем работника
}
internal void
DoJob(object worker) { // основная
функция теперь с параметром
filesErrorCount = 0;
int imageCheck = 0;
for (int i = 0; i ‹
listBox1.Items.Count; i++) {
imageCheck =
IsImageOk(((FileListItem)listBox1.Items[i]).fullFilename);
if (imageCheck == 0) {
File.Copy(((FileListItem)listBox1.Items[i]).fullFilename,
resdir +
((FileListItem)listBox1.Items[i]).shortFilename);
continue; }
else if (imageCheck == 4) {
continue; }
else if (imageCheck == 1)
{
SmallerImage(((FileListItem)listBox1.Items[i]).fullFilename,
false);
}
else if (imageCheck == 2)
{
ResizeImage(((FileListItem)listBox1.Items[i]).fullFilename);
}
else if (imageCheck == 3)
{
ConvertImage(((FileListItem)listBox1.Items[i]).fullFilename);
}
((BackgroundWorker)worker).ReportProgress((int)Math.Round((double)i
* 100/ listBox1.Items.Count)); // уточнить
прогресс
}
// в вызове окна
убираем привязку к форме
MessageBox.Show(String.Format("Успешно
преобразовано {0} файлов.nПроизошли
ошибки при работе с {1} файлами.",
listBox1.Items.Count-filesErrorCount,
filesErrorCount), "Ошибка");
}
Вот и все. Конечно, по уму,
надо бы еще перед запуском работника
выключать все элементы управления и
включать их по завершении. А еще можно кнопку
Отмена вставить.
Каталог проекта для MS
Visual Studio 2005 с примером находится
в архиве примеров, подкаталог
image_resizer.
4. Системный индикатор в заголовке
окна.
Работал тут с одной глючной
прогой и мне понадобился хороший
детектор свободного места на харде. А
чтобы его было всегда видно и он не мешал
работе, я решил его вывести в заголовок
активного окна. Теперь решил рассказать,
как я это сделал и показать, что еще можно
аналогично выводить.
Во-первых, создаем
пустое, маленькое и невидимое окно. Для
этого надо задать следующие свойства
созданной формы:
ClientSize = new
System.Drawing.Size(86, 26); // ширину
подберите под размер выводимого
текста, высоту будем ставить
программно
ControlBox = false; //
убираем кнопки минимизации и пр.
Text = ""; // убираем текст
из заголовка, чтобы заголовок
пропал
DoubleBuffered = true; //
двойной буфер - чтоб меньше мерцало при
рисовании
FormBorderStyle =
System.Windows.Forms.FormBorderStyle.None;
// убираем границу окна
ShowInTaskbar = false; // не
показывать в панели задач
TopMost = true; // самое
верхнее (выше только TaskManager)
TransparencyKey =
System.Drawing.SystemColors.Control; //
делаем окно всегда
прозрачным
Добавим в форму
компонент timer, чтобы
контролировать частоту с которой
ведется проверка диска, определим
события Paint и MouseUp для формы и Tick для
таймера.
Сделаем функцию,
записывающую текущее свободное
место на диске в глобальную переменную:
internal long space;
internal void
CheckSize() {
try {
space =
System.IO.DriveInfo.GetDrives()[1].AvailableFreeSpace;
// получить свободное место на первом
драйве
Refresh(); // обновить
окно
}
catch { // если была
ошибка
timer1.Stop(); //
остановить таймер
Close(); // выйти
}
}
Диски нумеруются
в порядке букв, т.е. в моем случае, 0 - A, 1 - C, 2 - D...
Если проверка не удалась - это означает сбой
диска, поэтому стоит закрытие программы,
чтобы не мучить драйв, с которым что-то
случилось.
Теперь надо сделать
функцию преобразования размера в
читаемую строку. Размер
возвращается в байтах, меня он
интересует до сотой мегабайта, так что я
сделал так:
internal string
MakeReadableSize(long size) {
double ret =
(double)size / 1024; // приводим к
килобайтам
size /= 1024;
if (size == 0) { // если
целых килобайт 0 - вернуть строку в
килобайтах до сотой
return String.Format("{0:f2}Kb",
ret);
}
ret = (double)size /
1024; // приводим к мегабайтам
return String.Format("{0:f2}Mb",
ret); // вернуть в мегабайтах до сотой
}
Теперь можно сделать
функцию рисования:
private void
Form1_Paint(object sender, PaintEventArgs e)
{
e.Graphics.DrawString(MakeReadableSize(space),
myFont, myBrush, 0, 0); // рисуем строку в окне
}
Надо бы определить и
задать кисть и шрифт, которым пользуемся для
написания строки, да и высоту окна надо
бы поставить. Пишем все это в инициализацию
окна:
internal Font myFont;
internal SolidBrush
myBrush;
public Form1() {
InitializeComponent();
myBrush = new
SolidBrush(SystemColors.ActiveCaptionText);
// кисть цвета текста заголовка
активного окна
myFont =
SystemFonts.CaptionFont; // шрифт - такой же
Height =
SystemInformation.CaptionHeight; //
задать высоту окна раную высоте
заголовка окна
timer1.Start(); //
запустить таймер
}
Сделаем еще закрытие по
правому щелчку:
private void
Form1_MouseUp(object sender, MouseEventArgs
e) {
if (e.Button ==
MouseButtons.Right) { // если правая кнопка
timer1.Stop(); //
остановить таймер
Close(); // закрыть
окно
}
}
Теперь функция
таймера. Если мы хотим проверять
состояние диска каждые 50 миллисекунд, а
положение окна (чтобы оно всегда
оставалось в заголовке) каждые 10, надо
сделать примерно так - задать интервал
таймера 10 и написать такую функцию
Tick:
internal int ticks;
private void
timer1_Tick(object sender, EventArgs e) {
ticks++;
CheckLocation(); //
проверить положение окна
if (ticks == 5) { // если 5
тиков прошло (50 миллисекунд)
ticks = 0; // обнулить
счетчик
CheckSize(); // проверить
диск
}
}
Осталось самое сложное -
научиться определять положение
нашего окна, чтобы оно попадало на
заголовок активного окна.
Для этого нам надо
определять текущее активное окно, его
размеры, видимо оно или скрыто, а также надо
определять окно десктопа и его
размеры - на случай, если активного
окна нет. Для всего этого есть функции в Windows
API, в библиотеке user32.dll. Прописываем их
вызовы:
[DllImport("user32.dll", CharSet
= CharSet.Auto)]
public static extern IntPtr
GetDesktopWindow(); // получить
указатель на окно десктопа
[DllImport("user32.dll", CharSet
= CharSet.Auto)]
public static extern IntPtr
GetForegroundWindow(); // получить
указатель на активное окно
[DllImport("user32.dll", CharSet
= CharSet.Auto)]
public static extern
bool GetWindowRect(IntPtr lpHwnd, ref
Rectangle lpRect); // получить размер окна по
указателю
[DllImport("user32.dll", CharSet
= CharSet.Auto)]
public static extern IntPtr
GetWindow(IntPtr lpHwnd, uint wCmd); // получить окно
(следующее, предыдущее, дочернее...)
[DllImport("user32.dll", CharSet
= CharSet.Auto)]
public static extern
bool IsWindowVisible(IntPtr lpHwnd); //
видимо ли окно?
Не забываем
добавить в начале кода
using
System.Runtime.InteropServices;
И пишем функцию
проверки и корректировки
положения:
internal void
CheckLocation() {
Rectangle rect = new
Rectangle(); // размер окна
IntPtr desktop_hWnd =
GetDesktopWindow(); // указатель на
дектоп
if (desktop_hWnd ==
IntPtr.Zero) { return; } // если дектоп не
определен - вернуться
IntPtr hWnd =
GetForegroundWindow(); // указатель
на активное окно
while
(!IsWindowVisible(hWnd) hWnd !=
IntPtr.Zero) { // если оно не видимо
hWnd = GetWindow(hWnd, 2);
// проверять дочерние окна, пока не надйем
видимое или пока они не кончаться
}
if (hWnd ==
IntPtr.Zero || hWnd == this.Handle) { // если
окно не найдено, или активное окно - наша
программа
if
(GetWindowRect(desktop_hWnd, ref rect)) { //
определить размер десктопа
Left = rect.Width - Width; //
расположить справа
Top = rect.Height -
Height-50; // внизу, над панелью задач
}
}
else if
(GetWindowRect(hWnd, ref rect)) { // получить
размер активного
if (rect.Left == rect.Width) { //
если окно нулевой ширины
if
(GetWindowRect(desktop_hWnd, ref rect)) {
Left = rect.Width - Width;
Top = rect.Height - Height
- 50;
}
}
Left = rect.Width -Width - 46; //
установить справа, минус кнопки
закрытия
Top = rect.Top+5; // 5
пикселей ниже границы
}
}
Обратите внимание, что
rect.Width - это на самом деле положение правой
стороны окна, а вовсе не ширина. Почему
так происходит - не знаю. Может я чего не так
сделал в вызове функций, а может это
ошибка преобразователя Microsoft из
struct rect в class Rectangle.
Вот и все с программой.
Желающие вывести что-то другое в
заголовок могут изменить функцию
CheckSize и MakeReadableSize
соответственно.
Ну, например,
свободная физическая память:
надо
декларировать структуру информации
о памяти и функцию API:
[DllImport("kernel32")]
static extern void
GlobalMemoryStatus(ref
MEMORYSTATUS buf);
[StructLayout(LayoutKind.Sequential)]
public struct
MEMORYSTATUS
{
public uint dwLength;
public uint
dwMemoryLoad;
public uint dwTotalPhys;
public uint
dwAvailPhys;
public uint
dwTotalPageFile;
public uint
dwAvailPageFile;
public uint
dwTotalVirtual;
public uint
dwAvailVirtual;
}
И изменить
функцию CheckSize:
MEMORYSTATUS memSt = new
MEMORYSTATUS();
GlobalMemoryStatus(ref memSt);
space =
memSt.dwAvailPhys;
Менять
MakeReadableSize не нужно - память в
ммегабайтах вполне читабельна :)
Мой подход - проверка
состояния по таймеру не совсем верен, ибо
более ресурсоемок, нежели
подлючение к сообщениям Windows об
изменении состояния окон и дисков. Другое
дело, что даже моя прога жрет мало, а
подключение к системным
сообщениям дело более трудное и
долгое.
Итого - программа
висит в заголовке активного окна,
если он есть. Если нет - по разному. Иногда
повисает в правом нижнем углу экрана,
иногда прячется где-то.
Каталог проекта для MS
Visual Studio 2005 с примером находится
в архиве примеров, подкаталог
hdd_in_caption.
5. "Ошкуривание"
программы.
Методов "ошкуривания"
множество, начиная от обычных тем и
заканчивая конструкторами
внешнего вида. Рассмотрим два приема - самый
простой и самый сложный, остальные
методы, в общем, похожи на
рассматриваемые. Немножко по другому
устроены стили Windows, хотя по сути они
тоже шкурка, просто чуть иначе сделанная.
В общем и целом, любая
шкурка состоит из набора графических
файлов и файлов описаний. Формат
графических файлов может быть любым, в
пределах легко доступных для чтения. В
частных случаях выбор графического
формата может быть ограничен
необходимостью сохранять значения
цветов неискаженными, что
исключает jpeg. Формат файла описания
может быть также любым - хоть текстовым, хоть
xml.
Что касается
способов хранения шкурок на диске - тут
свобода полная. Самый простой вариант -
создать каталог для шкурок, в котором для
каждой шкурки создавать свой подкаталог, и
писать в него файлы. Файл описания сохранять
в текстовом формате. Именно таким
способом буду пользоваться я в примерах.
Желающие могут вместо подкаталогов для
шкурок использовать архив-файлы или xml файлы
или все-что-угодно свое.
Метод первый
Не совсем шкурки,
скорее темы для своей программы.
Создается обычное приложение, без
поддержки Windows стилей, но
используемые цвета и фоновые картинки
для всех элементов управления берутся из
файлов. Так, или примерно так, сделаны темы
FlashGet.
Пример:
обычное приложение из
трех кнопок. Одна кнопка включает стиль 1, другая
стиль 2, третья - выход.
в Program.cs в функции
Main комментарим строчку
//Application.EnableVisualStyles();
если она есть (в Visual
Studio 2005 она прописывается
автоматически, в 2003 - нет)
Создаем класс для хранения
темы:
internal class SkinDesc1
{
internal Image bgButton1;
// фоновая картинка для кнопки 1
internal Image bgButton2;//
фоновая картинка для кнопки 2
internal Image bgButton3;//
фоновая картинка для кнопки 3
internal Image bgForm; //
фоновая картинка для формы
internal Color
colButton1; // цвет фона для кнопки 1
internal Color
colButton2;// цвет фона для кнопки 2
internal Color
colButton3;// цвет фона для кнопки 3
internal Color colForm;//
цвет фона для формы
}
Разумеется, вы можете в
класс прописать любые графические
свойства объектов - цвет шрифта, шрифт, стиль
шрифта, размер бордюра и прочее.
В классе формы
описываем нажатия кнопок, и
добавляем в конструктор загрузку
тем и их применение:
public Form1() {
InitializeComponent();
LoadSkins1(); // загрузить
темы
ApplySkin1(0); // применить
тему 1
}
private void
button1_Click(object sender, EventArgs e) { // кнопка
Стиль 1
ApplySkin1(0); // применить
стиль 1
}
private void
button2_Click(object sender, EventArgs e) { // кнопка
Стиль 2
ApplySkin1(1); // применить
стиль 2
}
private void
button3_Click(object sender, EventArgs e) { // кнопка
Выход
Close(); // закрыть
}
В нашем случае файлов
шкурки будет 5 - фоновые картинки для каждой
кнопки и для формы, и текстовый файл в
котором записаны цвета. Цвета
записаны в таком виде: 255,0,0, по одному на
строчку.
Теперь функция
загрузки тем:
internal void
LoadSkins1() {
DirectoryInfo di = new
DirectoryInfo(@"../../skins/"); // каталог со
шкурками
skins = new
SkinDesc1[di.GetDirectories().Length]; //
количество шкурок = количеству
подкаталогов
int i = 0;
string colStr = "";
string[] tsa;
foreach
(DirectoryInfo di2 in
di.GetDirectories()) { // для каждого
подкаталога
skins[i] = new SkinDesc1(); //
создать шкурку
StreamReader sr = new
StreamReader(di2.FullName + "\\colors.txt"); //
открыть файл цветов
try {
try { // считываем
цвета
colStr =
sr.ReadLine(); // строчка цвета
tsa = colStr.Split(','); //
разбить по запятым на значения RGB
skins[i].colButton1 =
Color.FromArgb(Int32.Parse(tsa[0]),
Int32.Parse(tsa[1]), Int32.Parse(tsa[2])); // создать цвет
colStr =
sr.ReadLine();
tsa =
colStr.Split(',');
skins[i].colButton2 =
Color.FromArgb(Int32.Parse(tsa[0]),
Int32.Parse(tsa[1]), Int32.Parse(tsa[2]));
colStr =
sr.ReadLine();
tsa =
colStr.Split(',');
skins[i].colButton3 =
Color.FromArgb(Int32.Parse(tsa[0]),
Int32.Parse(tsa[1]), Int32.Parse(tsa[2]));
colStr =
sr.ReadLine();
tsa =
colStr.Split(',');
skins[i].colForm =
Color.FromArgb(Int32.Parse(tsa[0]),
Int32.Parse(tsa[1]), Int32.Parse(tsa[2]));
}
catch { }
try { skins[i].bgButton1 = new
Bitmap(di2.FullName + "\\button1.jpg"); } //
загрузить картинку, если есть
catch { skins[i].bgButton1 =
null; } // если нет - поставить null
try { skins[i].bgButton2 = new
Bitmap(di2.FullName + "\\button2.jpg"); }
catch { skins[i].bgButton1 =
null; }
try { skins[i].bgButton3 = new
Bitmap(di2.FullName + "\\button3.jpg"); }
catch { skins[i].bgButton1 =
null; }
try { skins[i].bgForm = new
Bitmap(di2.FullName + "\\form.jpg"); }
catch { skins[i].bgButton1 =
null; }
}
catch { }
finally {
sr.Close();
}
i++;
}
}
И функция
применения тем:
internal void
ApplySkin1(int idx) {
button1.BackColor =
skins[idx].colButton1; // установить цвет
button2.BackColor =
skins[idx].colButton2;
button3.BackColor =
skins[idx].colButton3;
BackColor =
skins[idx].colForm;
if (skins[idx].bgButton1 !=
null) { button1.BackgroundImage =
skins[idx].bgButton1; } // если картинка
загружена - установить
else {
button1.BackgroundImage = null; } //
если нет - снять
if (skins[idx].bgButton2 !=
null) { button2.BackgroundImage =
skins[idx].bgButton2; }
else {
button2.BackgroundImage = null; }
if (skins[idx].bgButton3 !=
null) { button3.BackgroundImage =
skins[idx].bgButton3; }
else {
button3.BackgroundImage = null; }
if (skins[idx].bgForm != null) {
BackgroundImage = skins[idx].bgForm; }
else {
BackgroundImage = null; }
}
Запускаем, проверяем:
Метод второй
Уже не шкурка, скорее
конструктор внешнего вида. В файл
описания шкурки входят не только
графические параметры, но и размер и
положение элемента в форме. Для каждого
элемента имеется поддержка трех
положений - обычного,
подсвеченного и нажатого, плюс ко
всему, включена прозрачность. Так, или
примерно так, сделаны шкурки WinAmp modern и
BSPlayer.
Прозрачность в Windows
лучше всего подключать так, как это делает
Microsoft - подключаемые картинки
хранятся в формате bmp (или любом другом не
искажающем значения цветов при
сохранении), а прозрачным
объявляется один из цветов. Альфа-канал в
картинках не используется.
Структура шкурки
примерно такая: в текстовом файле
описаны все параметры формы (размер
формы и цвет, объявленный прозрачным) и все
элементы управления, которые должны
быть нарисованы. Для каждого элемента
управления задаются следующие
параметры - тип, размер, положение в
форме, цвет фона, цвет шрифта, шрифт, имена файлов для
трех положений, текст. Тип элемента
управления задает не только что это в
принципе (кнопка, ползунок и пр.), но и что это
конкретно (какая именно кнопка).
В нашем случае в
программе всего 3 разных кнопки, поэтому
типов элемента управления будет 4: по
одному на кнопку и еще один для pictureBox.
который будет основным наполнителем
графического содержания формы.
Пример:
Создаем форму и задаем
следующие свойства:
FormBorderStyle = None;
ControlBox = false;
Text = "";
Создаем класс хранения
шкурки:
internal class SkinDesc1
{
internal Color
transparentKey; // цвет объявленный
прозрачным
internal Size
formSize; // размер формы
internal ArrayList
controls; // коллекция описаний
элементов управления
internal SkinDesc1() { //
конструктор
controls = new
ArrayList();
}
}
и класс описания
элемента управления:
internal class
MyControl
{
internal byte type; // тип
internal Point
Location; // положение
internal Size Size; //
размер
internal Image normal; //
картинка нормального состояния
internal Image
highlighted; // картинка
подсвеченного состояния
internal Image pressed; //
картинка нажатого состояния
internal Color bgColor; //
цвет фона
internal Color fgColor; //
цвет шрифта
internal Font font; // шрифт
internal string Text; // текст
}
Теперь в форме создаем
поля хранения шкурки:
internal SkinDesc1[] skins; //
описания шкурок
internal int
currentSkin=-1; // текущая
шкурка
и нажатия
кнопок:
private void
button1_Click(object sender, EventArgs e) { // кнопка
1
ApplySkin1(0); // применить
стиль 1
}
private void
button2_Click(object sender, EventArgs e) { // кнопка
2
ApplySkin1(1); // применить
стиль 2
}
private void
button3_Click(object sender, EventArgs e) { // кнопка
3
Close(); // выход
}
Обратите внимание -
создаем только функции нажатия, самих
кнопок не создаем.
Также в форме надо
описать функции перетаскивания
окна, поскольку заголовок мы
отключили. Перетаскивать можно
будет за любую видимую часть формы, кроме
кнопок, т.е. в нашем случае за любой
pictureBox.
internal bool dragging; //
флаг тащат или нет
internal Point
dragStart; // точка начала
перетаскивания
private void
drag_mouseDown(object sender, MouseEventArgs
e) { // мышь нажата
dragging = true; // включить
перетаскивание
dragStart =
e.Location; // запомнить где нажали
}
private void
drag_mouseUp(object sender, MouseEventArgs e)
{ // мышь отпущена
dragging = false; //
выключить перетаскивание
}
private void
drag_mouseMove(object sender,
MouseEventArgs e) { // мышь подвинули
if (dragging) { // если
тащат
Left =
PointToScreen(e.Location).X -
dragStart.X; // переместить окно
Top =
PointToScreen(e.Location).Y -
dragStart.Y;
}
}
Ну и надо описать
функции изменения остояния кнопок -
подсветку и нажатие:
private void
mouseEnter(object sender, EventArgs e) { // мышь
над элементом управления
if (currentSkin
== -1) { return; } // если шкурка выбрана
int idx =
(int)((Control)sender).Tag; // получить индекс
элемента управления в коллекции
MyControl mc =
((MyControl)skins[currentSkin].controls[idx]);
// указатель на описание элемента
управления
if
(mc.highlighted != null) { // если картинка
подсвеченного состояния есть
if (mc.type == 0) { // если мышь
над pictureBox
((PictureBox)sender).Image =
mc.highlighted; // установить
картинку подсветки
}
else { // если над
кнопкой
((Button)sender).Image =
mc.highlighted; // установить
картинку подсветки
}
}
}
private void
mouseLeave(object sender, EventArgs e) { //
аналогично - мышь ушла с элемента
if (currentSkin
== -1) { return; }
int idx =
(int)((Control)sender).Tag;
MyControl mc =
((MyControl)skins[currentSkin].controls[idx]);
if (mc.normal != null) {
// проверка на тип
элемента - при смене шкурки событие будет
идти от элемента из другой шкурки
// что вызовет ошибку,
поэтому проверяем не только в коллекции,
но и реально
if (mc.type == 0
sender.GetType().Equals(typeof(PictureBox)))
{
((PictureBox)sender).Image =
mc.normal;
}
else if ((mc.type == 1 || mc.type ==
2 || mc.type == 3)
sender.GetType().Equals(typeof(Button)))
{
((Button)sender).Image =
mc.normal;
}
}
}
private void
mouseDown(object sender, MouseEventArgs e) {
// аналогично мышь нажата
if (currentSkin
== -1) { return; }
int idx =
(int)((Control)sender).Tag;
MyControl mc =
((MyControl)skins[currentSkin].controls[idx]);
if (mc.pressed != null) {
if (mc.type == 0) {
((PictureBox)sender).Image =
mc.pressed;
}
else {
((Button)sender).Image =
mc.pressed;
}
}
}
private void
mouseUp(object sender, MouseEventArgs e) { //
аналогично мышь отпущена
if (currentSkin
== -1) { return; }
int idx =
(int)((Control)sender).Tag;
MyControl mc =
((MyControl)skins[currentSkin].controls[idx]);
if (mc.normal != null) {
if (mc.type == 0
sender.GetType().Equals(typeof(PictureBox)))
{
((PictureBox)sender).Image =
mc.normal;
}
else if ((mc.type == 1 || mc.type ==
2 || mc.type == 3)
sender.GetType().Equals(typeof(Button)))
{
((Button)sender).Image =
mc.normal;
}
}
}
Теперь самое главное -
функции загрузки шкурки:
internal void
LoadSkins1() {
DirectoryInfo di = new
DirectoryInfo(@"../../skins/"); // каталог с
шкурками
skins = new
SkinDesc1[di.GetDirectories().Length]; //
количество шкурок = количеству
подкаталогов
int i = 0; // счетчик
string colStr = ""; //
строчка
string[] tsa, tsa2; // массив
строковых параметров
foreach
(DirectoryInfo di2 in
di.GetDirectories()) { // для каждого
подкаталога
skins[i] = new SkinDesc1(); //
создать шкурку
StreamReader sr = new
StreamReader(di2.FullName + "\\skin.txt",
System.Text.Encoding.GetEncoding(1251)); //
открыть файл описания
try {
colStr =
sr.ReadLine(); // считать строчку
tsa = colStr.Split(','); //
разбить по запятым на RGB
skins[i].transparentKey =
Color.FromArgb(Int32.Parse(tsa[0]),
Int32.Parse(tsa[1]), Int32.Parse(tsa[2])); // записать цвет
прозрачности
colStr =
sr.ReadLine();
tsa =
colStr.Split(',');
skins[i].formSize = new
Size(Int32.Parse(tsa[0]), Int32.Parse(tsa[1])); //
записать размер формы
MyControl mc; // объект
описания элемента управления
while ((colStr =
sr.ReadLine()) != "") { // пока строчки не
кончаться
tsa = colStr.Split(';'); //
разбить строку по точке с запятой
mc = new MyControl(); //
новый объект описания
mc.type = Byte.Parse(tsa[0]); //
записать тип
tsa2 = tsa[1].Split(',');
mc.Location = new
Point(Int32.Parse(tsa2[0]), Int32.Parse(tsa2[1])); //
записать положение
tsa2 = tsa[2].Split(',');
mc.Size = new
Size(Int32.Parse(tsa2[0]), Int32.Parse(tsa2[1])); //
записать размер
tsa2 = tsa[3].Split(',');
mc.bgColor =
Color.FromArgb(Int32.Parse(tsa2[0]),
Int32.Parse(tsa2[1]), Int32.Parse(tsa2[2])); // записать
цвет фона
tsa2 = tsa[4].Split(',');
mc.fgColor =
Color.FromArgb(Int32.Parse(tsa2[0]),
Int32.Parse(tsa2[1]), Int32.Parse(tsa2[2])); // записать
цвет шрифта
try { mc.normal = new
Bitmap(di2.FullName + "\" + tsa[5]); } // загрузить
картинку нормального состояния
catch { mc.normal = null; } //
если не получилось - установить null
try {
mc.highlighted = new Bitmap(di2.FullName +
"\" + tsa[6]); } // то же для подсвеченного
catch {
mc.highlighted = null; }
try { mc.pressed = new
Bitmap(di2.FullName + "\" + tsa[7]); } // то же для
нажатого
catch { mc.pressed = null;
}
tsa2 = tsa[8].Split(',');
mc.font = new Font(tsa2[0],
float.Parse(tsa2[1]),
(FontStyle)Int32.Parse(tsa2[2])); // создать
шрифт по параметрам
mc.Text = tsa[9]; //
записать текст
skins[i].controls.Add(mc); //
добавить объект описания в коллекцию
}
}
catch { }
finally {
sr.Close();
}
i++;
}
}
и применения:
internal void
ApplySkin1(int idx) {
Controls.Clear(); // убрать все
элементы управления с формы
TransparencyKey =
skins[idx].transparentKey; // установить
цвет прозрачности
BackColor =
TransparencyKey; // фон формы =
прозрачности
Size =
skins[idx].formSize; // установить размер
формы
for (int i = 0; i ‹
skins[idx].controls.Count; i++) { // для каждого
элемента управления из коллекции
if
(((MyControl)skins[idx].controls[i]).type == 0) { //
если элемент - pictureBox
PictureBox p = new
PictureBox(); // создать
p.Margin = new Padding(0); //
установить поля = 0
p.BorderStyle =
BorderStyle.None; // бордюра нет
p.Location =
((MyControl)skins[idx].controls[i]).Location;
// установить положение
p.Size =
((MyControl)skins[idx].controls[i]).Size; //
размер
p.BackColor =
((MyControl)skins[idx].controls[i]).bgColor; //
фон
p.Image =
((MyControl)skins[idx].controls[i]).normal; //
картинку
p.MouseDown += new
MouseEventHandler(drag_mouseDown); //
события перетаскивания
p.MouseUp += new
MouseEventHandler(drag_mouseUp);
p.MouseMove += new
MouseEventHandler(drag_mouseMove);
p.Tag = i; // индекс в
коллекции
Controls.Add(p); //
добавить к форме
}
else { // если кнопка
(любая из трех)
Button b = new Button(); //
создать
b.FlatStyle =
FlatStyle.Flat; // сделать плоской
b.FlatAppearance.BorderSize = 0; //
без бордюра
b.FlatAppearance.MouseOverBackColor =
skins[idx].transparentKey; // с прозрачным
бордюром
b.FlatAppearance.MouseDownBackColor =
skins[idx].transparentKey; // во всех
состояниях
b.Margin = new Padding(0); //
без полей
b.Location =
((MyControl)skins[idx].controls[i]).Location;
// расположить
b.Size =
((MyControl)skins[idx].controls[i]).Size; //
задать размер
b.BackColor =
((MyControl)skins[idx].controls[i]).bgColor; //
установить цвет фона
b.ForeColor =
((MyControl)skins[idx].controls[i]).fgColor; // цвет
шрифта
b.Font =
((MyControl)skins[idx].controls[i]).font; // шрифт
b.TextAlign =
ContentAlignment.MiddleCenter; //
выравнивание текста
b.Image =
((MyControl)skins[idx].controls[i]).normal; //
картинку
if
(((MyControl)skins[idx].controls[i]).type == 1) { //
если кнопка 1
b.Click += new
EventHandler(button1_Click); //
установить нажатие = нажатие кнопки
1
}
else if
(((MyControl)skins[idx].controls[i]).type == 2) { //
аналогично кнопка 2
b.Click += new
EventHandler(button2_Click);
}
else if
(((MyControl)skins[idx].controls[i]).type == 3) { //
аналогично кнопка 3
b.Click += new
EventHandler(button3_Click);
}
b.Text =
((MyControl)skins[idx].controls[i]).Text; //
установить текст
b.MouseDown += new
MouseEventHandler(mouseDown); //
события
b.MouseUp += new
MouseEventHandler(mouseUp); //
смены
b.MouseEnter += new
EventHandler(mouseEnter); //
картинок
b.MouseLeave += new
EventHandler(mouseLeave);
b.Tag = i; // индекс в
коллекции
Controls.Add(b); //
добавить к форме
}
}
currentSkin = idx; // сменить
индекс текущей шкурки
}
Добавим в
конструктор вызов этих двух
функций:
public Form1() {
InitializeComponent();
LoadSkins1(); // загрузить
шкурки
ApplySkin1(0); // применить
первую
}
Запускаем,
проверяем:
Рыжие кружочки
нажимаются и подсвечиваются,
за морковные линии можно
перетаскивать.
Девушка вся
перетаскивается, крылышки
приподнимаются при наведении. При
нажатии на крылышки - программа
выходит.
Можно было конечно
добавить возможность установки
фонового изображения для формы, и не
маятся с pictureBox, но с таким подходом
возможностей чуть больше.
Каталог проекта для MS
Visual Studio 2005 с примером находится
в архиве примеров, подкаталог skins1 для
первого метода и skins2 для второго.
6. Перехватчик всех
ошибок.
В современных
программах стало модно делать окошки
которые настоятельно рекомендуют
отправить отчет, предлагают
сохранить работу и перезапустить
программу после закрытия. Такое окно
появляется во всех приложениях
Microsoft, сделанных после 2002 года и еще в очень
многих. Если вы хотите сделать что-то
подобное, то это делается примерно так:
Сначала создадим
форму, с одной кнопкой - чтобы только
имитировать ошибку:
Теперь на кнопку
сажаем такой код:
private void
button1_Click(object sender, EventArgs e) {
throw new
ArgumentException("Неправильное
значение параметра", "index");
}
При нажатии - кинуть
ошибку о том что параметр index принял
недопустимое значение.
Теперь идем в файл Program.cs,
туда, где функция Main, и
переписываем ее:
static void Main() {
Application.SetUnhandledExceptionMode(UnhandledExceptionMode.CatchException);
//отлавливать все ошибки, вышедшие из
программы
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
Form1 f1 = new Form1();
Application.ThreadException += new
System.Threading.ThreadExceptionEventHandler(Application_ThreadException);//
задать обработчик ошибок
Application.Run(f1);
}
Теперь опишем функцию
обработки ошибок. Будем исходить из
того, что у нас будет форма для показа
информации об ошибке:
static void
Application_ThreadException(object
sender,
System.Threading.ThreadExceptionEventArgs e)
{
ApplicationCrashForm cf = new
ApplicationCrashForm(); // форма
показа информации об ошибке
cf.FillData(e.Exception); //
заполнить форму информацией
if (cf.ShowDialog() ==
DialogResult.OK) { // если нажали
сохранить
((Form1)Application.OpenForms["Form1"]).SaveAs();
// сохранить работу
}
if
(cf.checkBox1.Checked)
Application.Restart(); // если галочка
перезапустить установлена -
перезапустить программу
else
Application.Exit(); // если нет - просто
выйти
}
Ну собственно,
осталось только создать форму показа
ошибок:
Не забудьте
поставить для checkBox1
Modifiers=internal.
Теперь опишем код
формы:
internal void
FillData(Exception ex) {
StringBuilder sb = new
StringBuilder();
FillExceptionInfo(ex, sb);
textBox1.Text =
sb.ToString();
textBox1.SelectionLength = 0;
}
private void
FillExceptionInfo(Exception ex,
StringBuilder sb) {
sb.AppendFormat("Произошла
ошибка типа {0}\r\n", ex.GetType());
sb.AppendFormat("Объект
вызвавший ошибку: {0}\r\n", ex.Source);
sb.AppendFormat("Ошибка
произошла в методе {0}\r\n",
ex.TargetSite);
sb.AppendFormat("Основная
информация об ошибке: {0}\r\n", ex.Message);
sb.AppendFormat("Стек
вызова: {0}\r\n\r\n", ex.StackTrace);
if
(ex.InnerException != null)
FillExceptionInfo(ex.InnerException,
sb);
}
Думаю тут все
понятно.
Последние штрихи -
функции нажатия на линки в форме
ошибки:
private void
linkLabel1_LinkClicked(object
sender,
LinkLabelLinkClickedEventArgs
e) {
Process.Start("http://www.domain.com/techsupport_forum.cgi");
}
private void
linkLabel2_LinkClicked(object
sender,
LinkLabelLinkClickedEventArgs
e) {
StringBuilder sb = new
StringBuilder();
sb.Append("mailto:techsupport@domain.com?subject=Program_Bugbody=");
string ts =
textBox1.Text.Replace("\r\n", "‹br›");
string amp =
Uri.HexEscape('');
ts = ts.Replace("",
amp);
sb.Append(ts);
Process.Start(sb.ToString());
}
И функцию
сохранения работы SaveAs() для Form1 не
забудьте сделать.
Вот и все.
Каталог проекта для MS
Visual Studio 2005 с примером находится
в архиве примеров, подкаталог
exception.
7. Самодельный
бекапер.
У меня однажды на
повестке дня встал вопрос о
программе-бекапере, которая будет
делать следующие вещи:
1. Копировать все
файлы из указанных каталогов, чья дата
последнего изменения позднее
заданной даты (дня, когда я пересел на
ноут).
2. Для уже
скопированных файлов - копировать
файлы, только если дата изменения
позднее, чем у скопированной копии.
3. Каталог для
сохранения должно быть можно выбрать и в
локальной сети.
В очередной раз
напоролся: работы было много и я решил
что быстрее найду, чем напишу сам... поискал
- нашел с три десятка подобных программ.
Попробовал одну - не делает всего, что
надо, другую - не поддерживает сеть, третью -
хочет денег, хотя на сайте сказано
"бесплатно"... короче, потеряв час, я
решил что быстрее все-таки написать
самому.
Итак, создаем
примерно такое окно.
То, что внизу - это
progressBar. Дополняем окно
backgroundWorker и
folderBrowserCatalog.
Установим progressBar.Visible =
false;
Теперь создадим
настройки программы. Нам надо будет
помнить - каталоги, которые надо
спасать; каталог, куда надо спасать; дату,
после которой надо спасать.
Открываем Settings
проекта (Project Properties -› Settings)
и создаем такое вот:
DirsToSave - это
System.Collections.Specialized.StringCollection
SinceWhen -
System.DateTime
Значения пишите любые,
но действительные и не оставляйте поля
пустыми.
Переходим к коду
программы. Для начала создадим объект
settings:
SynchroSaver.Properties.Settings
sets;
Теперь опишем код
загрузки формы - инициализировать
backgroundWorker и загрузить
настройки.
private void
Form1_Load(object sender, EventArgs e) { //
загрузка формы
sets =
SynchroSaver.Properties.Settings.Default;
// загружаем настройки
for (int i = 0; i ‹
sets.DirsToSave.Count; i++) { // для каждого
каталога на сохранение
listBox1.Items.Add(sets.DirsToSave[i]); //
добавить в listBox
}
textBox1.Text =
sets.BackupDir; // написать каталог куда
сохранять
dateTimePicker1.Value =
sets.SinceWhen; // установить дату
}
Как следствие -
пропишем и закрытие формы:
private void
Form1_FormClosing(object sender,
FormClosingEventArgs e) {
if (e.CloseReason ==
CloseReason.WindowsShutDown) { //
если Windows закрывается
if
(backgroundWorker1.IsBusy) { // если
программа работает
backgroundWorker1.CancelAsync(); //
послать отмену
}
else { // если не
работает
sets.Save(); // сохранить
текущие настройки
}
}
else { // если прогу
закрывает не система
if
(backgroundWorker1.IsBusy) { // если
работает
e.Cancel = true; //
отменить закрытие
return;
}
else { // если не
работает
sets.Save(); // сохранить
настройки
}
}
}
Теперь опишем кнопки:
Добавить
каталог:
private void
button1_Click(object sender, EventArgs e) {
if
(folderBrowserDialog1.ShowDialog() ==
DialogResult.OK) { // если каталог
выбрали
listBox1.Items.Add(folderBrowserDialog1.SelectedPath);
// добавить в listBox
}
}
Убрать каталог:
private void
button2_Click(object sender, EventArgs e) {
if
(listBox1.SelectedIndex != -1) { // если
каталог выделен
listBox1.Items.RemoveAt(listBox1.SelectedIndex);
// убрать его из списка
}
}
Обзор... для каталога
сохранения:
private void
button3_Click(object sender, EventArgs e) {
if
(folderBrowserDialog1.ShowDialog() ==
DialogResult.OK) { // если каталог
выбрали
textBox1.Text =
folderBrowserDialog1.SelectedPath;
// прописать в textBox
}
}
Запуск:
проверяем ввод,
отключаем элементы управления,
подготавливаем к запуску и
запускаем...
Эта же кнопка будет
кнопкой Отмена, если прога уже
работает
private void
button4_Click(object sender, EventArgs e) {
if
(!backgroundWorker1.IsBusy) { // если
прога не работает
if
(listBox1.Items.Count == 0) { // если
каталогов нет
return; // вернуться
}
if
(!Directory.Exists(textBox1.Text)) return; //
если каталог куда спасать не
существует - вернуться
DisableControls(); //
отключить элементы управления
backgroundWorker1.DoWork += new
DoWorkEventHandler(backgroundWorker1_DoWork);
// назначить event начала работы
backgroundWorker1.ProgressChanged +=
new
ProgressChangedEventHandler(backgroundWorker1_ProgressChanged);
// назначить event изменения прогресса
backgroundWorker1.RunWorkerCompleted
+= new
RunWorkerCompletedEventHandler(backgroundWorker1_RunWorkerCompleted);
// назначить event окончания работы
sets.SinceWhen =
dateTimePicker1.Value; // изменить
дату в настройках на введенную
sets.BackupDir =
textBox1.Text; // каталог куда спасать
тоже
sets.DirsToSave.Clear(); //
очистить список каталогов
for (int i = 0; i ‹
listBox1.Items.Count; i++) { // поштучно
sets.DirsToSave.Add(listBox1.Items[i].ToString());
// добавить каталоги в настройки
}
backgroundWorker1.RunWorkerAsync();
// запустить работника
}
else { // если прога
работает
backgroundWorker1.CancelAsync(); //
послать отмену
}
}
Ну теперь опишем
заданные события для
backgroundWorker:
Начать работу:
void
backgroundWorker1_DoWork(object
sender, DoWorkEventArgs e) {
backgroundWorker1.ReportProgress(0);
// установить прогресс 0
for (int i = 0; i ‹
sets.DirsToSave.Count; i++) { // для каждого
каталога для сохранения
BackupDir(sets.DirsToSave[i].ToString(), i
* 100 / sets.DirsToSave.Count, 100 /
sets.DirsToSave.Count); // спасти
каталог
if
(backgroundWorker1.CancellationPending)
{ // если послали отмену
break;
}
}
backgroundWorker1.ReportProgress(100); //
установить прогресс 100
}
Изменился прогресс:
void
backgroundWorker1_ProgressChanged(object
sender, ProgressChangedEventArgs e) {
progressBar1.Value =
e.ProgressPercentage; // установить
значение progressBar = прогрессу
работника
}
Работа
завершена:
void
backgroundWorker1_RunWorkerCompleted(object
sender,
RunWorkerCompletedEventArgs e)
{
EnableControls(); // включить
элементы управления
}
Отключение элементов:
отключаем все, кроме кнопки
запуск, которую меняем на Отмену
private void
DisableControls() {
button1.Enabled = false;
button2.Enabled = false;
button3.Enabled = false;
progressBar1.Visible = true;
listBox1.Enabled = false;
textBox1.Enabled = false;
dateTimePicker1.Enabled =
false;
button4.Text = "Cancel";
}
Включение
элементов:
private void
EnableControls() {
button1.Enabled = true;
button2.Enabled = true;
button3.Enabled = true;
progressBar1.Visible =
false;
listBox1.Enabled = true;
textBox1.Enabled = true;
dateTimePicker1.Enabled = true;
button4.Text = "RUN";
}
Теперь самое главное:
Бекап каталога:
Поскольку сохранение
каталогов требует рекурсии, эта
функция будет запускаться для
каталогов всех уровней. Для этого и созданы
парамеры progrStart - начальный
прогресс и progr - доля этого каталога в
общем прогрессе:
private void
BackupDir(string dir, float progrStart, float
progr) {
DirectoryInfo mdi = new
DirectoryInfo(dir); // информация о
каталоге
DirectoryInfo[] dis =
mdi.GetDirectories(); // вложенные
папки
for (int i = 0; i ‹ dis.Length; i++)
{ // для каждой вложенной папки
BackupDir(dis[i].FullName,
progrStart + i * progr / dis.Length, progr / dis.Length); //
спасти
}
FileInfo[] fis =
mdi.GetFiles(); // файлы в каталоге
for (int i = 0; i ‹ fis.Length; i++)
{ // для каждого файла
if
(fis[i].LastWriteTime › sets.SinceWhen)
{ // если последнее изменение позднее
указанной даты
if
(NeedToCopy(fis[i].FullName)) { // сравнить с
бекап версией и если надо
копировать
CopyFile(fis[i].FullName); //
скопировать файл
}
}
}
backgroundWorker1.ReportProgress((int)(progrStart
+ progr)); // отчитаться о прогрессе
}
Проверка на
необходимость копирования:
private bool
NeedToCopy(string fn) {
string fnr = fn;
for (int i = 0; i ‹
sets.DirsToSave.Count; i++) { // для каждого
каталога на спасение
if
(fnr.Contains(sets.DirsToSave[i].ToString()))
{ // если путь файла содержит имя
каталога
fnr =
fnr.Replace(sets.DirsToSave[i].ToString(),
sets.BackupDir); // заменить каталог
откуда на каталог куда
break; // прервать цикл
}
}
if (File.Exists(fnr)) { //
если файл существует, т.е. уже
скопирован
FileInfo fi = new
FileInfo(fn); // информация о рабочем
файле
FileInfo fi2 = new
FileInfo(fnr); // информация о спасенном
файле
if
(fi.LastWriteTime ›
fi2.LastWriteTime) { // если файл был
изменен после спасения
return true; // вернуть
правду
}
else { // если нет
return false; // вернуть
ложь
}
}
else return true; // если
файл не существует - вернуть правду
}
Копирование:
private void
CopyFile(string fn) {
string fnr = fn;
for (int i = 0; i ‹
sets.DirsToSave.Count; i++) { // то же, что и пред.
функция
if
(fnr.Contains(sets.DirsToSave[i].ToString()))
{
fnr =
fnr.Replace(sets.DirsToSave[i].ToString(),
sets.BackupDir);
break;
}
}
if
(!Directory.Exists(Path.GetDirectoryName(fnr)))
{ // если каталог не существует
Directory.CreateDirectory(Path.GetDirectoryName(fnr));
// создать
}
File.Copy(fn, fnr, true); //
скопировать, с перезаписью
}
Вот собственно и
все.
Что сделано
неправильно, с точки зрения
коммерческого продукта - отмена
работает только когда программа
переходит от одного главного
каталога к другому главному.
Что неправильно, с
точки зрения "грамотной" программы -
имена каталогов не передаются
функциям, а те простым перебором
находят нужный.
Каталог проекта для MS
Visual Studio 2005 с примером находится
в архиве примеров, подкаталог
SynchroSaver.