Qt 로 만들자: 환율 계산기

이번에는 <환율 계산기> 를 만들어보자. 환율은 시시 각각 변하므로, 계산하는 시점의 환율을 아는 것이 중요하다. 이를 위해 웹페이지를 분석하는 방법을 사용할 것이다.

1. 요구 사항

  • 환율을 계산할 나라들을 정할 수 있다 

딱히 더 이상의 요구 사항은 없다. ^^

2. 코드 작성

2.1 프로젝트 생성

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

2.2 헤더 분석(exchange.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
#include <QMainWindow>
#include <QtWidgets>
#include <QtWebKitWidgets>

/**
 * @brief 환율 계산기 클래스
 */

class Exchange : public QMainWindow
{
    Q_OBJECT

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

private:
    QAction *_webPageViewAction;    ///< 웹페이지 보기 액션

    QComboBox *_fromCombo;  ///< 바꿀 나라
    QLineEdit *_fromLine;   ///< 바꿀 금액

    QComboBox *_toCombo;    ///< 바뀐 나라
    QLineEdit *_toLine;     ///< 바뀐 금액

    QWebView *_web;                 ///< 웹 보기/편집 위젯
    QProgressDialog *_progressDlg;  ///< 진행 상태 대화상자

    void initMenus();
    void initWidgets();
    void initConnections();

private slots:
    void webLoadFinished(bool ok);
    void webPageViewToggled(bool checked);
    void exchange();
};


3 번째 줄: "QtWebkitWidgets" 라는 헤더가 등장했다. 이 헤더는 웹에 접근하기 위한 클래스들을 모아 둔 헤더 파일이다. 그리고 이 기능을 쓰기 위해서는 프로젝트 파일(exchange.pro) 를 수정할 필요가 있다. 이 수정은 이후에 하도록 하자.

17 번째 줄: QAction 은 사실 그 이전부터 써 왔던 것이나, 이전에는 QMenu::addAction 을 통해 직접 지정했기 때문에 등장하지 않았었다. 하지만, 이번에는 다른 부분에서도 쓸 필요가 있기 때문에, 멤버 변수에 등장했다.

19번째 줄: QComboBox 는 버튼과 리스트를 합친 위젯이다. 현재 선택된 항목을 보여주고, 필요에 따라 전체 목록을 보여준다. 다음은 QComboBox 의 모습이다.

출처: Qt 도우말(QComboBox)

25 번째 줄: QWebView 는 웹에 접근하고, 그 내용을 수정하기 위한 위젯이다. 이번에 주로 사용할 위젯이다.

26 번째 줄: QProgressDialog 는 현재 진행상태를 보여주기 위한 대화 상자 위젯이다.

2.3 프로젝트 파일 수정(exchange.pro)

앞에서 QtWebkitWidgets 기능을 쓰기 위해서는 프로젝트 파일을 수정할 필요가 있다고 했는데, webkitwidgets 를 QT 변수에 추가해야 한다. 다음처럼 바꾸자.

QT += core gui webkitwidgets


2.4 소스 파일(exchange.cpp)

2.4.1 생성자와 소멸자


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

Exchange::Exchange(QWidget *parent)
    : QMainWindow(parent)
{
    initMenus();        // 메뉴 초기화
    initWidgets();      // 위젯 초기화
    initConnections();  // 시그널, 슬롯 연결

    // 윕페이지를 읽는다
    _web->load(QUrl("http://search.daum.net/search?w=tot&q=%ED%99%98%EC%9C%A8"));
}

Exchange::~Exchange()
{
}


초기화를 하고, 환율 웹페이지를 읽는다.

13 번째 줄: QWebView::load() 는 주어진 URL 을 읽는다. 위에 나타난 URL 은 <다음> 의 환율 페이지이다.

2.4.2 initMenus()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
void Exchange::initMenus()
{
    // "파일" 메뉴 생성
    QMenu *fileMenu = new QMenu(tr("파일(&F)"));
    // "끝내기" 액션 추가
    fileMenu->addAction(tr("끝내기(&x)"), qApp, SLOT(quit()),
                        QKeySequence(tr("Ctrl+Q")));

    // "웹페이지 보기" 액션 생성
    _webPageViewAction = new QAction(tr("웹페이지 보기(&W)"), this);
    // 체크 상태 표시
    _webPageViewAction->setCheckable(true);
    // 처음에는 체크하지 않는다
    _webPageViewAction->setChecked(false);

    // "보기" 메뉴 생생
    QMenu *viewMenu = new QMenu(tr("보기(&V)"));
    // "웹페이지 보기" 액션 추가
    viewMenu->addAction(_webPageViewAction);

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


메뉴를 초기화하고, 메뉴 항목의 속성을 설정한다.

10 번째 줄: "웹페이지 보기" 라는 이름의 메뉴 항목을 만든다.

12 번째 줄: QAction::setCheckable() 는 해당 메뉴 항목을 체크 가능한 상태로 만든다. 체크되었으면 메뉴 항목 왼쪽에 V 표시가 나타나고, 그렇지 않으면 나타나지 않는다.

14 번째 줄: QAction::setChecked() 는 메뉴 항목의 체크 상태를 설정한다.

2.4.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
/**
 * @brief 위젯을 초기화한다
 */

void Exchange::initWidgets()
{
    // 바꿀 나라 콤보 박스 생성
    _fromCombo = new QComboBox;
    // 내용에 맞게 크기 조정
    _fromCombo->setSizeAdjustPolicy(QComboBox::AdjustToContents);
    // 가로는 최소 크기 보존
    _fromCombo->setSizePolicy(QSizePolicy::Minimum,
                              _fromCombo->sizePolicy().verticalPolicy());

    // 바꿀 금액 라인 에디터 생성
    _fromLine = new QLineEdit;

    // 바뀐 나라 콤보 박스 생성
    _toCombo = new QComboBox;
    // 내용에 맞게 크기 조정
    _toCombo->setSizeAdjustPolicy(QComboBox::AdjustToContents);
    // 가로는 최소 크기 보존
    _toCombo->setSizePolicy(QSizePolicy::Minimum,
                            _toCombo->sizePolicy().verticalPolicy());

    // 바뀐 금액 라인 에디터 생성
    _toLine = new QLineEdit;


위젯을 생성하고 있다. 그리고 그 속성을 설정한다.

9 번째 줄: QComboBox::setSizeAdjustPolicy() 는 QComboBox 위젯의 크기를 결정하는 정책을 결정한다. QComboBox::AdjustToContents 는 추가된 항목을 모두 보여줄 수 있도록 크기를 변경한다.

11 번째 줄: QWidget::setSizePolicy() 는 크기를 바꾸는 정책을 결정한다. QSizePolicy::Minimum 은 최소 크기를 보존한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
    // 그리드 레이아웃 생성
    QGridLayout *gridLayout = new QGridLayout;
    gridLayout->addWidget(_fromCombo, 0, 0);
    gridLayout->addWidget(_fromLine, 1, 0);
    gridLayout->addWidget(new QLabel(tr("=")), 0, 1, 2, 1);
    gridLayout->addWidget(_toCombo, 0, 2);
    gridLayout->addWidget(_toLine, 1, 2);

    // 센트럴 위젯 생성
    QWidget *w = new QWidget;
    w->setLayout(gridLayout);

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


생성한 위젯들을 그리드 레이아웃으로 배치한다. 그리고 센트럴 위젯을 생성하고 설정한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
    // 웹 보기/편집 위젯 생성
    _web = new QWebView(this);
    // 가로/세로 최소 크기 보존
    _web->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum);
    // 보이기 상태 설정
    _web->setVisible(_webPageViewAction->isChecked());

    // 진행 상태 대화 상자 생성
    _progressDlg = new QProgressDialog(this);
    // 부모 윈도우 입력 막음
    _progressDlg->setWindowModality(Qt::WindowModal);
    // 출력 메세지 설정
    _progressDlg->setLabelText(tr("준비중입니다..."));
    // 3초 동안 완료되지 않으면 대화 상자 표시
    _progressDlg->setMinimumDuration(3000);
}


QWebView 위젯을 생성하고, 보임 상태를 "웹페이지 보기" 의 체크 상태에 따라 결정한다. 그리고 진행 상태 대화 상자를 만들고, 속성을설정한다.

6 번째 줄: QWidget::setVisible() 은 매개변수가 true 이면 위젯을 보이고, false 이면 감춘다.

11 번째 줄: QWidget::setWindowModality() 는 모달리티 상태를 결정한다. Qt::WindowModal 은 부모 위젯의 입력을 막는다.

13 번째 줄: QProgressDialog::setLabelText() 는 대화 상자에 표시할 텍스트를 지정한다.

15 번째 줄: QProgressDialog::setMinimumDuration() 은 지정한 시간이 지난 후에도 작업이 완료되지 않으면, 대화 상자를 나타낸다. 단위는 밀리초.

2.4.4 initConnections()

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 Exchange::initConnections()
{
    // "윕페이지 보기" 상태가 달라지면 wbPageViewToggled() 호출
    connect(_webPageViewAction, SIGNAL(toggled(bool)),
            this, SLOT(webPageViewToggled(bool)));

    // 바꿀 나라가 바뀌면 exchange() 호출
    connect(_fromCombo, SIGNAL(currentIndexChanged(int)),
            this, SLOT(exchange()));

    // 바꿀 금액이 바뀌면 exchange() 호출
    connect(_fromLine, SIGNAL(textChanged(QString)),
            this, SLOT(exchange()));

    // 바뀐 나라가 바뀌면 exchange() 호출
    connect(_toCombo, SIGNAL(currentIndexChanged(int)),
            this, SLOT(exchange()));

    // 바뀐 금액이 바뀌면 exchange() 호출
    connect(_toLine, SIGNAL(textChanged(QString)),
            this, SLOT(exchange()));

    // 웹 페이지 읽기 진행 상태를 진행 상태 대화 상자에 연결
    connect(_web, SIGNAL(loadProgress(int)),
            _progressDlg, SLOT(setValue(int)));

    // 웹 페이지를 모두 읽으면 webLoadFinished() 호출
    connect(_web, SIGNAL(loadFinished(bool)),
            this, SLOT(webLoadFinished(bool)));
}


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

7 번째 줄: QAction::toggled() 시그널은 메뉴 항목의 체크 상태가 바뀌면 발생한다.

11 번째 줄: QComboBox::currentIndexChange() 시그널은 콤보 박스의 선택 항목이 바뀌면 발생한다.

15 번째 줄: QLineEdit::textChanged() 시그널은 라인 에디터의 내용이 바뀌면 발생한다.

27 번째 줄: QWebView::loadProgress() 시그널은 웹 페이지를 읽으면서 처리한 요소들의 비율을 알려준다. 그리고 QProgressDialog::setValue() 는 진행 상태를 지정한다.진행 상태의 기본 범위는 0 ~ 100 이다.

31 번째 줄: QWebView::loadFinished() 시그널은 웹 페이지를 모두 읽었을 때 발생한다.

2.4.5 webLoadFinished()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
 * @brief 웹 페이지를 모두 읽으면 호출된다. 위젯들의 내용물을 초기화
 * @param ok 웹페이지를 읽었으면 true, 아니면 false
 */

void Exchange::webLoadFinished(bool ok)
{
    // 웹 페이지를 읽지 못했으면
    if (!ok)
    {
        QMessageBox::critical(this, qApp->applicationName(),
                              tr("웹페이지를 읽지 못했습니다."));

        return;
    }


웹페이지를 모두 읽었을 때 호출된다. 웹페이지를 읽지 못했으면 에러 메시지를 보여준다.

1
2
3
4
5
6
7
8
9
10
11
12
13
    // 현재 메인 프레임
    QWebFrame *frame = _web->page()->mainFrame();
    // 현재 문서 요소
    QWebElement document = frame->documentElement();

    // 나라 목록을 얻는다
    QVariant var = document.evaluateJavaScript("                    \
        var result = [];                                            \
        var list =  daum.$C(daum.$('exchangeColl'), 'select_list'); \
        for (var i = 0; i < list.length / 2; i++)                   \
            result.push(list[i].innerHTML);                         \
        result;"

    );


웹 페이지에서 지원되는 국가의 목록을 가져온다.

2 번째 줄: QWebFrame 은 웹 페이지의 프레임을 나타내는 클래스이다. QWebView::page() 는 웹 페이지를 나타내는 QWebPage * 를 돌려준다. QWebPage::mainFrame() 은 웹 페이지의 메인 프레임을  돌려준다.

4 번째 줄: QWebFrame::documentElement() 는 document 요소를 나타내는 QWebElement * 를 돌려준다.

7 번째 줄: QWebElement::evaluateJavaScript() 는 주어진 자바 스크립트를 처리한다. 이 함수는 QVariant 를 돌려주는데, QVariant 는 Qt 에서 지원하는 모든 자료형을 담을 수 있는 자료형이다. evaluateJavaScript() 가 돌려주는 값은 마지막에 실행되는 스크립트의 값이다. 이 스크립트는 국가 목록을 문자열 리스트로 변환해서 돌려준다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
    // span 태그를 제거할 정규식 생성
    QRegExp rx("(.*)<span.*>(.*)</span>");

    // 나라 목록을 문자열 목록으로 변환
    QStringList countries(var.toStringList());
    Q_FOREACH (QString country, countries)
    {
        // 정규식에 대입
        rx.indexIn(country);

        // span 태그 제거
        QString item(rx.cap(1) + " " + rx.cap(2));

        // 나라 목록 추가
        _fromCombo->addItem(item);
        _toCombo->addItem(item);
    }


웹 페이지에서 가져온 나라 목록을 콤보 박스에 추가한다.

2 번째 줄: QRegExp 는 정규식을 나타내는 클래스이다. 정규식에 대해 잘 모른다면 Qt 도움말을 보자.

5 번째 줄: QVariant::toStringList() 는 QVariant 에 담긴 내용이 문자열 리스트일 때, 해당 값을 QStringList 로 변환한다.

9 번째 줄: QRegExp::indexIn() 은 주어진 문자열을 정규식에 대입하여 처리한다.

12 번째 줄: QRegExp::cap() 은 QRegExp::indexIn() 의 처리 결과 중에서 캡쳐된 내용을 돌려준다. 0 은 전체, 1 은 첫번째, 2 는 두번째 등등...

15 번째 줄: QComboBox::addItem() 은 목록에 주어진 항목을 추가한다.

1
2
3
4
5
6
7
8
9
10
11
12
    // 바꿀 나라 선택(1 = 미국)
    _fromCombo->setCurrentIndex(1);
    // 바꿀 금액으로 입력 촛점 설정
    _fromLine->setFocus();
    // 바꿀 금액 설정
    _fromLine->setText("1");
    // 바뀐 나라 선택(0 = 대한민국)
    _toCombo->setCurrentIndex(0);

    // 환율 계산
    exchange();
}


각 위젯들의 값을 초기화하고, 환율을 계산한다.

2 번째 줄: QComboBox::setCurrentIndex() 는 주어진 위치에 해당하는 항목을 선택한다.

2.4.6 exchange()

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 환율을 계산한다
 */

void Exchange::exchange()
{
    // 실제 바꿀 금액/바뀐 금액 초기화
    // 실제 바꿀 금액
    static QLineEdit *fromAmountLine = _fromLine;
    // 실제 바꿀 금액 JavaScript 요소 이름
    static QString fromAmountJS = "extFromMoney";
    // 실제 바뀐 금액
    static QLineEdit *toAmountLine = _toLine;
    // 실제 바뀐 금액 JavaScript 요소 이름
    static QString toAmountJS = "extToMoney";

    // 입력 촛점이 _fromLine 이라면
    if (QApplication::focusWidget() == _fromLine)
    {
        // 바꿀 금액/바뀐 금액 원래대로
        fromAmountLine = _fromLine;
        fromAmountJS = "extFromMoney";
        toAmountLine = _toLine;
        toAmountJS = "extToMoney";
    }
    else if (QApplication::focusWidget() == _toLine)
    {
        // 바꿀 금액/바뀐 금액 바꾸기
        fromAmountLine = _toLine;
        fromAmountJS = "extToMoney";
        toAmountLine = _fromLine;
        toAmountJS = "extFromMoney";
    }


주어진 조건에 따라 실제로 환율을 계산하는 함수이다. 입력 촛점에 따라 바꿀 금액과 환율에 따라 바뀐 금액의 위치를 결정한다.

1 번째 줄: QApplication::focusWidget() 은 현재 입력 촛점을 가지고 있는 위젯을 돌려준다.

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
    // 현재 프레임
    QWebFrame *frame = _web->page()->mainFrame();
    // 현재 문서
    QWebElement document = frame->documentElement();

    // 웹의 바뀔 나라 설정
    document.evaluateJavaScript(QString("ExfromExchange.selectIdx(%1)")
                                    .arg(_fromCombo->currentIndex()));
    // 웹의 바뀐 나라 설정
    document.evaluateJavaScript(QString("ExtoExchange.selectIdx(%1)")
                                    .arg(_toCombo->currentIndex()));

    // 웹의 바꿀 금액을 설정하고, 계산
    document.evaluateJavaScript(QString("           \
        %1.value = '%2';                            \
        var e = document.createEvent('HTMLEvents'); \
        e.initEvent('input', true, true);           \
        %1.dispatchEvent(e);"

    ).arg(fromAmountJS).arg(fromAmountLine->text()));

    // 웹의 바뀐 금액 저장
    QVariant var = document.evaluateJavaScript(toAmountJS + ".value");
    // 바뀐 금액 설정
    toAmountLine->setText(var.toString());
}


나라와 금액을 설정하고 환율을 계산한다.

8 번째 줄: QComboBox::currentIndex() 는 현재 선택된 항목의 위치를 돌려준다.

24 번째 줄: QVariant::toString() 은 QVariant 의 값을 QString() 으로 바꾼다.

2.4.7 webPageViewToggled()

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
/**
 * @brief "웹페이지 보기" 상태가 달라지면 호출된다. 상태에 따라 웹페이지 표지
 * @param checked 체크되었으면 true, 아니면 false
 */

void Exchange::webPageViewToggled(bool checked)
{
    // 웹페이지 표시 전 크기
    static QSize compactSize;

    // 센트럴 위젯의 레이아웃
    QGridLayout *layout =
            qobject_cast<QGridLayout *>(centralWidget()->layout());

    if (checked)                                // 체크되었으면
    {
        compactSize = size();                   // 현재 크기 저장
        layout->addWidget(_web, 2, 0, 1, 3);    // 웹 위젯 추가
    }
    else                                        // 아니면
    {
        layout->removeWidget(_web);             // 위젯 제거
        setMinimumSize(0, 0);                   // 창 최소 크기 (0, 0) 으로
        resize(compactSize);                    // 웹 표시 이전으로 크기 조정
    }

    // 웹 표시 상태 바꾸기
    _web->setVisible(checked);
}


"웹페이지 보기" 상태에 따라 웹페이지 화면을 보여준다.

8 번째 줄: QSize 클래스는 폭과 높이를 관리하는 클래스이다.

12 번째 줄: QWidget::layout() 는 해당 위젯의 현재 레이아웃을 돌려준다.

21 번째 줄: QLayout::removeWidget() 는 해당 위젯을 레이아웃으로부터 제거한다. 다만 소유권은 그대로 남는다.

22 번째 줄: QWidget::setMinimumSize() 는 해당 위젯의 최소 크기기를 지정한다.

23 번째 줄: QWidget::resize() 는 해당 위젯의 크기를 변경한다. 다만 위젯의 최소 크기(QWidget::minimumSize()) 와 최대 크기(QWidget::maximumSize()) 를 벗어날 수는 없다. 최소 크기를 (0, 0) 으로 지정한 이유도 이 때문이다. 레이아웃은 위젯들의 최소 크기를 계산하여 자체 최소 크기를 결정한다. 이 경우, 레이아웃이 변경되기 이전보다 크기가 클 수 있다.

3. 마무리하면서...

Qt 5 는 웹 페이지에 접근하고, 웹 페이지를 제어하기 위한 클래스가 두 가지가 있다. 하나는 WebKit 를 사용하는 Qt WebKit 계열이고, 다른 하나는 크롬을 사용하는 Qt WebEngine 계열이다. 이 글에서 쓰이는  QWebView 는 Qt WebKit 계열이다. 하지만, 현재도 그렇고 앞으로도 QWebView 계열은 개선되지 않을 것이라고 한다. 그리고 이미 Qt WebEngine 계열이 충분히 개선되었고, 향후 지속적으로 지원이 된다고 하니, 최신 기술이 필요하다면 Qt WebEngine 계열을 사용하는 것이 좋을 것이다. 다만 Qt WebEngine 계열은 Qt WebKit 과 달리, 모든 것이 비동기로 이루어지고, 웹페이지 요소에 접근하거나 요소를 조작할 수 있는 C++ 인터페이스가 제공되지 않는다. 따라서 모든 것은 자바 스크립트로 처리하여야 한다. 최신 기술이 아니고 C++ 인터페이스를 많이 사용한다면 여전히 Qt WebKit 을 써도 괜찮다.

<환율 계산기> 를 통해 웹에 접근하고, 웹을 이용하는 방법을 보았다. 아무리 생각해도 웹을 가지고 무엇인가를 하고 싶다면 자바 스크립트를 해야 한다는 것이다. 뒤죽 박죽 조각 조각 나누어진 지식만을 가지고 있었던 터라, 한번쯤은 체계적으로 정리할 필요가 있겠다는 생각이 들었다. 웹에 관심있는 사람들이라면 자바스크립트는 꼭 배워두자.

다음은 <환율 계산기>의 실행 모습이다.


전체 소스는 다음 링크를 열어 확인하자.



끝으로 이 프로그램은 웹페이지를 보다가 링크를 타고 다른 페이로 이동하면 작동하지 않는다는 것을 주의하자.

댓글

이 블로그의 인기 게시물

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

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

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