Qt 로 만들자: 계산기

이번에는 계산기를 만들어보고자 한다. 모델은 Windows 7 에 있는 "계산기" 일반용이다.

1. 요구 사항

  • 사칙연산
  • 제곱근, 백분율, 역수 계산
  • 부호바꾸기
  • 수식 수정

이 정도의 기능을 구현하도록 하자.

2. 구현

2.1 프로젝트 생성

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

2.1 헤더

헤더 파일을 보자

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
/*! \file calc.h
 */


#ifndef CALC_H
#define CALC_H

#include <QMainWindow>
#include <QtWidgets>

/*!
 * \brief 계산기 클래스
 */

class Calc : public QMainWindow
{
    Q_OBJECT

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

protected:
    void keyPressEvent(QKeyEvent *e);

private:
    /*!
     * \brief 연산 모드
     */

    enum OperationMode {None = 0,   //!< 초기 상태
                        Plus,       //!< 더하기
                        Minus,      //!< 빼기
                        Multiply,   //!< 곱하기
                        Divide,     //!< 나누기
                       };

    static const int CalcRows = 5;  //!< 계산기 레이아웃 세로줄
    static const int CalcCols = 5;  //!< 계산기 레이아웃 가로줄

    float _ans;             //!< 계산 결과
    float _operand;         //!< 마지막 피연산자
    OperationMode _opMode;  //!< 연산 모드
    bool _freezed;          //!< 계산 결과가 산출된 상태

    QLineEdit *_exprLine;           //!< 수식
    QList<QPushButton *> _buttons;  //!< 버튼들

    void initMenus();
    void initWidgets();
    void initConnections();
    void calculate(const QString &s);

private slots:
    void buttonClicked();
};

#endif // CALC_H


이전의 것들과 크게 다르지 않다. 주석만으로 쉽게 이해할 수 있을 것이다.

2.2 생성자와 소멸자

지난번 처럼 메뉴, 위젯을 초기화하고  시그널과 슬롯을 연결한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/*!
 * \brief 생성자
 * \param parent 부모 위젯
 */

Calc::Calc(QWidget *parent)
    : QMainWindow(parent)
    , _ans(0)
    , _operand(0)
    , _opMode(None)
    , _freezed(false)
{
    initMenus();        // 메뉴 초기화
    initWidgets();      // 위젯 초기화
    initConnections();  // 시그널과 슬롯 연결
}

/*!
 * \brief 소멸자
 */

Calc::~Calc()
{
}


2.3 initMenus()

메뉴를 초기화한다.

1
2
3
4
5
6
7
8
9
10
11
/*!
 * \brief 메뉴를 초기화한다
 */

void Calc::initMenus()
{
    QMenu *fileMenu = new QMenu(tr("파일(&F)"));
    fileMenu->addAction(tr("끝내기(&x)"), qApp, SLOT(quit()),
                        QKeySequence(tr("Ctrl+Q")));

    menuBar()->addMenu(fileMenu);
}


2.4 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
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
/*!
 * \brief 위젯 초기화
 */

void Calc::initWidgets()
{
    // 수식 생성
    _exprLine = new QLineEdit;
    // 읽기 전용
    _exprLine->setReadOnly(true);
    // 입력 촛점 받지 않음
    _exprLine->setFocusPolicy(Qt::NoFocus);
    // 수평 오른쪽 정렬, 수직 가운데 정렬
    _exprLine->setAlignment(Qt::AlignVCenter | Qt::AlignRight);
    _exprLine->setText(QString::number(_ans));

    // 버튼 생성
    _buttons.append(new QPushButton(Backspace));
    _buttons.append(new QPushButton("CE"));
    _buttons.append(new QPushButton("C"));
    _buttons.append(new QPushButton(Negation));
    _buttons.append(new QPushButton(SquareRoot));

    _buttons.append(new QPushButton("7"));
    _buttons.append(new QPushButton("8"));
    _buttons.append(new QPushButton("9"));
    _buttons.append(new QPushButton("/"));
    _buttons.append(new QPushButton("%"));

    _buttons.append(new QPushButton("4"));
    _buttons.append(new QPushButton("5"));
    _buttons.append(new QPushButton("6"));
    _buttons.append(new QPushButton("*"));
    _buttons.append(new QPushButton("1/x"));

    _buttons.append(new QPushButton("1"));
    _buttons.append(new QPushButton("2"));
    _buttons.append(new QPushButton("3"));
    _buttons.append(new QPushButton("-"));
    _buttons.append(new QPushButton("="));

    _buttons.append(new QPushButton("0"));
    _buttons.append(new QPushButton("."));
    _buttons.append(new QPushButton("+"));

    QGridLayout *gridLayout = new QGridLayout;
    gridLayout->addWidget(_exprLine, 0, 0, 1, CalcCols);

    int row = 1, col = 0;

    for (int i = 0; i < _buttons.count(); ++i)
    {
        QPushButton *button = _buttons.at(i);
        // 크기 정책 설정, 최소 크기는 보존하되, 확장은 자유
        button->setSizePolicy(QSizePolicy::MinimumExpanding,
                              QSizePolicy::MinimumExpanding);
        // 입력 촛점 받지 않음
        button->setFocusPolicy(Qt::NoFocus);

        // "=" 는 세로 두 칸 차지
        int rowSpan = 1 + (button->text() == "=");
        // "0" 은 가로 두 칸 차지
        int colSpan = 1 + (button->text() == "0");

        gridLayout->addWidget(button, row, col, rowSpan, colSpan);

        col = (col + colSpan) % CalcCols;
        row += col == 0;
    }

    // 기본 위젯 생성
    QWidget *w = new QWidget;
    w->setLayout(gridLayout);

    // 센트럴 위젯 설정
    setCentralWidget(w);
}


11 번째 줄의 QWidget::setFocusPolicy() 는 위젯이 입력 촛점을 받을 것인지 결정한다. Qt::NoFocus 는 어떤 입력 촛점도 받지 않겠다는 뜻이다.

13 번째 줄의 QLineEdit::setAlignment() 는 내부 내용물은 어떻게 정렬할지 결정한다. Qt::AlignVCenter 는 세로 가운데 정렬, Qt::AlignRight 는 가로 오른쪽 정렬이다.

54 번째 QWidget::setSizePolicy() 는 위젯의 크기 변화 정책을 결정한다. QSizePolicy::MinimumExpanding 은 최소 크기보다 작아지지는 않지만, 원하는만큼 커질 수 있다. 다른 정책들도 있으니 도움말을 확인하자.

사용된 레이아웃은 QGridLayout 이다. QGridLayout 은 모눈 형태로 위젯을 배치하는 레이아웃인데, 위젯 하나가 차지하는 모눈의 갯수를 정할 수 있다. 64 번째 줄의 QGridLayout::addWidget() 이 그 일을 한다. 지난 번에 사용된 QGridLayout::addWidget() 과는 다른 용법의 오버로딩 함수이다. 네번째와 다섯번째 매개변수가 각각 위젯이 차지할 세로 줄 수, 가로 칸 수이다.

2.5 initConnections()

시그널과 슬롯은 연결한다.

1
2
3
4
5
6
7
8
9
/*!
 * \brief 시그널과 슬롯 연결
 */

void Calc::initConnections()
{
    // 모든 버튼의 clicked() 시그널을 buttonClicked() 슬롯에 연결
    Q_FOREACH (QPushButton *button, _buttons)
        connect(button, SIGNAL(clicked(bool)), this, SLOT(buttonClicked()));
}


2.6 buttonClicked()

버튼들의 clicked() 시그널이 발생했을 때, 해당 버튼의 텍스트를 calculate() 로 전달한다.

1
2
3
4
5
void Calc::buttonClicked()
{
    // 시그널을 보낸 버튼의 텍스트를 calculate() 에 전달
    calculate(qobject_cast<QPushButton *>(sender())->text());
}


2.7 calculate()

실질적으로 계산이 이루어지는 함수이다.

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
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
/*!
 * \brief \a s 에 따라 계산한다
 * \param s 숫자 또는 명령어
 */

void Calc::calculate(const QString &s)
{
    if (s.length() == 1 && (s.at(0).isNumber() || s == "." || s == Backspace))
    {
        // 수식 편집

        // 수식의 형태는 숫자[ 연산자 [숫자]]
        QString expr(_exprLine->text());

        if (s == Backspace)
        {
            if (!_freezed && !expr.endsWith(' '))
            {                       // 산출된 상태가 아니고 숫자 입력 중이면
                expr.chop(1);       // 마지막 문자 지움
                if (expr.isEmpty()) // 모두 지워졌으면
                    expr = "0";     // "0" 으로
            }
        }
        else
        {
            if (_freezed)           // 산출된 상태면
            {
                expr.clear();       // 기존의 수식 지움
                _freezed = false;   // 산출되지 않음
            }

            if (s == ".")
            {
                // 두 개 이상의 "." 은 허용 안됨
                if (!expr.contains('.'))
                {
                    if (expr.isEmpty()) // "." 부터 입력되었으면
                        expr = "0";     // "0." 으로

                    expr.append('.');
                }
            }

            // 맨 앞자리의 "0" 은 하나만 허용
            if (expr != "0" || s != "0")
            {
                if (expr == "0")    // 수식이 "0" 이면
                    expr.clear();   // 수식 지움

                // 문자열 추가
                expr.append(s);
            }
        }

        // 수식 설정
        _exprLine->setText(expr);
    }
    else
    {
        // 연산 처리

        // 수식의 형태는 숫자[ 연산자 [숫자]]
        QString expr(_exprLine->text());
        // 수식을 공백을 기준으로 토큰 분리
        QStringList tokens(expr.split(' '));

        bool ok;
        // 현재 입력 숫자
        float num = tokens.last().toFloat(&ok);

        if (s == "C")
        {
            // 계산 새로 시작
            _ans = 0;
            _operand = 0;
            _opMode = None;
            _freezed = false;
            _exprLine->setText("0");

            return;
        }

        if (!ok)    // 마지막 토큰이 숫자가 아니면
            return;

        if (s == "CE")
        {
            tokens.last().clear();      // 현재 수식 지우기
            expr = tokens.join(' ');    // 토큰사이를 공백으로 연결
            if (expr.isEmpty())         // 모든 수식이 지워졌으면
                expr = "0";             // "0" 으로

            _exprLine->setText(expr);

            return;
        }
        else if (s == Negation || s == SquareRoot || s == "%" || s == "1/x")
        {
            if (s == Negation)
            {
                if (num != 0)   // 0 은 부호가 없으니까
                    num *= -1;
            }
            else if (s == SquareRoot)
            {
                if (num > 0)            // 양수일 때만
                    num = sqrt(num);    // 제곱근 계산
            }
            else if (s == "%")
                num = _ans * num / 100;
            else /* if (s == "1/x") */
                num = 1 / num;

            // 현재 입력 숫자 설정
            tokens.last().setNum(num);

            // 수식 설정
            _exprLine->setText(tokens.join(' '));

            return;
        }

        // 연산자 처리

        if (tokens.count() == 1)    // 숫자만 있으면
        {
            if (s == "=")           // "=" 이면
                _ans = _operand;    // 기존의 계산 반복
            else                    // 아니면
            {                       // 계산 새로 시작
                _ans = 0;
                _operand = 0;
                _opMode = None;
                _freezed = false;
            }
        }

        // 연산 수행
        switch (_opMode)
        {
        case None:          // 초기화 상태면
            _ans = num;     // 수식 저장
            break;

        case Plus:
            _ans += num;
            break;

        case Minus:
            _ans -= num;
            break;

        case Multiply:
            _ans *= num;
            break;

        case Divide:
            _ans /= num;
            break;
        }

        // 연산자 토큰으로 바꾸기
        QString opStr;
        opStr.append(" ");
        opStr.append(s);
        opStr.append(" ");

        // 산출되지 않음
        _freezed = false;

        if (s == "+")
            _opMode = Plus;
        else if (s == "-")
            _opMode = Minus;
        else if (s == "*")
            _opMode = Multiply;
        else if (s == "/")
            _opMode = Divide;
        else /*if (s == "=")*/
        {
            // 연산자 토큰 지우기
            opStr.clear();

            if (tokens.count() > 1) // 숫자 연산자 숫자 형태이면
                _operand = num;     // 마지막 숫자 저장

            // 산출된 상태
            _freezed = true;
        }

        // 토큰을 수직으로 바꾸기
        expr.setNum(_ans);
        expr.append(opStr);

        // 수식 설정
        _exprLine->setText(expr);
    }
}


34 번째 줄의 QString::contains() 는 주어진 문자 또는 문자열을 포함하고 있는 확인한다.

64 번째 줄의 QStringListQList<QString> 를 상속한 클래스이다. QString::split() 은 주어진 sep 으로 문자열을 분리하여 QStringList 를 돌려준다.
 
68 번째 줄의 QList::last() 는 마지막 요소를 돌려준다. QString::toFloat() 은 float 으로 변환하지 못하면 ok 를 false 로 설정하고, 변환하면 true 로 설정한다.

88 번째 줄의  QStringList::join() 은 모든 문자열을 합쳐서 하나의 문자열로 만드는데, 연결할 때 주어진 separator 를 사이 사이에 넣는다.

114 번째 줄의 QString::setNum() 은 주어진 수를 문자열로 변환하여 저장한다.

2.8 keyPressEvent()

키보드 입력 이벤트를 문자열로 바꿔서 calculate() 에 전달한다.

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
/*!
 * \brief 키보드 입력 이벤트를 처리한다
 * \param e 키보드 입력 이벤트
 */

void Calc::keyPressEvent(QKeyEvent *e)
{
    switch(e->key())
    {
    case Qt::Key_0:
    case Qt::Key_1:
    case Qt::Key_2:
    case Qt::Key_3:
    case Qt::Key_4:
    case Qt::Key_5:
    case Qt::Key_6:
    case Qt::Key_7:
    case Qt::Key_8:
    case Qt::Key_9:
    case Qt::Key_Period:
    case Qt::Key_Backspace:
    case Qt::Key_Plus:
    case Qt::Key_Minus:
    case Qt::Key_Asterisk:
    case Qt::Key_Slash:
    case Qt::Key_Percent:
    case Qt::Key_Equal:
    case Qt::Key_Enter:
    case Qt::Key_Return:
    {
        QString text;

        // 입력된 키를 문자열로 전환
        if (e->key() == Qt::Key_Backspace)
            text = Backspace;
        else if (e->key() == Qt::Key_Enter || e->key() == Qt::Key_Return)
            text = "=";
        else
            text = e->text();

        calculate(text);
        break;
    }

    default:
        // 처리되지 않은 것은 부모 클래스에 전달
        QMainWindow::keyPressEvent(e);

        return;
    }

    // 처리 완료
}


QWidget::keyPressEvent() 는 accept 상태에서 호출되므로, e->accept() 를 호출할 필요는 없다. 대신 부모 클래스에 전달하지만 않으면 된다. 이러한 이유로 51 번째 줄에서 지난 번과는 다르게 e->accept() 를 호출하지 않고 있다.

해당 위젯의 keyPressEvent() 에서 QKeyEvent 를 처리하지 않은 경우, e->ignore() 를 호출해도 되지만, 해당 키입력 이벤트에 따라 부모 클래스에서 처리하는 일이 있다면, 46 번째 줄에서 처럼 반드시 부모 클래스의 멤버 함수를 호출해 주어야 한다.

3. 마무리 하면서...

간단한 계산기를 구현해 보았다. 새롭게 등장하는 클래스나 함수가 점점 적어지고 있는데, 기본 수준이 점점 마무리 되어가고 있다고 생각하면 될 것 같다.

다음은 실행 모습이다.


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


















댓글

이 블로그의 인기 게시물

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

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

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