- •Ползунки
- •Инвертирование цветов и вывод цветовых констант
- •Отображение оттенков серого цвета
- •Вывод цветовых имен
- •Связывание компонентов с подписями к ним
- •Привязка компонентов
- •2 Работа с графическими файлами, рисование тонким пером
- •Создание, сохранение и загрузка графических файлов
- •Рисование пером
- •Отслеживание текущих координат изображения
- •Очистка изображения
- •Рисование цветным пером
- •Рисование прямых линий
- •Настройка фонового цвета
- •Рисование прямоугольников
- •Рисование эллипсов
- •Рисование прозрачных фигур
- •Рисование квадратов и окружностей
- •Отмена предыдущей операции
- •Задание цветов с помощью пипетки
- •Добавление в рисунок текста
- •Настройка стиля изображения линии
2 Работа с графическими файлами, рисование тонким пером
Проект PNGEDIT посвящен работе с графической библиотекой GDI+, используемой в .NET-приложениях. В нем рассматриваются основные действия с графическими файлами (создание, сохранение и загрузка) и описываются особенности основных графических Классов: Image, Bitmap, Graphics, PictureBox.
В проекте Также реализуется отслеживание текущих координат изображения, рисование тонким пером (с использованием классов Pen и Pens) и очистка изображения.
Создание, сохранение и загрузка графических файлов
После создания проекта PNGEDIT добавьте в форму Form1 три кнопки (button1—button3), панель panel1 и невизуальные компоненты типа OpenFileDialog и SaveFileDialog (эти компоненты получат имена openFileDialog1 И saveFileDialog1; они будут размещены В области невизуальных компонентов под изображением формы). На панели panel1 разместите компонент типа PictureBox (он получит имя pictureBox1). Настройте свойства формы Form1 и всех добавленных компонентов (листинг 1).
Добавьте к проекту новую форму (она получит имя Form2) и разместите в ней две метки (label1 и 1аbеl2), два компонента типа NumericUpDown (numericUpDown1 и numericUpDown2) и две кнопки (button1 и button2). Настройте свойства формы Form2 и ее компонентов (листинг 2) и расположите компоненты в соответствии с рис. 2 (компонент numericUpDown1 должен находиться рядом с меткой label1, а компонент numericUpDown2 — рядом С меткой 1аbе12).
В начало файла Form1.cs добавьте оператор
using System.IO;
В описание класса Form1 добавьте поле
private Form2 form2 = new Form2() ;
В конструктор класса Form1 добавьте новые операторы (листинг 3) и определите обработчики события Click для кнопок button1, button2 И button3, размещенных в форме Form1 (листинг 4).
Рис. 1. Вид формы 1 для проекта PNGEDIT
Рис. 2. Вид формы 2 для проекта PNGEDIT
Листинг 1. Настройка роиста формы Form1 и ее компонентов ;
Form1: Text -= Image Editor, MaximizeBox = False,
FormBorderStyle = FixedSingle, StartPosition = CenterScreen
button1: Text = &New
button2: Text = &Open
button3: Text = &Save
panel1: AutoScroll = True, BorderStyle = Fixed3D
pictureBox1: Location = 0; 0, SizeMode= AutoSize
openFileDialogl: DefaultExt = png, FileName = пустая строка, Filter = Image files (*.bmp, *.jpg, *.png, *.gif)|*.bmp;*.jpg;*.png;*.gif
saveFileDialogl: DefaultExt = png, Filter = PNG-files (*.png)|*.png
Листинг 2. Настройка свойств формы Form2 и ее компонентов
Form2: Text = New Image, MaximizeBox = False, MinimizeBox = False, FormBorderStyle = FixedDialog, StartPosition = CenterScreen, ShowInTaskbar = False, AcceptButton = button1, CancelButton = button2
label1: Text = Width:
label2: Text = Height:
numericUpDownl: Minimum = 10, Maximum = 800, Increment = 10,
Modifiers = Internal
numericUpDown2: Minimum = 10, Maximum = 600, Increment = 10,
Modifiers = Internal
button1: Text = OK, DialogResult = OK
button2: Text = Cancel
Листинг 3. Новый вариант конструктора формы Form1
public Form1 ()
{InitializeComponent();
AddOwnedForm (form2) ;
openFileDialogl.InitialDirectory = saveFileDialogl. InitialDirectory = Directory.GetCurrentDirectory();
form2.numericUpDownl.Value = panel1. ClientSize .Width ; f orm2 . numericUpDown2 . Value = panel1. ClientSize. Height ;}
Листинг 4. Обработчики button1_Click, button2_Click И button3_Click (для кнопок формы Form1)
private void buttonl_Click(object sender, EventArgs e)
{form2.ActiveControl = form2.numericUpDownl;
if (form2. ShowDialog() = DialogResult.OK)
{saveFileDialogl. FileName = 1111;
Text = "Image Editor";
int w = (int) form2 .numericUpDownl .Value,
h = (int) form2 . numericUpDown2 .Value;
Image im = new Bitmap(w, h);
Graphics g = Graphics.Fromlmage(im);
g.Clear(Color .White);
g.Dispose();
if (pictureBox1.Image != null) pictureBox1.Image.Dispose(); pictureBox1. Image = im;} }
private void button2_Click(object sender, EventArgs e)
{if (openFileDialogl. ShowDialog () = DialogResult. OK)
{string s = openFileDialogl.FileName;
if (pictureBox1.Image != null) pictureBox1.Image.Dispose();
pictureBox1. Image = new Bitmap (s) ;
Text = "Image Editor - " + s;
saveFileDialogl.FileName = Path.ChangeExtension(s, "png");
openFileDialogl.FileName = ""; }}
private void button3_Click(object sender, EventArgs e)
{if (saveFileDialogl. ShowDialog () = DialogResult. OK)
{string s = saveFileDialogl.FileName; pictureBox1.Image.Save(s);
Text = "Image Editor - " + s; }}
В редактор можно загружать графические файлы формата BMP, JPG, PNG, GIF любого размера (кнопка Open). Если размер файла превосходит размер выделенной для его отображения панели panel1, то панель снабжается полосами прокрутки, позволяющими перемещаться к любому фрагменту изображения. При загрузке файла его полное имя отображается в заголовке окна. Загруженный файл можно сохранить под новым именем в формате PNG (кнопка Save). В качестве каталога для загруженных и сохраняемых файлов по умолчанию предлагается рабочий каталог приложения. Кроме того, редактор позволяет создавать новые изображения (кнопка New). Размер нового изображения (в пикселах) запрашивается в диалоговом окне New Image и может изменяться от 10 х 10 до 800 х 600. При первом отображении диалогового окна предлагаются размеры, обеспечивающие заполнение всей отображаемой части панели panel1; в дальнейшем в окне сохраняются размеры, указанные пользователем. Созданное изображение автоматически закрашивается белым цветом.
При запуске программы изображение в редакторе отсутствует.
Чтобы не дублировать код, отвечающий за создание нового изображения (см. метод button1 click в листинге 4), "делегируем" задачу создания нового изображения форме form2. Для этого определим обработчик события Click для кнопки button1, размещенной в форме Form2 (листинг 5), и изменим метод button1 click формы Form1 (листинг 6; в тексте метода следует удалить несколько операторов, ничего не добавляя). Кроме того, в конструктор класса Form1 добавим новый оператор:
form2.buttonl_Click(this, null);
Листинг 5. Обработчик button1. Click (для кнопки формы Form2)
internal void buttonl_Click(object sender, EventArgs e)
{int w = (int)numericUpDownl.Value, h = (int)numericUpDown2.Value;
Image im = new Bitmap(w, h);
Graphics g = Graphics.Fromlmage(im); g.Clear(Color.White); g. Dispose();
PictureBox p = Owner.Controls["panel1"].Controls["pictureBox1"] as PictureBox; if (p.Image != null)
р. Image. Dispose () ; p. Image = im;}
Листинг 6. Новый вариант метода buttonl_click формы Form1
private void buttonl_Click(object sender, EventArgs e)
{form2.ActiveControl = form2.numericUpDownl; if (form2.ShowDialog() == DialogResult.OK)
{saveFileDialogl.FileName = "";
Text = "Image Editor"; }}
Теперь при запуске программы в ней автоматически создается новое изображение, размеры которого совпадают с размерами клиентской части панели, содержащей данное изображение.
При попытке открыть файл, имеющий расширение графического файла, но содержащий данные в неверном формате (например, пустой файл с именем empty.bmp), возникает ошибка времени выполнения ArgumentException с сообщением "Parameter is not valid" (имеется в виду параметр s конструктора Bitmap). Ясно, что в подобной ситуации достаточно вывести сообщение об ошибке, однако это сообщение должно быть более информативным.
Измените метод button2_click формы Form1 (листинг 7).
Листинг 7. Новый вариант метода button2_Click формы Form1
private void button2_Click(object sender, EventArgs e)
{if (openFileDialogl.ShowDialog() == DialogResult.OK)
{string s = openFileDialogl.FileName; try {
Image im = new Bitmap (s) ;
if (pictureBox1.Image != null)
pictureBox1.Image.Dispose() ; pictureBox1. Image = im;}
catch
{MessageBox.Show("File " + s + " has a wrong format.","Error"); return;}
Text = "Image Editor - " + s;
saveFileDialogl.FileName = Path.ChangeExtension(s, "png");
openFileDialogl.FileName = "";}
Теперь при попытке загрузить файл в неверном формате выводится сообщение ’’File <имя файла> has a wrong format”, причем прерывание работы программы не происходит.
Как обычно, в подобных ситуациях для перехвата ошибок используется механизм явной обработки исключений.
При попытке сохранить загруженный файл под тем же именем (и с тем же расширением) возникает ошибка времени выполнения ExternaiException с сообщением "A generic error occurred in GDI+". В документации .NET, посвященной методу Save класса image, сказано, что сохранить изображение в том же файле, из которого оно было загружено, нельзя. Таким образом, выявленная ошибка связана с особенностями реализации класса image.
Измените метод button3_click формы Form1 (листинг 8).
Листинг 8. Новый вариант метода button3_Click формы Form1
private void button3_Click(object sender, EventArgs e)
{string sO = saveFileDialogl.FileName;
if (saveFileDialogl.ShowDialog() == DialogResult.OK)
{string s = saveFileDialogl.FileName; if (s.ToUpper() = sO.ToUpper())
{sO = Path.GetDirectoryName(sO) + "\\ ($$##$$) .png";
pictureBox1.Image.Save(sO);
pictureBox1.Image.Dispose();
File.Delete(s);
File.Move (sO, s);
pictureBox1.Image = new Bitmap(s);}
else
pictureBox1.Image.Save(s);
Text = "Image Editor - " + s;}}
Теперь при попытке сохранить изображение под тем же именем (и в том же каталоге) вначале выполняется сохранение изображения во временном файле, после чего объект Bitmap, связанный с данным изображением, разрушается с помощью метода Dispose, исходный файл с изображением удаляется с диска (методом Delete класса File из пространства имен System,IO), а имя временного файла заменяется на имя исходного (методом Move класса File). После этого в компонент pictureBox1 загружается новый вариант файла. Подчеркнем, что до разрушения объекта Bitmap связанный с ним файл удалить с диска нельзя, так как доступ к этому файлу заблокирован.
В конструкторе формы Form1 (см. листинг 3) для определения рабочего каталога приложения используется метод GetCurrentDirectory класса Directory из пространства имен System.IO. Вместо него можно было бы использовать свойство Current Directory класса Environment.
Для отображения на экране стандартного диалогового окна, связанного с компонентом OpenFileDialog или SaveFileDialog, необходимо вызвать метод ShowDialog этого компонента. Если окно диалога закрывается кнопкой Открыть (для OpenFileDialog) или Сохранить (для SaveFileDialog), то метод ShowDialog возвращает значение DialogResult.Oк, а имя выбранного файла сохраняется в
свойстве FileName. Если диалог закрыт кнопкой отмены, то метод ShowDialog возвращает DialogResult .Cancel. Заметим,что метод ShowDialog имеется У всех форм (см. метод buttonl_Click в листинге 4).
Из часто используемых свойств компонентов OpenFileDialog и SaveFileDialog отметим DefaultExt (расширение файла по умолчанию; точка в начале расширения не указывается), Filter (фильтры для файлов, отображаемых в диалоговом окне; для каждого фильтра вначале указывается его описание, а затем, после вертикальной черты, — сам фильтр в виде списка масок файлов через точку с запятой; символ "вертикальная черта" также используется ДЛЯ отделения фильтров друг от друга), InitialDirectory (начальный каталог, отображаемый при открытии диалогового окна—см. листинг 3). Обратите внимание на очистку свойства FileName компонента openFileDialog1 в конце метода button2_click. Это делается для того, чтобы при следующем открытии окна диалога в поле ввода не содержалось имя уже загруженного файла. Здесь же определяется имя, которое будет предложено при сохранении загруженного файла (это свойство FileName компонента saveFileDialog1). Поскольку сохранение всегда выполняется в PNG-формате, расширение в имени загруженного файла заменяется на png (для этого используется статический метод changeExtension класса Path из пространства имен System.IO).
Компонент NumericUpDown, называемый счетчиком с прокруткой (spin control), позволяет задавать числа из определенного диапазона, используя как явный ввод с клавиатуры, так и вспомогательные кнопки со стрелками для увеличения/уменьшения значения счетчика на фиксированную величину. Диапазон допустимых значений определяют свойства Minimum и Maximum (значения по умолчанию 0 и 100 соответственно), шаг изменения счетчика — свойство increment (значение по умолчанию l), а текущее значение счетчика — свойство value (значение по умолчанию 0). Все эти свойства имеют тип decimal, что позволяет использовать счетчик для задания не только целых, но и дробных чисел. Количество знаков в дробной части числа определяется свойством DecimaiPiaces (значение по
умолчанию 0, в этом случае разрешен ввод только целых чисел). Поскольку свойство Value имеет тип decimal, необходимо выполнять явное преобразование типа при присваивании значения этого свойства переменным других числовых типов (см., например, первый вариант метода button1_click формы Form1, приведенный в листинге 4).
Имеется также компонент DomainUpDown, предназначенный для прокрутки текстовых строк. Этот компонент используется достаточно редко, поскольку по удобству использования он уступает компоненту ComboBox имеющему аналогичные функциональные возможности.
Следует четко понимать роли объектов, участвующих в выводе и обработке изображения. Для визуализации любого изображения проще всего использовать компонент PictureBox, являющийся графическим контейнером. Само изображение входит в компонент PictureBox как свойство image и является объектом одного из классов — потомков абстрактного класса image. Одним из таких потомков является класс Bitmap, позволяющий хранить растровые изображения. Класс Bitmap можно создать с помощью конструктора, указав размеры изображения (это делается в первом варианте метода button1_Click формы Form1, а затем, после исправления недочета, в одноименном методе формы Form2). Кроме того, имеется вариант конструктора класса Bitmap, в котором можно указать имя существующего графического файла (отметим, что данный конструктор распознает большинство графических форматов; причем он ориентируется не на расширение, а на содержимое графического файла). Этот вариант конструктора использован в обработчике button2_Click. Созданный объект типа Bitmap достаточно присвоить свойству image компонента PictureBox; при этом он будет сразу изображен на экране.
Объект Bitmap можно в дальнейшем сохранить в файле с указанным именем, используя метод save (см. обработчик button3_click); в качестве второго, необязательного параметра можно указать графический формат, который надо
применять при сохранении. Формат задается с помощью статических свойств класса ImageFormat, определенного В пространстве имен System.Drawing.imaging. Перечислим свойства класса ImageFormat, связанные с наиболее распространенными растровыми графическими форматами: Bmp, Gif, icon, Jpeg, Png. Если при сохранении изображения его формат не указывается (как в нашей программе), то по умолчанию используется формат Png.
В то время как за сохранение изображения отвечает сам объект типа Bitmap, за рисование (на изображении, на форме или на компоненте) отвечает особый объект — графическая канва типа Graphics. Именно в классе Graphics реализованы все методы рисования (в том числе и метод clear, использованный в нашей программе и позволяющий закрасить все изображение указанным цветом). В отличие от объектов класса Bitmap, объекты класса Graphics создаются не с помощью конструктора, а другими способами. Так, для получения объекта Graphics, связанного с растровым изображением, надо использовать статический метод Fromimage класса Graphics с параметром — объектом типа Bitmap, в котором хранится требуемое изображение.
Для получения объекта Graphics, связанного с визуальным компонентом, можно использовать метод CreateGraphics этого компонента. В обработчиках события Paint объект Graphics дается в качестве одного из свойств второго параметра е, а именно — е.Graphics.
Объекты типа Graphics являются "короткоживущими" объектами (в отличие, например, от компонентов или объектов типа Bitmap). Объект Graphics должен быть создан непосредственно перед выполнением требуемых действий по рисованию, а после выполнения этих действий его надо разрушить с помощью метода Dispose. Следует заметить, что остальные объекты, связанные с графикой, также имеют метод Dispose, который необходимо вызывать после окончания работы с объектом. В частности, метод Dispose надо вызывать для прежнего изображения Bitmap перед подключением к компоненту PictureBox нового изображения. Метод Dispose для
компонента PictureBox явно вызывать, как правило, не требуется: этот компонент существует на протяжении выполнения программы, а при ее завершении автоматически разрушается вместе со всеми компонентами и формами приложения.
Обратите внимание на способ доступа к компоненту pictureBox1 формы Form1 из метода buttonl_Click формы Form2 (см. листинг 5): поскольку этот компонент принадлежит не форме Form1, а панели panel1, которая, в свою очередь, принадлежит форме Form1, для доступа к этому компоненту пришлось последовательно вызвать два свойства Controls (первое — для формы, второе — для ее дочернего компонента panel1). Для возможности вызова из формы Form1 обработчика button1_click, определенного в форме Form2, его модификатор доступа был изменен с private на internal (см. листинг 5). Благодаря наличию обработчика button1_click для формы Form2, выполняющегося при закрытии диалогового окна New Image кнопкой Oк, код обработчика button1_click формы Form1 удалось существенно упростить (см. листинг 6).
При сравнении имен файлов использована функция. ToUpper(), возвращающая строку s, преобразованную к верхнему регистру (преобразуются как латинские, так и русские буквы). Необходимость использования подобной функции обусловлена тем, что имена файлов в системе Windows нечувствительны к регистру букв.
Предложенный вариант исправления ошибки 2 (см. листинг 8) не обеспечивает стопроцентной защиты. Если пользователь загрузит gif-файл, а затем захочет сохранить его под тем же именем и расширением, то условие s.ToUpper () == sO.ToUpper () вернет значение false (так как свойство FileName компонента saveFileDialogl Содержит имя файла с расширением png) и будет сделана попытка сохранить изображение в том же файле, из которого оно было загружено, что, естественно, приведет к ошибке времени выполнения. Однако подобные действия пользователя маловероятны. В самом деле, диалоговое окно Сохранить недвусмысленно сообщает в поле Тип файла, что оно предназначено для сохранения
данных только в PNG-формате, и в этой ситуации указывать при сохранении расширение, отличное от png, неразумно. Впрочем, защиту от подобных действий пользователя реализовать несложно: достаточно после оператора
string s = saveFileDialogl.FileName;
Выполнять проверку вида
Path.GetExtension(s).ToUpper() != ".PNG",
и, если это условие окажется истинным, выводить сообщение о том, что изображение можно сохранять только в PNG-формате, после чего немедленно выходить из обработчика.
