이제 지금까지 만든 GraphicEditor에 마우스를 이용하여 객체를 그리는 기능을 추가해보겠습니다.
객체를 그리는 방법은 mspaint 처럼 Image에 직접 그림을 그려 그 그림을 저장하는 형태와 일러스트 처럼 각 그리기 객체를 내부적으로 메모리를 관리하는 형태(Vector형태로 관리) 두가지가 있습니다. 여기서는 내부적으로 메모리를 관리하는 형태로 구성해보도록 하겠습니다.
필자가 처음 프로그램을 시작할때 그래픽 편집기를 만들기 위해, 선그리기, 사각 그리기 등을 화면에 그리는 루틴들을 열심히 작성했습니다. 그 당시 DOS 시절이다 보니 개별 알고리즘으로 하나씩 직접 구현해야 해서 많은 시간을 들여 구현했으며, 마우스 처리 등 그래픽 편집기에서 필요한 거의 대부분의 기능을 구현하고 마지막으로 파일로 저장하려고 하니 저장할 방법이 없었습니다. 어쩔수 없이 화면이 아닌 메모리에 그림을 그리고, 그 메모리의 내용을 화면에 저장하도록 루틴을 처음부터 다시 작성하여 구현을 완료하였습니다. 이 사례로 얻을수 있는 교훈은 프로그램을 만들때 눈에 보이는 기능을 위주로 구현을 하면 안되고, 메모리 구조 부터 먼저 설계하고 구현해야 한다는 것입니다.
이제 그 메모리 구조에 해당하는 객체의 Class들을 구현해 보도록 하겠습니다.
프로젝트 이름에서 마우스 오른쪽 버튼을 누르면 다음과 같이 팝업 메뉴가 화면에 나타납니다.
Add New를 실행하면 다음과 같이 화면에 나타납니다.
C++ Class를 선택하고 Choose를 클릭하여 나타난 화면에서 Class name에서 GraphicObj를 입력하면 다음과 같이 화면에 나타납니다.
Next를 클릭하여 나타난 화면에서 Finish를 클릭하면 GraphicObj Class가 추가됩니다.
같은 방식으로 GraphicLine, GraphicRect, GraphicRoundRect, GraphicEllipse, GraphicObjList를 각각 추가한후 다음과 같이 작성합니다.
graphicobj.h
#ifndef GRAPHICOBJ_H
#define GRAPHICOBJ_H
#include "crect.h"
#include "datatype.h"
#include "DrawTools.h"
#define DRAW_TYPE_SELECT 0
#define DRAW_TYPE_LINE 1
#define DRAW_TYPE_RECTANGLE 2
#define DRAW_TYPE_ROUND_RECT 3
#define DRAW_TYPE_ELLIPSE 4
class GraphicObj
{
public:
GraphicObj();
virtual ~GraphicObj(){}
int m_nDrawType;
CRect m_rectPos;
COLORREF m_nLineColor;
COLORREF m_nFaceColor;
CDrawTools m_DrawTools;
virtual void Draw(QPainter *){}
void SetStartPos(QPoint point);
void SetEndPos(QPoint point);
};
#endif // GRAPHICOBJ_H
여기서는 사각형 위치와 선, 면 색상만 두었습니다. virtual 로 Draw 함수를 하나 구성하였으며 이후에 설명할 예정이니 virtual로 선언한 이 함수를 기억하기 바랍니다.GraphicObj는 그림 객체의 Base 객체로 기본적으로 가져야 할 정보를 구성합니다.
graphicobj.cpp
#include "graphicobj.h"
GraphicObj::GraphicObj()
{
m_nLineColor = RGB(0, 0, 0);
m_nFaceColor = RGB(255, 255, 255);
}
void GraphicObj::SetStartPos(QPoint point)
{
m_rectPos.SetRect(point.x(), point.y(), point.x(), point.y());
}
void GraphicObj::SetEndPos(QPoint point)
{
m_rectPos.SetRect(m_rectPos.x(), m_rectPos.y(), point.x(), point.y());
}
GraphicLine은 GraphicObj로부터 상속 받아 구성합니다. 앞에서 virtual 로 Draw 함수를 여기서는 override로 구성하였습니다. DrawRect, DrawRoundRect, DrawEllipse는 그리는 내용만 다르므로 작성해 보기 바랍니다.
graphicline.h
#ifndef GRAPHICLINE_H
#define GRAPHICLINE_H
#include "graphicobj.h"
class GraphicLine : public GraphicObj
{
public:
GraphicLine();
void Draw(QPainter *painter) override;
};
#endif // GRAPHICLINE_H
GraphicLine은 GraphicObj로부터 상속 받아 구성합니다. 앞에서 virtual 로 Draw 함수를 여기서는 override로 구성하였습니다. DrawRect, DrawRoundRect, DrawEllipse는 그리는 내용만 다르므로 작성해 보기 바랍니다.
graphicline.cpp
#include "graphicline.h"
GraphicLine::GraphicLine()
{
}
void GraphicLine::Draw(QPainter *painter)
{
m_DrawTools.DrawLine(painter, m_rectPos, m_nLineColor);
}
graphicrect.h
#ifndef GRAPHICRECT_H
#define GRAPHICRECT_H
#include "graphicobj.h"
class GraphicRect : public GraphicObj
{
public:
GraphicRect();
void Draw(QPainter *painter) override;
};
#endif // GRAPHICRECT_H
graphicrect.cpp
#include "graphicrect.h"
GraphicRect::GraphicRect()
{
}
void GraphicRect::Draw(QPainter *painter)
{
m_DrawTools.DrawRect(painter, m_rectPos, m_nLineColor, m_nFaceColor);
}
graphicroundrect.h
#ifndef GRAPHICROUNDRECT_H
#define GRAPHICROUNDRECT_H
#include "graphicobj.h"
class GraphicRoundRect : public GraphicObj
{
public:
GraphicRoundRect();
void Draw(QPainter *painter) override;
};
#endif // GRAPHICROUNDRECT_H
graphicroundrect.cpp
#include "graphicroundrect.h"
GraphicRoundRect::GraphicRoundRect()
{
}
void GraphicRoundRect::Draw(QPainter *painter)
{
m_DrawTools.DrawRoundRect(painter, m_rectPos, m_nLineColor, m_nFaceColor);
}
graphicellipse.h
#ifndef GRAPHICELLIPSE_H
#define GRAPHICELLIPSE_H
#include "graphicobj.h"
class GraphicEllipse : public GraphicObj
{
public:
GraphicEllipse();
void Draw(QPainter *painter) override;
};
#endif // GRAPHICELLIPSE_H
graphicellipse.cpp
#include "graphicellipse.h"
GraphicEllipse::GraphicEllipse()
{
}
void GraphicEllipse::Draw(QPainter *painter)
{
m_DrawTools.DrawEllipse(painter, m_rectPos, m_nLineColor, m_nFaceColor);
}
graphicobjlist.h
#ifndef GRAPHICOBJLIST_H
#define GRAPHICOBJLIST_H
#include "graphicobj.h"
class GraphicObjList
{
protected:
QList<GraphicObj*> m_Array;
public:
GraphicObjList();
~GraphicObjList();
bool Add(GraphicObj *pObj);
void Delete(int nPos = -1);
void Draw(QPainter *painter);
static GraphicObj *CreateObj(int nDrawType);
};
#endif // GRAPHICOBJLIST_H
graphicobjlist.cpp
#include "graphicobjlist.h"
GraphicObjList::GraphicObjList()
{
}
GraphicObjList::~GraphicObjList()
{
Delete();
}
bool GraphicObjList::Add(GraphicObj *pObj)
{
m_Array.append(pObj);
return true;
}
void GraphicObjList::Delete(int nPos)
{
GraphicObj *pObj;
if(nPos>=0){
pObj = m_Array.at(nPos);
if(!pObj)
return;
delete pObj;
m_Array.removeAt(nPos);
}
else{
int nCount = m_Array.count();
for(int i = 0; i < nCount; i++){
pObj = (GraphicObj*)m_Array.at(i);
if(pObj){
delete pObj;
}
}
m_Array.clear();
}
}
#include "graphicline.h"
#include "graphicrect.h"
#include "graphicroundrect.h"
#include "graphicellipse.h"
GraphicObj *GraphicObjList::CreateObj(int nDrawType)
{
GraphicObj *pObj = nullptr;
switch(nDrawType)
{
case DRAW_TYPE_LINE:
pObj = new GraphicLine();
break;
case DRAW_TYPE_RECTANGLE:
pObj = new GraphicRect();
break;
case DRAW_TYPE_ROUND_RECT:
pObj = new GraphicRoundRect();
break;
case DRAW_TYPE_ELLIPSE:
pObj = new GraphicEllipse();
break;
}
return pObj;
}
void GraphicObjList::Draw(QPainter *painter)
{
GraphicObj *pObj;
int nCount = m_Array.count();
for (int i = 0; i < nCount; i++)
{
pObj = m_Array.at(i);
pObj->Draw(painter);
}
}
여기서 Draw 함수를 보겠습니다. 그냥 단순히 GraphicObj의 포인트로 Draw함수를 호출합니다(pObj->Draw(painter)). 생성시 각 Class로 생성하여 m_Array에 넣고, 이렇게 호출하면 알아서 선이면 선으로, 사각형이면 사각형으로, 둥근 사각형이면 둥근 사각형으로, 원이면 원으로 그려집니다. 이 기능을 사용하면 루틴을 아주 많이 줄여 줍니다. 여기서 만약 이 기능을 사용하지 않는다면 조건에 따라 각 Class에 맞추어 Draw함수를 호출하도록 조건문이 들어 가야합니다. 만약 지금처럼 객체가 4개이면 차이가 많이 느껴지지 않겠지만, 많을 경우 그 수만큼 늘어 나 차이가 많아집니다.
이 기능을 가능하게 해주는 것이 override 기능입니다. override 기능은 base class에 virtual 로 선언한 함수를 class에서 override로 선언하여 재구성하고, base class함수를 호출하면 알아서 실제 class함수가 호출되는 기능입니다.
static MainWindow *theMain;
int m_nDrawType;
quint32 m_nLineColor;
quint32 m_nFaceColor;
MainWindow에 선언한 변수를 어디서나 사용할 수 있도록 하기위한 theMain 변수를 선언하고, 그리기 종류, 선색, 면색을 담을 수 있는 변수를 선언합니다.
mainwindow.cpp를 열어 다음과 같이 변수에 초기값을 넣습니다.
#include "datatype.h"
#include "graphicobj.h"
MainWindow *MainWindow::theMain = nullptr;
theMain = this;
m_nDrawType = DRAW_TYPE_SELECT;
m_nLineColor = RGB(0, 0, 0);
m_nFaceColor = RGB(255, 255, 255);
theMain에 MainWindow자기 자신의 클래스인 this를 넣어 주면 이후 MainWindow에 public으로 선언한 변수인 m_nDrawType 등을 사용하기 위해 MainWindow::theMain을 통해 사용하면 됩니다(예. MainWindow::theMain->m_nDrawType).
이제 추가한 변수들을 설정할수 있는 메뉴 및 툴바를 만들어 보도록 하겠습니다.
QT02. 기본 프로그램 만들기에서 설명한 방식으로하면 훨씬 더 간단하나, Form 기반이 아니라 Mdi 기반으로 작성했으므로 소스레벨에서 작성해야 합니다.
우선 툴바에 넣을 아이콘을 추가해 보겠습니다.
아래를 클릭하면 아이콘이 다운되며 프로젝트 폴더에 images에 압축을 풀어 넣고 다음과 같이 Resources에 추가합니다.
mainwindow.h를 열어 paste를 찾으면 다음과 같이 화면에 나타납니다.
다음과 같이 5개의 Act에서 호출할 함수와 선색, 면색 설정시 호출할 함수를 정의 합니다.
void select();
void line();
void rectangle();
void roundrect();
void ellipse();
void lineColor();
void faceColor();
다시 찾기를 하면 다음과 같이 화면에 나타나며 5개의 Action과 2개의 PushButton을 추가합니다.
QAction *selectAct;
QAction *lineAct;
QAction *rectangleAct;
QAction *roundRectAct;
QAction *ellipseAct;
QPushButton *m_pLineColorButton;
QPushButton *m_pFaceColorButton;
mainwindow.cpp를 열어 paste를 찾아 5개의 Act에서 호출할 함수와 선색, 면색 설정시 호출할 함수를 정의 합니다.
#include "widgettool.h"
bool MainWindow::GetColor(quint32 &nColor)
{
QColor color = QColorDialog::getColor(GetQColor(nColor), this );
if(!color.isValid())
return false;
nColor = RGBA(color.red(), color.green(), color.blue(), color.alpha());
return true;
}
void MainWindow::select()
{
m_nDrawType = DRAW_TYPE_SELECT;
}
void MainWindow::line()
{
m_nDrawType = DRAW_TYPE_LINE;
}
void MainWindow::rectangle()
{
m_nDrawType = DRAW_TYPE_RECTANGLE;
}
void MainWindow::roundrect()
{
m_nDrawType = DRAW_TYPE_ROUND_RECT;
}
void MainWindow::ellipse()
{
m_nDrawType = DRAW_TYPE_ELLIPSE;
}
void MainWindow::lineColor()
{
GetColor(m_nLineColor);
WidgetTool::SetButtonColor(m_pLineColorButton, m_nLineColor);
}
void MainWindow::faceColor()
{
GetColor(m_nFaceColor);
WidgetTool::SetButtonColor(m_pFaceColorButton, m_nFaceColor);
}
5개의 Act와 2개의 PushButton은 무조건 Enable이 되어야 하므로 아무것도 하지 않고 다음 찾기를 합니다.다시 찾기를 하면 다음과 같이 화면에 나타납니다.
다시 찾기를 하면 다음과 같이 화면에 나타납니다.
pastAct를 구성한 내용을 참조하여 아래와 같이 5개의 Action과 2개의 PushButton을 구성합니다.
QMenu *drawMenu = menuBar()->addMenu(tr("&Draw")); //Draw를 메뉴에 추가합니다.
QToolBar *drawToolBar = addToolBar(tr("Draw")); //Draw라는 툴바를 추가합니다.
selectAct = new QAction(QIcon(":/images/select.png"), tr("&Select"), this);
connect(selectAct, &QAction::triggered, this, &MainWindow::select);
drawMenu->addAction(selectAct);
drawToolBar->addAction(selectAct);
lineAct = new QAction(QIcon(":/images/line.png"), tr("&Line"), this);
connect(lineAct, &QAction::triggered, this, &MainWindow::line);
drawMenu->addAction(lineAct);
drawToolBar->addAction(lineAct);
rectangleAct = new QAction(QIcon(":/images/rectangle.png"), tr("&Rectangle"), this);
connect(rectangleAct, &QAction::triggered, this, &MainWindow::rectangle);
drawMenu->addAction(rectangleAct);
drawToolBar->addAction(rectangleAct);
roundRectAct = new QAction(QIcon(":/images/roundrect.png"), tr("&Round rectangle"), this);
connect(roundRectAct, &QAction::triggered, this, &MainWindow::roundrect);
drawMenu->addAction(roundRectAct);
drawToolBar->addAction(roundRectAct);
ellipseAct = new QAction(QIcon(":/images/ellipse.png"), tr("&Ellipse"), this);
connect(ellipseAct, &QAction::triggered, this, &MainWindow::ellipse);
drawMenu->addAction(ellipseAct);
drawToolBar->addAction(ellipseAct);
m_pLineColorButton = new QPushButton("");
WidgetTool::SetButtonColor(m_pLineColorButton, m_nLineColor);
connect(m_pLineColorButton, SIGNAL(clicked()), this, SLOT(lineColor()));
drawToolBar->addWidget(m_pLineColorButton);
m_pFaceColorButton = new QPushButton("");
WidgetTool::SetButtonColor(m_pFaceColorButton, m_nFaceColor);
connect(m_pFaceColorButton, SIGNAL(clicked()), this, SLOT(faceColor()));
drawToolBar->addWidget(m_pFaceColorButton);
위 내용을 보면 중간 중간에 connect라는 함수를 사용했습니다.pasteAct에서 사용한 setShortcuts는 단축키를 설정하는 함수이며, setStatusTip은 툴바나 메뉴에 마우스를 올렸을때 상태명칭에 표시할 설명문을 설정하는 함수입니다. 둘다 무조건 사용해야하는 함수는 아니므로 루틴을 간단히 하기위해 제외하고 작성했습니다.
앞에서 설명한 virtual override를 보면 자식 class에서 virtual로 선언한 함수를 호출하면 부모 class의 함수가 자동으로 호출되는 기능입니다. 만약 부모 자식간이 아니라 관련이 없는 class에서 같은 개념으로 자동으로 호출되게 하려고 한다면 SIGNAL과 SLOT을 사용하여 구성하면 됩니다. SIGNAL과 SLOT을 연결해 주는 역할을 하는 것이 connect입니다.
connect(m_pFaceColorButton, SIGNAL(clicked()), this, SLOT(faceColor()));를 예로 들어 설명해 보겠습니다.
QPushButton class를 보면 아래와 같이 signal 형선언으로 clicked함수가 선언되어 있을 것이며 버튼이 마우스로 클릭을 하면 clicked 함수를 호출할 것입니다.
signals:
void clicked();
faceColor함수는 아래와 같이 slot 형선언으로 선언되어 있습니다.
private slots:
void faceColor();
connect(m_pFaceColorButton, SIGNAL(clicked()), this, SLOT(faceColor()));로 두함수를 연결하면 clicked SIGNAL이 실행되면 faceColor SLOT이 자동으로 실행되게 됩니다.
이제 마우스 처리를 해보도록 하겠습니다.
imagefile.h를 열어 GraphicObjList Class로 변수를 하나 정의합니다.
mdiview.h를 열어 mouse이벤트를 실행하는 세개의 함수를 정의하고 그리는 중인 객체의 정보를 가지는 변수인 m_pObj를 선언합니다.
#include <QMouseEvent>
#include "graphicobj.h"
void mousePressEvent(QMouseEvent *e) override;
void mouseReleaseEvent(QMouseEvent *e) override;
void mouseMoveEvent(QMouseEvent *e) override;
GraphicObj *m_pObj;
GraphicObj *CreateObj();
mdiview.cpp를 열어 마우스를 눌렀을때 설정한 그리기 종류와, 선색, 면색으로 객체를 생성하는 CreateObj라는 함수를 만듭니다.
#include "mainwindow.h"
GraphicObj *MdiView::CreateObj()
{
GraphicObj *pObj = GraphicObjList::CreateObj(MainWindow::theMain->m_nDrawType);
if(pObj)
{
pObj->m_nDrawType = MainWindow::theMain->m_nDrawType;
pObj->m_nLineColor = MainWindow::theMain->m_nLineColor;
pObj->m_nFaceColor = MainWindow::theMain->m_nFaceColor;
}
return pObj;
}
마우스를 누를때, 놓을때, 움직일때 다음과 같이 작성합니다.
void MdiView::mousePressEvent(QMouseEvent *e)
{
if(e->button()==Qt::RightButton)
{
e->ignore();
return;
}
m_pObj = CreateObj();
if(!m_pObj)
return;
grabMouse();
m_pObj->SetStartPos(e->pos());
update();
}
void MdiView::mouseReleaseEvent(QMouseEvent *e)
{
if(e->button()==Qt::RightButton)
{
e->ignore();
return;
}
if(!m_pObj)
return;
releaseMouse();
m_ImageFile.m_ObjList.Add(m_pObj);
m_pObj = nullptr;
}
void MdiView::mouseMoveEvent(QMouseEvent *e)
{
if(!m_pObj)
return;
m_pObj->SetEndPos(e->pos());
update();
}
마우스를 누르면 CreateObj 함수를 이용하여 객체를 생성하고 SetStartPos함수를 이용하여 클릭한 위치를 설정합니다.
마우스를 누른 상황에서 이동하면 SetEndPos를 이용하여 끝위치를 설정합니다. update() 함수를 이용하여 paintEvent를 호출하게 되고 그리는 상황이 화면에 표시됩니다.
마두스를 떼면 m_ObjList에 추가하고 m_pObj는 nullptr로 설정합니다.
grapMouse는 마우스를 누른 상황에서 윈도우 영역을 넘어가면 이상동작을 하지 않도록, 마우스 이동 이벤트를 다른 윈도우에 넘어가더라도 계속 받을 수 있도록 하는 함수이며 releaseMouse는 해제하는 함수입니다.
paintEvent 함수를 다음과 같이 수정합니다.
m_ImageFile.m_ObjList.Draw(&painter);
if(m_pObj)
m_pObj->Draw(&painter);
실행하면 그리고자하는 객체를 선택하고 마우스를 이용하여 그리게 되면 해당 그림이 그려지는 그래픽 편집기의 기본기능을 구현한 것입니다.
마지막으로 툴바 메뉴에서 그릴 종류를 선택하면 그 선택한 종류를 선택하여 표시하도록 구성해 보겠습니다.
mainwindow.cpp를 열어 selectAct를 찾아 메뉴 생성하는 부분으로 간 후 setCheckable함수를 이용하여 체크(선택된 모양으로 표시)가 가능하도록 합니다. 같은 방식으로 lineAct, rectangleAct, roundRectAct, ellipseAct에 setCheckable(true)를 호출합니다. selectAct는 setChecked함수를 이용하여 초기에 체크되도록 합니다.
selectAct->setCheckable(true);
selectAct->setChecked(true);
전체 체크를 빼는 UncheckAll이라는 함수를 만들고, 그리기 종류를 선택하는 메뉴를 눌렀을때 해당함수를 호출한 후 setChecked 함수를 이용하여 선택된 모양으로 변경합니다.
void MainWindow::UncheckAll()
{
selectAct->setChecked(false);
lineAct->setChecked(false);
rectangleAct->setChecked(false);
roundRectAct->setChecked(false);
ellipseAct->setChecked(false);
}
void MainWindow::select()
{
m_nDrawType = DRAW_TYPE_SELECT;
UncheckAll();
selectAct->setChecked(true);
}
void MainWindow::line()
{
m_nDrawType = DRAW_TYPE_LINE;
UncheckAll();
lineAct->setChecked(true);
}
void MainWindow::rectangle()
{
m_nDrawType = DRAW_TYPE_RECTANGLE;
UncheckAll();
rectangleAct->setChecked(true);
}
void MainWindow::roundrect()
{
m_nDrawType = DRAW_TYPE_ROUND_RECT;
UncheckAll();
roundRectAct->setChecked(true);
}
void MainWindow::ellipse()
{
m_nDrawType = DRAW_TYPE_ELLIPSE;
UncheckAll();
ellipseAct->setChecked(true);
}
예제 프로그램
'QT' 카테고리의 다른 글
설치 파일 만들기 (0) | 2022.03.12 |
---|---|
QT08. 파일 및 인쇄 (0) | 2022.03.09 |
QT05. 대화상자 (0) | 2022.03.04 |
QT04. GUI (0) | 2022.03.01 |
QT03. MDI 및 아이콘 (0) | 2022.02.27 |