Qt로 C++ 첫 GUI 만들기: CMake 프로젝트, 위젯과 레이아웃, 시그널과 슬롯
들어가며: C++로 창 띄우고 버튼 넣고 싶습니다
C++로 GUI를 만들려고 하면 플랫폼별 API(Win32, Cocoa, X11)가 달라서 코드가 복잡해지고, 이벤트 처리·메모리 관리도 직접 해야 합니다. Qt는 이런 문제를 한 번에 해결합니다. Windows, macOS, Linux에서 같은 코드로 빌드하며, 시그널/슬롯으로 “버튼 클릭 → 함수 호출”을 타입 안전하게 연결하며, 부모-자식 관계로 위젯 메모리를 자동 관리합니다. 이 글은 CMake로 Qt 프로젝트를 만들고, 위젯과 레이아웃으로 화면을 구성하고, 시그널/슬롯으로 동작을 연결한 뒤, 처음 Qt를 쓸 때 자주 만나는 에러를 정리합니다.
플랫폼 분기, 반응 없는 버튼, 위젯 누수: Qt 없이 겪는 문제
시나리오 1: 플랫폼별 코드 분기
// ❌ Qt 없이: 플랫폼마다 다른 API
#ifdef _WIN32
HWND hwnd = CreateWindowEx(...);
#elif __APPLE__
NSWindow* window = [[NSWindow alloc] init...];
#else
Display* display = XOpenDisplay(NULL);
Window window = XCreateWindow(...);
#endif
Qt로 해결: QMainWindow 하나로 모든 플랫폼에서 동일하게 동작합니다.
시나리오 2: 버튼 클릭 시 반응 없음
콜백을 등록했는데 클릭해도 호출되지 않는 경우. Win32에서는 WndProc에서 WM_COMMAND를 처리하며, Cocoa에서는 @selector를 연결해야 합니다. Qt는 connect(button, &QPushButton::clicked, ...) 한 줄로 해결합니다.
시나리오 3: 위젯 메모리 누수
new QPushButton()만 하고 부모를 지정하지 않으면, 창을 닫아도 버튼이 해제되지 않습니다. Qt는 부모 위젯이 소멸할 때 자식 위젯을 자동 삭제하므로, 부모만 지정하면 됩니다.
시나리오 4: 레이아웃이 창 크기에 맞지 않음
절대 좌표로 배치하면 창을 리사이즈할 때 UI가 깨집니다. Qt 레이아웃(QVBoxLayout, QHBoxLayout 등)을 쓰면 자동으로 비율에 맞춰 재배치됩니다.
Qt 아키텍처 개요
flowchart TB
subgraph app[QApplication]
eventLoop[이벤트 루프]
end
subgraph mainWindow[QMainWindow]
menuBar[메뉴바]
toolbar[툴바]
central[중앙 위젯]
statusBar[상태바]
end
subgraph widgets[위젯 계층]
QWidget
QPushButton
QLabel
QLineEdit
end
subgraph layout[레이아웃]
VBox[QVBoxLayout]
HBox[QHBoxLayout]
Grid[QGridLayout]
end
subgraph signals[시그널/슬롯]
signal[시그널 발생]
slot[슬롯 실행]
signal --> slot
end
app --> mainWindow
mainWindow --> central
central --> layout
layout --> widgets
widgets --> signals
핵심 흐름: QApplication::exec()가 이벤트 루프를 돌리며 마우스·키보드 이벤트를 처리하며, 위젯이 시그널을 발생시키면 연결된 슬롯이 호출됩니다.
Qt란
역할
- GUI: 버튼, 리스트, 테이블, 다이얼로그 등 위젯과 레이아웃을 제공합니다.
- 크로스 플랫폼: Windows, macOS, Linux, Android, iOS 등에서 같은 소스로 빌드할 수 있습니다.
- 시그널/슬롯: 위젯 간 “이벤트가 났을 때 이 함수를 호출”하는 연결을 타입 안전하게 표현합니다.
- 추가 모듈: 네트워크(Qt Network), DB(Qt SQL), 멀티미디어, 차트 등이 있어 데스크톱 앱에 필요한 기능을 한 번에 다룰 수 있습니다.
라이선스
Qt는 상용 라이선스와 오픈소스 라이선스로 함께 배포됩니다. Qt Widgets 같은 핵심 모듈은 LGPLv3로 쓸 수 있어 상용 앱에도 동적 링크 등 LGPL 조건을 지키면 사용할 수 있지만, 일부 모듈은 GPL이나 상용 라이선스로만 제공되므로 쓰려는 모듈의 라이선스를 확인해야 합니다.
프로젝트 만들기 (CMake 예시)
CMakeLists.txt
cmake_minimum_required(VERSION 3.16)
project(MyQtApp LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_AUTOMOC ON) # Q_OBJECT 클래스를 moc로 자동 처리
find_package(Qt6 REQUIRED COMPONENTS Widgets) # Qt5면 Qt5
add_executable(MyQtApp main.cpp)
target_link_libraries(MyQtApp PRIVATE Qt6::Widgets)
- Qt6 (또는 Qt5)의 Widgets 모듈을 링크하면 기본 창·위젯을 쓸 수 있습니다.
CMAKE_AUTOMOC를 켜야Q_OBJECT가 있는 클래스에 moc가 실행됩니다. Qt 6.3 이상이면qt_standard_project_setup()이 이 설정을 대신해 줍니다.- main.cpp에서 QApplication과 메인 윈도우를 만들고 exec()로 이벤트 루프를 돌립니다.
빌드
mkdir build && cd build
cmake .. -DCMAKE_PREFIX_PATH=/path/to/Qt/6.x/gcc_64 # Qt 경로
cmake --build .
./MyQtApp
Qt 경로가 PATH나 CMAKE_PREFIX_PATH에 잡혀 있어야 합니다. Qt Creator를 쓰면 자동으로 설정됩니다.
Qt 설치 방법 (플랫폼별)
Windows:
# Qt 온라인 인스톨러 다운로드
# https://www.qt.io/download-qt-installer
# MSVC 2022 + Qt 6.x 선택
macOS:
brew install qt@6
# CMAKE_PREFIX_PATH 설정
export CMAKE_PREFIX_PATH="/opt/homebrew/opt/qt@6"
Linux (Ubuntu/Debian):
sudo apt install qt6-base-dev qt6-tools-dev
# 또는 온라인 인스톨러 사용
최소 창 띄우기
main.cpp (최소)
QApplication은 Qt 앱당 하나만 두며, exec()가 이벤트 루프를 돌리며 창 이벤트·그리기를 처리합니다. QMainWindow로 메인 창을 만들고 setWindowTitle, resize로 제목과 크기를 정한 뒤 show()로 보이게 합니다.
#include <QApplication>
#include <QMainWindow>
// 변수 선언 및 초기화
int main(int argc, char *argv[]) {
QApplication app(argc, argv);
QMainWindow window;
window.setWindowTitle("My Qt App");
window.resize(400, 300);
window.show();
return app.exec();
}
- QApplication: 한 번만 생성하며, exec()로 이벤트 루프를 돌립니다.
- QMainWindow: 메뉴·툴바·중앙 위젯을 넣을 수 있는 메인 창입니다. show()로 표시합니다.
- app.exec(): 종료 시까지 이벤트를 처리합니다.
위젯과 레이아웃
기본 위젯
| 위젯 | 용도 |
|---|---|
QPushButton | 클릭 가능한 버튼 |
QLabel | 텍스트·이미지 표시 |
QLineEdit | 한 줄 입력 |
QTextEdit | 여러 줄 텍스트 편집 |
QComboBox | 드롭다운 선택 |
QCheckBox | 체크박스 |
QSpinBox | 숫자 입력 |
QVBoxLayout (세로 배치)
#include <QWidget>
#include <QVBoxLayout>
#include <QPushButton>
#include <QLabel>
QWidget* central = new QWidget(&window);
QVBoxLayout* layout = new QVBoxLayout(central);
QLabel* label = new QLabel("Hello, Qt!");
QPushButton* button = new QPushButton("Click Me");
layout->addWidget(label);
layout->addWidget(button);
window.setCentralWidget(central);
- setCentralWidget로 QMainWindow의 중앙에 위젯을 설정하면, 그 위젯이 레이아웃의 부모가 되어 창 크기에 맞춰 배치됩니다.
- addWidget은 위에서부터 순서대로 추가됩니다.
QHBoxLayout (가로 배치)
QHBoxLayout* hLayout = new QHBoxLayout();
hLayout->addWidget(new QLabel("이름:"));
hLayout->addWidget(new QLineEdit());
hLayout->addWidget(new QPushButton("확인"));
- addWidget은 왼쪽에서 오른쪽 순으로 추가됩니다.
QGridLayout (그리드 배치)
QGridLayout* grid = new QGridLayout();
grid->addWidget(new QLabel("이름:"), 0, 0); // row 0, col 0
grid->addWidget(new QLineEdit(), 0, 1); // row 0, col 1
grid->addWidget(new QLabel("나이:"), 1, 0); // row 1, col 0
grid->addWidget(new QSpinBox(), 1, 1); // row 1, col 1
grid->addWidget(new QPushButton("저장"), 2, 0, 1, 2); // row 2, col 0, rowSpan 1, colSpan 2
- (행, 열) 좌표로 위젯을 배치합니다.
addWidget(widget, row, col, rowSpan, colSpan)으로 여러 셀을 합칠 수 있습니다.
QFormLayout (폼 스타일)
QFormLayout* form = new QFormLayout();
form->addRow("이름:", new QLineEdit());
form->addRow("이메일:", new QLineEdit());
form->addRow("", new QPushButton("제출")); // 라벨 없이 위젯만
- 라벨-입력 쌍을 자동으로 정렬합니다.
레이아웃 중첩
QVBoxLayout* mainLayout = new QVBoxLayout();
QHBoxLayout* topRow = new QHBoxLayout();
topRow->addWidget(new QLabel("검색:"));
topRow->addWidget(new QLineEdit());
topRow->addWidget(new QPushButton("찾기"));
mainLayout->addLayout(topRow); // addLayout으로 하위 레이아웃 추가
mainLayout->addWidget(new QTextEdit());
- addLayout으로 하위 레이아웃을 추가하면 복잡한 UI를 구성할 수 있습니다.
레이아웃 시각화
flowchart TB
subgraph VBox[QVBoxLayout]
L1[QLabel]
B1[QPushButton]
end
subgraph HBox["QHBoxLayout (중첩)"]
L2[QLabel]
E1[QLineEdit]
B2[QPushButton]
end
VBox --> L1
VBox --> HBox
VBox --> B1
HBox --> L2
HBox --> E1
HBox --> B2
시그널과 슬롯
개념
- 시그널: “무언가 일어났다” (클릭, 텍스트 변경 등). 위젯이 발생시킵니다.
- 슬롯: “그럴 때 이것을 실행합니다.” 일반 함수, 멤버 함수, 람다가 될 수 있습니다.
- connect(발신자, 시그널, 수신자, 슬롯) 또는 connect(발신자, 시그널, 람다)로 연결합니다.
람다로 연결
#include <QObject>
QObject::connect(button, &QPushButton::clicked, label, [label]() {
label->setText("Button clicked!");
});
- clicked 시그널에 람다를 연결해, 클릭 시 라벨 텍스트를 바꿉니다.
- 세 번째 인자
label은 컨텍스트 객체입니다. 컨텍스트 객체가 소멸하면 Qt가 이 연결을 자동으로 끊으므로, 라벨이 먼저 삭제된 뒤 버튼을 눌러도 해제된 포인터에 접근하지 않습니다. 컨텍스트 객체 없이 람다만 넘기면 연결은 발신자(버튼)가 살아 있는 동안 유지됩니다.
멤버 함수를 슬롯으로
class MyWindow : public QMainWindow {
Q_OBJECT
public:
MyWindow() {
QPushButton* btn = new QPushButton("클릭", this);
connect(btn, &QPushButton::clicked, this, &MyWindow::onButtonClicked);
}
private slots:
void onButtonClicked() {
qDebug() << "버튼 클릭됨";
}
};
- private slots: (또는 public slots:)로 선언한 멤버 함수를 슬롯으로 사용합니다.
- Q_OBJECT 매크로가 있어야 moc(Meta-Object Compiler)가 시그널/슬롯 메타데이터를 생성합니다.
시그널에 인자 전달
// QSpinBox::valueChanged(int) 시그널
// Qt5에서는 valueChanged가 int/QString으로 오버로드되어 있어
// qOverload<int>(&QSpinBox::valueChanged)로 골라야 한다. Qt6는 오버로드가 없다.
connect(spinBox, &QSpinBox::valueChanged, [](int value) {
qDebug() << "값 변경:" << value;
});
// 커스텀 시그널 (클래스 내부)
signals:
void dataReady(const QString& data);
Qt5 vs Qt6 연결 문법
// Qt5 (구 문법, 여전히 동작)
connect(button, SIGNAL(clicked()), this, SLOT(onClicked()));
// Qt6 권장 (타입 안전)
connect(button, &QPushButton::clicked, this, &MyWindow::onClicked);
- Qt6에서는 함수 포인터 문법을 권장합니다. 컴파일 시점에 시그널/슬롯 시그니처를 검사할 수 있습니다.
클릭 카운터, 입력 폼, 텍스트 에디터 예제
예제 1: 클릭 카운터
#include <QApplication>
#include <QMainWindow>
#include <QWidget>
#include <QVBoxLayout>
#include <QPushButton>
#include <QLabel>
int main(int argc, char *argv[]) {
QApplication app(argc, argv);
QMainWindow window;
QWidget* central = new QWidget(&window);
QVBoxLayout* layout = new QVBoxLayout(central);
QLabel* label = new QLabel("클릭 횟수: 0");
QPushButton* button = new QPushButton("클릭");
int count = 0;
QObject::connect(button, &QPushButton::clicked, [&label, &count]() {
++count;
label->setText(QString("클릭 횟수: %1").arg(count));
});
layout->addWidget(label);
layout->addWidget(button);
window.setCentralWidget(central);
window.setWindowTitle("클릭 카운터");
window.resize(300, 150);
window.show();
return app.exec();
}
- 람다에서
count를[&]로 캡처했지만,main이app.exec()동안 살아 있으므로 안전합니다. - QString::arg()로 포맷 문자열을 만듭니다.
예제 2: 입력 폼 + 버튼
#include <QApplication>
#include <QMainWindow>
#include <QWidget>
#include <QVBoxLayout>
#include <QFormLayout>
#include <QPushButton>
#include <QLabel>
#include <QLineEdit>
class FormWindow : public QMainWindow {
Q_OBJECT
public:
FormWindow() {
QWidget* central = new QWidget(this);
QVBoxLayout* mainLayout = new QVBoxLayout(central);
QFormLayout* form = new QFormLayout();
nameEdit = new QLineEdit();
emailEdit = new QLineEdit();
form->addRow("이름:", nameEdit);
form->addRow("이메일:", emailEdit);
mainLayout->addLayout(form);
resultLabel = new QLabel("입력 대기 중...");
mainLayout->addWidget(resultLabel);
QPushButton* submitBtn = new QPushButton("제출");
connect(submitBtn, &QPushButton::clicked, this, &FormWindow::onSubmit);
mainLayout->addWidget(submitBtn);
setCentralWidget(central);
setWindowTitle("입력 폼");
resize(400, 200);
}
private slots:
void onSubmit() {
QString text = QString("이름: %1, 이메일: %2")
.arg(nameEdit->text())
.arg(emailEdit->text());
resultLabel->setText(text);
}
private:
QLineEdit* nameEdit;
QLineEdit* emailEdit;
QLabel* resultLabel;
};
int main(int argc, char *argv[]) {
QApplication app(argc, argv);
FormWindow window;
window.show();
return app.exec();
}
- QFormLayout으로 라벨-입력 쌍을 깔끔하게 배치합니다.
Q_OBJECT클래스를 main.cpp 안에 정의했다면, AUTOMOC가 생성한 코드를 포함하도록 파일 끝에#include "main.moc"를 추가해야 링크 에러가 나지 않습니다. 실제 프로젝트에서는 뒤에서 설명하듯 헤더와 소스로 나누는 편이 깔끔합니다.- connect로 버튼 클릭 시 onSubmit 슬롯을 호출합니다.
예제 3: 간단한 텍스트 에디터
#include <QApplication>
#include <QMainWindow>
#include <QTextEdit>
#include <QMenuBar>
#include <QFileDialog>
#include <QFile>
#include <QTextStream>
class SimpleEditor : public QMainWindow {
Q_OBJECT
public:
SimpleEditor() {
editor = new QTextEdit(this);
setCentralWidget(editor);
QMenu* fileMenu = menuBar()->addMenu("파일");
fileMenu->addAction("열기", this, &SimpleEditor::openFile);
fileMenu->addAction("저장", this, &SimpleEditor::saveFile);
setWindowTitle("간단한 에디터");
resize(600, 400);
}
private slots:
void openFile() {
QString filename = QFileDialog::getOpenFileName(this);
if (!filename.isEmpty()) {
QFile file(filename);
if (file.open(QIODevice::ReadOnly | QIODevice::Text)) {
editor->setPlainText(QTextStream(&file).readAll());
}
}
}
void saveFile() {
QString filename = QFileDialog::getSaveFileName(this);
if (!filename.isEmpty()) {
QFile file(filename);
if (file.open(QIODevice::WriteOnly | QIODevice::Text)) {
QTextStream(&file) << editor->toPlainText();
}
}
}
private:
QTextEdit* editor;
};
int main(int argc, char *argv[]) {
QApplication app(argc, argv);
SimpleEditor editor;
editor.show();
return app.exec();
}
- QMenuBar와 addAction으로 메뉴를 추가하며, 시그널/슬롯으로 연결합니다.
- QFileDialog로 파일 선택, QFile과 QTextStream으로 읽기/쓰기를 합니다.
Could not find Qt6, vtable 링크 에러, 호출 안 되는 슬롯 같은 에러
에러 1: Could not find Qt6
원인: CMake가 Qt 설치 경로를 찾지 못함. 해결법:
# Qt 경로를 CMAKE_PREFIX_PATH에 지정
cmake ...-DCMAKE_PREFIX_PATH="/opt/homebrew/opt/qt@6" # macOS
cmake ...-DCMAKE_PREFIX_PATH="C:/Qt/6.5.0/msvc2019_64" # Windows
에러 2: undefined reference to 'vtable for MyClass'
원인: Q_OBJECT를 사용하는 클래스인데 moc가 실행되지 않았거나, 생성된 코드가 링크되지 않았습니다.
해결법: Qt6::Widgets를 링크하는 것만으로는 moc가 실행되지 않습니다. CMAKE_AUTOMOC를 켜고(또는 qt_standard_project_setup()), Q_OBJECT가 있는 헤더를 add_executable에 포함합니다. Q_OBJECT를 나중에 추가했다면 CMake를 다시 구성해야 하고, 클래스가 .cpp 파일 안에 정의되어 있으면 그 파일 끝에 #include "파일명.moc"가 필요합니다.
set(CMAKE_AUTOMOC ON)
add_executable(MyQtApp main.cpp mywindow.cpp)
target_link_libraries(MyQtApp PRIVATE Qt6::Widgets)
에러 3: 버튼 클릭해도 슬롯이 호출되지 않음
원인 1: 수신 객체가 이미 소멸했습니다. 예를 들어 함수 안의 지역 변수로 창을 만들면 함수가 끝날 때 창이 사라지고, Qt는 소멸한 수신자의 연결을 자동으로 끊으므로 슬롯이 호출되지 않습니다.
해결법: 수신 객체를 new로 만들고 부모를 지정하거나, 멤버로 두어 필요한 동안 살아 있게 합니다.
원인 2: 수신 객체가 이벤트 루프가 돌지 않는 스레드에 속해 있습니다. 기본 Qt::AutoConnection은 발신 스레드와 수신 객체의 스레드가 다르면 자동으로 Queued 연결이 되어, 수신 객체 스레드의 이벤트 루프가 슬롯을 실행합니다. 그 스레드에서 이벤트 루프가 돌지 않으면 슬롯은 영원히 실행되지 않습니다.
해결법: 수신 객체를 메인 스레드(또는 exec()를 실행하는 QThread)에 두고, 필요하면 moveToThread로 소속 스레드를 바꿉니다.
에러 4: 메모리 누수 — 위젯이 삭제되지 않음
원인: new QWidget()로 만들고 부모를 지정하지 않음.
해결법:
// ❌ 잘못된 예
QPushButton* btn = new QPushButton("클릭");
// ✅ 올바른 예: 부모 지정
QPushButton* btn = new QPushButton("클릭", this);
// 또는 레이아웃에 추가하면 레이아웃의 부모 위젯이 자동으로 부모가 됨
layout->addWidget(new QPushButton("클릭"));
에러 5: 람다 캡처로 인한 dangling reference
원인: 람다가 참조나 포인터로 캡처한 객체가 람다보다 먼저 소멸했습니다. 포인터를 값으로 캡처해도 가리키는 객체가 삭제되면 마찬가지로 해제된 메모리에 접근합니다. 해결법:
// ❌ 위험: label이 먼저 삭제될 수 있음
QObject::connect(button, &QPushButton::clicked, [&]() {
label->setText("...");
});
// ✅ label을 컨텍스트 객체로 넘김: label이 삭제되면 연결이 자동으로 끊김
QObject::connect(button, &QPushButton::clicked, label, [label]() {
label->setText("...");
});
에러 6: QWidget: Must construct a QApplication before a QPaintDevice
원인: QApplication 생성 전에 위젯을 생성함.
해결법:
// ✅ 순서 지키기
int main(int argc, char *argv[]) {
QApplication app(argc, argv); // 반드시 먼저
QMainWindow window;
// ...
}
부모-자식 관계, 함수 포인터 connect, UI 스레드 규칙
부모-자식 관계 일관성
- 모든 위젯에 부모를 지정하세요. 레이아웃에
addWidget하면 해당 레이아웃의 부모 위젯이 자동으로 부모가 됩니다. - 부모가 소멸할 때 자식이 자동 삭제되므로
delete를 호출하지 마세요.
시그널/슬롯은 함수 포인터 문법 사용
// ✅ Qt6 권장
connect(btn, &QPushButton::clicked, this, &MyWindow::onClicked);
// ❌ Qt5 구 문법 (문자열이라 오타 시 런타임 에러)
connect(btn, SIGNAL(clicked()), this, SLOT(onClicked()));
슬롯 네이밍
on<Widget><Signal>형식을 쓰면 가독성이 좋습니다. 예:onButtonClicked,onLineEditTextChanged.
UI 스레드에서만 위젯 접근
- Qt 위젯은 메인 스레드에서만 생성·수정해야 합니다. 워커 스레드에서 결과를 전달할 때는 시그널을
Qt::QueuedConnection으로 연결하세요.
리소스 경로
- 이미지 등 리소스는 Qt Resource System(.qrc)을 사용하면 실행 파일에 포함할 수 있습니다.
// :/images/icon.png 형태로 접근
label->setPixmap(QPixmap(":/images/icon.png"));
MVC 분리, QSettings, QSS, 스레드 간 시그널
패턴 1: MVC 분리
- Model: 데이터 (QAbstractItemModel 등)
- View: QTableView, QListView
- Controller: 시그널/슬롯으로 Model과 View 연결
// Model-View 사용 예
QStandardItemModel* model = new QStandardItemModel(this);
QTableView* view = new QTableView(this);
view->setModel(model);
패턴 2: 다이얼로그 모달/모달리스
// 모달: 다이얼로그가 닫힐 때까지 메인 창 조작 불가
QDialog dialog(this);
if (dialog.exec() == QDialog::Accepted) {
// 사용자가 OK 클릭
}
// 모달리스: 다이얼로그와 메인 창 동시 조작
QDialog* dialog = new QDialog(this);
dialog->setAttribute(Qt::WA_DeleteOnClose);
dialog->show();
패턴 3: 설정 저장 (QSettings)
void MainWindow::closeEvent(QCloseEvent* event) {
QSettings settings("MyCompany", "MyApp");
settings.setValue("geometry", saveGeometry());
settings.setValue("windowState", saveState());
QMainWindow::closeEvent(event);
}
// 생성자에서
QSettings settings("MyCompany", "MyApp");
restoreGeometry(settings.value("geometry").toByteArray());
restoreState(settings.value("windowState").toByteArray());
패턴 4: 스타일시트 (QSS)
// 다크 테마 예시
setStyleSheet(
"QMainWindow { background-color: #2d2d2d; }"
"QPushButton { background-color: #404040; color: white; border-radius: 4px; }"
"QPushButton:hover { background-color: #505050; }"
);
패턴 5: 로깅 및 에러 처리
#include <QMessageBox>
void MainWindow::onSaveFailed(const QString& error) {
QMessageBox::critical(this, "저장 실패", error);
}
패턴 6: 시그널/슬롯 시퀀스 (다중 스레드)
sequenceDiagram
participant Worker as 워커 스레드
participant Signal as 시그널
participant Main as 메인 스레드
participant Slot as 슬롯
Worker->>Signal: emit resultReady(data)
Signal->>Main: QueuedConnection
Main->>Slot: handleResult(data)
Note over Slot: UI 업데이트 안전
- 워커 스레드에서
emit한 시그널은 Qt::QueuedConnection으로 메인 스레드의 이벤트 큐에 넣어지고, 메인 스레드가 슬롯을 실행합니다. 따라서 UI 위젯을 직접 건드리지 않고도 안전하게 업데이트할 수 있습니다.
UI가 멈추지 않게: 워커 스레드와 위젯 가상화
무거운 작업은 워커 스레드로
// ❌ 메인 스레드에서 대용량 처리 → UI 멈춤
void onProcessClicked() {
processHugeData(); // 5초 걸림 → 창이 5초간 응답 없음
}
// ✅ QThread 또는 QtConcurrent 사용 (QtConcurrent는 Qt6::Concurrent 링크 필요)
void onProcessClicked() {
QtConcurrent::run([this]() {
auto result = processHugeData();
QMetaObject::invokeMethod(this, [this, result]() {
updateUI(result); // 메인 스레드에서 UI 업데이트
}, Qt::QueuedConnection);
});
}
대량 위젯은 가상화
QListWidget에 수천 개 항목을 넣으면 느려집니다.QListView+QAbstractItemModel을 사용하면 화면에 보이는 항목만 그립니다.
스타일시트 최소화
- 위젯마다
setStyleSheet를 따로 호출하거나 자주 바꾸면, 호출할 때마다 해당 위젯과 자식들의 스타일을 다시 계산합니다. 공통 스타일은 애플리케이션이나 최상위 창에 한 번만 적용하고, 색상 테마만 바꾸는 경우라면QPalette로 처리하는 편이 가볍습니다.
Q_OBJECT와 moc: 다중 파일 프로젝트
Q_OBJECT를 사용하는 클래스는 moc(Meta-Object Compiler)가 처리해야 합니다. CMake에서 AUTOMOC가 켜져 있으면 자동으로 처리됩니다.
단일 파일 예제에서 FormWindow나 SimpleEditor를 쓰려면, 해당 클래스를 헤더(.h)와 소스(.cpp)로 분리하는 것이 좋습니다:
// formwindow.h
#pragma once
#include <QMainWindow>
#include <QLineEdit>
#include <QLabel>
class FormWindow : public QMainWindow {
Q_OBJECT
public:
FormWindow();
private slots:
void onSubmit();
private:
QLineEdit* nameEdit;
QLineEdit* emailEdit;
QLabel* resultLabel;
};
// formwindow.cpp
#include "formwindow.h"
#include <QVBoxLayout>
#include <QFormLayout>
#include <QPushButton>
FormWindow::FormWindow() {
// ... 생성자 구현
}
void FormWindow::onSubmit() {
// ... 슬롯 구현
}
# CMakeLists.txt
add_executable(MyQtApp main.cpp formwindow.cpp formwindow.h)
target_link_libraries(MyQtApp PRIVATE Qt6::Widgets)
formwindow.h에Q_OBJECT가 있으면 CMake가moc_formwindow.cpp를 자동 생성하고 링크합니다.
같이 보면 좋은 글
- C++ GUI | Qt 프레임워크: 위젯부터 MOC·시그널/슬롯·이벤트 루프·Graphics View까지
- C++ CMake 고급 | 멀티 타겟·외부 라이브러리 관리 (대규모 프로젝트 빌드)
- C++ 현대적인 C++ GUI: Dear ImGui로 디버깅 툴·대시보드 만들기 [#36-1]
- C++ Python과 C++의 만남 | pybind11으로 고성능 엔진 만들기 [#35-1]
- C++ WebAssembly(Wasm)와 Emscripten | C++을 브라우저에서 돌리기 [#35-2]
- C++ 미리 사용해 보는 C++23 핵심 기능 [#37-1]
- Windows에서만 파일을 못 찾을 때
자주 묻는 질문 (FAQ)
Q. 위젯이 창 크기에 맞춰 늘어나지 않는 이유는 무엇인가요?
A. setGeometry나 move로 위젯 위치와 크기를 직접 지정하면 창 크기가 바뀌어도 그대로 고정됩니다. 레이아웃 객체를 만들어 부모 위젯에 setLayout으로 적용해야 창 크기 변화에 따라 위젯이 재배치되며, QMainWindow는 central widget을 만들고 그 위젯에 레이아웃을 설정합니다. 어떤 위젯이 더 많이 늘어날지는 addWidget의 stretch 인자와 QSizePolicy로 조절합니다.
Q. Qt5와 Qt6 중 무엇을 써야 하나요?
A. 신규 프로젝트는 Qt6를 권장합니다. Qt5의 오픈소스 지원은 5.15에서 끝났고, Qt6는 C++17을 기반으로 하며 CMake를 공식 빌드 시스템으로 씁니다. 레거시 프로젝트는 Qt5를 유지하다가, QRegExp·QStringRef 제거처럼 깨지는 API를 Qt 5.15의 호환 경고로 미리 정리한 뒤 옮기는 것이 일반적입니다.
이전 글: C++ GUI #36-1: Dear ImGui
다음 글: [C++23 프리뷰 #37-1] 미리 사용해 보는 C++23 핵심 기능