Qt 로 만들자: BMI 계산기

BMI(Body Mass Index, 체질량 지수) 는 자신의 비만도를 확인해 볼 수 있는 있는 지수로, 비교적 간단한 계산을 통해 그 값을 알 수 있다. 이번에는 BMI 를 계산해서 비만도를 알려주는 BMI 계산기를 만들어보자.

1. 요구 사항

BMI 를 계산하기 위해서는 키(m) 와 몸무게(kg) 가 필요하기 때문에, 키와 몸무게를 입력받기 위한 부분이 필요하다. 그런데 일반적으로 키는 cm 단위가 더 익숙하므로, m 단위 대신에 cm 단위로 입력받는다.

사용자가 키와 몸무게를 입력한 후에, BMI 계산을 수행하게 하는 장치가 필요하다. 그리고 그 계산 결과를 보여줄 필요가 있다.

이를 기반으로 실제 코드를 작성해 보자.

2. 구현

앞서 <Hello, world!!!> 를 작성할 때처럼 <BMI 계산기> 를 위한 새로운 프로젝트를 작성하고, 이 때 프로젝트 이름과 메인 윈도의 클래스 이름은 모두 BMI 로 한다.

프로젝트가 만들어지고, bmi.cpp 가 열리면 bmi.h 로 이동한다. 왼쪽에 있는 [Projects] 창에서 선택해도 되고, [F4] 를 눌러도 된다. [F4] 는 .cpp 와 .h 를 전환하는 단축키이다.

우선  

#include <QMainWindow> 

다음에

#include <QtWidgets> 

를 추가하자. Qt 는 모든 클래스에 해당하는 헤더 파일을 제공하고, 해당 클래스를 쓰기 위해서는 반드시 해당 헤더 파일을 포함해야 한다. 예를 들어, QLabel 클래스를 사용하고자 한다면 #include <QLabel> 를 추가해야 한다. 하지만, 매번 개별 클래스를 사용하기 위해 개별 헤더 파일을 포함하는 것은 매우 귀찮은 일이다. 그래서 Qt 는 이를 해소하기 위한 헤더 파일을 제공하는데 QtWidgets 도 그 중의 하나이다. 이 파일은 위젯 관련 헤더파일들을 모두 포함한다. 따라서 이 파일 하나만 포함하면 필도의 개별 헤더 파일을 일일이 포함할 필요가 없다.

이제는 요구 사항에 맞는 위젯들을 선언한 필요가 있다. 위젯은 window/control 과 비슷한 개념으로 실질적인 사용자 인터페이스를 담당한다.

BMI 클래스 선언부에 다음을 추가하자.

1
2
3
4
5
6
7
8
9
10
private:
    QLineEdit *_heightLine;
    QLineEdit *_massLine;
    QPushButton *_calcPush;
    QLineEdit *_resultLine;

    QFormLayout *_formLayout;

    void initWidgets();
    void initActions();


_heightLine 은 키를 입력받기 위한 위젯이고, QLineEdit 클래스를 사용한다. QLineEdit 클래스는 사용자로부터 한 줄을 입력받을 수 있는 위젯이다. 마찬가지로 몸무게를 입력받기 위한 _massLine 을 선언했다. 그리고 계산을 실행하기 위해 _calcPush 를 선언했고, QPushButton 클래스를 사용한다. QPushButton 클래스는 눌림/떼어짐만을 구현하는 푸시 버튼 위젯이다. _resultLine 은 결과를 출력하기 위한 위젯이다.

_formLayout 은 이러한 위젯들을 배치하기 위한 레이아웃이다. Qt 는 여러가지 레이아웃을 제공하는데, 여기에서는 QFormLayout 을 사용한다. QFormLayout 은 [이름: 내용] 이 수직으로 나열된 형태에 적합하다.

이제 다시 [F4] 를 눌러 bmi.cpp 로 전환하자. 그리고 BMI::BMI() 구현부에 다음을 추가하자.

1
2
        initWidgets();
        initActions();


initWidgets() 는 헤더 파일에서 선언한 위젯들을 초기화하기 위한 것이고, initActions() 는사용자가 [계산하기] 버튼을 눌렀을 때, 실제 계산이 수행될 수 있도록 하는 Qt 의 시그널/슬롯을 초기화하기 위한 것이다.

이제 initWidgets() 와 initActions() 를 구현해보자. bmi.cpp 에서 이 함수들의 구현부를 입력해도 되지만, Qt Creator 의 [Refactor] 기능을 이용하자. [F4] 를 눌러 bmi.h 로 전환하자. 그리고 initWidgets() 가 있는 줄로 이동해서 [Alt-Enter] 를 누르자. 또는 마우스 오른쪽 버튼을 누르고 팝업 메뉴에서 [Refactor] 선택하고 [Add definition in bmi.cpp] 를 누르면, bmi.cpp 로 전환되면서 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
    // 키 입력창 생성
    _heightLine = new QLineEdit;

    // 몸무게 입력창 생성
    _massLine = new QLineEdit;

    // 계산 푸시 버튼 생성
    _calcPush = new QPushButton(tr("계산하기(&C)"));

    // 결과 입력창 생성
    _resultLine = new QLineEdit;
    _resultLine->setReadOnly(true); // 읽기 전용

    // 폼 레이아웃 생성
    _formLayout = new QFormLayout;
    _formLayout->addRow(tr("키(c&m)"), _heightLine);
    _formLayout->addRow(tr("몸무게(&kg):"), _massLine);
    _formLayout->addWidget(_calcPush);
    _formLayout->addRow(tr("결과:"), _resultLine);

    // 센트럴 위젯을 생성 및 설정
    setCentralWidget(new QWidget);

    // 센트럴 위젯의 레이아웃을 설정
    centralWidget()->setLayout(_formLayout);



위에서 보듯이 모든 위젯은 new 로 만들어진다. 이것은 Qt 의 기본 원칙으로 하나의 위젯에 대한 객체는 오직 하나뿐임을 보장하기 위한 것이다. 아울러 다른 위젯의 자식 위젯이 된다든가 또는 레이아웃에 속하게 되면 해당 위젯의 소유권이 부모 위젯이나 레이아웃을 가지는 위젯으로 넘어가기 때문이다. 이렇게 소유권이 넘어가게 되면, 해당 위젯이 소멸될 때 그에 속한 모든 위젯은 자동으로 소멸된다. 다만 일시적으로 생성하고, 위젯이 생성된 스코프내에서 사용되는 경우라면 인스턴스를 직접 선언하기도 한다.

그리고 주의깊게 본 사람이라면 알 수 있겠지만, 모든 문자열들이 tr() 로 둘러싸여 있다. tr() 은 나중에 국제화 프로그램을 만들 때 유용하게 쓰인다. tr() 로 둘러싸인 모든 문자열들은 나중에 Qt Linguist 라는 프로그램으로 번역할 수 있다. 물론 국제화를 고려하지 않는다면 tr() 을 쓰지 않아도 상관 없지만, 항상 tr() 을 쓰려는 습관을 가지는 것이 좋다. tr() 은 QString 문자열을 돌려준다.

_calcPush 와 _formLayout 의 경우 문자열에 "&" 표시가 있는 것을 알 수 있다. 이것은 키보드 단축키를 설정하기 위한 것이다. 예를 들어 "&C" 는 Alt-C 를 누르면 해당 위젯으로 바로 이동할 수 있다. 그리고 Alt 키를 누르면 C 처럼 해당 문자 밑에 밑줄이 생긴다.

_resultLine 은 사용자 입력을 받지 않는 위젯이므로, 읽기 전용으로 설정한다.

_formLayout 은 앞서 말했듯이 [이름: 내용] 형태로 나열되는 레이아웃이다. 다음 그림은 Windows 에서 볼수 있는 QFormLayout 의모습이다.

출처: Qt 도움말(QFormLayout)




QFormLayout 의 addRow() 는 [이름:내용] 을 한 줄 추가한다. addRow() 의 또다른 오버로딩 함수들도 있으니, addRow() 위에서 [F1] 을 눌러 내용을 확인하기 바란다. addWidget() 은 "이름" 없이 위젯만 추가한다.

22 번째 줄에서는 센트럴 위젯을 생성하고 설정하고 있다. 센트럴 위젯이란 메인 윈도우의 한 영역으로, 한 가운데 부분을 뜻한다. 클라이언트 영역이라고 볼 수 있다. 다음 그림을 보면 쉽게 이해할 수 있을 것이다.

출처: Qt 도움말(QMainWindow)

25 번째 줄에서 센트럴 위젯의 레이아웃을 설정함으로써 initWidgets() 는 마무리 된다.

이번에는 initActions() 을 위해서 bmi.h 전환한 후, 마찬가지로 [Alt-Enter] 를 눌러 definition 을 bmi.cpp 에 추가하자.

이 상태에서 빌드해서 실행하면 다음과 같은 결과를 얻을 수 있다.


하지만, 키를 입력하고 몸무게를 입력한 후 [계산하기] 를 클릭하더라도, 아무일도 일어나지 않는다. 이것은 아직 [계산하기] 버튼에 아무것도 연결되어 있지 않기 때문이다. 이를 해결하기 위해서는 initActions() 을 구현해야 한다.

그에 앞서, BMI 를 계산하고 결과를 출력할 함수를 먼저 작성하자. 이 함수는 [계산하기] 버튼과 연결되는데, 이 함수처럼 다른 위젯과 연동되는 함수를 특별히 슬롯(slot)이라고 부른다. bmi.h 로 전환환 후, 선언부에 다음을 추가하자.

1
2
private slots:
    void calcBMI();


slots 는 public 과 private 이 있는데, public 은 외부에 공개되는 것이고, private 은 내부에서만 쓰이는 것이다. 이 슬롯은 외부에 공개할 필요가 없기 때문에 private slots 로 선언했다. 이제 calcBMI() 를 구현하자. 이번에도 역시 [Alt-Enter] 로 bmi.cpp 에 골격을 추가하자. 그리고 다음을 입력하자.

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
    float height;
    float mass;
    float bmi;
    QString result;

    // 체질량 계산
    height = _heightLine->text().toFloat() / 100;   // 미터로 환산
    mass = _massLine->text().toFloat();
    bmi = mass / height / height;   // 체질량 = 몸무게(kg) / 키(m)^2

    // 체질량에 따라 비만도 판정
    // 대한비만학회 기준,
    // https://ko.wikipedia.org/wiki/%EC%B2%B4%EC%A7%88%EB%9F%89_%EC%A7%80%EC%88%98
    if (bmi < 18.5)
        result = tr("저체중");
    else if (bmi < 23)
        result = tr("정상");
    else if (bmi < 30)
        result = tr("경도 비만");
    else if (bmi < 35)
        result = tr("중등도 비만");
    else
        result = tr("고도 비만");

    // 비만도에 BMI 추가. BMI 는 소숫점 아래 1 자리까지
    result.append(tr("(%1)").arg(bmi, 0, 'f', 1));  // 소숫점 아래 1 자리

    // 결과 출력
    _resultLine->setText(result);


7 번째, 8 번째 줄에서 text() 는 QLineEdit 위젯에 입력된 텍스트를 QString 문자열로 가져오는 QLineEdit 멤버 함수이다. 그리고 toFloat() 는 QString 문자열을 부동소숫점(float) 으로 바꿔주는 QString 멤버 함수이다.

9 번째 줄에서 보듯이, BMI 계산 공식은 다음과 같다.

W : 몸무게(kg), H: 키(m)

26 번째 줄에 append() 는 문자열을 추가하는 QString 멤버 함수이다. 그런데 "%1" 이라는 특이한 문자가 있다. 이는 QString 의 기능으로, 뒤에 붙어 있는 arg() 의 내용으로 대체된다. arg() 는 QString 의 멤버 함수이다. 여러가지 형태로 오버로딩된 함수들이 있으니 [F1] 을 눌러 확인하기 바란다. 대체 순서는 %n 에서 n 이 작을수록 먼저 대체 된다. 예를 들어,

QString("%1 + %3 = %2").arg(3).arg(8).arg(5)



QString("3 + 5 = 8")

와 같다.

29 번째 줄의 setText() 는 QLineEdit 의 텍스트를 설정하는 QLineEdit 멤버 함수이다.

[계산하기] 버튼에 연결할 슬롯도 만들었으니, 실제로 연결해 보자. initActions() 에 다음의 내용을 추가하자.

1    connect(_calcPush, SIGNAL(clicked(bool)), this, SLOT(calcBMI()));


connect() 는 시그널을 슬롯에 연결해 주는 함수로, 모든 Qt 클래스들의 부모인 QObject 클래스의 정적 멤버함수이다. connect() 의 기본 형태는 다음과 같다.

connect(보내는 객체, 시그널, 받는 객체, 슬롯)


시그널은 반드시 SIGNAL() 매크로로 감싸야 하며, 슬롯은 SLOT() 매크로로 감싸야 한다. 수많은 시그널과 슬롯을 일일이 적기란 쉽지 않은 일이다. 하지만, 다행히도, Qt Creator 는 시그널 칸에서 "SIGNAL(" 을 입력하는 순간 앞에 지정된 객체의 모든 시그널을 보여주니 그 중에서 고르면 된다. 슬롯칸에서 "SLOT(" 을 입력하면 마찬가지로 모든 슬롯을 보여준다.

결국 위 내용은 _calcPush 객체([계산하기] 버튼) 의 clicked() 시그널이 발생하면 this 객체(BMI) 의 calcBMI() 슬롯을 호출하라는 것이다. 그런데 주의할 점은 슬롯 함수의 매개변수 갯수 및 유형은 시그널 함수의 매개변수 갯수 및 유형과 반드시 일치해야 한다는 점이다. 하지만 슬롯의 매개변수 갯수가 시그널의 매개변수 갯수보다 작은 경우도 인정이 된다. 다만 이 때 시그널의 나머지 매개변수들은 무시된다.


이제 모든 것이 마무리 되었다. [Ctrl-B] 로 빌드해서, [Ctrl+R] 로 실행해 보자. 다음은 실행 화면이다.


3. 마무리하면서...

간단하게나마 BMI 계산기를 만들어보았다. 다만 이 프로그램은 키나 몸무게가 입력되지 않거나, 잘못된 값이 입력되었을 때에 대한 처리가 들어있지 않다. 이와 관련된 부분은 다음에 다룰 예정이다.

그리고 Qt 는 도움말이 꽤 잘 되어 있다. Qt Creator 에서 [F1] 을 눌러 문맥 도움말(Context Help)을 잘 활용하면 매우 큰 도움이 된다. Qt Creator 가 아니더라도 Qt Assistant 에서 Qt 에 관한 모든 것을 얻을 수 있으니, 항상 살펴보기 바란다.

// ----- 2015/07/31

BMI 전체 소스는 다음 링크에서 확인할 수 있다.


다음은 bmi.cpp 와 bmi.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
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
/****************************************************************************
**
** BMI, BMI calculator
**
** Copyright (C) 2015 by KO Myung-Hun
** All rights reserved.
** Contact: KO Myung-Hun (komh@chollian.net)
**
** This file is part of BMI
**
** $BEGIN_LICENSE$
**
** GNU Lesser General Public License Usage
** This file may be used under the terms of the GNU Lesser
** General Public License version 2.1 as published by the Free Software
** Foundation and appearing in the file LICENSE.LGPL included in the
** packaging of this file.  Please review the following information to
** ensure the GNU Lesser General Public License version 2.1 requirements
** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.
**
** $END_LICENSE$
**
****************************************************************************/


#include "bmi.h"

#include <QtWidgets>

BMI::BMI(QWidget *parent)
    : QMainWindow(parent)
{
    initWidgets();
    initActions();
}

BMI::~BMI()
{
    // 생성된 위젯들은 자동으로 해제됨
}

// 체질량 계산
void BMI::calcBMI()
{
    float height;
    float mass;
    float bmi;
    QString result;

    // 체질량 계산
    height = _heightLine->text().toFloat() / 100;   // 미터로 환산
    mass = _massLine->text().toFloat();
    bmi = mass / height / height;   // 체질량 = 몸무게(kg) / 키(m)^2

    // 체질량에 따라 비만도 판정
    // 대한비만학회 기준,
    // https://ko.wikipedia.org/wiki/%EC%B2%B4%EC%A7%88%EB%9F%89_%EC%A7%80%EC%88%98
    if (bmi < 18.5)
        result = tr("저체중");
    else if (bmi < 23)
        result = tr("정상");
    else if (bmi < 30)
        result = tr("경도 비만");
    else if (bmi < 35)
        result = tr("중등도 비만");
    else
        result = tr("고도 비만");

    // 비만도에 BMI 추가. BMI 는 소숫점 아래 1 자리까지
    result.append(tr("(%1)").arg(bmi, 0, 'f', 1));  // 소숫점 아래 1 자리

    // 결과 출력
    _resultLine->setText(result);
}

// Widget 초기화
void BMI::initWidgets()
{
    // 키 입력창 생성
    _heightLine = new QLineEdit;

    // 몸무게 입력창 생성
    _massLine = new QLineEdit;

    // 계산 푸시 버튼 생성
    _calcPush = new QPushButton(tr("계산하기(&C)"));

    // 결과 입력창 생성
    _resultLine = new QLineEdit;
    _resultLine->setReadOnly(true); // 읽기 전용

    // 폼 레이아웃 생성
    _formLayout = new QFormLayout;
    _formLayout->addRow(tr("키(c&m)"), _heightLine);
    _formLayout->addRow(tr("몸무게(&kg):"), _massLine);
    _formLayout->addWidget(_calcPush);
    _formLayout->addRow(tr("결과:"), _resultLine);

    // 센트럴 위젯을 생성 및 설정
    setCentralWidget(new QWidget);

    // 센트럴 위젯의 레이아웃을 설정
    centralWidget()->setLayout(_formLayout);
}

// Action 초기화
void BMI::initActions()
{
    connect(_calcPush, SIGNAL(clicked(bool)), this, SLOT(calcBMI()));
}



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
/****************************************************************************
**
** BMI, BMI calculator
**
** Copyright (C) 2015 by KO Myung-Hun
** All rights reserved.
** Contact: KO Myung-Hun (komh@chollian.net)
**
** This file is part of BMI
**
** $BEGIN_LICENSE$
**
** GNU Lesser General Public License Usage
** This file may be used under the terms of the GNU Lesser
** General Public License version 2.1 as published by the Free Software
** Foundation and appearing in the file LICENSE.LGPL included in the
** packaging of this file.  Please review the following information to
** ensure the GNU Lesser General Public License version 2.1 requirements
** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.
**
** $END_LICENSE$
**
****************************************************************************/


#ifndef BMI_H
#define BMI_H

#include <QMainWindow>
#include <QtWidgets>

class BMI : public QMainWindow
{
    Q_OBJECT

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

private slots:
    void calcBMI();

private:
    QLineEdit *_heightLine;
    QLineEdit *_massLine;
    QPushButton *_calcPush;
    QLineEdit *_resultLine;

    QFormLayout *_formLayout;

    void initWidgets();
    void initActions();
};

#endif // BMI_H


// -----


댓글

이 블로그의 인기 게시물

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

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

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