Sources.RU Magazine Поиск по журналу
 

TopList

Векторный редактор

Автор: orb (сайт автора)

Работа с векторной графикой

В предыдущем номере описывалось создание растрового редактора, теперь рассмотрим самый простой векторный редактор.

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


Оболочка

Будем использовать VisualC++ и библиотеку Microsoft Foundation Class Library (MFC).

Создаем новый проект со следующими параметрами:

Имя проекта Vector;
Single document;
Document/View architecture support;
Класс вида CScrollView.

Остальные параметры проекта установите по своему усмотрению.

Заходим в класс документа CVectorDoc и забиваем переменные, которые будем использовать для управления программой.

int iPaperWidth; - ширина листа бумаги, на котором мы рисуем
int iPaperHeight; - высота

Используем бумагу формата А4 (210х297 мм), офисную бумагу для принтеров (программа будет уметь печатать).

int iMapMode; - режим отображения.

Для того, чтобы рисунки выводились в размере, соответствующем изображению на экране, используем режим MM_HIMETRIC. В этом режиме будет соблюдаться следующее условие:

1 логическая единица = 0,01 мм

то есть, чтобы нарисовать линию длинной 10 мм, необходимо нарисовать прямую длинной 1000 единиц.

В конструкторе прописываем:

iMapMode=MM_HIMETRIC;
iPaperWidth=21000;
iPaperHeight=29700;

Для установки режима отображения переопределим функцию CScroolView::OnUpdate:

void CVectorView::OnUpdate(CView * pSender, LPARAM lHint, CObject* pHint)
{
	CVectorDoc *pDoc=GetDocument();
	CSize sizePaper;
	sizePaper.cx=pDoc->iPaperWidth;
	sizePaper.cy=pDoc->iPaperHeight;
	SetScrollSizes(pDoc->iMapMode, sizePaper);
	CScrollView::OnUpdate(pSender, lHint, pHint);
}

Чтобы пользователю была видна область для рисования, переопределим метод CScrollView::OnPrepareDC, который будет ограничивать область рисования:

void CVectorView::OnPrepareDC(CDC* pDC, CPrintInfo * pInfo)
{
	CScrollView::OnPrepareDC(pDC, pInfo);
	CVectorDoc *pDoc=GetDocument();
	CPoint oPt(0, -pDoc->iPaperHeight);	//создаем точку в левом нижнем углу
	pDC->LPtoDP(&oPt);	//переводим точку из логических координат в координаты физического устройства
	pDC->SetViewportOrg(oPt);	//устанавливаем начало координат
	pDC->IntersectClipRect(0, 0, pDoc->iPaperWidth, pDoc->iPaperHeight);  //ограничиваем область рисования
}

Теперь выделим область рисования, залив недоступную область серым фоном. Для этого используем сообщение WM_ERASEBKGND. Создадим обработчик этого сообщения:

BOOL CVectorView::OnEraseBkgnd(CDC *pDC)
{
	BOOL bResult=CScrollView::OnEraseBkgnd(pDC);
	CBrush oBrushGray(GetSysColor(COLOR_GRAYTEXT));	//кисть для заливки
	FillOutsideRect(pDC, &oBrushGray);	//заливка неиспользуемой области
	return(bResult);
}

Для изменения пользователем размеров бумаги создадим диалоговое окно. В шаблон диалогового окна нужно поместить два поля ввода (Edit box) "Ширина" и "Высота" и подписать, используя Static text. Рядом с каждым полем ввода разместим элемент Spin, включив в его параметрах свойства Auto buddy и Set buddy integer. Кнопки OK и Cancel уже присутствуют. Для работы с диалоговым окном создадим отдельный класс CPaperSizeDlg. Для полей ввода создадим переменные UINT m_uHeight, UINT m_uWidth и привяжем их к соответствующим идентификаторам. Также можно ограничить диапазон вводимых пользователем значений: 0...210 для ширины и 0...297 для высоты листа бумаги. Для управления Spin-элементами создадим переменные CSpinButtonCtrl m_ctrlSpinHeigh и CSpinButtonCtrl m_ctrlSpinWidth.

Создадим функцию-обработчик сообщения WM_INITDIALOG, которое будет поступать перед созданием диалогового окна:

BOOL CPaperSizeDlg::OnInitDialog()
{
	CDHtmlDialog::OnInitDialog();
	m_ctrlSpinWidth.SetRange(0, 210);	// диапазон для значений элементов Spin
	m_ctrlSpinHeight.SetRange(0, 297);
	return(TRUE);
}

Перейдем на вкладку Resource View ->Menu ->IDR_MAINFRAME. Откроется шаблон меню программы. Здесь добавим пункт меню для вызова диалогового окна и назначим функцию-обработчик:

void CVectorDoc::OnFilePapersize()
{
	CPaperSizeDlg oPaperSize;
	oPaperSize.m_uHeight=iPaperHeight/100;	//передаем в окно текущие размеры бумаги в миллиметрах 
	oPaperSize.m_uWidth=iPaperWidth/100;
	if(oPaperSize.DoModal()==IDOK)
	{	//если пользователь нажал ОК, принимаем новые параметры размера бумаги
		iPaperHeight=oPaperSize.m_uHeight*100;
		iPaperWidth=oPaperSize.m_uWidth*100;
		UpdateAllViews(NULL);
	
}
}

Рисование примитивов

Линии:

Создадим в классе документа следующие переменные:

#define MAXPOINTS 1000	//максимальное количество точек
int iNumPoint;	//количество уже поставленных точек
CPoint aPoints[MAXPOINTS];	//массив координат точек

При каждом щелчке левой кнопкой мыши (OnLButtonDown) будем добавлять новую координату:

if(pDoc->iNumPoint==MAXPOINTS)
	AfxMessageBox("Слишком много точек");
else
{
	pDoc->aPoints[pDoc->iNumPoint++]= point;
}

Примитивы:

Добавим в программу возможность рисования примитивов: точка, круг, квадрат.

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

class CBasePoint : public CObject, public CPoint
{
	DECLARE_SERIAL(CBasePoint)	//макрос необходим для сериализации класса
protected:
	CPen oPen;	//контур фигуры
	CBrush oBrush;	//заливка фигуры
public:
	int iSize;	//размер
	int iPenStyle;	//стиль контура
	int iPenWidth;	//толщина контура
	COLORREF rgbPenColor;	//цвет контура
	COLORREF rgbBrushColor;	//цвет кисти
	int iBrushStyle;	//стиль кисти
	DWORD dwPattern_ID;	//идентификатор шаблона заливки
public:
	CBasePoint();	//конструктор без параметров
	CBasePoint(int x, int y, int size);	//конструктор с начальной инициализацией
	virtual ~CBasePoint();
	virtual void Show(CDC *pDC);	//метод вывода на контекст устройства (монитор, принтер)
	virtual void GetRegion(CRgn &Rgn);	//метод, определяющий занимаемую фигурой площадь
	virtual BOOL SetPen(COLORREF rgbColor, int iWidth=1, int iStyle=PS_SOLID);	//задание параметров контура
	virtual BOOL SetBrush(COLORREF rgbColor, DWORD dwPattern=0, int iStyle=-1);	//задание параметров кисти
protected:
	virtual void Serialize(CArchive &ar);	//сохранение/восстановления картинки в/из файла
	virtual BOOL PrepareDC(CDC *pDC);	//подготовка контекста устройства
	virtual BOOL RestoreDC(CDC *pDC);	//восстанавление контекста устройства
};

Конструкторы инициализируют свойства и параметры фигуры:

CBasePoint::CBasePoint(int x, int y, int size) : CPoint(x, y)
{
	iSize=size;
	iPenWidth=1;
	iPenStyle=PS_SOLID;
	rgbPenColor=RGB(0, 0, 0);
	iBrushStyle=-1;
	rgbBrushColor=RGB(0, 0 ,0);
    dwPattern_ID=0;
}

Установка параметров для контура и заливки фигуры:

BOOL CBasePoint::SetPen(COLORREF rgbColor, int iWidth, int iStyle)
{
	iPenStyle=iStyle;
	iPenWidth=iWidth;
	rgbPenColor=rgbColor;
	if(HPEN(oPen)!=NULL)
		if(!oPen.DeleteObject())
			return(FALSE);
	return(oPen.CreatePen(iPenStyle, iPenWidth, rgbPenColor));
}
BOOL CBasePoint::SetBrush(COLORREF rgbColor, DWORD dwPattern, int iStyle)
{
	iBrushStyle=iStyle;
	dwPattern_ID=dwPattern;
	rgbBrushColor=rgbColor;
	if(HBRUSH(oBrush)!=NULL)
		if(!oBrush.DeleteObject())
			return(FALSE);
	if(dwPattern_ID>0)
	{
		CBitmap oPattern;
		if(!oPattern.LoadBitmap(dwPattern_ID))
			return(FALSE);
		return(oBrush.CreatePatternBrush(&oPattern));
	}
	if(iBrushStyle>=0)
		return(oBrush.CreateHatchBrush(iBrushStyle, rgbBrushColor));
	return(oBrush.CreateSolidBrush(rgbBrushColor));
}

Сохранение/восстановление рисунка в/из файла:

IMPLEMENT_SERIAL(CBasePoint, CObject, VERSIONABLE_SCHEMA|1)	//в паре с макросом DECLARE_SERIAL(CBasePoint)
void CBasePoint::Serialize(CArchive &ar)
{
	if(ar.IsStoring())
	{	//сохранение фигуры
		ar<>x;
		ar>>y;
		ar>>iSize;
		ar>>iPenStyle;
		ar>>iPenWidth;
		ar>>rgbPenColor;
		ar>>iBrushStyle;
		ar>>rgbBrushColor;
		ar>>dwPattern_ID;
		SetPen(rgbPenColor, iPenWidth, iPenStyle);	//задание параметров для фигуры
		SetBrush(rgbBrushColor, dwPattern_ID, iBrushStyle);
	}
}

Подготовка контекста устройства и сброс параметров в значения по умолчанию:

BOOL CBasePoint::PrepareDC(CDC *pDC)
{
	if(!pDC->SaveDC())	return(FALSE);
	if(HPEN(oPen)!=NULL)	pDC->SelectObject(&oPen);
	if(HBRUSH(oBrush)!=NULL)	pDC->SelectObject(&oBrush);
	return(TRUE);
}
BOOL CBasePoint::RestoreDC(CDC *pDC)
{
	return(pDC->RestoreDC(-1));
}

Вывод на контекст устройства:

void CBasePoint::Show(CDC *pDC)
{
	PrepareDC(pDC);
	pDC->Ellipse(x-iSize, y-iSize, x+iSize, y+iSize);
	RestoreDC(pDC);
}

Метод, возвращающий область, занимаемую фигурой:

void CBasePoint::GetRegion(CRgn &Rgn)
{
	Rgn.CreateEllipticRgn(x-iSize, y-iSize, x+iSize, y+iSize);
}

После определения базового класса, создадим производные от него для квадрата и круга:


Квадрат:

class CSquare : public CBasePoint
{
	DECLARE_SERIAL(CSquare)
protected:
	void Serialize(CArchive &ar);
public:
	CSquare(int x, int y, int size);
	CSquare();
	~CSquare()
		{};
	void Show(CDC *pDC);
	void GetRegion(CRgn &Rgn);
};

Переопределим конструкторы и методы вывода и расчета занимаемой области:

CSquare::CSquare(int x, int y, int size):CBasePoint(x, y, size)
{
	iSize=size;
}
CSquare::CSquare():CBasePoint()
{
	iSize=40;
}
void CSquare::Show(CDC *pDC)
{
	int t=iSize/2;
	PrepareDC(pDC);
	pDC->Rectangle(x-t, y-t, x+t, y+t);
	RestoreDC(pDC);
}
void CSquare::GetRegion(CRgn &Rgn)
{
	int t=iSize/2;
	Rgn.CreateRectRgn(x-t, y-t, x+t, y+t);
}

В отличии от количества точек в линии, количество фигур, которые нарисует пользователь, невозможно оценить зарание, а создание слишком большого массива для хранения фигур приведет к перерасходу памяти компьютера. Поэтому организуем динамическое создание объектов, а указатели на фигуры будем сохранять в списке CTypedPtrList. Для поддержки работы с шаблонными классами требуется подключить заголовок:

#include <afxtempl.h>

в файле stdafx.h

Определим список в классе документа:

CTypedPtrList oShapeList;

Также создадим функцию для очистки памяти при удалении объектов:

void CVectorDoc::ClearShapesList()
{
	POSITION pos=NULL;
	while(oShapeList.GetCount() > 0)
		delete oShapeList.RemoveHead();
}

Нужно уметь определять, какую именно фигуру рисует пользователь в данный момент. Создадим идентификаторы операций и определим переменную, которая будет хранить код текущей операции int iCurrentShape:

#define CURRENT_NONE 0
#define CURRENT_LINE 1
#define CURRENT_POINT 2
#define CURRENT_CIRCLE 3
#define CURRENT_SQUARE 4

Зайдем опять на вкладку ресурсов меню и создадим меню для выбора фигуры. К каждому пункту меню привяжем обработчик, в теле которого переменной iCurrentShape будет присваиваться новый код операции. Например, для линии:

void CVectorView::OnShapeLine()
{
	iCurrentShape=CURRENT_LINE;
}

аналогично - для остальных фигур.

Теперь необходимо в коде обработчика щелчка левой кнопки мыши проанализировать, какую фигуру нужно создавать:

afx_msg void CVectorView::OnLButtonDown(UINT nFlags, CPoint point)
{
	CVectorDoc *pDoc=GetDocument();
	CPoint oLogPoint=point;
	CDC *pDC=GetDC();
	OnPrepareDC(pDC, NULL);
	pDC->DPtoLP(&oLogPoint);	//преобразование физических координат в логические единицы
	ReleaseDC(pDC);
	switch(iCurrentShape)	//проверка кода текущей операции
	{
	case CURRENT_LINE:	//создание точки для построения линии
		if(pDoc->iNumPoint==MAXPOINTS)
			AfxMessageBox("Слишком много точек");
		else
		{
			pDoc->aPoints[pDoc->iNumPoint++]=oLogPoint;
			Invalidate();
			pDoc->SetModifiedFlag();
		}
		break;
	case CURRENT_POINT:	//создаем точку
	case CURRENT_CIRCLE:	//создаем круг
	case CURRENT_SQUARE:	//создаем квадрат
		AddShape(iCurrentShape, oLogPoint);
		break;
	}
	CScrollView::OnLButtonDown(nFlags, point);
}

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

void CVectorView::AddShape(int shape, CPoint point)
{
	CVectorDoc *pDoc=GetDocument();
	CBasePoint *pShape=NULL;	//указатель на фигуру
	switch(shape)
	{
	case CURRENT_POINT:	//создание точки
		pShape=new CBasePoint(point.x, point.y, 100);
		pShape->SetPen(RGB(0, 0, 0), 3, PS_GEOMETRIC);
		pShape->SetBrush(RGB(255, 0, 255));
		break;
	case CURRENT_CIRCLE:	//создание круга
		pShape=new CBasePoint(point.x, point.y, 1000);
		pShape->SetPen(RGB(10, 10, 10), 50, PS_GEOMETRIC);
		pShape->SetBrush(RGB(74, 180, 20));
		break;
	case CURRENT_SQUARE:	//создание квадрата
		pShape=new CSquare(point.x, point.y, 2000);
		pShape->SetPen(RGB(0, 0, 255), 80, PS_GEOMETRIC);
		pShape->SetBrush(RGB(255, 255, 0));
		break;
	}
	if(pShape!=NULL)
	{
		pDoc->oShapeList.AddTail(pShape);	//добавление новой фигуры в список
		Invalidate();
		pDoc->SetModifiedFlag();
	}
}

Сохранение рисунка в файл

Сразу скажу, что в дальнейшем этот редактор предполагается усовершенствовать, поэтому необходимо различать версии редактора и загружать данные только в том случае, если версия файла с данными не "младше" текущей версии редактора:

void CVectorDoc::Serialize(CArchive& ar)
{
	CString sVersion;
	int iVersion;
	int i;
	if (ar.IsStoring())
	{
		iVersion=1;	//номер текущей версии
		sVersion.Format("vc%d", iVersion);
		ar<>sVersion;	//загрузка номера версии
		iVersion=atoi((LPCTSTR)sVersion.Right(1));
		switch(iVersion)	//проверка версии
		{
		case 1:	//первая версия! Наша, можно загружать рисунок
			ar >> iNumPoint;
			for(int i=0; i> aPoints[i];
			ar>>iMapMode;
			ar>>iPaperWidth;
			ar>>iPaperHeight;
			break;
		default:	//более новая версия,
//т.к. мы не знаем формата, в котором файл сохранялся, предупредим пользователя и остановим загрузку
			AfxMessageBox("Неизвестный формат файла", MB_OK);
			return;
		}
	}
	oShapeList.Serialize(ar);	//сохраним все фигуры в списке
}

На этом все. Компилируем, запускаем и .... совершенствуем. Хотя это уже совсем другая история.

PS: данный текст не описывает 100% создания графического редактора, при его создании я предполагал, что читающий знает наизусть книгу "Visual C++ за 21 урок" или что-то из разряда "Освой Visual C++ за 2 дня". Если у Вас возникают трудности, можете обратиться на наш форум: forum.sources.ru, где Вам с радостью помогут. Можно также просмотреть исходники, прилагающиеся в конце, или запустить готовый файл для просмотра результата.


Скачать исходник: VectorSource.rar (62 КБ)
  VectorEXE.zip (199 КБ)


 Design by Шишкин Алексей (Лёха)  ©2004-2008 by sources.ru