Автор: 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; Также создадим функцию для очистки памяти при удалении объектов: <code cpp> 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<<< iNumPoint; //сохраняем количество точек for(i=0; i<<<<>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 КБ)