Qt 로 만들자: 다항 함수 그래프 그리기

지금까지 GUI 위젯들을 활용한 프로그램들을 만들어 보았다. 이번에는 다항 함수의 그래프를 그려주는 <Plot> 을 만들어 보면서, 사용자 정의 위젯과 Qt 가 제공하는 페이팅 API  를 살펴볼 것이다.

1. 요구사항

  • 다항식을 입력 받고,
  • x 의 범위를 입력 받아,
  • 다항식의 그래프를 그린다 

2. 코드 작성

2.1 프로젝트 작성

  • 프로젝트 이름: Plot
  • 메인 클래스 이름: Plot
  • 메인 클래스 유형: QMainWindow

2.2 헤더 분석(plot.h)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#include <QMainWindow>

#include <QtWidgets>

class GraphWidget;

/**
 * @brief 그래프 그리기
 */

class Plot : public QMainWindow
{
    Q_OBJECT

public:
    Plot(QWidget *parent = 0);
    ~Plot();

private:
    QLineEdit *_polyLine;           ///< 다항식 편집기
    QLineEdit *_startLine;          ///< 시작값 편집기
    QLineEdit *_endLine;            ///< 끝값 편집기
    QPushButton *_drawGraphPush;    ///< 그래프 그리기 버튼
    GraphWidget *_graph;            ///< 그래프 위젯

    void initMenus();
    void initWidgets();

private slots:
    void axisFixed(bool checked);
    void drawGraph();
};


특별한 내용은 없다. 화면 구성에 필요한 위젯들을 선언하고, 내부 함수와 슬롯을 선언하고 있다.

23 번째 줄: GraphWidget 은 다항 함수의 그래프를 그릴 위젯이다. 나중에 보도록 하자.

2.3 소스 분석(plot.cpp)

2.3.1 Plot 클래스

다항 함수의 그래프를 그리는 메인 클래스이다.

2.3.1.1 생성자와 소멸자

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
 * @brief Plot 생성자
 * @param parent 부모 위젯
 */

Plot::Plot(QWidget *parent)
    : QMainWindow(parent)
{
    initMenus();    // 메뉴 초기화
    initWidgets();  // 위젯 초기화
}

/**
 * @brief Plot 소멸자
 */

Plot::~Plot()
{
}


특이 사항은 없다. 예전처럼 생성자에서 메뉴와 위젯을 초기화한다.

2.3.1.2 initMenus()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
 * @brief 메뉴를 초기화한다
 */

void Plot::initMenus()
{
    // '파일' 메뉴 생성
    QMenu *fileMenu = new QMenu(tr("파일(&F)"));
    // '끝내기' 항목 추가
    fileMenu->addAction(tr("끝내기(&x)"), this, SLOT(close()),
                        QKeySequence(tr("Ctrl+Q")));

    // '보기' 메뉴 생성
    QMenu *viewMenu = new QMenu(tr("보기(&V)"));
    // 체크 가능한 '좌표축 고정하기' 항목 추가
    viewMenu->addAction(tr("좌표축 고정하기(&A)"), this, SLOT(axisFixed(bool)),
                        QKeySequence(tr("Ctrl+F")))->setCheckable(true);

    // 메뉴바에 추가
    menuBar()->addMenu(fileMenu);
    menuBar()->addMenu(viewMenu);
}


'파일' 메뉴와 '보기' 메뉴를 추가한다. 그리고 각각 '끝내기' 항목과 '좌표축 고정하기' 항목을 추가한다.

5 번째 줄: '좌표축 고정하기' 항목을 QAction::setCheckable() 을 통해 '체크' 가능한 상태로 만든다. 이렇게 하면 체크 상태에 따라 메뉴 항목 왼쪽에 V 자가 나타난다.

2.3.1.3 initWidgets()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
/**
 * @brief 위젯을 초기화한다
 */

void Plot::initWidgets()
{
    _polyLine = new QLineEdit;
    _startLine = new QLineEdit;
    _endLine = new QLineEdit;

    _drawGraphPush = new QPushButton(tr("그래프 그리기(&G)"));
    connect(_drawGraphPush, SIGNAL(clicked(bool)),
            this, SLOT(drawGraph()));

    QFormLayout *formLayout = new QFormLayout;
    formLayout->addRow(tr("f(x) ="), _polyLine);
    formLayout->addRow(tr("시작값"), _startLine);
    formLayout->addRow(tr("끝값"), _endLine);
    formLayout->addRow(_drawGraphPush);

    _graph = new GraphWidget;

    QVBoxLayout *vboxLayout = new QVBoxLayout;
    vboxLayout->addLayout(formLayout);
    vboxLayout->addWidget(_graph, 1);

    QWidget *w = new QWidget;
    w->setLayout(vboxLayout);

    setCentralWidget(w);

    // 초기 크기 설정
    resize(640, 480);
}


여러번 보아왔던 패턴이라 따로 설명할 것이 없다. 다만 초기 크기를 설정하고 있는 것이 조그만 차이이다.

2.3.1.4 axisFixed()

1
2
3
4
5
6
7
8
9
10
11
/**
 * @brief '좌표축 고정하기' 항목이 선택될 때 호출된다
 * @param checked true 이면 체크 된 상태이고, false 이면 해제된 상태임
 */

void Plot::axisFixed(bool checked)
{
    // 그래프 위젯에 상태를 전달
    _graph->setAxisFixed(checked);
    // 그래프 다시 그림
    _graph->update();
}


'좌표축 고정하기' 메뉴 항목이 선택될 때마다 호출된다. 호출될 때 메뉴 항목 체크 상태를 그래프 위젯에 전달하고, 내부를 다시 그리도록 한다.

10 번째 줄: QWidget::update() 는 위젯을 다시 그리도록 한다. QWidget::repaint() 도 비슷한 일을 하지만, repaint() 는 즉시 그리도록 하는 것과 달리, update() 는 이벤트 큐에 작업을 등록한 후에 한꺼번에 모아서 처리한다. 이런 이유로, 정말 필요한 경우가 아니라면 update() 를 쓰는 것이 성능 향상에 도움이 된다.

2.3.1.5 drawGraph()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
/**
 * @brief 그래프를 그린다
 */

void Plot::drawGraph()
{
    bool ok;

    // 다항식 입력 여부 확인
    if (_polyLine->text().isEmpty())
    {
        QMessageBox::warning(this, qApp->applicationDisplayName(),
                             tr("다항식을 입력해 주세요"));

        _polyLine->setFocus();

        return;
    }

    // 시작값 입력 여부 확인
    if (_startLine->text().isEmpty())
    {
        QMessageBox::warning(this, qApp->applicationDisplayName(),
                             tr("시작값을 입력해 주세요"));

        _startLine->setFocus();

        return;
    }

    // 시작값 숫자 여부 확인
    _startLine->text().toFloat(&ok);
    if (!ok)
    {
        QMessageBox::warning(this, qApp->applicationDisplayName(),
                             tr("시작값에 숫자를 입력해 주세요."));

        _startLine->setFocus();

        return;
    }

    // 끝값 입력 여부 확인
    if (_endLine->text().isEmpty())
    {
        QMessageBox::warning(this, qApp->applicationDisplayName(),
                             tr("끝값을 입력해 주세요"));

        _endLine->setFocus();

        return;
    }

    // 끝값 숫자 여부 확인
    _endLine->text().toFloat(&ok);
    if (!ok)
    {
        QMessageBox::warning(this, qApp->applicationDisplayName(),
                             tr("끝값에 숫자를 입력해 주세요."));

        _endLine->setFocus();

        return;
    }

    // 시작값과 끝값이 같은지 확인
    if (_startLine->text().toFloat() == _endLine->text().toFloat())
    {
        QMessageBox::warning(this, qApp->applicationDisplayName(),
                             tr("시작값과 끝값에 다른 숫자를 입력해 주세요."));

        _startLine->setFocus();

        return;
    }

    // 입력 결과를 그래프 위젯에 전달
    _graph->setPoly(_polyLine->text());
    _graph->setRange(_startLine->text().toFloat(), _endLine->text().toFloat());
    // 그래프 다시 그림
    _graph->update();
}


내용은 길지만, 사실 별 내용 아니다. 사용자가 입력한 사항들이 올바른지 점검하고, 필요하면 경고 메세지를 출력하여 다시 입력하도록 하는 것이다.

그리고 내용이 올바르게 입력되었으면, 그 내용을 그래프 위젯에 전달한다.

31 번째 줄: QString::toFloat() 은 문자열을 부동 소숫점으로 바꾸는 함수이다.

2.3.2 GraphWidget 클래스

실질적으로 다항 함수의 그래프를 클래스이다.

2.3.2.1 생성자

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
 * @brief 그래프 위젯
 */

class GraphWidget : public QWidget
{
public:
    /**
     * @brief GraphWidget 생성자
     * @param parent 부모 위젯
     */

    GraphWidget(QWidget *parent = 0)
            : QWidget(parent)
            , _axisFixed(false)
            , _start(0)
            , _end(0)
    {
    }


단순히 멤버 변수들을 초기화하고 있다.

2.3.2.2 setAxisFixed()

1
2
3
4
5
6
7
8
    /**
     * @brief 좌표축 고정 상태 설정
     * @param fixed true 이면 고정되고, false 이면 고정되지 않음
     */

    void setAxisFixed(bool fixed)
    {
        _axisFixed = fixed;
    }


'좌표축 고정 상태' 를 설정한다. 고정 상태이면 한 가운데 좌표축을 그리고, 그렇지 않으면 주어진 범위와 함수의 결과값에 따라 다른 곳에 좌표축을 그린다.

2.3.2.3 setPoly()

1
2
3
4
5
6
7
8
    /**
     * @brief 다항식을 설정한다
     * @param poly 다항식
     */

    void setPoly(const QString &poly)
    {
        _poly = poly;
    }


그래프를 그릴 다항식을 설정한다.

2.3.2.4 setRange()

1
2
3
4
5
6
7
8
9
10
    /**
     * @brief 범위를 설정한다
     * @param start 시작값
     * @param end 끝값
     */

    void setRange(float start, float end)
    {
        _start = qMin(start, end);
        _end = qMax(start, end);
    }


그래프를 그릴 x 값의 범위를 설정한다.

2.3.2.5 paintEvent()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
protected:
    /**
     * @brief 위젯 내부를 그린다
     */

    void paintEvent(QPaintEvent */*e*/)
    {
        if (_poly.isEmpty())
            return;

        float xStart = _start;
        float xEnd = _end;
        float xDelta = (xEnd - xStart) / 1000; // 범위를 1,000 등분함

        QList<QPointF> ptfs;

        PolyCalc pc(_poly);

        // 최솟값과 최댓값 초기화
        pc.setX(xStart);
        float yMin = pc.calc();
        float yMax = yMin;


QWidget::paintEvent() 는 위젯을 다시 그릴 필요가 있을 때 호출된다. 예를 들면 QWidget::update() 가 호출되었을 때.

함수를 그리기 위한 초기화를 하고 있다.

5 번째 줄: QPaintEvent 는 위젯을 그릴 때 필요한 정보를 담고 있다. 예를 들면, 새로 그려야 할 영역.

14 번째 줄: QPointF 는 점의 위치를 float 형으로 표현한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
        // 점의 위치 계산
        for (float x = xStart; x <= xEnd; x += xDelta)
        {
            QPointF ptf;

            ptf.setX(x);

            pc.setX(x);
            ptf.setY(pc.calc());

            ptfs.append(ptf);

            // 최솟값 찾기
            if (ptf.y() < yMin)
                yMin = ptf.y();

            // 최댓값 찾기
            if (ptf.y() > yMax)
                yMax = ptf.y();
        }


주어진 범위에서 함수의 결과값을 계산하고, 각 점들의 위치를 저장한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
        // 상수 함수에 대한 보정
        if (yMax == yMin)
        {
            if (yMax == 0)
            {
                yMax = 10;
                yMin = -10;
            }
            else
            {
                yMax = qAbs(yMax);
                yMin = -yMax;
            }
        }


최댓값과 최솟값이 차이가 없는 상수 함수를 특별하게 처리한다. 이는 나중에 배율을 계산할 때 0 으로 나누는 경우를 피하기 위한 것이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
        float xScale;   // 수평 배율
        float yScale;   // 수직 배율

        int xOrg;   // x 축 원점
        int yOrg;   // y 축 원점

        int w = width() - 1;    // 실제로 그릴 수 있는 폭
        int h = height() - 1;   // 실제로 그릴 수 있는 높이

        if (_axisFixed)         // 좌표축이 고정되어 있으면,
        {
            // 위젯의 중심을 기준으로 배율 계산
            xScale = (w / 2) / qMax(qAbs(xStart), qAbs(xEnd));
            yScale = (h / 2) / qMax(qAbs(yMin), qAbs(yMax));

            // 위젯의 중심이 원점
            xOrg = w / 2;
            yOrg = h / 2;
        }
        else                    // 좌표축이 고정되어 있지 않으면
        {
            // 수평 배율 계산
            if (xEnd * xStart < 0 )
                xScale = w * (xStart / (xEnd - xStart)) / xStart;
            else
                xScale = w / (xEnd - xStart);

            // 수직 배율 계산
            if (yMax * yMin < 0)
                yScale = h * (yMin / (yMax - yMin)) / yMin;
            else
                yScale = h / (yMax - yMin);

            // 실제 그래프에 따라 원점 설정
            xOrg = -xStart * xScale;
            yOrg = yMax * yScale;
        }


함수의 좌표와 위젯의 좌표를 상호 변환하기 위해 원점과 배율을 찾는다. 좌표축이 고정되어 있다면 위젯의 가운데에 원점을 잡고, 그에 따라 배율을 결정한다. 반면에, 고정되어 있지 않다면 주어진 범위와 함수값에 따라 원점을 잡고 배율을 결정한다.

7~8 번째 줄: 폭과 높이에서 1 을 빼는 것은 위젯의 오른쪽 경계와 아랫쪽 경계는 위젯의 내부 영역이 아니기 때문이다. 예를 들어, 640x480 위젯이 있다면, 이 위젯의 내부 영역은 (0,0)-(639,479) 이다. 따라서 실제로 그릴 수 있는 영역으로 폭과 높이를 계산하기 위해 1을 뺀다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
        QPainter painter(this);

        // 평행이동/원점 변경
        painter.translate(xOrg, yOrg);
        // 배율 설정, x 축 대칭.
        painter.scale(1, -1);

        // 좌표축의 색깔은 검은색
        painter.setPen(Qt::black);

        if (_axisFixed) // 좌표축이 고정되어 있으면
        {
            // 위젯 중심에 좌표축 그림
            painter.drawLine(-xOrg, 0, xOrg, 0);
            painter.drawLine(0, -yOrg, 0, yOrg);
        }
        else            // 좌표축이 고정되어 있지 않으면
        {
            // 실제 그래프에 따라 좌표축 그림
            painter.drawLine(xStart * xScale, 0, xEnd * xScale, 0);
            painter.drawLine(0, yMin * yScale, 0, yMax * yScale);
        }


원점을 기준으로 좌표축을 그린다.

1 번째 줄: QPainter 는 위젯 내부에 사용자가 임의로 그리고자 할 때 사용하는 클래스이다. 이 클래스는 반드시 paintEvent() 내부 또는 내부에서 호출된 함수에서만 사용되어야 한다. 그렇지 않으면 아무것도 하지 않는다. 여기에서는 QPainter 의 생성자를 이용하였지만, 생성자대신에 QPainter::begin()QPainter::end() 를 사용해도 된다.

4~6 번째 줄: QPainter::translate() 는 주어진 값을 더하여 그리도록 한다. 고등학교 수학시간에 배운 것을 기억해 보면 평행이동이라 할 수 있다. 또는 원점이 그만큼 이동한다고 생각해도 된다. QPainter::scale() 은 주어진 값을 곱하여 그리도록 한다. 이 경우에는 y 값에 -1 을 곱하므로 x 축 대칭이 이루어진다. 이 작업을 하는 까닭은 Qt 의 좌표계는 왼쪽 위가 원점이고, 아래로 갈수록 값이 커지지만, 직교 좌표계는 왼쪽 아래가 원점이고, 위로 갈수록 값이 커지기 때문이다. 다만, QPainter::translate() 와 QPainter::scale() 은 텍스트에도 영향을 미치므로 주의하도록 하자.

9 번째 줄: QPainter::setPen() 는 선의 색깔을 결정한다.

11~22 번째 줄: QPainter::drawLine() 은 주어진 두 점 사이를 선으로 잇는다.

1
2
3
4
5
6
7
8
9
10
        // 그래프의 색깔은 빨간색
        painter.setPen(Qt::red);

        // 각 점들을 선으로 이음
        for (int i = ptfs.count() - 1; i >= 1; --i)
            painter.drawLine(ptfs.at(i).x() * xScale,
                             ptfs.at(i).y() * yScale,
                             ptfs.at(i-1).x() * xScale,
                             ptfs.at(i-1).y() * yScale);
    }


계산을 통해 얻은 각 점들을 선으로 잇는다.

1
2
3
4
5
6
private:
    bool _axisFixed;    ///< 좌표축 고정 상태
    QString _poly;      ///< 다항식
    float _start;       ///< 시작값
    float _end;         ///< 끝값
};


클래스 선언부의 나머지 부분이다.


2.3.3 PolyCalc 클래스

다항식을 계산하는 클래스이다. 많은 기능을 담고 있지는 않지만, 사칙연산과 거듭제곱을 수행할 수 있다. 다만, 곱하기는 생략할 수 없고, 연속된 - 부호는 하나의 - 부호로 인식한다. 예를 들어, 2--1 은 3이 아니라 1이 되어 버린다. 이런 경우에는 () 를 쓰면 된다. 2-(-1) 로.

2.3.3.1 생성자

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
 * @brief 다항식 계산기
 */

class PolyCalc
{
public:
    /**
     * @brief PolyCalc 생성자
     * @param 다항식
     */

    explicit PolyCalc(const QString &poly)
        : _poly(poly)
        , _pos(0)
        , _x(0)
    {
    }


멤버 변수들을 초기화한다.

2.3.3.2 setX()

1
2
3
4
5
6
7
8
    /**
     * @brief x 값을 설정한다
     * @param x x 로 설정할 값
     */

    void setX(float x)
    {
        _x = x;
    }


다항식에서 사용될 x 값을 설정한다.

2.3.3.3 calc()

1
2
3
4
5
6
7
8
9
10
    /**
     * @brief 다항식을 계산한다
     * @return 계산 결과를 돌려준다
     */

    float calc()
    {
        _pos = 0;

        return level1(getNumber());
    }


다항식을 계산하고 결과를 돌려준다.


2.3.3.4 peekToken()

1
2
3
4
5
6
7
8
9
10
11
12
13
private:
    QString _poly;  ///< 다항식
    int _pos;       ///< 현재 파싱 위치
    float _x;       ///< x 값

    /**
     * @brief 토큰을 읽지만, 파싱 위치 바꾸지 않는다
     * @return 현재 토큰
     */

    inline QString peekToken()
    {
        return token(false);
    }


현재 토큰을 읽지만, 파싱 위치를 옮기지는 않는다.

2.3.3.5 nextToken()

1
2
3
4
5
6
7
8
    /**
     * @brief 토큰을 읽고, 파싱 위치를 바꾼다
     * @return 현재 토큰
     */

    inline QString nextToken()
    {
        return token(true);
    }


현재 토큰을 읽고, 파싱 위치를 옮긴다.

2.3.3.6 getNumber()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
    /**
     * @brief 수를 읽는다
     * @return 현재 토큰이 숫자이면, 해당 숫자를 돌려주고, 아니면 0 을 돌려준다.
     * @remark 숫자를 읽었으면 파싱 위치를 옮기고, 아니면 옮기지 않는다.
     */

    inline float getNumber()
    {
        bool ok;
        float num = peekToken().toFloat(&ok);
        if (ok)
            nextToken();
        else
            num = 0;

        return num;
    }


현재 토큰이 수라면 값을 읽은 후 파싱 위치를 옮기고, 아니면 파싱 위치는 그대로 두고 0 을 돌려준다.

2.3.3.7 token()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
    /**
     * @brief 토큰을 읽는다
     * @param next true 이면 파싱 위치를 바꾸고, false 이면 바꾸지 않는다
     * @return 현재 토큰
     */

    QString token(bool next)
    {
        // 현재 위치 저장
        int savedPos = _pos;

        QString result;

        // 공백 문자 무시
        while (_pos < _poly.length() && _poly.at(_pos).isSpace())
            _pos++;


현재 토큰을 읽고, next 에 따라 파싱 위치를 옮기거나 옮기지 않는다. 공백은 무시한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
        if (_pos < _poly.length())
        {
            QChar ch = _poly.at(_pos++);

            if (ch.isNumber())
            {
                // 숫자 파싱
                do
                {
                    result.append(ch);

                    if (_pos >= _poly.length())
                        break;

                    ch = _poly.at(_pos++);
                } while (ch.isNumber());

                if (!ch.isNumber())
                    _pos--;
            }


숫자는 연결해서 읽는다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
            else if (ch.isLetter())
            {
                // 문자 파싱
                do
                {
                    result.append(ch);

                    if (_pos >= _poly.length())
                        break;

                    ch = _poly.at(_pos++);
                } while (ch.isLetter());

                if (!ch.isLetter())
                    _pos--;
            }


문자는 연결해서 읽는다.

1
2
3
4
5
6
            else
            {
                // 나머지는 그대로 추가
                result.append(ch);
            }
        }


나머지는 한 문자씩 읽는다.

1
2
3
4
5
6
7
8
9
10
        // peek 모드이면 파싱 위치 복원
        if (!next)
            _pos = savedPos;

        // x 대체
        if (result == "x")
            result = QString::number(_x);

        return result;
    }


변수 x 를 실제 값으로 대체하여 결과를 돌려준다.

7 번째 줄: QString::number() 는 수를 문자열로 바꾼다.

2.3.3.8 level1()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
    /**
     * @brief 더하기/빼기를 계산한다
     * @param lv 왼쪽값
     * @return 계산 결과를 돌려준다
     */

    float level1(float lv)
    {
        QString op(peekToken());

        if (op == "+" || op == "-")
        {
            nextToken();

            // 오른쪽 값 계산
            float rv = level2(getNumber());

            // 더하기/빼기 수행
            if (op == "+")
                lv += rv;
            else /*if (op == "-") */
                lv -= rv;
        }
        else if (!op.isEmpty()) // 더하기/빼기가 아니면
            lv = level2(lv);    // 더 높은 우선순위로 넘김

        // 더하기/빼기가 이어지면 계속 계산
        op = peekToken();
        if (op == "+" || op == "-")
            return level1(lv);

        return lv;
    }


더하기/빼기를 계산한다.

2.3.3.9 level2()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
    /**
     * @brief 곱하기/나누기를 계산한다
     * @param lv 왼쪽값
     * @return 계산 결과를 돌려준다
     */

    float level2(float lv)
    {
        QString op(peekToken());

        if (op == "*" || op == "/")
        {
            nextToken();

            // 오른쪽 값 계산
            float rv = level3(getNumber());

            // 곱하기/나누기 수행
            if (op == "*")
                lv *= rv;
            else /*if (op == "/") */
                lv /= rv;
        }
        else if (!op.isEmpty()) // 곱하기/나누기가 아니면
            lv = level3(lv);    // 더 높은 우선 순위로 넘김

        // 곱하기/나누기가 이어지면 계속 계산
        op = peekToken();
        if (op == "*" || op == "/")
            return level2(lv);

        return lv;
    }


곱하기/나누기를 계산한다.

2.3.3.10 level3()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
    /**
     * @brief 거듭제곱을 계산한다
     * @param lv
     * @return
     */

    float level3(float lv)
    {
        QString op(peekToken());

        if (op == "^")
        {
            nextToken();

            // 오른쪽 값 계산
            float rv = level4(getNumber());

            // 거듭제곱 수행
            lv = pow(lv, rv);
        }
        else if (!op.isEmpty()) // 거듭제곱이 아니면
            lv = level4(lv);    // 더 높은 우선 순위로 넘김

        // 거듭제곱이 이어지면 계속 계산
        op = peekToken();
        if (op == "^")
            return level3(lv);

        return lv;
    }


거듭제곱을 계산한다.

2.3.3.11 level4()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
    /**
     * @brief 괄호를 계산한다
     * @param lv 왼쪽값
     * @return 계산 결과를 돌려준다
     */

    float level4(float lv)
    {
        QString op(peekToken());

        if (op == "(")
        {
            nextToken();

            // 괄호를 계산한다
            lv = level1(getNumber());

            // 괄호 대응여부 확인
            if (nextToken() != ")")
                qDebug() << "Missing ')'";
        }

        return lv;
    }
};


괄호를 계산한다.

3. 마무리하면서...

다항 함수에 대한 그래프를 그리는 <Plot> 을 만들어보면서 위젯 내부에 다양한 그래픽 작업을 하는 방법에 대해 간단히 알아보았다. 다시 한 번 말하지만, QPainter 는 QWidget::paintEvent() 내부에서만 작동한다. 그 이외에서 그리기 작업이 필요하다면 QWidget::update() 나 QWidget::repaint() 를 호출하자. 물론 QWidget::reapint() 보다는 QWidget::update() 가 성능 향상에 도움이 된다는 것이도 기억하자.

다음은 <Plot> 의 실행 모습이다.

전체 소스는 여기에서 확인하자.



보다 수준 높은 그래프를 원한다면 다음 프로젝트들을 참조하자.


댓글

이 블로그의 인기 게시물

토렌트: < 왕좌의 게임 > 시즌 1 ~ 시즌 8 완결편 마그넷

토렌트: NGC < 코스모스 > 우리말 더빙 전편(1편~13편) 마그넷

토렌트: < 스타워즈 > Ep.1 ~ Ep.6 마그넷