Qt 로 만들자: 일기장
한동안 너무 쉬었다. 오랜만에 다시 만들어보는 프로그램은 <일기장> 이다. 프로그래밍을 연습할 때 주소록과 함께 한 번쯤 만들어보는 프로그램이다. 일기장과 주소록같은 프로그램들은 주로 데이터베이스를 공부하기 위한 소재들이다. 마찬가지로 이번에 <일기장> 을 만들어 보면서 Qt 에서 제공하는 데이터베이스 기능을 알아보도록 하자.
1. 요구사항
2. 코드 분석
2.1 프로젝트 작성
2.1.1 프로젝트 파일 수정(Diary.pro)
이 프로그램은 SQL 데이터베이스를 쓰기 때문에 sql 모듈을 추가해주어야 한다.
2.2 헤더 분석(diary.h)
2.2.1 헤더 파일 목록
4 번째 줄: QtSql 은 SQL 데이터베이스를 쓰기 위해 필요한 헤더이다.
2.2.2 public 멤버 함수
2.2.3 protected 멤버 함수
2 번째 줄: QWidget::closeEvent() 는 창이 닫힐 때 발생하는 이벤트이다. 창을 닫을 때 저장되지 않은 일기를 저장할지 물어보기 위해 재정의한다.
2.2.4 private 멤버 변수
16 번째 줄: QSqlDatabase 는 데이터베이스 접속을 위한 클래스이다.
2.2.5 private 멤버 함수
24 번째 줄: QMessageBox::warning() 은 경고 메시지를 보여주는 정적 멤버 함수이다.
2.2.6 private slots
2.3 소스 분석(diary.cpp)
2.3.1 Diary 클래스
<일기장> 의 메인 클래스이다.
2.3.1.1 생성자와 소멸자
14 번째 줄: QSqlDatabase::addDatabase() 는 프로그램에서 사용할 데이터베이스를 추가하는 정적 멤버 함수이다. <일기장> 은 내장 데이터베이스인 SQLite 를 쓴다. Qt 는 SQLite 외에도 MS SQL Server, My SQL Server, PostgreSQL, Oracle, Sybase, Interbase, DB2 를 지원한다.
16 번째 줄: QCoreApplication::setApplicationName() 은 프로그램의 이름을 설정한다.
19 번째 줄: QSqlDatabase::setDatabaseName() 을 사용할 데이터베이스의 이름을 설정하는 멤버 함수이다. <일기장> 에서는 데이터베이스의 파일 이름이다. QApplication::applicationDirPath() 는 실행파일의 디렉토리를 돌려준다. 예를 들어, 실행 파일이름이 x:/path/to/exefile 라면, x:/path/to 를 돌려준다.
2.3.1.2 initMenus()
파일 메뉴, 도움말 메뉴를 추가한다.
20 번째 줄: 메뉴 항목의 이름을 설정할 때 qApp->applicationName() + tr() 형태를 하지 않은 이유는 나중에 국제화를 위해서이다. 국제화를 위한 번역 과정에서 어순 등에 문제가 될 수도 있다. 이 경우 QString::arg() 를 사용하지 않으면 많은 어려움이 있다. 예를 들어,
의 경우, 문맥도 알기 어려울뿐만 아니라, 번역하더라도
정도가 된다. 참으로 이상한 문장이된다. 만일 이것을 다음처럼 바꾸면
번역자는 이를
으로 바꿀 수 있고, 따로 분리한 것보다 훨씬 매끄러운 번역이 된다.
따라서 어떤 문장을 표현할 때는 따로 따로 나누어서 표현하기 보다, QString::arg() 를 이용해서 전체 문장으로 표현하도록 하자.
2.3.1.3 initWidgets()
위젯을 초기화하고 레이아웃을 배치한다.
연도/월/일 콤보박스와 레이블을 생성하고 배치한다.
36 번째 줄: QBoxLayout::addStretch() 은 늘임자를 추가하는데, 창의 크기가 달라질 때 확장된 부분을 빈 공간으로 메꾼다.
제목과 내용 관련 위젯을 생성하고 초기화한다.
전체 레이아웃을 배치하고, 창의 크기를 조정한다.
2.3.1.4 newDiary()
새 일기를 준비한다.
7 번째 줄: sender() 는 시그널을 보낸 QObject 의 포인터를 돌려준다. 만약 시그널로 호출된 것이 아니라면 sender() 는 0 을 돌려준다. 따라서 프로그램 초기화 중에 호출될 경우 저장 여부를 묻는 것을 피할 수 있다.
2.3.1.5 askToSave()
내용이 바뀐 일기를 저장할지를 묻는다.
QMessageBox::Save 또는 QMessageBox::Discard 일 때는 계속 진행을 하고(true), QMessageBox::Cancel 일 때는 진행을 멈춘다(false).
2.3.1.6 setDay()
연도와 월에 따라 일 콤보 박스를 조정한다.
14 번째 줄: QDate::daysInMonth() 는 주어진 연도와 주어진 월의 일 수를 돌려준다. 윤년에 따라 2 월의 일 수가 달라지기 때문에 이를 위한 작업이다.
16 번째 줄: QObject::disconnect() 는 해당 객체의 시그널/슬롯 연결을 끊는다. 일 콤보박스의 인덱스가 수정되면 setDay() 가 다시 호출되므로, 시그널/슬롯을 연결을 끊지 않으면, 무한 루프에 빠진다.
2.3.1.6 setDiaryModified()
일기가 수정되었는지 설정한다.
2.3.1.7 save()
현재 편집 중인 일기를 저장한다.
20 번째 줄: QSqlQuery 는 Qt 에서 SQL 데이터베이스에 질의할 때 쓰이는 클래스이다. QSqlQuery 는 SQL 질의문을 그대로 쓸 수 있다. QSqlQuery 기본 생성자는 기본 데이터베이스에 연결된다. 기본 데이터베이스는 QSqlDatabase::addDatabase() 의 두번째 인자를 제공하지 않은 경우에 생성된다. Qt 는 QSqlQuery 말고도 고수준의 질의 클래스가 있는데, QSqlTableModel() 계열이 그것이다. SQL 질의문 대신에 추상화한 멤버 함수를 제공하며, 이름에서 볼 수 있듯이, 모델이므로 QListView 나 QTableView 에 모델로 쓰일 수 있다.
2.3.1.8 diaryOpenDb()
데이터베이스에 접속한다.
7 번째 줄: QSqlDatabase::isOpen() 은 데이터베이스가 열려 있는지 알려주고, QSqlDatabase::open() 은 데이터베이스를 연다. 데이터베이스를 열기 전에는 사용하는 SQL 드라이버에 따라 데이터베이스 이름외에도 사용자 이름, 암호, 호스트 이름, 포트 등을 먼저 설정해야 하는 경우도 있다.
2.3.1.9 diaryCreateTable()
데이터베이스에 일기장 테이블을 생성한다.
8 번째 줄: QSqlQuery::exec() 는 주어진 SQL 질의문을 실행한다. SQL 질의문은 연결된 데이터베이스의 백엔드가 받아들일 수 있는 것이어야 한다.
2.3.1.10 diaryFind()
현재 작성 중인 일기 ID 를 이용하여 일기를 찾는다.
8~9 번째 줄: QSqlQuery::prepare() 는 질의문이 길어나 변수들을 따로 지정하고 싶을 때 쓰인다. 질의문 중에 [:변수이름](Oracle 스타일)은 이후에 QSqlQuery::bindValue() 에서 주어진 값으로 대체된다. [:변수이름] 대신에 Qt 는 '?'(ODBC 스타일) 도 지원한다. 이 때에는 QSqlQuery::addBindValue() 를 순서에 맞춰 쓰면 된다. 하지만 둘을 섞어서 쓸 수는 없다.
10 번째 줄: QSqlQuery::next() 는 질의문을 실행하고 나서 결과셋을 탐색할 때 쓰인다. 질의문을 실행한 직후에 결과셋은 invalid 상태이므로, 반드시 QSqlQuery::next() 를 실행시켜야 실제 결과셋을 확인할 수 있다.
2.3.1.11 diaryInsert()
일기를 데이터베이스에 추가한다. 만약 일기 ID 가 주어져 있으면, 해당 ID 로 저장하고, 아니면 새로운 ID 를 생성한다.
34 번째 줄: QSqlQuery::value() 는 주어진 컬럼의 값을 QVariant 로 돌려준다.
2.3.1.12 diaryDelete()
일기를 데이터베이스에서 지운다.
2.3.1.13 diaryUpdate()
일기를 갱신한다.
2.3.1.14 load()
일기를 불러온다.
15 번째 줄: QDialog::exec() 는 대화상자를 modal 상태로 실행하고, 성공했을 경우에는 QDialog::Accepted 를 돌려주고, 실패했을 경우에는 QDialog::Rejected 를 돌려준다.
2.3.1.15 about()
<일기장> 에 대한 정보를 보여준다.
13 번째 줄: QLocale() 은 로케일 관련 기능을 제공하는 클래스이다. QLocale::C 는 C 로케일을 뜻하며, QLocale::toDate() 는 주어진 문자열을 주어진 형식 문자열에 따라 QDate 로 바꾼다.
14 번째 줄: QTime::fromString() 은 주어진 문자열을 주어진 형식 문자열에 따라 QTime 으로 바꾼다.
16 번째 줄: QMessageBox::about() 은 주어진 인자를 바탕으로 About 대화상자를 보여준다.
About 대화 상자는 프로그램을 소개하고 라이센스를 보여주는 역할에 많이 쓰인다.
2.3.1.16 aboutQt()
Qt 에 대한 정보를 보여준다.
6 번째 줄: QMessageBox::aboutQt() 는 현재 사용되고 있는 Qt 에 대한 정보를 보여준다.
2.3.1.17 closeEvent()
창이 닫힐 때 호출되며, 바뀐 내용이 있으면 저장할지 물어본다.
8~10 번째 줄: QCloseEvent::accept() 는 창을 닫아도 된다는 것을 뜻하고, QCloseEvent::ignore() 는 창을 닫지 말라는 것을 뜻한다.
2.3.2 LoadDialog 클래스
일기를 불러오기 위한 대화상자이다.
2.3.2.1 멤버 변수
18 번째 줄: QSqlTableModel 은 SQL 데이터베이스를 모델로 추상화한 클래스이다. SQL 데이터베이스를 조작할 수도 있고, QListView 나 QTableView 에 모델로 제공될 수도 있다.
19 번째 줄: QItemSelectionModel 은 여러 뷰의 선택된 모델을 나타낸다.
20 번째 줄: QSqlRecord 는 QSqlTableModel() 에서 한 줄(row)에 해당하는 정보를 추상화한 클래스이다.
2.3.2.2 생성자
클래스를 생성하고 레이아웃을 배치한다.
11 번째 줄: QSqlTableModel::setTable() 은 사용할 테이블을 설정한다.
13 번째 줄: QSqlTableModel::setEditStrategy() 은 데이터베이스를 수정하는 방식을 결정한다. QSqlTableModel::OnManualSubmit 은 개발자가 직접 결정하겠다는 뜻이다. 데이터베이스에 적용하려면 반드시 QSqlTableModel::submitAll() 을 실행해야 한다.
15~17 번째 줄: QSqlTableModel::setHeaderData() 는 주어진 컬럼의 데이터를 설정한다. 여기에서는 날짜와 제목에 대한 컬럼 제목이다.
19 번째 줄: QSqlTableModel::select() 는 SQL 의 SELECT 문에 해당하며, WHERE 절을 조절하고 싶으면 QSqlTableModel::setFilter() 를 쓰면 된다.
4 번째 줄: QAbstractItemView::setEditTriggers() 는 편집을 시작하는 수단을 지정한다. QAbstractItemView::NoEditTriggers 는 읽기 전용이라는 뜻이다.
6 번째 줄: QAbstractItemView::setTabKeyNavigation() 은 탭 이동 여부를 결정한다. 여기에서는 대화상자의 탭 이동을 위해서 테이블 뷰의 탭 이동을 쓰지 않는다.
8 번째 줄: QAbstractItemView::setSelectionMode() 는 몇 개를 어떻게 선택할지를 설정한다. QAbstractItemView::SingleSelection 은 하나만 고른다는 것을 뜻한다.
10 번째 줄: QAbstractItemView::setSelectionBehavior() 는 어떤 것을 선택할지를 설정한다. QAbstractItemView::SelectRows 는 줄단위로 선택하는 것을 뜻한다.
13~14 번째 줄: QAbstractItemView::setModel() 은 나타낼 모델을 설정하고, QAbstractItemView::setSelectionModel() 은 선택 모델을 설정한다.
16~17 번째 줄: QAbstractItemView::hideColumn() 은 주어진 컬럼을 감춘다.
19 번째 줄: QAbstractItemView::resizeColumnToContents() 는 내용에 맞게 컬럼 폭을 조절한다.
21~22 번째 줄: QAbstractItemView::setSortingEnabled() 는 소팅 여부를 설정하고, QAbstractItemView::sortByColumn() 은 소팅 기준으로 사용할 컬럼을 설정한다.
25 번째 줄: QHeaderView::setStretchLastSection() 은 마지막 컬럼의 폭을 창 크기에 맞게 조절한다.
30~32 번째 줄: 사용자가 선택한 일기가 바뀌면 새로이 선택된 일기의 내용을 보여준다. 그런데, 주목한 것은 QTableView 의 시그널을 연결하는 것이 아니라는 것이다. QTableView 에도 selectedIndexes() 나 currentChanged() 라는 멤버 함수가 있으나 있는 protected 멤버 함수이다. 따라서 외부에서 쓸 수가 없다.이를 해결하기 위한 것이 QItemSelectionModel() 이다. 코드에서 보듯이 QItemSelectionModel() 은 우리가 원하는 바로 그 시그널을 제공하고 있다.
레이아웃을 설정하고, 크기를 조정한다.
38 번째 줄: QWidget::setWindowFlags 는 위젯의 플래그를 설정한다. QWidget::windowFlags() 은 플래그를 돌려준다. Qt::WindowContextHelpButtonHint 는 문맥 도움말 버튼을 뜻하며, 여기에서는 해당 버튼을 보이지 않도록 설정한다.
2.3.2.3 id(), year(), month(), day(), title(), content()
대화상자의 결과를 돌려준다.
2.3.2.4 accept()
사용자가 대화상자를 종료할 때 선택한 결과를 저장한다.
8 번째 줄: QTableView::currentIndex() 는 현재 아이템의 인덱스를 돌려준다.
10 번째 줄: QModelIndex::isValid() 는 모델 인덱스가 올바른지 알려준다.
14 번째 줄: QSqlTableModel::record() 는 주어진 컬럼의 레코드를 돌려준다. 컬럼이 주어지지 않는다면 필드 이름들을 돌려준다. QModelIndex::row() 는 모델 인덱스의 줄 위치를 알려준다.
16 번째 줄: 대화상자를 올바르게 마무리하기 위해 QDialog::accept() 를 호출한다.
2.3.2.5 deleteDiary()
선택된 일기를 데이터베이스에서 지운다.
12 번째 줄: QSqlTableModel::removeRow() 는 주어진 줄 하나를 지운다.
13 번째 줄: QSqlTableModel::submitAll() 은 그동안 수정한 내용을 데이터베이스에 실제로 적용한다. 앞서 QSqlTableMode::OnManualSubmit 전략을 쓰기로 했기 때문에, QSqlTableModel 로 수정한 내용을 데이터베이스에 반영하기 위해서는 반드시 QSqlTableModel::submitAll() 을 호출해야 한다.
2.3.2.6 showCurrentContent()
현재 선택된 일기의 내용을 보여준다.
3. 마무리하면서...
<일기장> 을 만들면서 Qt 에서 데이터베이스를 다루는 방법을 살펴보았다. 언제나 그렇듯이 여기에 서술된 기능들은 매우 기초적인 것들이다. 그리고 이외에도 오버로딩된 멤버 함수들이 많이 존재하기 때문에 반드시 Qt 도움말과 Qt Assistant 를 살펴보자.
번역을 고려해서 QObject::tr() 을 쓰고 있지만, 정작 Qt 의 위젯들, 예를 들어 QMessageBox, 같은 경우에는 번역되지 않은채 나타나고 있는데, 이는 Qt 번역 파일을 설치하지 않았기 때문이다. 이에 대해서는 차후에 기회가 되면 살펴보도록 하자.
다음은 <일기장> 의 실행 모습이다.
전체 소스는 여기에서 확인하자.
1. 요구사항
- 날짜, 제목, 내용 편집 지원
- 일기 불러오기/저장/삭제/수정 지원
2. 코드 분석
2.1 프로젝트 작성
- 프로젝트 이름: Diary
- 메인 클래스 이름: Diary
- 메인 클래스 유형: QMainWindow
2.1.1 프로젝트 파일 수정(Diary.pro)
이 프로그램은 SQL 데이터베이스를 쓰기 때문에 sql 모듈을 추가해주어야 한다.
QT += core gui sql
2.2 헤더 분석(diary.h)
2.2.1 헤더 파일 목록
1 2 3 4 | #include <QMainWindow> #include <QtWidgets> #include <QtSql> |
4 번째 줄: QtSql 은 SQL 데이터베이스를 쓰기 위해 필요한 헤더이다.
2.2.2 public 멤버 함수
1 2 3 4 5 6 7 8 9 10 11 12 13 | /** * @brief 일기장 메인 클래스 */ class Diary : public QMainWindow { Q_OBJECT public: Diary(QWidget *parent = 0); ~Diary(); void initMenus(); void initWidgets(); |
2.2.3 protected 멤버 함수
1 2 | protected: void closeEvent(QCloseEvent *e); |
2 번째 줄: QWidget::closeEvent() 는 창이 닫힐 때 발생하는 이벤트이다. 창을 닫을 때 저장되지 않은 일기를 저장할지 물어보기 위해 재정의한다.
2.2.4 private 멤버 변수
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | private: static const int StartYear = 2010; ///< 일기장 시작 년도 static const int EndYear = 2020; ///< 일기장 마지막 년도 QComboBox *_yearCombo; ///< 연도 콤보 박스 QComboBox *_monthCombo; ///< 월 콤보 박스 QComboBox *_dayCombo; ///< 일 콤보 박스 QLineEdit *_titleLine; ///< 제목 QTextEdit *_contentText; ///< 내용 int _id; ///< 일기 DB ID int _year; ///< 연도 int _month; ///< 월 int _day; ///< 일 QSqlDatabase _db; ///< 데이터베이스 |
16 번째 줄: QSqlDatabase 는 데이터베이스 접속을 위한 클래스이다.
2.2.5 private 멤버 함수
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 | bool diaryOpenDb(); bool diaryCreateTable(); bool diaryFind(QSqlQuery *query); bool diaryInsert(QSqlQuery *query); bool diaryDelete(QSqlQuery *query); bool diaryUpdate(QSqlQuery *query); /** * @brief 창 제목을 돌려준다 * @return 창 제목 */ inline QString title() const { return QDate(_year, _month, _day) .toString(Qt::SystemLocaleLongDate) + "[*]"; } /** * @brief 경고창을 보여준다 * @param msg 경고 메세지 */ inline void warning(const QString &msg) { QMessageBox::warning(this, qApp->applicationDisplayName(), msg); } bool askToSave(); bool askToOverwrite(); |
24 번째 줄: QMessageBox::warning() 은 경고 메시지를 보여주는 정적 멤버 함수이다.
2.2.6 private slots
1 2 3 4 5 6 7 8 9 10 11 12 | private slots: void about(); void aboutQt(); void setDay(); void setDiaryModified(bool modified = true); void newDiary(); bool load(); bool save(); }; |
2.3 소스 분석(diary.cpp)
2.3.1 Diary 클래스
<일기장> 의 메인 클래스이다.
2.3.1.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 | /** * @brief Diary 생성자 * @param parent 부모 위젯 */ Diary::Diary(QWidget *parent) : QMainWindow(parent) , _yearCombo(0) , _monthCombo(0) , _dayCombo(0) , _id(-1) , _year(-1) , _month(-1) , _day(-1) , _db(QSqlDatabase::addDatabase("QSQLITE")) { qApp->setApplicationName(tr("일기장")); // 데이터베이스 파일 설정 _db.setDatabaseName(QApplication::applicationDirPath() + "/diary.sqlite"); initMenus(); initWidgets(); newDiary(); } /** * @brief Diary 소멸자 */ Diary::~Diary() { } |
14 번째 줄: QSqlDatabase::addDatabase() 는 프로그램에서 사용할 데이터베이스를 추가하는 정적 멤버 함수이다. <일기장> 은 내장 데이터베이스인 SQLite 를 쓴다. Qt 는 SQLite 외에도 MS SQL Server, My SQL Server, PostgreSQL, Oracle, Sybase, Interbase, DB2 를 지원한다.
16 번째 줄: QCoreApplication::setApplicationName() 은 프로그램의 이름을 설정한다.
19 번째 줄: QSqlDatabase::setDatabaseName() 을 사용할 데이터베이스의 이름을 설정하는 멤버 함수이다. <일기장> 에서는 데이터베이스의 파일 이름이다. QApplication::applicationDirPath() 는 실행파일의 디렉토리를 돌려준다. 예를 들어, 실행 파일이름이 x:/path/to/exefile 라면, x:/path/to 를 돌려준다.
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 22 23 24 25 26 27 | /** * @brief 메뉴를 초기화한다 */ void Diary::initMenus() { // 파일 메뉴 생성 QMenu *fileMenu = new QMenu(tr("파일(&F)")); fileMenu->addAction(tr("새 일기(&N)"), this, SLOT(newDiary()), QKeySequence::New); fileMenu->addAction(tr("불러오기(&L)..."), this, SLOT(load()), QKeySequence(tr("Ctrl+L"))); fileMenu->addAction(tr("저장하기(&S)..."), this, SLOT(save()), QKeySequence::Save); fileMenu->addSeparator(); fileMenu->addAction(tr("끌내기(&x)"), this, SLOT(close()), QKeySequence(tr("Ctrl+Q"))); // 도움말 메뉴 생성 QMenu *helpMenu = new QMenu(tr("도움말(&H)")); helpMenu->addAction(tr("%1 정보(&A)...").arg(qApp->applicationName()), this, SLOT(about())); helpMenu->addAction(tr("Qt 정보(&Q)..."), this, SLOT(aboutQt())); // 메뉴바에 메뉴 추가 menuBar()->addMenu(fileMenu); menuBar()->addMenu(helpMenu); } |
20 번째 줄: 메뉴 항목의 이름을 설정할 때 qApp->applicationName() + tr() 형태를 하지 않은 이유는 나중에 국제화를 위해서이다. 국제화를 위한 번역 과정에서 어순 등에 문제가 될 수도 있다. 이 경우 QString::arg() 를 사용하지 않으면 많은 어려움이 있다. 예를 들어,
tr("나는") + 2014 + tr("년에 태어났다")
의 경우, 문맥도 알기 어려울뿐만 아니라, 번역하더라도
"I was" + year + "born at"
정도가 된다. 참으로 이상한 문장이된다. 만일 이것을 다음처럼 바꾸면
tr("나는 %1 년에 태어났다").arg(year)
번역자는 이를
"I was born at %1"
으로 바꿀 수 있고, 따로 분리한 것보다 훨씬 매끄러운 번역이 된다.
따라서 어떤 문장을 표현할 때는 따로 따로 나누어서 표현하기 보다, QString::arg() 를 이용해서 전체 문장으로 표현하도록 하자.
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 34 35 36 37 38 39 40 41 42 43 44 | /** * @brief 위젯을 초기화한다 */ void Diary::initWidgets() { // 연도 콤보 박스/레이블 생성 및 초기화 _yearCombo = new QComboBox; for (int i = StartYear; i < EndYear; ++i) _yearCombo->addItem(QString::number(i)); QLabel *yearLabel = new QLabel(tr("연도(&Y):")); yearLabel->setBuddy(_yearCombo); // 월 콤보 박스/레이블 생성 및 초기화 _monthCombo = new QComboBox; for (int i = 1; i <= 12; ++i) _monthCombo->addItem(QString::number(i)); QLabel *monthLabel = new QLabel(tr("월(&M):")); monthLabel->setBuddy(_monthCombo); // 일 콤보 박스/레이블 생성 및 초기화 _dayCombo = new QComboBox; QLabel *dayLabel = new QLabel(tr("일(&D):")); dayLabel->setBuddy(_dayCombo); connect(_yearCombo, SIGNAL(currentIndexChanged(int)),this, SLOT(setDay())); connect(_monthCombo, SIGNAL(currentIndexChanged(int)), this, SLOT(setDay())); connect(_dayCombo, SIGNAL(currentIndexChanged(int)), this, SLOT(setDay())); QHBoxLayout *dateLayout = new QHBoxLayout; dateLayout->addStretch(); dateLayout->addWidget(yearLabel); dateLayout->addWidget(_yearCombo); dateLayout->addStretch(); dateLayout->addWidget(monthLabel); dateLayout->addWidget(_monthCombo); dateLayout->addStretch(); dateLayout->addWidget(dayLabel); dateLayout->addWidget(_dayCombo); dateLayout->addStretch(); |
연도/월/일 콤보박스와 레이블을 생성하고 배치한다.
36 번째 줄: QBoxLayout::addStretch() 은 늘임자를 추가하는데, 창의 크기가 달라질 때 확장된 부분을 빈 공간으로 메꾼다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | // 제목 편집기/레이블 생성 및 초기화 _titleLine = new QLineEdit; _titleLine->setMaxLength(80); connect(_titleLine, SIGNAL(textEdited(QString)), this, SLOT(setDiaryModified())); QLabel *titleLabel = new QLabel(tr("제목(&T):")); titleLabel->setBuddy(_titleLine); QHBoxLayout *titleLayout = new QHBoxLayout; titleLayout->addWidget(titleLabel); titleLayout->addWidget(_titleLine); // 내용 편집기 생성 및 초기화 _contentText = new QTextEdit; connect(_contentText, SIGNAL(textChanged()), this, SLOT(setDiaryModified())); |
제목과 내용 관련 위젯을 생성하고 초기화한다.
1 2 3 4 5 6 7 8 9 10 11 12 | QVBoxLayout *vboxLayout = new QVBoxLayout; vboxLayout->addLayout(dateLayout); vboxLayout->addLayout(titleLayout); vboxLayout->addWidget(_contentText); QWidget *w = new QWidget; w->setLayout(vboxLayout); setCentralWidget(w); resize(640, 480); } |
전체 레이아웃을 배치하고, 창의 크기를 조정한다.
2.3.1.4 newDiary()
새 일기를 준비한다.
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 Diary::newDiary() { // 사용자가 호출했을 때만 저장 여부 확인 if (sender() && !askToSave()) return; // 멤버 변수 및 위젯 초기화 _id = -1; QDate now(QDate::currentDate()); _yearCombo->setCurrentIndex(now.year() - StartYear); _monthCombo->setCurrentIndex(now.month() - 1); setDay(); _titleLine->clear(); _contentText->clear(); _contentText->setFocus(); setDiaryModified(false); } |
7 번째 줄: sender() 는 시그널을 보낸 QObject 의 포인터를 돌려준다. 만약 시그널로 호출된 것이 아니라면 sender() 는 0 을 돌려준다. 따라서 프로그램 초기화 중에 호출될 경우 저장 여부를 묻는 것을 피할 수 있다.
2.3.1.5 askToSave()
내용이 바뀐 일기를 저장할지를 묻는다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | /** * @brief 일기를 저장할지 물어본다 * @return 계속 진행해도 되면 true, 중단해야 되면 false */ bool Diary::askToSave() { if (isWindowModified()) { QMessageBox::StandardButton button = QMessageBox::information(this, qApp->applicationName(), tr("바뀐 일기를 저장하시겠습니까?"), QMessageBox::Save | QMessageBox::Discard | QMessageBox::Cancel); return button == QMessageBox::Discard || (button == QMessageBox::Save && save()); } return true; } |
QMessageBox::Save 또는 QMessageBox::Discard 일 때는 계속 진행을 하고(true), QMessageBox::Cancel 일 때는 진행을 멈춘다(false).
2.3.1.6 setDay()
연도와 월에 따라 일 콤보 박스를 조정한다.
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 | /** * @brief 일기의 일을 설정한다 */ void Diary::setDay() { QDate now(QDate::currentDate()); // 콤보 박스를 연도/월/일로 바꿈 int y = _yearCombo->currentIndex() + StartYear; int m = _monthCombo->currentIndex() + 1; int d = _dayCombo->currentIndex() + 1; // 해당 연도와 월의 일수를 얻음 int daysInMonth = QDate(y, m, 1).daysInMonth(); disconnect(_dayCombo, SIGNAL(currentIndexChanged(int)), this, 0); _dayCombo->clear(); for (int i = 1; i <= daysInMonth; ++i) _dayCombo->addItem(QString::number(i)); // 일기의 일을 조정 if (d == 0) d = now.day(); else if (d > daysInMonth) d = daysInMonth; _dayCombo->setCurrentIndex(d - 1); connect(_dayCombo, SIGNAL(currentIndexChanged(int)), this, SLOT(setDay())); // 조정된 연도/월/일 저장 _year = y; _month = m; _day = d; setWindowTitle(title()); // 사용자가 조정했을 때에만 바뀐 것으로 설정 if (sender()) setDiaryModified(); } |
14 번째 줄: QDate::daysInMonth() 는 주어진 연도와 주어진 월의 일 수를 돌려준다. 윤년에 따라 2 월의 일 수가 달라지기 때문에 이를 위한 작업이다.
16 번째 줄: QObject::disconnect() 는 해당 객체의 시그널/슬롯 연결을 끊는다. 일 콤보박스의 인덱스가 수정되면 setDay() 가 다시 호출되므로, 시그널/슬롯을 연결을 끊지 않으면, 무한 루프에 빠진다.
2.3.1.6 setDiaryModified()
일기가 수정되었는지 설정한다.
1 2 3 4 5 6 7 8 | /** * @brief 문서 내용이 변경되었는지 여부를 설정한다 * @param modified 변경되었으면 true, 아니면 false */ void Diary::setDiaryModified(bool modified) { setWindowModified(modified); } |
2.3.1.7 save()
현재 편집 중인 일기를 저장한다.
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 | /** * @brief 일기를 저장한다 * @return */ bool Diary::save() { if (!isWindowModified()) { if (QMessageBox::information(this, tr("저장하기"), tr("바뀐 내용이 없습니다. " "그래도 저장하시겠습니까?"), QMessageBox::Yes | QMessageBox::No) == QMessageBox::No) return true; } if (!diaryOpenDb() || !diaryCreateTable()) return false; QSqlQuery query; bool ok = diaryFind(&query) ? diaryUpdate(&query) : diaryInsert(&query); if (ok) setDiaryModified(false); return ok; } |
20 번째 줄: QSqlQuery 는 Qt 에서 SQL 데이터베이스에 질의할 때 쓰이는 클래스이다. QSqlQuery 는 SQL 질의문을 그대로 쓸 수 있다. QSqlQuery 기본 생성자는 기본 데이터베이스에 연결된다. 기본 데이터베이스는 QSqlDatabase::addDatabase() 의 두번째 인자를 제공하지 않은 경우에 생성된다. Qt 는 QSqlQuery 말고도 고수준의 질의 클래스가 있는데, QSqlTableModel() 계열이 그것이다. SQL 질의문 대신에 추상화한 멤버 함수를 제공하며, 이름에서 볼 수 있듯이, 모델이므로 QListView 나 QTableView 에 모델로 쓰일 수 있다.
2.3.1.8 diaryOpenDb()
데이터베이스에 접속한다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | /** * @brief 데이터베이스를 연다 * @return 성공하면 true, 실패하면 false */ bool Diary::diaryOpenDb() { if (!_db.isOpen() && !_db.open()) { warning(tr("DB 를 열 수 없습니다.")); return false; } return true; } |
7 번째 줄: QSqlDatabase::isOpen() 은 데이터베이스가 열려 있는지 알려주고, QSqlDatabase::open() 은 데이터베이스를 연다. 데이터베이스를 열기 전에는 사용하는 SQL 드라이버에 따라 데이터베이스 이름외에도 사용자 이름, 암호, 호스트 이름, 포트 등을 먼저 설정해야 하는 경우도 있다.
2.3.1.9 diaryCreateTable()
데이터베이스에 일기장 테이블을 생성한다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | /** * @brief 데이터베이스에 일기 테이블을 만든다 * @return 성공하면 true, 실패하면 false */ bool Diary::diaryCreateTable() { QSqlQuery query; if (!query.exec("CREATE TABLE IF NOT EXISTS diary (" "id INTEGER PRIMARY KEY AUTOINCREMENT," "date DATE NOT NULL," "title VARCHAR(80) NOT NULL," "content TEXT NOT NULL" ")" )) { warning("테이블을 만들지 못했습니다."); return false; } return true; } |
8 번째 줄: QSqlQuery::exec() 는 주어진 SQL 질의문을 실행한다. SQL 질의문은 연결된 데이터베이스의 백엔드가 받아들일 수 있는 것이어야 한다.
2.3.1.10 diaryFind()
현재 작성 중인 일기 ID 를 이용하여 일기를 찾는다.
1 2 3 4 5 6 7 8 9 10 11 | /** * @brief 데이터베이스에서 일기를 찾는다 * @param[out] query 질의 결과 * @return 찾았으면 true, 못찾았으면 false */ bool Diary::diaryFind(QSqlQuery *query) { query->prepare("SELECT id FROM diary WHERE id = :id"); query->bindValue(":id", _id); return query->exec() && query->next(); } |
8~9 번째 줄: QSqlQuery::prepare() 는 질의문이 길어나 변수들을 따로 지정하고 싶을 때 쓰인다. 질의문 중에 [:변수이름](Oracle 스타일)은 이후에 QSqlQuery::bindValue() 에서 주어진 값으로 대체된다. [:변수이름] 대신에 Qt 는 '?'(ODBC 스타일) 도 지원한다. 이 때에는 QSqlQuery::addBindValue() 를 순서에 맞춰 쓰면 된다. 하지만 둘을 섞어서 쓸 수는 없다.
10 번째 줄: QSqlQuery::next() 는 질의문을 실행하고 나서 결과셋을 탐색할 때 쓰인다. 질의문을 실행한 직후에 결과셋은 invalid 상태이므로, 반드시 QSqlQuery::next() 를 실행시켜야 실제 결과셋을 확인할 수 있다.
2.3.1.11 diaryInsert()
일기를 데이터베이스에 추가한다. 만약 일기 ID 가 주어져 있으면, 해당 ID 로 저장하고, 아니면 새로운 ID 를 생성한다.
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 | /** * @brief 데이터베이스 일기를 저장한다 * @param[out] query 질의 결과 * @return 성공하면 true, 실패하면 false */ bool Diary::diaryInsert(QSqlQuery *query) { query->prepare("INSERT INTO diary (id, date, title, content) " "VALUES (:id, :date, :title, :content)"); if (_id != -1) query->bindValue(":id", _id); else query->bindValue(":id", QVariant::Int); query->bindValue(":date", QDate(_year, _month, _day)); query->bindValue(":title", _titleLine->text()); query->bindValue(":content", _contentText->toPlainText()); if (!query->exec()) { warning(tr("일기를 저장하지 못했습니다.")); return false; } if (_id == -1) { // 마지막에 삽입된 일기의 ID 를 얻는다 if (!query->exec("SELECT last_insert_rowid() from diary") || !query->next()) { warning(tr("DB 에서 일기 ID 를 얻지 못했습니다.")); return false; } _id = query->value(0).toInt(); } return true; } |
34 번째 줄: QSqlQuery::value() 는 주어진 컬럼의 값을 QVariant 로 돌려준다.
2.3.1.12 diaryDelete()
일기를 데이터베이스에서 지운다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | /** * @brief 데이터베이스에서 일기를 지운다 * @param[out] query 질의 결과 * @return 성공하면 true, 실패하면 false */ bool Diary::diaryDelete(QSqlQuery *query) { query->prepare("DELETE FROM diary WHERE id = :id"); query->bindValue(":id", _id); if (!query->exec()) { warning(tr("일기를 지우지 못했습니다.")); return false; } return true; } |
2.3.1.13 diaryUpdate()
일기를 갱신한다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | /** * @brief 데이터베이스 일기를 갱신한다 * @param[out] query 질의 결과 * @return 성공하면 true, 실패하면 false */ bool Diary::diaryUpdate(QSqlQuery *query) { query->prepare("UPDATE diary " "SET date = :date, title = :title, content = :content " "WHERE id = :id"); query->bindValue(":date", QDate(_year, _month, _day)); query->bindValue(":title", _titleLine->text()); query->bindValue(":content", _contentText->toPlainText()); query->bindValue(":id", _id); if (!query->exec()) { warning(tr("일기를 갱신하지 못했습니다.")); return false; } return true; } |
2.3.1.14 load()
일기를 불러온다.
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 일기를 불러온다 * @return 성공하면 true, 실패하면 false */ bool Diary::load() { if (!askToSave()) return false; if (!diaryOpenDb()) return false; LoadDialog dlg(_db, this); if (dlg.exec() == QDialog::Accepted) { _id = dlg.id(); _yearCombo->setCurrentIndex(dlg.year() - StartYear); _monthCombo->setCurrentIndex(dlg.month() - 1); _dayCombo->setCurrentIndex(dlg.day() - 1); _titleLine->setText(dlg.title()); _contentText->setPlainText(dlg.content()); setDiaryModified(false); } return true; } |
15 번째 줄: QDialog::exec() 는 대화상자를 modal 상태로 실행하고, 성공했을 경우에는 QDialog::Accepted 를 돌려주고, 실패했을 경우에는 QDialog::Rejected 를 돌려준다.
2.3.1.15 about()
<일기장> 에 대한 정보를 보여준다.
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 | /** * @brief 일기장에 대한 정보를 보여준다 */ void Diary::about() { // 컴파일한 날짜와 시간을 현지 시간으로 바꿈 QString dateStr(__DATE__); if (dateStr.at(4) == ' ') dateStr[4] = '0'; QDateTime dt; dt.setDate(QLocale(QLocale::C).toDate(dateStr, "MMM dd yyyy")); dt.setTime(QTime::fromString(__TIME__, "hh:mm:ss")); QMessageBox::about( this, tr("%1 정보").arg(qApp->applicationName()), tr( "<h2>%1</h2>" "<p>%2 에 만듦</p>" "<p>이 프로그램에 대한 어떤 책임도 지지 않습니다. 이 프로그램은 공개 " "소프트웨어이며, WTFPL v2 에 따라 재배포 및 수정될 수 있습니다.</p>" "<p>자세한 것은 <a href=http://www.wtfpl.net>http://www.wtfpl.net</a> 를 " "보십시오.</p>" ).arg(qApp->applicationName()) .arg(dt.toString(Qt::SystemLocaleLongDate))); } |
13 번째 줄: QLocale() 은 로케일 관련 기능을 제공하는 클래스이다. QLocale::C 는 C 로케일을 뜻하며, QLocale::toDate() 는 주어진 문자열을 주어진 형식 문자열에 따라 QDate 로 바꾼다.
14 번째 줄: QTime::fromString() 은 주어진 문자열을 주어진 형식 문자열에 따라 QTime 으로 바꾼다.
16 번째 줄: QMessageBox::about() 은 주어진 인자를 바탕으로 About 대화상자를 보여준다.
About 대화 상자는 프로그램을 소개하고 라이센스를 보여주는 역할에 많이 쓰인다.
2.3.1.16 aboutQt()
Qt 에 대한 정보를 보여준다.
1 2 3 4 5 6 7 | /** * @brief 사용된 Qt 에 대한 정보를 보여준다 */ void Diary::aboutQt() { QMessageBox::aboutQt(this); } |
6 번째 줄: QMessageBox::aboutQt() 는 현재 사용되고 있는 Qt 에 대한 정보를 보여준다.
2.3.1.17 closeEvent()
창이 닫힐 때 호출되며, 바뀐 내용이 있으면 저장할지 물어본다.
1 2 3 4 5 6 7 8 9 10 11 | /** * @brief 창을 닫을 때 저장할지 확인한다 * @param e 이벤트 */ void Diary::closeEvent(QCloseEvent *e) { if (askToSave()) e->accept(); else e->ignore(); } |
8~10 번째 줄: QCloseEvent::accept() 는 창을 닫아도 된다는 것을 뜻하고, QCloseEvent::ignore() 는 창을 닫지 말라는 것을 뜻한다.
2.3.2 LoadDialog 클래스
일기를 불러오기 위한 대화상자이다.
2.3.2.1 멤버 변수
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | private: /** An enum of diary column */ enum { Diary_Id = 0, ///< 'ID' 컬럼 Diary_Date = 1, ///< '날짜' 컬럼 Diary_Title = 2, ///< '제목' 컬럼 Diary_Content = 3 ///< '내용' 컬럼 }; QTableView *_view; ///< 테이블 뷰 QTextEdit *_contentText; ///< '내용' 편집기 QPushButton *_loadButton; ///< '불러오기' 버튼 QPushButton *_deleteButton; ///< '지우기' 버튼 QPushButton *_cancelButton; ///< '취소' 버튼 QSqlTableModel *_model; ///< SQL 모델 QItemSelectionModel *_selectionModel; ///< 선택 모델 QSqlRecord _record; ///< 일기 정보 |
18 번째 줄: QSqlTableModel 은 SQL 데이터베이스를 모델로 추상화한 클래스이다. SQL 데이터베이스를 조작할 수도 있고, QListView 나 QTableView 에 모델로 제공될 수도 있다.
19 번째 줄: QItemSelectionModel 은 여러 뷰의 선택된 모델을 나타낸다.
20 번째 줄: QSqlRecord 는 QSqlTableModel() 에서 한 줄(row)에 해당하는 정보를 추상화한 클래스이다.
2.3.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 LoadDialog 생성자 * @param db 연결된 데이터베이스 * @param parent 부모 위젯 */ LoadDialog(QSqlDatabase db, QWidget *parent = 0) : QDialog(parent) { // 모델 생성 _model = new QSqlTableModel(this, db); _model->setTable("diary"); // 직접 submitAll() 호출할 예정 _model->setEditStrategy(QSqlTableModel::OnManualSubmit); // '날짜' 컬럼 이름 설정 _model->setHeaderData(Diary_Date, Qt::Horizontal, tr("날짜")); // '제목' 컬럼 이름 설정 _model->setHeaderData(Diary_Title, Qt::Horizontal, tr("제목")); // 데이터베이스에서 읽어들임 _model->select(); // 선택 모델 생성 _selectionModel = new QItemSelectionModel(_model, this); |
11 번째 줄: QSqlTableModel::setTable() 은 사용할 테이블을 설정한다.
13 번째 줄: QSqlTableModel::setEditStrategy() 은 데이터베이스를 수정하는 방식을 결정한다. QSqlTableModel::OnManualSubmit 은 개발자가 직접 결정하겠다는 뜻이다. 데이터베이스에 적용하려면 반드시 QSqlTableModel::submitAll() 을 실행해야 한다.
15~17 번째 줄: QSqlTableModel::setHeaderData() 는 주어진 컬럼의 데이터를 설정한다. 여기에서는 날짜와 제목에 대한 컬럼 제목이다.
19 번째 줄: QSqlTableModel::select() 는 SQL 의 SELECT 문에 해당하며, WHERE 절을 조절하고 싶으면 QSqlTableModel::setFilter() 를 쓰면 된다.
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 | // 테이블 뷰 생성 _view = new QTableView; // 읽기 전용 _view->setEditTriggers(QAbstractItemView::NoEditTriggers); // 탭 키 쓰지 않음 _view->setTabKeyNavigation(false); // 하나만 선택 _view->setSelectionMode(QAbstractItemView::SingleSelection); // 줄 단위로 선택 _view->setSelectionBehavior(QAbstractItemView::SelectRows); // 모델 및 선택 모델 설정 _view->setModel(_model); _view->setSelectionModel(_selectionModel); // 'ID' 와 '내용' 은 감춤 _view->hideColumn(Diary_Id); _view->hideColumn(Diary_Content); // 내용에 맞게 컬럼 크기 조정 _view->resizeColumnsToContents(); // '날짜' 기준으로 내림차순 정렬 _view->setSortingEnabled(true); _view->sortByColumn(Diary_Date, Qt::DescendingOrder); // 마지막 컬럼은 창 크기에 맞게 늘임 _view->horizontalHeader()->setStretchLastSection(true); // 더블 클릭하면 해당 자료 불러들임 connect(_view, SIGNAL(doubleClicked(QModelIndex)), this, SLOT(accept())); connect(_selectionModel, SIGNAL(currentRowChanged(QModelIndex,QModelIndex)), this, SLOT(showCurrentContent(QModelIndex))); |
4 번째 줄: QAbstractItemView::setEditTriggers() 는 편집을 시작하는 수단을 지정한다. QAbstractItemView::NoEditTriggers 는 읽기 전용이라는 뜻이다.
6 번째 줄: QAbstractItemView::setTabKeyNavigation() 은 탭 이동 여부를 결정한다. 여기에서는 대화상자의 탭 이동을 위해서 테이블 뷰의 탭 이동을 쓰지 않는다.
8 번째 줄: QAbstractItemView::setSelectionMode() 는 몇 개를 어떻게 선택할지를 설정한다. QAbstractItemView::SingleSelection 은 하나만 고른다는 것을 뜻한다.
10 번째 줄: QAbstractItemView::setSelectionBehavior() 는 어떤 것을 선택할지를 설정한다. QAbstractItemView::SelectRows 는 줄단위로 선택하는 것을 뜻한다.
13~14 번째 줄: QAbstractItemView::setModel() 은 나타낼 모델을 설정하고, QAbstractItemView::setSelectionModel() 은 선택 모델을 설정한다.
16~17 번째 줄: QAbstractItemView::hideColumn() 은 주어진 컬럼을 감춘다.
19 번째 줄: QAbstractItemView::resizeColumnToContents() 는 내용에 맞게 컬럼 폭을 조절한다.
21~22 번째 줄: QAbstractItemView::setSortingEnabled() 는 소팅 여부를 설정하고, QAbstractItemView::sortByColumn() 은 소팅 기준으로 사용할 컬럼을 설정한다.
25 번째 줄: QHeaderView::setStretchLastSection() 은 마지막 컬럼의 폭을 창 크기에 맞게 조절한다.
30~32 번째 줄: 사용자가 선택한 일기가 바뀌면 새로이 선택된 일기의 내용을 보여준다. 그런데, 주목한 것은 QTableView 의 시그널을 연결하는 것이 아니라는 것이다. QTableView 에도 selectedIndexes() 나 currentChanged() 라는 멤버 함수가 있으나 있는 protected 멤버 함수이다. 따라서 외부에서 쓸 수가 없다.이를 해결하기 위한 것이 QItemSelectionModel() 이다. 코드에서 보듯이 QItemSelectionModel() 은 우리가 원하는 바로 그 시그널을 제공하고 있다.
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 | // 읽기 전용으로 '내용' 편집기 생성 _contentText = new QTextEdit; _contentText->setReadOnly(true); // '불러오기' 버튼 생성 _loadButton = new QPushButton(tr("불러오기(&L)")); connect(_loadButton, SIGNAL(clicked(bool)), this, SLOT(accept())); _loadButton->setDefault(true); // '지우기' 버튼 생성 _deleteButton = new QPushButton(tr("지우기(&D)")); connect(_deleteButton, SIGNAL(clicked(bool)), this, SLOT(deleteDiary())); // '취소' 버튼 생성 _cancelButton = new QPushButton(tr("취소(&C)")); connect(_cancelButton, SIGNAL(clicked(bool)), this, SLOT(reject())); QHBoxLayout *hboxLayout = new QHBoxLayout; hboxLayout->addStretch(); hboxLayout->addWidget(_loadButton); hboxLayout->addStretch(); hboxLayout->addWidget(_deleteButton); hboxLayout->addStretch(); hboxLayout->addWidget(_cancelButton); hboxLayout->addStretch(); QVBoxLayout *vboxLayout = new QVBoxLayout; vboxLayout->addWidget(_view); vboxLayout->addWidget(_contentText); vboxLayout->addLayout(hboxLayout); setLayout(vboxLayout); resize(500, 375); // 도움말 버튼 감추기 setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint); setWindowTitle(tr("불러오기")); } |
레이아웃을 설정하고, 크기를 조정한다.
38 번째 줄: QWidget::setWindowFlags 는 위젯의 플래그를 설정한다. QWidget::windowFlags() 은 플래그를 돌려준다. Qt::WindowContextHelpButtonHint 는 문맥 도움말 버튼을 뜻하며, 여기에서는 해당 버튼을 보이지 않도록 설정한다.
2.3.2.3 id(), year(), month(), day(), title(), content()
대화상자의 결과를 돌려준다.
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 | /** * @brief 일기 ID 를 돌려준다 * @return 일기 ID */ int id() const { return _record.value(Diary_Id).toInt(); } /** * @brief 일기의 연도를 돌려준다 * @return 일기의 연도 */ int year() const { return _record.value(Diary_Date).toDate().year(); } /** * @brief 일기의 월을 돌려준다 * @return 일기의 월 */ int month() const { return _record.value(Diary_Date).toDate().month(); } /** * @brief 일기의 일을 돌려준다 * @return 일기의 일 */ int day() const { return _record.value(Diary_Date).toDate().day(); } /** * @brief 일기의 제목을 돌려준다 * @return 일기의 제목 */ QString title() const { return _record.value(Diary_Title).toString(); } /** * @brief 일기의 내용을 돌려준다 * @return 일기의 내용 */ QString content() const { return _record.value(Diary_Content).toString(); } |
2.3.2.4 accept()
사용자가 대화상자를 종료할 때 선택한 결과를 저장한다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | public slots: /** * @brief 사용자가 일기를 선택하면 대화상자를 마무리한다 */ void accept() { // 현재 선택된 일기 QModelIndex index = _view->currentIndex(); if (!index.isValid()) return; // 일기 정보를 저장 _record = _model->record(index.row()); QDialog::accept(); } |
8 번째 줄: QTableView::currentIndex() 는 현재 아이템의 인덱스를 돌려준다.
10 번째 줄: QModelIndex::isValid() 는 모델 인덱스가 올바른지 알려준다.
14 번째 줄: QSqlTableModel::record() 는 주어진 컬럼의 레코드를 돌려준다. 컬럼이 주어지지 않는다면 필드 이름들을 돌려준다. QModelIndex::row() 는 모델 인덱스의 줄 위치를 알려준다.
16 번째 줄: 대화상자를 올바르게 마무리하기 위해 QDialog::accept() 를 호출한다.
2.3.2.5 deleteDiary()
선택된 일기를 데이터베이스에서 지운다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | private slots: /** * @brief 선택된 일기를 지운다 */ void deleteDiary() { QModelIndex index = _view->currentIndex(); if (!index.isValid()) return; _model->removeRow(index.row()); _model->submitAll(); } |
12 번째 줄: QSqlTableModel::removeRow() 는 주어진 줄 하나를 지운다.
13 번째 줄: QSqlTableModel::submitAll() 은 그동안 수정한 내용을 데이터베이스에 실제로 적용한다. 앞서 QSqlTableMode::OnManualSubmit 전략을 쓰기로 했기 때문에, QSqlTableModel 로 수정한 내용을 데이터베이스에 반영하기 위해서는 반드시 QSqlTableModel::submitAll() 을 호출해야 한다.
2.3.2.6 showCurrentContent()
현재 선택된 일기의 내용을 보여준다.
1 2 3 4 5 6 7 8 9 10 | /** * @brief 선택된 일기의 내용을 보여준다 * @param current 선택된 모델 인덱스 */ void showCurrentContent(const QModelIndex ¤t) { _contentText->setText(_model->record(current.row()) .value("content").toString()); } }; |
3. 마무리하면서...
<일기장> 을 만들면서 Qt 에서 데이터베이스를 다루는 방법을 살펴보았다. 언제나 그렇듯이 여기에 서술된 기능들은 매우 기초적인 것들이다. 그리고 이외에도 오버로딩된 멤버 함수들이 많이 존재하기 때문에 반드시 Qt 도움말과 Qt Assistant 를 살펴보자.
번역을 고려해서 QObject::tr() 을 쓰고 있지만, 정작 Qt 의 위젯들, 예를 들어 QMessageBox, 같은 경우에는 번역되지 않은채 나타나고 있는데, 이는 Qt 번역 파일을 설치하지 않았기 때문이다. 이에 대해서는 차후에 기회가 되면 살펴보도록 하자.
다음은 <일기장> 의 실행 모습이다.
전체 소스는 여기에서 확인하자.
댓글
댓글 쓰기