Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Лабораторный_практикум.doc
Скачиваний:
74
Добавлен:
15.11.2019
Размер:
45.35 Mб
Скачать

6. Модификация приложения Windows Forms: функциональность векторного редактора

Для начала определим все необходимые для работы приложения класса внутри самого приложения.

Первый класс с именем файла DrawObject.cs станет основным классом для рисования любых объектов. На его основе будут созданы классы для рисования конкретных объектов типа линий или прямоугольников. Класс будет содержать все возможные функции, события и свойства, необходимы для работы с графическим объектом. А именно этот класс будет определять функция рисования объекта (Draw), выделения, перемещения, изменения размеров и определения количества ключевых точек. Что такое ключевая точка? Для линии это точки на концах линии, которые «подсвечиваются» небольшими прямоугольниками. Если выделить линию и навести мышь на такую точку, курсор будет изменён (зависит от типа точки и объекта рисования). Нажатие на ключевую точку обеспечивает операцию изменения размеров объекта либо перемещения (зависит от объекта рисования). Любой объект во время выделения мышью подсвечивается ключевыми точками. Например, нарисованный и выделенный прямоугольник имеет 8 ключевых точек:

Код файла DrawObject.cs: [искомый код можно найти в приложении к данной лабораторной работе в (описания можно того или иного приложения можно посмотреть в пунтке № 8 протокола работы), а именно необходимо открыть Приложение № 2 (Файлы классов Draw...)].

Класс GraphicsProperties напрямую связан с дочерней формой LWP15Properties. Обработаем форму LWP15Properties и проинициализируем все необходимые свойства и события.

Откроем код формы (выделим LWP15Properies.cs -> нажмём правую кнопку мыши -> Перейти к коду (F7)). Найдём:

public LWP15Properties()

{

InitializeComponent();

}

Добавим после:

private GraphicsProperties properties;

private const string undefined = "Не задано";

private const int maxWidth = 10;

public GraphicsProperties Properties

{

get { return properties; }

set { properties = value; }

}

В самое начало формы добавим код:

using System.Globalization;

Найдём:

namespace LWP15Draw

{

public partial class LWP15Properties : Form

{

Изменим на:

namespace LWP15Draw

{

partial class LWP15Properties : Form

{

Проинициализируем первое событие формы LWP15Properties: Load со следующим кодом:

private void LWP15Properties_Load(object sender, EventArgs e)

{

InitControls();

SetColor();

SetPenWidth();

}

Вспомогательные функции добавим сразу после предыдущего фрагмента кода:

private void InitControls()

{

for (int i = 1; i <= maxWidth; i++)

{

CB_PenWidth.Items.Add(i.ToString(CultureInfo.InvariantCulture));

}

}

private void SetColor()

{

if (properties.Color.HasValue) L_Color.BackColor = properties.Color.Value;

else L_Color.Text = undefined;

}

private void SetPenWidth()

{

if (properties.PenWidth.HasValue)

{

int penWidth = properties.PenWidth.Value;

if (penWidth < 1) penWidth = 1;

if (penWidth > maxWidth) penWidth = maxWidth;

label2.Text = penWidth.ToString(CultureInfo.InvariantCulture);

CB_PenWidth.SelectedIndex = penWidth - 1;

}

else { label2.Text = undefined; }

}

private void ReadValues()

{

if (CB_PenWidth.Text != undefined) { properties.PenWidth = CB_PenWidth.SelectedIndex + 1; }

if (L_Color.Text.Length == 0) { properties.Color = L_Color.BackColor; }

}

Событие SelectedIndexChanged для ComboBox этой формы:

private void CB_PenWidth_SelectedIndexChanged(object sender, EventArgs e)

{

int width = CB_PenWidth.SelectedIndex + 1;

L_PenWidth.Text = width.ToString(CultureInfo.InvariantCulture);

}

Событие Click нажатия кнопки B_SelectColor:

private void B_SelectColor_Click(object sender, EventArgs e)

{

ColorDialog dlg = new ColorDialog();

dlg.Color = L_Color.BackColor;

if (dlg.ShowDialog(this) == DialogResult.OK)

{

L_Color.BackColor = dlg.Color;

L_Color.Text = "";

}

}

Событие Click нажатия кнопки B_OK:

private void B_OK_Click(object sender, EventArgs e)

{

ReadValues();

this.DialogResult = DialogResult.OK;

}

Форма готова. Если возникнут ошибки, перепроверяем имена элементов управления.

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

Логика работы с объектами после рисования будет такой:

Каждый новый объект помещается вначале Z-порядка (списка расположения «под курсором»). Таким образом, нарисовав в одном и том же месте несколько объектов и нажав инструментов «Выделение» на этом же месте «по умолчанию» будет выделен последний нарисованный объект. Если изменить положение объекта в Z-списке, отправив объект «назад» («Переместить назад»), наверху окажется предыдущий нарисованный объект. Сам же объект, который мы отправили «назад», будет расположен в конце Z-порядка и будет выделен, если удалить из области все другие объекты.

Файл для класса назовём GraphicsList.cs, код файла будет таким: [искомый код можно найти в приложении к данной лабораторной работе в (описания можно того или иного приложения можно посмотреть в пунтке № 8 протокола работы), а именно необходимо открыть Приложение № 3 (Файлы классов Graphics...)].

Игнорируем возникшие ошибки (3 штуки, исправим позже добавлением других классов).

Также добавим вспомогательный класс для передачи параметров: GraphicsProperties (файл GraphicsProperties.cs)со следующим кодом: [искомый код можно найти в приложении к данной лабораторной работе в (описания можно того или иного приложения можно посмотреть в пунтке № 8 протокола работы), а именно необходимо открыть Приложение № 3 (Файлы классов Graphics...)].

Добавляем новый класс, являющийся базовым для команд типа «Отменить» и «Вернуть» («Undo» и «Redo»). Имя файла класса будет Command.cs, код файла вставляем следующий: [искомый код можно найти в приложении к данной лабораторной работе в (описания можно того или иного приложения можно посмотреть в пунтке № 8 протокола работы), а именно необходимо открыть Приложение № 4 (Файлы классов Command...)].

Следующий класс (CommandChangeState.cs) станет основным классом для поддержки команд перемещений, изменения размеров и изменения параметров объекта рисования для операций «Отменить» и «Вернуть». Код файла будет следующим: [искомый код можно найти в приложении к данной лабораторной работе в (описания можно того или иного приложения можно посмотреть в пунтке № 8 протокола работы), а именно необходимо открыть Приложение № 4 (Файлы классов Command...)].

Класс CommandAdd (CommandAdd.cs), отвечающий за команду добавления объекта для операций «Отменить» и «Вернуть»: [искомый код можно найти в приложении к данной лабораторной работе в (описания можно того или иного приложения можно посмотреть в пунтке № 8 протокола работы), а именно необходимо открыть Приложение № 4 (Файлы классов Command...)].

Непосредственно реализация команд «Отменить» и «Вернуть» будет выполнена следующим классом UndoManager (файл UndoManager.cs) с кодом: [искомый код можно найти в приложении к данной лабораторной работе в (описания можно того или иного приложения можно посмотреть в пунтке № 8 протокола работы), а именно необходимо открыть Приложение № 5 (Прочие файлы)].

Класс CommandDelete (CommandDelete.cs), отвечающий за команду удаления выделенного объекта для операций «Отменить» и «Вернуть»: [искомый код можно найти в приложении к данной лабораторной работе в (описания можно того или иного приложения можно посмотреть в пунтке № 8 протокола работы), а именно необходимо открыть Приложение № 4 (Файлы классов Command...)].

Класс CommandDeleteAll (CommandDeleteAll.cs), отвечающий за команду удаления всех объектов для операций «Отменить» и «Вернуть»: [искомый код можно найти в приложении к данной лабораторной работе в (описания можно того или иного приложения можно посмотреть в пунтке № 8 протокола работы), а именно необходимо открыть Приложение № 4 (Файлы классов Command...)].

Теперь перейдём к классам, поддерживающим рисование графических объектов. Первый и самый примитивный графический объект это линия. Класс на основе DrawObject: DrawLine (DrawLine.cs) реализует всё необходимое для поддержки рисования объекта линии, выделения нарисованного объекта, изменения положения и перемещения объекта линии. Линия рисуется через два три события (нажатие левой кнопки мыши, перемещения и отжатия левой кнопки мыши). Объект имеет де ключевые точки (точка начала и точка конца линии). Сериализуется в файл путём указания следующих параметров: точка начала и точка конца линии, а также строки записей для сохранения в файле: Start и End. Перемещение линии обеспечивается созданием для линии небольшой линейной области (толщина в 7 пикселей от центра линии). Нажатие левой кнопки мыши в этой области приведёт к выделению объекта и активации возможности перемещение, изменения. Изменение размеров линии осуществляется в ключевых точках.

Код файла для реализации класса: [искомый код можно найти в приложении к данной лабораторной работе в (описания можно того или иного приложения можно посмотреть в пунтке № 8 протокола работы), а именно необходимо открыть Приложение № 2 (Файлы классов Draw...)].

Объект получает параметры из инструмента «Линия» следующим обрахом:

private Point startPoint;

private Point endPoint;

public DrawLine(int x1, int y1, int x2, int y2) : base()

{

startPoint.X = x1;

startPoint.Y = y1;

endPoint.X = x2;

endPoint.Y = y2;

Initialize();

}

Рисует линию так:

/// <summary>

/// Главная функция рисования линии на форме

/// </summary>

public override void Draw(Graphics g)

{

g.SmoothingMode = SmoothingMode.AntiAlias;

Pen pen = new Pen(Color, PenWidth);

g.DrawLine(pen, startPoint.X, startPoint.Y, endPoint.X, endPoint.Y);

pen.Dispose();

}

Прямоугольник. Класс на основе DrawObject: DrawRectangle (DrawRectangle.cs) реализует всё необходимое для поддержки рисования объекта прямоугольника, выделения нарисованного объекта, изменения положения и перемещения объекта прямоугольника. Объект имеет воесь ключевых точек (угловые точки, точки в центре линий). Сериализуется в файл путём указания следующих параметров: область прямоугольника, а также строки записей для сохранения в файле: Rect. Прямоугольник является цельным объектом, область для перемещения которого ограничена стронами прямоугольника. Для перемещения, как и в случае с линией нужно выделить объект (в любом месте прямоугольника) и зажать левую кнопку мыши.

Код файла для реализации класса: [искомый код можно найти в приложении к данной лабораторной работе в (описания можно того или иного приложения можно посмотреть в пунтке № 8 протокола работы), а именно необходимо открыть Приложение № 2 (Файлы классов Draw...)].

Нормализация нужна для правильного определения направления рисования (первое нажатие в левом уголу, движение мыши в правый нижний угол и прочее) и получение положительных значений координат для прямоугольника.

Эллипс. Класс на основе DrawRectangle: DrawEllipse (DrawEllipse.cs) реализует всё необходимое для поддержки рисования объекта эллипса, выделения нарисованного объекта, изменения положения и перемещения объекта эллипса. Объект имеет воесь ключевых точек (угловые точки, точки в центре линий). Сериализуется в файл путём указания следующих параметров: область прямоугольника, а также строки записей для сохранения в файле: Rect. Прямоугольник является цельным объектом, область для перемещения которого ограничена стронами прямоугольника. Для перемещения, как и в случае с линией нужно выделить объект (в любом месте прямоугольника) и зажать левую кнопку мыши.

Код файла для реализации класса: [искомый код можно найти в приложении к данной лабораторной работе в (описания можно того или иного приложения можно посмотреть в пунтке № 8 протокола работы), а именно необходимо открыть Приложение № 2 (Файлы классов Draw...)].

Объект получает следующие параметры из инструмента «Эллипс»:

public DrawEllipse(int x, int y, int width, int height) : base()

{

Rectangle = new Rectangle(x, y, width, height);

Initialize();

}

Рисование эллипса реализовано так:

/// <summary>

/// Главная функция рисования эллипса на форме

/// </summary>

/// <param name="g"></param>

public override void Draw(Graphics g)

{

Pen pen = new Pen(Color, PenWidth);

g.DrawEllipse(pen, DrawRectangle.GetNormalizedRectangle(Rectangle));

pen.Dispose();

}

Карандаш: последний объект рисования. Класс на основе DrawLine: DrawPolygon (DrawPolygon.cs) реализует всё необходимое для поддержки рисования объекта непрерывной линии с малым расстоянием между точками, выделения нарисованного объекта, изменения положения и перемещения объекта карандаша. Объект имеет столько ключевых точек, сколько нарисовано «кусков» + 1 точка). Сериализуется в файл путём указания следующих параметров: точка и длина (для одного участка), а также строки записей для сохранения в файле: Point и Length. Карандаш не является цельным объектом (даже в случае замыкания линий). Для перемещения, как и в случае с линией нужно выделить объект (в любом месте нарисоанной ломаной) и зажать левую кнопку мыши. Путь для выделения создаётся также как и в случае с линией, но для каждого участка. Параметр расстояния (до ближайшей точки) регулируется так (будущий файл ToolPolygon.cs):

private const int minDistance = 15*15; // Дистанция между ключевыми точками

Кусок кода событиея перемещения мыши во время рисования:

Point point = new Point(e.X, e.Y);

int distance = (e.X - lastX)*(e.X - lastX) + (e.Y - lastY)*(e.Y - lastY);

if (distance < minDistance)

{

// Если расстояние между последними двумя точками меньше минимального -

// перемещаем последнюю точку

newPolygon.MoveHandleTo(point, newPolygon.HandleCount);

}

else

{

// Добавляем новую точку

newPolygon.AddPoint(point);

lastX = e.X;

lastY = e.Y;

}

Код файла для реализации класса: [искомый код можно найти в приложении к данной лабораторной работе в (описания можно того или иного приложения можно посмотреть в пунтке № 8 протокола работы), а именно необходимо открыть Приложение № 2 (Файлы классов Draw...)].

Последние 7 классов относятся к классам реализации инструмента рисования, и именно они инициализируются до вызова класса рисования объекта. Экземпляр класса инструмента передаёт в экземпляр класса рисования все параметры. Первый класс из цепочки является абстрактным базовым классом для реализации работы инструментов. Реализует три простых события действия с мышью (нажатие, перемещение и снятие нажатия). Имя: Tool.cs (класс Tool). Код файла с классом: [искомый код можно найти в приложении к данной лабораторной работе в (описания можно того или иного приложения можно посмотреть в пунтке № 8 протокола работы), а именно необходимо открыть Приложение № 6 (Файлы классов Tool...)].

ToolObject.cs (класс ToolObject). Это базовый класс для реализации остальных объектов инструментов. Реализует свойства курсора для инструмента, а также функцию добавления нового объекта в список объектов документа:

/// <summary>

/// Добавление нового объекта в область рисования.

/// Функция вызывается когда пользователь нажимает ЛКМ на области рисования,

/// и один из полученных ToolObject-инструментов активен.

/// </summary>

/// <param name="drawArea"></param>

/// <param name="o"></param>

protected void AddNewObject(DrawArea drawArea, DrawObject o)

{

drawArea.GraphicsList.UnselectAll();

o.Selected = true;

drawArea.GraphicsList.Add(o);

drawArea.Capture = true;

drawArea.Refresh();

drawArea.SetDirty();

}

Код файла с классом: [искомый код можно найти в приложении к данной лабораторной работе в (описания можно того или иного приложения можно посмотреть в пунтке № 8 протокола работы), а именно необходимо открыть Приложение № 6 (Файлы классов Tool...)].

ToolLine.cs (класс ToolLine). Это базовый класс для реализации инструмента рисования «Линия». Передаёт параметры объекту рисования. Реализует свойства курсора для инструмента, а также функцию добавления нового объекта линии в список объектов документа.

Пример события нажатия ЛКМ в области DrawArea с созданием экземпляра класса DrawLine и передачей параметров нажатия:

public override void OnMouseDown(DrawArea drawArea, MouseEventArgs e)

{

AddNewObject(drawArea, new DrawLine(e.X, e.Y, e.X + 1, e.Y + 1));

}

Код файла с классом: [искомый код можно найти в приложении к данной лабораторной работе в (описания можно того или иного приложения можно посмотреть в пунтке № 8 протокола работы), а именно необходимо открыть Приложение № 6 (Файлы классов Tool...)].

ToolRectangle.cs (класс ToolRectnagle). Это базовый класс для реализации инструмента рисования «Прямоугольник». Передаёт параметры объекту рисования. Реализует свойства курсора для инструмента, а также функцию добавления нового объекта прямоугольника в список объектов документа. Событие нажатия кнопки мыши:

public override void OnMouseDown(DrawArea drawArea, MouseEventArgs e)

{

AddNewObject(drawArea, new DrawRectangle(e.X, e.Y, 1, 1));

}

Код файла с классом: [искомый код можно найти в приложении к данной лабораторной работе в (описания можно того или иного приложения можно посмотреть в пунтке № 8 протокола работы), а именно необходимо открыть Приложение № 6 (Файлы классов Tool...)].

ToolEllipse.cs (класс ToolEllipse). Это базовый класс для реализации инструмента рисования «Эллипс». Передаёт параметры объекту рисования. Реализует свойства курсора для инструмента, а также функцию добавления нового объекта эллипса в список объектов документа. Событие нажатия кнопки мыши:

public override void OnMouseDown(DrawArea drawArea, MouseEventArgs e)

{

AddNewObject(drawArea, new DrawEllipse(e.X, e.Y, 1, 1));

}

Код файла с классом: [искомый код можно найти в приложении к данной лабораторной работе в (описания можно того или иного приложения можно посмотреть в пунтке № 8 протокола работы), а именно необходимо открыть Приложение № 6 (Файлы классов Tool...)].

ToolPolygon.cs (класс ToolPolygon). Это базовый класс для реализации инструмента рисования «Карандаш». Передаёт параметры объекту рисования. Реализует свойства курсора для инструмента, а также функцию добавления нового объекта непрерывной линии в список объектов документа.

Код файла с классом: [искомый код можно найти в приложении к данной лабораторной работе в (описания можно того или иного приложения можно посмотреть в пунтке № 8 протокола работы), а именно необходимо открыть Приложение № 6 (Файлы классов Tool...)].

ToolPointer.cs (класс ToolPointer). Это базовый класс для реализации инструмента рисования «Выделение». Передаёт параметры объекту рисования. Реализует события связанные с выделение объектов в области рисования, работает с перемещением объекта, изменением размеров объекта и чистым выделением объект(-ов). Этот инструмент также управляет пунктирным прямоугольником выделения:

if (selectMode == SelectionMode.NetSelection)

{

// Удаляем прямоугольник предыдущего выделения

ControlPaint.DrawReversibleFrame(

drawArea.RectangleToScreen(DrawRectangle.GetNormalizedRectangle(startPoint, oldPoint)),

Color.Black,

FrameStyle.Dashed);

// Рисуем прямоугольник нового выделения

ControlPaint.DrawReversibleFrame(

drawArea.RectangleToScreen(DrawRectangle.GetNormalizedRectangle(startPoint, point)),

Color.Black,

FrameStyle.Dashed);

return;

}

Код файла с классом: [искомый код можно найти в приложении к данной лабораторной работе в (описания можно того или иного приложения можно посмотреть в пунтке № 8 протокола работы), а именно необходимо открыть Приложение № 6 (Файлы классов Tool...)].

На этом формирование классов закончено. Теперь можно перейти к реализации функциональности непосредственно для главной формы и элемента, в котором будем рисовать (DrawArea).

Первым делом «добьём» форму LWP15About. Событие Load для формы будет таким:

private void LWP15About_Load(object sender, EventArgs e)

{

this.Text = "О программе " + Application.ProductName;

L_About.Text =

"Программа: " + Application.ProductName + "\n" +

"Версия: " + Application.ProductVersion;

}

Событие Click кнопки B_OK:

private void B_OK_Click(object sender, EventArgs e)

{

this.Close();

}

Перейдём к DrawArea.cs. Это основной элемент, в котором происходит «рисование». Любые действия мышью совершаемые в фокусе элемента перехватываются инструментами Tool... и реализуют то или иной действие в зависимости от активного инструмента и действия с мышью. Сам элемент будет растягиваться до строки состояния внизу, границ с боку формы и панели инструментов сверху. Абсолютное значение размеров будет влиять лишь на доступную для размещения объектов область. Эти размеры будут важны лишь при создании растрового изображения (об этом в конце данного материала) на основе графики в элементе.

Откроем код элемента DrawArea.cs и заменим все директивы using в начале файла следующим кодом:

#region Директивы Using

using System;

using System.Collections.Generic;

using System.ComponentModel;

using System.Drawing;

using System.Data;

using System.Text;

using System.Windows.Forms;

using LWP15Tools; // Подключаем библиотеку классов

#endregion

Подключаем к проекту ссылку на нашу библиотеку LWP15tools: жмём правую кнопку мыши на имени проекта LWP15Draw, затем Добавить ссылку...:

Рис. 6. 1. Добавить ссылку: вкладка Проекты

В открывшемся окне переходим на вкладку Проекты и выбираем LWP15Tools, жмём ОК.

В коде находим:

namespace LWP15Draw

{

public partial class DrawArea : UserControl

{

public DrawArea()

{

InitializeComponent();

}

}

Заменяем на (последнюю фигурную скобку в файле не трогаем):

namespace LWP15Draw

{

partial class DrawArea : UserControl

{

#region Конструктор

public DrawArea()

{

InitializeComponent();

}

#endregion

#region Перечисления

public enum DrawToolType

{

Pointer, Rectangle, Ellipse, Line, Polygon, NumberOfDrawTools

};

#endregion

#region Члены

private GraphicsList graphicsList; // Список объектов рисования

private DrawToolType activeTool; // Активный инструмент рисования

private Tool[] tools; // Массив инструментов

private LWP15Main owner;

private DocManager docManager;

private ContextMenuStrip m_ContextMenu;

private UndoManager undoManager;

#endregion

#region Свойства

/// <summary>

/// Ссылка на владельца формы

/// </summary>

public LWP15Main Owner

{

get { return owner; }

set { owner = value; }

}

/// <summary>

/// Ссылка на DocManager

/// </summary>

public DocManager DocManager

{

get { return docManager; }

set

{

docManager = value;

}

}

/// <summary>

/// Активный инструмент рисования

/// </summary>

public DrawToolType ActiveTool

{

get { return activeTool; }

set { activeTool = value; }

}

/// <summary>

/// Список объектов рисования

/// </summary>

public GraphicsList GraphicsList

{

get { return graphicsList; }

set

{

graphicsList = value;

undoManager = new UndoManager(graphicsList);

}

}

/// <summary>

/// true - если операция отмены возможна

/// </summary>

public bool CanUndo

{

get

{

if (undoManager != null) { return undoManager.CanUndo; }

return false;

}

}

/// <summary>

/// true - если операция возврата возможна

/// </summary>

public bool CanRedo

{

get

{

if (undoManager != null) { return undoManager.CanRedo; }

return false;

}

}

#endregion

#region Прочие функции

/// <summary>

/// Инициализация

/// </summary>

/// <param name="owner"></param>

/// <param name="docManager"></param>

public void Initialize(LWP15Main owner, DocManager docManager)

{

SetStyle(ControlStyles.AllPaintingInWmPaint | ControlStyles.UserPaint | ControlStyles.OptimizedDoubleBuffer, true);

// Сохраняем ссылку на владельца формы

this.Owner = owner;

this.DocManager = docManager;

// Устанавливаем инструмент по умолчанию

activeTool = DrawToolType.Pointer;

// Создаём список графических объектов

graphicsList = new GraphicsList();

// Создаём экземпляр UndoManager для текущего файла

undoManager = new UndoManager(graphicsList);

// Создаём массив инструментов рисования

tools = new Tool[(int)DrawToolType.NumberOfDrawTools];

tools[(int)DrawToolType.Pointer] = new ToolPointer();

tools[(int)DrawToolType.Rectangle] = new ToolRectangle();

tools[(int)DrawToolType.Ellipse] = new ToolEllipse();

tools[(int)DrawToolType.Line] = new ToolLine();

tools[(int)DrawToolType.Polygon] = new ToolPolygon();

}

/// <summary>

/// Добавления команды в историю

/// </summary>

public void AddCommandToHistory(Command command)

{

undoManager.AddCommandToHistory(command);

}

/// <summary>

/// Очистка истории

/// </summary>

public void ClearHistory()

{

undoManager.ClearHistory();

}

/// <summary>

/// Отменить

/// </summary>

public void Undo()

{

undoManager.Undo();

Refresh();

}

/// <summary>

/// Вернуть

/// </summary>

public void Redo()

{

undoManager.Redo();

Refresh();

}

/// <summary>

/// Устанавливаем флаг "грязный" (файл был изменён после последней операции сохранения)

/// </summary>

public void SetDirty()

{

DocManager.Dirty = true;

}

/// <summary>

/// Обработчик нажатия правой кнопки мышки

/// </summary>

/// <param name="e"></param>

private void OnContextMenu(MouseEventArgs e)

{

// Измененяем текущий выбор при необходимости

Point point = new Point(e.X, e.Y);

int n = GraphicsList.Count;

DrawObject o = null;

for (int i = 0; i < n; i++)

{

if (GraphicsList[i].HitTest(point) == 0)

{

o = GraphicsList[i];

break;

}

}

if (o != null)

{

if (!o.Selected) GraphicsList.UnselectAll();

// Выбор объекта произведён

o.Selected = true;

}

else

{

GraphicsList.UnselectAll();

}

Refresh(); // В случае изменения выбора

// Выводин контекстное меню (всплывающее).

// Элементы меню вставлены из строки меня, главного элемента "Правка"

m_ContextMenu = new ContextMenuStrip();

int nItems = owner.ContextParent.DropDownItems.Count;

// Получаем элементы меню "Правка" и перемещаем их на контекстное-всплывающее меню.

// Так как каждый шаг уменьшает количество элементов, читая их в обратном порядке.

// Чтобы получить элементы в прямом порядке, вставим каждый из них в начало

for (int i = nItems - 1; i >= 0; i--)

{

m_ContextMenu.Items.Insert(0, owner.ContextParent.DropDownItems[i]);

}

// Выводит контекстное меню для владельца формы, а также обрабатывает элементы выбора.

// Преобразует координаты точки в этом окне к координатам владельца

point.X += this.Left;

point.Y += this.Top;

m_ContextMenu.Show(owner, point);

Owner.SetStateOfControls(); // Включение/выключение элементов меню

// Контекстное меню вызвано, но меню "Правка" владельца теперь пусто.

// Подписываемся на событие закрытия контекстного меню и восстанавливаем там элементы

m_ContextMenu.Closed += delegate(object sender, ToolStripDropDownClosedEventArgs args)

{

if (m_ContextMenu != null)

{

nItems = m_ContextMenu.Items.Count;

for (int k = nItems - 1; k >= 0; k--) { owner.ContextParent.DropDownItems.Insert(0, m_ContextMenu.Items[k]); }

}

};

}

#endregion

}

По очереди инициализируем для элемента DrawArea следующие события с кодом. Вначале идёт событие Paint:

#region Обработчики событий

/// <summary>

/// Рисование графического объекта и группировка прямоугольника выделения (опционально)

/// </summary>

private void DrawArea_Paint(object sender, PaintEventArgs e)

{

SolidBrush brush = new SolidBrush(Color.FromArgb(255, 255, 255));

e.Graphics.FillRectangle(brush, this.ClientRectangle);

if (graphicsList != null) { graphicsList.Draw(e.Graphics); }

//DrawNetSelection(e.Graphics);

brush.Dispose();

}

Событие MouseDown, MouseMove и MouseUp для DrawArea:

/// <summary>

/// Событие MouseDown для DrawArea

/// ЛКМ: перехватывается активным инструментом

/// ПКМ: перехватывается и описано в данном классе

/// </summary>

private void DrawArea_MouseDown(object sender, MouseEventArgs e)

{

if (e.Button == MouseButtons.Left) tools[(int)activeTool].OnMouseDown(this, e);

else if (e.Button == MouseButtons.Right) OnContextMenu(e);

}

/// <summary>

/// Событие MouseMove для DrawArea

/// Перемещение без нажатия кнопок или с нажатие левой кнопки мыши перехватываетсмя активным инструментом

/// </summary>

private void DrawArea_MouseMove(object sender, MouseEventArgs e)

{

if (e.Button == MouseButtons.Left || e.Button == MouseButtons.None) tools[(int)activeTool].OnMouseMove(this, e);

else this.Cursor = Cursors.Default;

}

/// <summary>

/// Событие MouseUp для DrawArea

/// ЛКМ: перехватывается активным инструментом

/// </summary>

private void DrawArea_MouseUp(object sender, MouseEventArgs e)

{

if (e.Button == MouseButtons.Left) tools[(int)activeTool].OnMouseUp(this, e);

}

#endregion

Переходим к главной форме LWP15Main. Переписываем директивы using:

#region Директивы Using

using System;

using System.Collections.Generic;

using System.ComponentModel;

using System.Data;

using System.Drawing;

using System.Text;

using System.Windows.Forms;

using System.Runtime.Serialization;

using System.Diagnostics;

using System.Security;

using Microsoft.Win32;

using LWP15Tools;

#endregion

Находим:

public partial class LWP15Main : Form

{

Заменяем на:

partial class LWP15Main : Form

{

#region Члены

private DrawArea drawArea;

private DocManager docManager;

private DragDropManager dragDropManager;

private MruManager mruManager;

private PersistWindowState persistState;

private string argumentFile = ""; // Имя файла из командной строки

const string registryPath = "Software\\LWP15Draw";

#endregion

#region Свойства

/// <summary>

/// Имя файла из командной строки

/// </summary>

public string ArgumentFile

{

get { return argumentFile; }

set { argumentFile = value; }

}

/// <summary>

/// Получаем ссылку на элемент строки меню "Правка".

/// Используется при вызове контекстного-всплывающего меню в классе DrawArea

/// </summary>

/// <value></value>

public ToolStripMenuItem ContextParent

{

get { return правкаToolStripMenuItem; }

}

#endregion

#region Конструктор

public LWP15Main()

{

InitializeComponent();

persistState = new PersistWindowState(registryPath, this);

}

#endregion

#region Обработчики событий для DocManager

/// <summary>

/// Загрузка документа из потока поставляемая DocManager

/// </summary>

/// <param name="sender"></param>

/// <param name="args"></param>

private void docManager_LoadEvent(object sender, SerializationEventArgs e)

{

// DocManager просит загрузить документ в поставляемый поток

try

{

drawArea.GraphicsList = (GraphicsList)e.Formatter.Deserialize(e.SerializationStream);

}

catch (ArgumentNullException ex) { HandleLoadException(ex, e); }

catch (SerializationException ex) { HandleLoadException(ex, e); }

catch (SecurityException ex) { HandleLoadException(ex, e); }

}

/// <summary>

/// Сохранение документа в поток поставляемый DocManager

/// </summary>

/// <param name="sender"></param>

/// <param name="args"></param>

private void docManager_SaveEvent(object sender, SerializationEventArgs e)

{

// DocManager просит сохранить документ в поставляемый поток

try

{

e.Formatter.Serialize(e.SerializationStream, drawArea.GraphicsList);

}

catch (ArgumentNullException ex) { HandleSaveException(ex, e); }

catch (SerializationException ex) { HandleSaveException(ex, e); }

catch (SecurityException ex) { HandleSaveException(ex, e); }

}

#endregion

Теперь поочерёдно создаём события строки меню (двойное нажатие на элементе меню для инициализации события Click). В качестве кнопок будут выступать все подпункты (дочерние пункты) меню кроме подпункта «Последние файлы» меню «Файл». Блок с кодом событий Click всей строки меню будет таким:

#region Обработчики событий строки меню

private void создатьToolStripMenuItem_Click(object sender, EventArgs e)

{

CommandNew();

}

private void открытьToolStripMenuItem_Click(object sender, EventArgs e)

{

CommandOpen();

}

private void сохранитьToolStripMenuItem_Click(object sender, EventArgs e)

{

CommandSave();

}

private void сохранитькакToolStripMenuItem_Click(object sender, EventArgs e)

{

CommandSaveAs();

}

private void выходToolStripMenuItem_Click(object sender, EventArgs e)

{

this.Close();

}

private void отменитьToolStripMenuItem_Click(object sender, EventArgs e)

{

CommandUndo();

}

private void вернутьToolStripMenuItem1_Click(object sender, EventArgs e)

{

CommandRedo();

}

private void выделитьвсеToolStripMenuItem_Click(object sender, EventArgs e)

{

drawArea.GraphicsList.SelectAll();

drawArea.Refresh();

}

private void снятьВыделениеToolStripMenuItem_Click(object sender, EventArgs e)

{

drawArea.GraphicsList.UnselectAll();

drawArea.Refresh();

}

private void удалитьToolStripMenuItem_Click(object sender, EventArgs e)

{

CommandDelete command = new CommandDelete(drawArea.GraphicsList);

if (drawArea.GraphicsList.DeleteSelection())

{

drawArea.SetDirty();

drawArea.Refresh();

drawArea.AddCommandToHistory(command);

}

}

private void удалитьВсеToolStripMenuItem_Click(object sender, EventArgs e)

{

CommandDeleteAll command = new CommandDeleteAll(drawArea.GraphicsList);

if (drawArea.GraphicsList.Clear())

{

drawArea.SetDirty();

drawArea.Refresh();

drawArea.AddCommandToHistory(command);

}

}

private void переместитьНазадToolStripMenuItem_Click(object sender, EventArgs e)

{

if (drawArea.GraphicsList.MoveSelectionToBack())

{

drawArea.SetDirty();

drawArea.Refresh();

}

}

private void переместитьВпередToolStripMenuItem_Click(object sender, EventArgs e)

{

if (drawArea.GraphicsList.MoveSelectionToFront())

{

drawArea.SetDirty();

drawArea.Refresh();

}

}

private void параметрыToolStripMenuItem_Click(object sender, EventArgs e)

{

if (drawArea.GraphicsList.ShowPropertiesDialog(drawArea))

{

drawArea.SetDirty();

drawArea.Refresh();

}

}

private void выделениеToolStripMenuItem_Click(object sender, EventArgs e)

{

CommandPointer();

}

private void карандашToolStripMenuItem_Click(object sender, EventArgs e)

{

CommandPolygon();

}

private void линияToolStripMenuItem_Click(object sender, EventArgs e)

{

CommandLine();

}

private void эллипсToolStripMenuItem_Click(object sender, EventArgs e)

{

CommandEllipse();

}

private void прямоугольникToolStripMenuItem_Click(object sender, EventArgs e)

{

CommandRectnagle();

}

private void опрограммеToolStripMenuItem_Click(object sender, EventArgs e)

{

CommandAbout();

}

#endregion

Тоже самое проделываем для строки с иконками (панелью инструментов). Двойное нажатие на иконку инициализирует событие Click:

#region Обработчики событий панели инструментов

private void создатьToolStripButton_Click(object sender, EventArgs e)

{

CommandNew();

}

private void открытьToolStripButton_Click(object sender, EventArgs e)

{

CommandOpen();

}

private void сохранитьToolStripButton_Click(object sender, EventArgs e)

{

CommandSave();

}

private void выделениеToolStripButton_Click(object sender, EventArgs e)

{

CommandPointer();

}

private void карандашToolStripButton_Click(object sender, EventArgs e)

{

CommandPolygon();

}

private void линияToolStripButton_Click(object sender, EventArgs e)

{

CommandLine();

}

private void эллипсToolStripButton_Click(object sender, EventArgs e)

{

CommandEllipse();

}

private void прямоугольникToolStripButton_Click(object sender, EventArgs e)

{

CommandRectangle();

}

private void отменитьToolStripButton_Click(object sender, EventArgs e)

{

CommandUndo();

}

private void вернутьToolStripButton_Click(object sender, EventArgs e)

{

CommandRedo();

}

private void справкаToolStripButton_Click(object sender, EventArgs e)

{

CommandAbout();

}

#endregion

Инициализируем три события для главной формы. Событие Load формы LWP15Main:

#region Обработчики событий

private void LWP15Main_Load(object sender, EventArgs e)

{

toolStripStatusLabel.Text = "Готов к работе";

// Создаём область рисования

drawArea = new DrawArea();

drawArea.Location = new System.Drawing.Point(0, 0);

drawArea.Size = new System.Drawing.Size(10, 10);

drawArea.Owner = this;

this.Controls.Add(drawArea);

// Вспомогательные объекты (DocManager и прочие)

InitializeHelperObjects();

drawArea.Initialize(this, docManager);

ResizeDrawArea();

LoadSettingsFromRegistry();

// Submit to Idle event to set controls state at idle time

Application.Idle += delegate(object o, EventArgs a)

{

SetStateOfControls();

};

// Открытый файл передаётся в командную строку

if (ArgumentFile.Length > 0) OpenDocument(ArgumentFile);

// Подписываемся на событие DropDownOpened для каждого всплывающего меню

foreach (ToolStripItem item in menuStrip1.Items)

{

if (item.GetType() == typeof(ToolStripMenuItem)) { ((ToolStripMenuItem)item).DropDownOpened += LWP15Main_DropDownOpened; }

}

}

Событие Resize:

/// <summary>

/// Изменение размеров DrawArea, Когда меняются размеры формы

/// </summary>

private void LWP15Main_Resize(object sender, EventArgs e)

{

if (this.WindowState != FormWindowState.Minimized && drawArea != null) { ResizeDrawArea(); }

}

Событие FormClosing:

/// <summary>

/// Событие закрытия формы

/// </summary>

private void LWP15Main_FormClosing(object sender, FormClosingEventArgs e)

{

if (e.CloseReason == CloseReason.UserClosing)

{

if (!docManager.CloseDocument()) e.Cancel = true;

}

SaveSettingsToRegistry();

}

Последнее событие формы без инициализации (просто вставляем код после последнего события):

/// <summary>

/// Всплывающая строка меню ("Файл", "Правка" и прочее) открыто

/// </summary>

/// <param name="sender"></param>

/// <param name="e"></param>

void LWP15Main_DropDownOpened(object sender, EventArgs e)

{

// Устанавливаем активный инструмент на "Выделение".

// Педотвращает редкий сбой, когда выбран иной инструмент, пользователь открывает главное менюuser opens

// и после нажатия в DrawArea событие MouseDown не вызывается и MouseMove работает неправильно

drawArea.ActiveTool = DrawArea.DrawToolType.Pointer;

}

#endregion

Наконец добавляем последний участок кода сразу же после предыдущего:

#region Прочие функции

/// <summary>

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

/// Функция вызывается при простое

/// </summary>

public void SetStateOfControls()

{

// Выбор активного инструмента

выделениеToolStripButton.Checked = (drawArea.ActiveTool == DrawArea.DrawToolType.Pointer);

прямоугольникToolStripButton.Checked = (drawArea.ActiveTool == DrawArea.DrawToolType.Rectangle);

эллипсToolStripButton.Checked = (drawArea.ActiveTool == DrawArea.DrawToolType.Ellipse);

линияToolStripButton.Checked = (drawArea.ActiveTool == DrawArea.DrawToolType.Line);

карандашToolStripButton.Checked = (drawArea.ActiveTool == DrawArea.DrawToolType.Polygon);

выделениеToolStripMenuItem.Checked = (drawArea.ActiveTool == DrawArea.DrawToolType.Pointer);

прямоугольникToolStripMenuItem.Checked = (drawArea.ActiveTool == DrawArea.DrawToolType.Rectangle);

эллипсToolStripMenuItem.Checked = (drawArea.ActiveTool == DrawArea.DrawToolType.Ellipse);

линияToolStripMenuItem.Checked = (drawArea.ActiveTool == DrawArea.DrawToolType.Line);

карандашToolStripMenuItem.Checked = (drawArea.ActiveTool == DrawArea.DrawToolType.Polygon);

bool objects = (drawArea.GraphicsList.Count > 0);

bool selectedObjects = (drawArea.GraphicsList.SelectionCount > 0);

// Операции с файлом

сохранитьToolStripMenuItem.Enabled = objects;

сохранитьToolStripButton.Enabled = objects;

сохранитьКакToolStripMenuItem.Enabled = objects;

// Операции правки

удалитьToolStripMenuItem.Enabled = selectedObjects;

удалитьВсеToolStripMenuItem.Enabled = objects;

выделитьВсеToolStripMenuItem.Enabled = objects;

снятьВыделениеToolStripMenuItem.Enabled = objects;

переместитьВпередToolStripMenuItem.Enabled = selectedObjects;

переместитьНазадToolStripMenuItem.Enabled = selectedObjects;

параметрыToolStripMenuItem.Enabled = selectedObjects;

// "Отменить"

отменитьToolStripMenuItem.Enabled = drawArea.CanUndo;

отменитьToolStripButton.Enabled = drawArea.CanUndo;

// "Вернуть"

вернутьToolStripMenuItem.Enabled = drawArea.CanRedo;

вернутьToolStripButton.Enabled = drawArea.CanRedo;

}

/// <summary>

/// Установка области рисования формы на основе пространство свободной области,

/// за исключением панели инструментов, строки состояния и строки меню

/// </summary>

private void ResizeDrawArea()

{

Rectangle rect = this.ClientRectangle;

drawArea.Left = rect.Left;

drawArea.Top = rect.Top + menuStrip1.Height + toolStrip1.Height;

drawArea.Width = rect.Width;

drawArea.Height = rect.Height - menuStrip1.Height - toolStrip1.Height - statusStrip1.Height;

}

/// <summary>

/// Инициализация вспомогательных объектов из библиотеки классов LWP15Tools.

/// </summary>

private void InitializeHelperObjects()

{

// DocManager

DocManagerData data = new DocManagerData();

data.FormOwner = this;

data.UpdateTitle = true;

data.FileDialogFilter = "Файлы LWP15Draw (*.lwp)|*.lwp|Все файлы (*.*)|*.*";

data.NewDocName = "New.lwp";

data.RegistryPath = registryPath;

docManager = new DocManager(data);

docManager.RegisterFileType("lwp", "lwpfile", "Файл LWP15Draw");

// Подписываемся на события класса DocManager

docManager.SaveEvent += docManager_SaveEvent;

docManager.LoadEvent += docManager_LoadEvent;

// Делаем "встроенные подписки" с помощью анонимных методов

docManager.OpenEvent += delegate(object sender, OpenFileEventArgs e)

{

// Обновляем список последних файлов

if (e.Succeeded) mruManager.Add(e.FileName);

else mruManager.Remove(e.FileName);

};

docManager.DocChangedEvent += delegate(object o, EventArgs e)

{

try

{

drawArea.Refresh();

drawArea.ClearHistory();

}

catch { }

};

docManager.ClearEvent += delegate(object o, EventArgs e)

{

if (drawArea.GraphicsList != null)

{

drawArea.GraphicsList.Clear();

drawArea.ClearHistory();

drawArea.Refresh();

}

};

docManager.NewDocument();

// DragDropManager

dragDropManager = new DragDropManager(this);

dragDropManager.FileDroppedEvent += delegate(object sender, FileDroppedEventArgs e)

{

OpenDocument(e.FileArray.GetValue(0).ToString());

};

// MruManager

mruManager = new MruManager();

mruManager.Initialize(

this, // Владелец формы (this)

последниеФайлыToolStripMenuItem, // Элемент меню последних исользованных файлов

файлToolStripMenuItem, // Родительский элемент ("Файл")

registryPath); // Путь в системном реестре для хранения списка последних файлов

mruManager.MruOpenEvent += delegate(object sender, MruFileOpenEventArgs e)

{

OpenDocument(e.FileName);

};

}

/// <summary>

/// Обрабатываем исключение из функции docManager_LoadEvent

/// </summary>

/// <param name="ex"></param>

/// <param name="fileName"></param>

private void HandleLoadException(Exception ex, SerializationEventArgs e)

{

MessageBox.Show(this,

"Операция открытия файла завершилась неудачей. Имя файла: " + e.FileName + "\n" +

"Причина: " + ex.Message,

Application.ProductName);

e.Error = true;

}

/// <summary>

/// Обрабатываем исключение из функции docManager_SaveEvent

/// </summary>

/// <param name="ex"></param>

/// <param name="fileName"></param>

private void HandleSaveException(Exception ex, SerializationEventArgs e)

{

MessageBox.Show(this,

"Операция сохранения файла завершилась неудачей. Имя файла: " + e.FileName + "\n" +

"Причина: " + ex.Message,

Application.ProductName);

e.Error = true;

}

/// <summary>

/// Открытие документа.

/// Используется для открытия файла переданного в командной строке или "переброшенного" в окно

/// </summary>

/// <param name="file"></param>

public void OpenDocument(string file)

{

docManager.OpenDocument(file);

}

/// <summary>

/// Загрузка настроек приложения из системного реестра

/// </summary>

void LoadSettingsFromRegistry()

{

try

{

RegistryKey key = Registry.CurrentUser.CreateSubKey(registryPath);

DrawObject.LastUsedColor = Color.FromArgb((int)key.GetValue("Color", Color.Black.ToArgb()));

DrawObject.LastUsedPenWidth = (int)key.GetValue("Width", 1);

}

catch (ArgumentNullException ex) { HandleRegistryException(ex); }

catch (SecurityException ex) { HandleRegistryException(ex); }

catch (ArgumentException ex) { HandleRegistryException(ex); }

catch (ObjectDisposedException ex) { HandleRegistryException(ex); }

catch (UnauthorizedAccessException ex) { HandleRegistryException(ex); }

}

/// <summary>

/// Сохранение настроек приложения в системный реестр

/// </summary>

void SaveSettingsToRegistry()

{

try

{

RegistryKey key = Registry.CurrentUser.CreateSubKey(registryPath);

key.SetValue("Color", DrawObject.LastUsedColor.ToArgb());

key.SetValue("Width", DrawObject.LastUsedPenWidth);

}

catch (SecurityException ex) { HandleRegistryException(ex); }

catch (ArgumentException ex) { HandleRegistryException(ex); }

catch (ObjectDisposedException ex) { HandleRegistryException(ex); }

catch (UnauthorizedAccessException ex) { HandleRegistryException(ex); }

}

private void HandleRegistryException(Exception ex)

{

Trace.WriteLine("Выполнение операции с системны реестром завершилась неудачей: " + ex.Message);

}

/// <summary>

/// Выделение

/// </summary>

private void CommandPointer()

{

drawArea.ActiveTool = DrawArea.DrawToolType.Pointer;

toolStripStatusLabel.Text = "Выбран инструмент \"Выделение\"";

}

/// <summary>

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

/// </summary>

private void CommandRectangle()

{

drawArea.ActiveTool = DrawArea.DrawToolType.Rectangle;

toolStripStatusLabel.Text = "Выбран инструмент \"Прямоугольник\"";

}

/// <summary>

/// Эллипс

/// </summary>

private void CommandEllipse()

{

drawArea.ActiveTool = DrawArea.DrawToolType.Ellipse;

toolStripStatusLabel.Text = "Выбран инструмент \"Эллипс\"";

}

/// <summary>

/// Линия

/// </summary>

private void CommandLine()

{

drawArea.ActiveTool = DrawArea.DrawToolType.Line;

toolStripStatusLabel.Text = "Выбран инструмент \"Линия\"";

}

/// <summary>

/// Карандаш

/// </summary>

private void CommandPolygon()

{

drawArea.ActiveTool = DrawArea.DrawToolType.Polygon;

toolStripStatusLabel.Text = "Выбран инструмент \"Карандаш\"";

}

/// <summary>

/// Диалог "О программе..."

/// </summary>

private void CommandAbout()

{

LWP15About frm = new LWP15About();

frm.ShowDialog(this);

toolStripStatusLabel.Text = "Вызвана форма \"О программе\"";

}

/// <summary>

/// Создать

/// </summary>

private void CommandNew()

{

docManager.NewDocument();

toolStripStatusLabel.Text = "Создан новый документ";

}

/// <summary>

/// Открыть

/// </summary>

private void CommandOpen()

{

docManager.OpenDocument("");

toolStripStatusLabel.Text = "Документ открыт";

}

/// <summary>

/// Сохранить

/// </summary>

private void CommandSave()

{

docManager.SaveDocument(DocManager.SaveType.Save);

toolStripStatusLabel.Text = "Документ сохранён";

}

/// <summary>

/// Сохранить как

/// </summary>

private void CommandSaveAs()

{

docManager.SaveDocument(DocManager.SaveType.SaveAs);

toolStripStatusLabel.Text = "Документ сохранён";

}

/// <summary>

/// Отменить

/// </summary>

private void CommandUndo()

{

drawArea.Undo();

toolStripStatusLabel.Text = "Произведена операция \"Отменить\"";

}

/// <summary>

/// Вернуть

/// </summary>

private void CommandRedo()

{

drawArea.Redo();

toolStripStatusLabel.Text = "Произведена операция \"Вернуть\"";

}

#endregion

Почти всё. Наше приложение способно создавать свои собственные документы (файлы *.lwp). А как быть, если необходимо сохранить рисунок в нормальном графическом формате? Всё достаточно просто. Нужно сохранить содержимое элемента управления DrawArea в одно из желаемых форматов растрового изображения.

Для реализации этой функции создаём в меню «Файл» новую кнопку «Сохранить как изображение...»:

Свойства такие:

(Name):

сохранитьКакИзображениеToolStripMenuItem

Text:

Сохранить как &изображение...

Image:

Импортируем иконку

Событие Click по кнопке меню «Сохранить как изображение...»:

#region Добавленная функция экспорта DrawArea в изображение

private Rectangle drawAreaRect;

private string fileName;

private string strFilExtn;

private void сохранитьКакИзображениеToolStripMenuItem_Click(object sender, EventArgs e)

{

SaveFileDialog SFD_Picture = new SaveFileDialog();

SFD_Picture.Title = "Сохранить ка изображение";

SFD_Picture.OverwritePrompt = true;

SFD_Picture.CheckPathExists = true;

SFD_Picture.Filter = "Изображение в формате PNG|*.png|" + "Изображение в формате JPEG|*.jpg|" + "Изображение в формате BMP|*.bmp|" + "Изображение в формате GIF|*.gif|" + "Изображение в формате TIF|*.tif";

drawAreaRect = new Rectangle();

drawAreaRect.Width = drawArea.Size.Width;

drawAreaRect.Height = drawArea.Size.Height;

Bitmap drawAreaBitmap = new Bitmap(drawArea.Size.Width, drawArea.Size.Height);

drawArea.DrawToBitmap(drawAreaBitmap, drawAreaRect);

// Если выбрали, сохраняем

if (SFD_Picture.ShowDialog() == DialogResult.OK)

{

// Получаем имя файла из диалога

fileName = SFD_Picture.FileName;

// Получаем расширения файла из диалога

strFilExtn = fileName.Remove(0, fileName.Length - 3);

// Сохраняем файл в выбранном расширении

switch (strFilExtn)

{

case "bmp":

drawAreaBitmap.Save(fileName, System.Drawing.Imaging.ImageFormat.Bmp);

break;

case "jpg":

drawAreaBitmap.Save(fileName, System.Drawing.Imaging.ImageFormat.Jpeg);

break;

case "gif":

drawAreaBitmap.Save(fileName, System.Drawing.Imaging.ImageFormat.Gif);

break;

case "tif":

drawAreaBitmap.Save(fileName, System.Drawing.Imaging.ImageFormat.Tiff);

break;

case "png":

drawAreaBitmap.Save(fileName, System.Drawing.Imaging.ImageFormat.Png);

break;

default:

break;

}

toolStripStatusLabel.Text = "Изображение " + System.IO.Path.GetFileName(SFD_Picture.FileName) + " успешно сохранено! Полный путь: " + SFD_Picture.FileName;

}

SFD_Picture.Dispose();

drawAreaBitmap.Dispose();

}

#endregion

Хорошо, а если, например нам потребуется вставить изображение, да ещё и изменить его размеры? Тоже не проблема. Выполним полноценное расширение возможностей нашего приложения добавление нового полноценного объекта рисования изображения. Изображение будет добавлять на форму через диалог выбора файла. По умолчанию у него будет чёрная прямоугольная рамка с толщиной пера в один пиксель. После добавления изображения, цвет и толщину пера рамки можно будет поменять (как и у любого другого объекта). Изображение будет полноценно сериализовываться в файле наравне с остальными объектами. Во время рисования, размер изображения будет автоматически подстраиваться под нарисованную рамку. Вставленное изображение можно будет переместить и изменить его размер.

Первое что нужно сделать, это добавить новый класс для рисования объекта изображения. Назовём его DrawImage (файл DrawImage.cs). За основу возьмём класс DrawRectangle, так как он наиболее походит нам для реализации нового класса.

Код класс рисования будет таким:

using System;

using System.Diagnostics;

using System.Drawing;

using System.Drawing.Drawing2D;

using System.Globalization;

using System.Runtime.Serialization;

using System.Windows.Forms;

namespace LWP15Draw

{

/// <summary>

/// Рисование графического объекта "Изображение"

/// </summary>

class DrawImage : LWP15Draw.DrawObject

{

private Rectangle rectangle;

private Bitmap _image;

private Bitmap _originalImage; // Оригинальное изображение

public Bitmap TheImage

{

get { return _image; }

set

{

_originalImage = value;

ResizeImage(rectangle.Width, rectangle.Height);

}

}

private const string entryRectangle = "Rect";

private const string entryImage = "Image";

private const string entryImageOriginal = "OriginalImage";

/// <summary>

/// Клонирование данного экземпляра

/// </summary>

public override DrawObject Clone()

{

DrawImage drawImage = new DrawImage();

drawImage._image = _image;

drawImage._originalImage = _originalImage;

drawImage.rectangle = rectangle;

FillDrawObjectFields(drawImage);

return drawImage;

}

protected Rectangle Rectangle

{

get { return rectangle; }

set { rectangle = value; }

}

public DrawImage()

{

SetRectangle(0, 0, 1, 1);

Initialize();

}

public DrawImage(int x, int y)

{

rectangle.X = x;

rectangle.Y = y;

rectangle.Width = 1;

rectangle.Height = 1;

Initialize();

// Устанавливаем перо по умолчанию для каждого нового изображения

Color = Color.Black;

PenWidth = 0;

}

public DrawImage(int x, int y, Bitmap image)

{

rectangle.X = x;

rectangle.Y = y;

_image = (Bitmap)image.Clone();

SetRectangle(rectangle.X, rectangle.Y, image.Width, image.Height);

Initialize();

}

/// <summary>

/// лавная функция рисования изображения на форме

/// </summary>

/// <param name="g"></param>

public override void Draw(Graphics g)

{

Pen pen = new Pen(Color, PenWidth);

if (_image == null)

{

g.DrawRectangle(pen, rectangle);

}

else

{

g.DrawImage(_image, new Point(rectangle.X, rectangle.Y));

g.DrawRectangle(pen, rectangle);

}

}

protected void SetRectangle(int x, int y, int width, int height)

{

rectangle.X = x;

rectangle.Y = y;

rectangle.Width = width;

rectangle.Height = height;

}

public override int HandleCount

{

get { return 8; }

}

/// <summary>

/// Получение ключевых точек изображения

/// </summary>

/// <param name="handleNumber"></param>

/// <returns></returns>

public override Point GetHandle(int handleNumber)

{

int x, y, xCenter, yCenter;

xCenter = rectangle.X + rectangle.Width / 2;

yCenter = rectangle.Y + rectangle.Height / 2;

x = rectangle.X;

y = rectangle.Y;

switch (handleNumber)

{

case 1:

x = rectangle.X;

y = rectangle.Y;

break;

case 2:

x = xCenter;

y = rectangle.Y;

break;

case 3:

x = rectangle.Right;

y = rectangle.Y;

break;

case 4:

x = rectangle.Right;

y = yCenter;

break;

case 5:

x = rectangle.Right;

y = rectangle.Bottom;

break;

case 6:

x = xCenter;

y = rectangle.Bottom;

break;

case 7:

x = rectangle.X;

y = rectangle.Bottom;

break;

case 8:

x = rectangle.X;

y = yCenter;

break;

}

return new Point(x, y);

}

/// <summary>

/// Проверка нажатия.

/// Возвращаемые параметры: -1 - нет нажатия

/// 0 - нажатие где угодно

/// > 1 - обработка ключевой точки

/// </summary>

/// <param name="point"></param>

/// <returns></returns>

public override int HitTest(Point point)

{

if (Selected)

{

for (int i = 1; i <= HandleCount; i++)

{

if (GetHandleRectangle(i).Contains(point)) return i;

}

}

if (PointInObject(point)) return 0;

return -1;

}

protected override bool PointInObject(Point point)

{

return rectangle.Contains(point);

}

/// <summary>

/// Получение курсора для ключевых точек

/// </summary>

/// <param name="handleNumber"></param>

/// <returns></returns>

public override Cursor GetHandleCursor(int handleNumber)

{

switch (handleNumber)

{

case 1:

return Cursors.SizeNWSE;

case 2:

return Cursors.SizeNS;

case 3:

return Cursors.SizeNESW;

case 4:

return Cursors.SizeWE;

case 5:

return Cursors.SizeNWSE;

case 6:

return Cursors.SizeNS;

case 7:

return Cursors.SizeNESW;

case 8:

return Cursors.SizeWE;

default:

return Cursors.Default;

}

}

/// <summary>

/// Изменение размеров объекта изображения

/// </summary>

/// <param name="point"></param>

/// <param name="handleNumber"></param>

public override void MoveHandleTo(Point point, int handleNumber)

{

int left = Rectangle.Left;

int top = Rectangle.Top;

int right = Rectangle.Right;

int bottom = Rectangle.Bottom;

switch (handleNumber)

{

case 1:

left = point.X;

top = point.Y;

break;

case 2:

top = point.Y;

break;

case 3:

right = point.X;

top = point.Y;

break;

case 4:

right = point.X;

break;

case 5:

right = point.X;

bottom = point.Y;

break;

case 6:

bottom = point.Y;

break;

case 7:

left = point.X;

bottom = point.Y;

break;

case 8:

left = point.X;

break;

}

Dirty = true;

SetRectangle(left, top, right - left, bottom - top);

ResizeImage(rectangle.Width, rectangle.Height);

}

protected void ResizeImage(int width, int height)

{

if (_originalImage != null)

{

Bitmap b = new Bitmap(_originalImage, new Size(width, height));

_image = (Bitmap)b.Clone();

b.Dispose();

}

}

public override bool IntersectsWith(Rectangle rectangle)

{

return Rectangle.IntersectsWith(rectangle);

}

/// <summary>

/// Перемещение объекта изображения

/// </summary>

/// <param name="deltaX"></param>

/// <param name="deltaY"></param>

public override void Move(int deltaX, int deltaY)

{

rectangle.X += deltaX;

rectangle.Y += deltaY;

Dirty = true;

}

public override void Dump()

{

base.Dump();

Trace.WriteLine("rectangle.X = " + rectangle.X.ToString(CultureInfo.InvariantCulture));

Trace.WriteLine("rectangle.Y = " + rectangle.Y.ToString(CultureInfo.InvariantCulture));

Trace.WriteLine("rectangle.Width = " + rectangle.Width.ToString(CultureInfo.InvariantCulture));

Trace.WriteLine("rectangle.Height = " + rectangle.Height.ToString(CultureInfo.InvariantCulture));

}

/// <summary>

/// Нормализация объекта прямоугольника

/// </summary>

public override void Normalize()

{

rectangle = DrawRectangle.GetNormalizedRectangle(rectangle);

}

/// <summary>

/// Сохранение объекта изображения в потоке сериализации

/// </summary>

/// <param name="info"></param>

/// <param name="orderNumber"></param>

public override void SaveToStream(SerializationInfo info, int orderNumber)

{

info.AddValue(

String.Format(CultureInfo.InvariantCulture,

"{0}{1}", entryRectangle, orderNumber), rectangle);

info.AddValue(

String.Format(CultureInfo.InvariantCulture,

"{0}{1}",

entryImage, orderNumber), _image);

info.AddValue(

String.Format(CultureInfo.InvariantCulture,

"{0}{1}",

entryImageOriginal, orderNumber), _originalImage);

base.SaveToStream(info, orderNumber);

}

/// <summary>

/// Загрузка объекта изображения из потока сериализации

/// </summary>

/// <param name="info"></param>

/// <param name="orderNumber"></param>

public override void LoadFromStream(SerializationInfo info, int orderNumber)

{

rectangle = (Rectangle)info.GetValue(

String.Format(CultureInfo.InvariantCulture,

"{0}{1}", entryRectangle, orderNumber),

typeof(Rectangle));

_image = (Bitmap)info.GetValue(

String.Format(CultureInfo.InvariantCulture,

"{0}{1}", entryImage, orderNumber),

typeof(Bitmap));

_originalImage = (Bitmap)info.GetValue(

String.Format(CultureInfo.InvariantCulture,

"{0}{1}", entryImageOriginal, orderNumber),

typeof(Bitmap));

base.LoadFromStream(info, orderNumber);

}

#region Вспомогательные функции

public static Rectangle GetNormalizedRectangle(int x1, int y1, int x2, int y2)

{

if (x2 < x1)

{

int tmp = x2;

x2 = x1;

x1 = tmp;

}

if (y2 < y1)

{

int tmp = y2;

y2 = y1;

y1 = tmp;

}

return new Rectangle(x1, y1, x2 - x1, y2 - y1);

}

public static Rectangle GetNormalizedRectangle(Point p1, Point p2)

{

return GetNormalizedRectangle(p1.X, p1.Y, p2.X, p2.Y);

}

public static Rectangle GetNormalizedRectangle(Rectangle r)

{

return GetNormalizedRectangle(r.X, r.Y, r.X + r.Width, r.Y + r.Height);

}

#endregion

}

}

Второй класс нам понадобится для реализации инструмента для рисования изображения. ToolImage (ToolImage.cs) выглядит так:

using System;

using System.Drawing;

using System.Windows.Forms;

namespace LWP15Draw

{

/// <summary>

/// Изображение

/// </summary>

internal class ToolImage : ToolObject

{

public ToolImage()

{

Cursor = new Cursor(GetType(), "C_Image.cur");

}

public override void OnMouseDown(DrawArea drawArea, MouseEventArgs e)

{

AddNewObject(drawArea, new DrawImage(e.X, e.Y));

}

public override void OnMouseMove(DrawArea drawArea, MouseEventArgs e)

{

drawArea.Cursor = Cursor;

if (e.Button == MouseButtons.Left)

{

Point point = new Point(e.X, e.Y);

drawArea.GraphicsList[0].MoveHandleTo(point, 5);

drawArea.Refresh();

}

}

public override void OnMouseUp(DrawArea drawArea, MouseEventArgs e)

{

OpenFileDialog ofd = new OpenFileDialog();

ofd.Title = "Вставка изображения";

ofd.Filter = "Изображение в формате PNG|*.png|" + "Изображение в формате JPEG|*.jpg|" + "Изображение в формате BMP|*.bmp|" + "Изображение в формате GIF|*.gif|" + "Изображение в формате TIF|*.tif|" + "Изображения в формате ICO|*.ico|" + "Все файлы|*.*";

ofd.InitialDirectory = Environment.SpecialFolder.MyPictures.ToString();

if (ofd.ShowDialog() == DialogResult.OK)

{

((DrawImage)drawArea.GraphicsList[0]).TheImage = (Bitmap)Bitmap.FromFile(ofd.FileName);

}

ofd.Dispose();

base.OnMouseUp(drawArea, e);

}

}

}

Курсор под инструмент такой:

Не забываем изменить свойство для файла курсора: «Действие при построении» меняем на «Внедрённый ресурс». Добавляем кнопку на панель инструментов сразу после кнопки «Прямоугольник», изображение «по умолчанию» не трогаем:

(Name):

изображениеToolStripButton

Text:

&Изображение

ToolTipText:

Изображение

В меню «Рисование» добавим новый пункт «Изображение» со следующими свойствами:

(Name):

изображениеToolStripMenuItem

Text:

&Изображение

Image:

Импортируем иконку

В файле DrawObject.cs находим:

private const string entryPenWidth = "PenWidth";

Добавляем после:

// Прочее

private bool dirty;

В том же файле DrawObject.cs находим:

/// <summary>

/// ID объекта

/// </summary>

public int ID

{

get { return id; }

set { id = value; }

}

Добавляем после:

/// <summary>

/// true когда объект изменён

/// </summary>

public bool Dirty

{

get { return dirty; }

set { dirty = value; }

}

В файле DrawArea.cs находим:

public enum DrawToolType

{

Pointer, Polygon, Line, Ellipse, Rectangle, NumberOfDrawTools

};

Заменяем на:

public enum DrawToolType

{

Pointer, Polygon, Line, Ellipse, Rectangle, Image, NumberOfDrawTools

};

В том же файле DrawArea.cs находим:

tools[(int)DrawToolType.Ellipse] = new ToolEllipse();

tools[(int)DrawToolType.Rectangle] = new ToolRectangle();

Добавляем после:

tools[(int)DrawToolType.Image] = new ToolImage();

На главной форме инициализируем события двух кнопок «Изображение» в строке меню и на панели инструментов с иконками, а также добавляем общую функцию для двух обработчиков Click:

#region Добавленный инструмент вставки изображения

private void изображениеToolStripButton_Click(object sender, EventArgs e)

{

CommandImage();

}

private void изображениеToolStripMenuItem_Click(object sender, EventArgs e)

{

CommandImage();

}

/// <summary>

/// Изображение

/// </summary>

private void CommandImage()

{

drawArea.ActiveTool = DrawArea.DrawToolType.Image;

toolStripStatusLabel.Text = "Выбран инструмент \"Изображение\"";

}

#endregion

Готово. Можно компилировать и проверять работоспособность.