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를 자동 생성하고 링크합니다.

같이 보면 좋은 글


자주 묻는 질문 (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 핵심 기능