Qt for C++ Beginners: Event Loop, Signals and Slots, Layouts and Custom Widgets

Key takeaways

Core concepts and practical notes for building C++ GUIs with Qt: first app, signals/slots, layouts, widgets, and common pitfalls.

Why Qt, and why the event loop changes how you think

Qt is not just a widget toolkit bolted onto C++ — it is a runtime with its own object model (QObject), its own memory ownership convention (the parent-child tree), its own compilation step (moc), and its own concurrency rule (one thread owns the GUI, full stop). Most of the pain people report with Qt has nothing to do with the widgets themselves. It comes from fighting one of those four systems without realizing it exists.

This guide walks through the standard “Hello Qt” progression — application object, signals and slots, layouts, custom widgets, menus — but at each step it stops to explain why Qt is built that way, what breaks when you don’t respect the underlying model, and how to recognize the failure mode when it hits you in a debugger instead of in documentation. If you already know the mechanics and just want the pitfalls, jump to “Common issues” near the end; if you’re building your mental model from scratch, read straight through, because the ownership and threading sections explain why later examples are structured the way they are.

Your first Qt application

#include <QApplication>
#include <QPushButton>

int main(int argc, char *argv[]) {
    QApplication app(argc, argv);

    QPushButton button("Hello Qt!");
    button.resize(200, 100);
    button.show();

    return app.exec();
}

QApplication looks like boilerplate, but it is doing real work before your first widget is even constructed: it parses platform-specific command-line arguments (-style, -platform, high-DPI flags), loads the platform plugin (xcb, windows, cocoa, wayland), initializes the font database, and sets up the object that the entire event system depends on. Constructing a QWidget before QApplication exists is undefined behavior — on some platforms it crashes immediately, on others it silently misbehaves (wrong fonts, no icons, broken DPI scaling) in a way that only shows up on a machine different from yours. This is why “why does my GUI work on my dev machine but not in CI/on a colleague’s laptop” so often traces back to construction order, not to the widget code itself.

app.exec() is the other half of the story: it starts the event loop, a blocking call that pulls OS events (mouse clicks, paint requests, timers, socket notifications) off a queue and dispatches them to the right QObject. Everything past this line runs as a reaction to an event, not as sequential top-to-bottom code. That single fact — the GUI is driven by an event loop you don’t control — is the source of almost every “gotcha” in the rest of this guide.

Build:

Run the following in a terminal.

# Using qmake
qmake -project
qmake
make

# Using CMake
find_package(Qt6 REQUIRED COMPONENTS Widgets)
target_link_libraries(myapp Qt6::Widgets)

Both build paths exist because Qt predates modern CMake by more than a decade. qmake is Qt’s own project generator; it understands .pro files, invokes moc/uic/rcc automatically, and is still the default in a fair number of embedded and legacy codebases. find_package(Qt6 ...) is the CMake-native path introduced as Qt5/Qt6 tooling matured, and it’s what you should default to for any new project — it integrates with CMAKE_AUTOMOC, plays well with package managers like vcpkg/Conan, and doesn’t require learning a second build DSL. The one thing both paths share, and the one thing that trips people up regardless of which you pick, is that Qt classes using signals/slots require a separate compilation pass — more on that below.

The event loop rule that governs everything

Before signals and slots make sense, one rule has to be explicit: only the thread that created a QObject may touch it directly, and for widgets specifically, that thread must be the one running QApplication::exec(). This isn’t a style guideline — it’s enforced by Qt’s internal event dispatching, and violating it produces symptoms that look nothing like the actual bug: intermittent crashes, corrupted paint events, assertions deep inside QCoreApplication::notifyInternal2 that point at your code but not at your mistake.

The corollary that catches people who come from a background of “just spawn a thread for slow work” is this: the GUI thread must never block. Any expensive operation — file I/O, a network call, image decoding, a database query — run directly inside a slot connected to a button click will freeze every widget in your application, because the event loop cannot process the next paint or click event until your slot returns. Windows stop repainting, the OS marks the process as “Not Responding,” and users assume the app crashed. The fix is never “just add a QApplication::processEvents() call” (a common piece of bad advice that reintroduces reentrancy bugs); the real fix is QThread + moveToThread, or a QtConcurrent::run task with a signal back to the GUI thread, or a queued connection dispatching to a worker QObject that lives on its own thread.

Signals and slots: the mechanism and its gotchas

#include <QApplication>
#include <QPushButton>
#include <QMessageBox>

int main(int argc, char *argv[]) {
    QApplication app(argc, argv);

    QPushButton button("Click me");

    // Signal-slot connection
    QObject::connect(&button, &QPushButton::clicked, [] {
        QMessageBox::information(nullptr, "Notice", "The button was clicked!");
    });

    button.show();

    return app.exec();
}

Signals and slots are Qt’s answer to the observer pattern, but with two properties plain C++ callbacks don’t give you for free: type-safe compile-time checking (with the modern function-pointer syntax used above) and automatic connection lifetime management when a receiving QObject is destroyed. What’s easy to miss is that connect() silently picks a connection type based on where sender and receiver live, and that choice changes behavior in ways that only show up under threading:

  • Qt::DirectConnection — the slot runs synchronously, on the emitting thread, the instant the signal fires. This is the default when sender and receiver live on the same thread.
  • Qt::QueuedConnection — the call is packaged as an event and posted to the receiver’s thread’s event queue; the slot runs later, whenever that thread’s event loop gets to it. This is the default when sender and receiver live on different threads.
  • Qt::AutoConnection (the actual default) — Qt decides direct vs. queued at connection time based on thread affinity, checked again at emit time.
  • Qt::BlockingQueuedConnection — like queued, but the emitting thread blocks until the slot has run on the receiver’s thread. Useful for a narrow set of producer/consumer patterns; deadlocks instantly if sender and receiver are the same thread.

The gotcha that costs people a debugging afternoon: with AutoConnection, moving a QObject to a different thread with moveToThread() after you’ve already wired up connections doesn’t retroactively convert direct connections to queued ones for connections made afterward — but connections made before the move use whichever thread affinity existed at connect time for some Qt versions’ resolution timing, and mixing this up produces a slot that runs on the wrong thread with no error, just quietly corrupted state or a hidden race. If a widget’s data looks intermittently wrong only under load, check whether a producer thread is emitting to a receiver that never got the connection it expected to be queued.

A slot that fired after its receiver no longer existed

I want to describe a specific failure mode rather than a generic warning, because it’s more useful once you’ve seen the shape of it. Picture a worker object living on its own QThread, emitting a resultsReady(QVector<Item>) signal that’s connected — with Qt::QueuedConnection, since it crosses threads — to a slot on a dialog widget. The dialog gets closed by the user (and deleted) at roughly the same moment the worker finishes and emits. What actually happens depends entirely on how the connection was made.

If you connect with the modern syntax and pass the receiving QObject as the context argument — connect(worker, &Worker::resultsReady, dialog, &Dialog::showResults) — Qt tracks that connection against the receiver’s lifetime. When dialog is destroyed, ~QObject walks its connection list and disconnects everything, including any events already sitting in the queue get discarded by the receiver-destroyed check the event dispatcher performs before delivery. Safe by construction.

The trap is connecting a signal to a free function or a lambda that captures a raw pointer to the widget, with no context object supplied: connect(worker, &Worker::resultsReady, [rawDialogPtr](auto items){ rawDialogPtr->showResults(items); }). Qt has no QObject to associate the connection with, so it cannot disconnect it when the dialog is deleted. The queued event is already sitting in the event loop’s queue; the dialog gets destroyed; the event loop delivers the queued call anyway; the lambda dereferences a dangling pointer. The crash report points at showResults(), but the actual bug is a lambda written a hundred lines away with the wrong capture. The fix, once you know to look for it, is mechanical: always pass a context object as the third argument to connect() when the slot is a lambda that touches a QObject, even if the signature doesn’t strictly require it — it’s the difference between “Qt disconnects this for you” and “you’re on your own.”

Layouts

Layouts exist to solve a problem manual positioning can’t: different platforms, fonts, and DPI settings render the same widget at different sizes, so hardcoded pixel geometry that looks right on your monitor will clip text or leave dead space on someone else’s. A QLayout recalculates every child widget’s geometry whenever the parent resizes, based on size hints and stretch factors, rather than fixed coordinates.

Vertical layout

#include <QApplication>
#include <QWidget>
#include <QPushButton>
#include <QVBoxLayout>

int main(int argc, char *argv[]) {
    QApplication app(argc, argv);

    QWidget window;
    QVBoxLayout *layout = new QVBoxLayout(&window);

    layout->addWidget(new QPushButton("Button 1"));
    layout->addWidget(new QPushButton("Button 2"));
    layout->addWidget(new QPushButton("Button 3"));

    window.show();

    return app.exec();
}

Horizontal layout

QHBoxLayout *layout = new QHBoxLayout(&window);
layout->addWidget(new QPushButton("Left"));
layout->addWidget(new QPushButton("Middle"));
layout->addWidget(new QPushButton("Right"));

Grid layout

QGridLayout *layout = new QGridLayout(&window);
layout->addWidget(new QPushButton("1"), 0, 0);
layout->addWidget(new QPushButton("2"), 0, 1);
layout->addWidget(new QPushButton("3"), 1, 0);
layout->addWidget(new QPushButton("4"), 1, 1);

The recurring mistake in this area isn’t choosing the wrong layout type — it’s calling resize() or setGeometry() on a widget that’s already managed by a layout. Once a widget is inside a QLayout, the layout owns its geometry calculation and will overwrite manual size changes on the next layout pass (triggered by almost anything: a resize event, a child widget’s size hint changing, a stylesheet reapply). The symptom is a widget that briefly appears at the size you set and then “snaps back” — which reads as a bug in Qt but is actually the layout doing exactly what it’s supposed to do. If you need a widget at a specific size regardless of layout, the correct tool is setFixedSize()/setMinimumSize()/setMaximumSize(), which the layout reads as constraints, rather than resize(), which the layout treats as a value to override.

Custom widgets and the parent-child ownership model

#include <QApplication>
#include <QWidget>
#include <QPushButton>
#include <QLabel>
#include <QVBoxLayout>

class CounterWidget : public QWidget {
    Q_OBJECT

private:
    int count;
    QLabel *label;
    QPushButton *button;

public:
    CounterWidget(QWidget *parent = nullptr) : QWidget(parent), count(0) {
        label = new QLabel("Count: 0", this);
        button = new QPushButton("Increment", this);

        QVBoxLayout *layout = new QVBoxLayout(this);
        layout->addWidget(label);
        layout->addWidget(button);

        connect(button, &QPushButton::clicked, this, &CounterWidget::increment);
    }

private slots:
    void increment() {
        count++;
        label->setText(QString("Count: %1").arg(count));
    }
};

int main(int argc, char *argv[]) {
    QApplication app(argc, argv);

    CounterWidget widget;
    widget.show();

    return app.exec();
}

Notice new QLabel("Count: 0", this) — the this argument isn’t cosmetic. Qt objects form a parent-child tree, and when a parent QObject is destroyed, its destructor walks the list of children and deletes each one. This is why the constructor never calls delete label anywhere, and why you almost never see manual cleanup code in idiomatic Qt: ownership is expressed structurally, at construction time, not managed imperatively at destruction time.

The rule this implies is stricter than it first looks: never call delete directly on a QObject that has a parent. If you do, the object is destroyed immediately, but its parent’s internal child list still holds a pointer to it. When the parent is later destroyed (or the parent’s destructor runs for any reason, including normal application shutdown), it iterates that list and calls delete again on the same address — a double-free, and depending on the allocator and heap state, either an immediate crash, silent heap corruption that surfaces somewhere unrelated, or nothing at all until the build changes. If a QObject genuinely needs to be removed before its parent, the correct call is deleteLater(), which schedules deletion for the next iteration of the event loop and lets Qt’s internal bookkeeping stay consistent — never a bare delete. This single rule is probably the most common source of “random crash on exit” bug reports in Qt codebases, and it’s almost always traced back to someone applying plain-C++ RAII instinct (own it, delete it yourself) to an object that Qt already owns.

#include <QMainWindow>
#include <QMenuBar>
#include <QToolBar>
#include <QAction>
#include <QMessageBox>

class MainWindow : public QMainWindow {
    Q_OBJECT

public:
    MainWindow() {
        // Menus
        QMenu *fileMenu = menuBar()->addMenu("File");
        QMenu *editMenu = menuBar()->addMenu("Edit");

        // Actions
        QAction *newAction = new QAction("New", this);
        QAction *openAction = new QAction("Open", this);
        QAction *saveAction = new QAction("Save", this);

        // Add actions to the menu
        fileMenu->addAction(newAction);
        fileMenu->addAction(openAction);
        fileMenu->addSeparator();
        fileMenu->addAction(saveAction);

        // Toolbar
        QToolBar *toolbar = addToolBar("Main toolbar");
        toolbar->addAction(newAction);
        toolbar->addAction(openAction);
        toolbar->addAction(saveAction);

        // Connections
        connect(newAction, &QAction::triggered, this, &MainWindow::newFile);
        connect(openAction, &QAction::triggered, this, &MainWindow::openFile);
    }

private slots:
    void newFile() {
        QMessageBox::information(this, "Notice", "Create new file");
    }

    void openFile() {
        QMessageBox::information(this, "Notice", "Open file");
    }
};

QAction is worth calling out specifically because it decouples “what happens” from “where the user can trigger it.” The same QAction object added to both the menu and the toolbar above fires one triggered signal regardless of which UI element the user clicked, and disabling or renaming the action (newAction->setEnabled(false)) updates both the menu item and the toolbar button automatically. This matters in real applications where the same command needs a menu entry, a toolbar icon, and a keyboard shortcut (setShortcut(QKeySequence::New)) — without QAction, you’d be manually synchronizing enabled/disabled state and text across three separate widgets, and it’s exactly the kind of state you forget to update in one place after a refactor.

A text editor, a calculator, and an image viewer

A minimal text editor

#include <QMainWindow>
#include <QTextEdit>
#include <QMenuBar>
#include <QFileDialog>
#include <QFile>
#include <QTextStream>

class TextEditor : public QMainWindow {
    Q_OBJECT

private:
    QTextEdit *textEdit;

public:
    TextEditor() {
        textEdit = new QTextEdit(this);
        setCentralWidget(textEdit);

        createMenus();

        setWindowTitle("Simple text editor");
        resize(800, 600);
    }

private:
    void createMenus() {
        QMenu *fileMenu = menuBar()->addMenu("File");

        QAction *openAction = fileMenu->addAction("Open");
        connect(openAction, &QAction::triggered, this, &TextEditor::openFile);

        QAction *saveAction = fileMenu->addAction("Save");
        connect(saveAction, &QAction::triggered, this, &TextEditor::saveFile);

        fileMenu->addSeparator();

        QAction *exitAction = fileMenu->addAction("Exit");
        connect(exitAction, &QAction::triggered, this, &QWidget::close);
    }

private slots:
    void openFile() {
        QString filename = QFileDialog::getOpenFileName(this, "Open file");

        if (!filename.isEmpty()) {
            QFile file(filename);
            if (file.open(QIODevice::ReadOnly | QIODevice::Text)) {
                QTextStream in(&file);
                textEdit->setText(in.readAll());
                file.close();
            }
        }
    }

    void saveFile() {
        QString filename = QFileDialog::getSaveFileName(this, "Save file");

        if (!filename.isEmpty()) {
            QFile file(filename);
            if (file.open(QIODevice::WriteOnly | QIODevice::Text)) {
                QTextStream out(&file);
                out << textEdit->toPlainText();
                file.close();
            }
        }
    }
};

This is deliberately the minimal version of a text editor, and it’s worth naming what it’s missing so you don’t mistake “compiles and runs” for “production-ready.” There’s no unsaved-changes tracking (QTextEdit::document()->isModified()), so Exit discards work silently. QFileDialog::getOpenFileName blocks the calling thread until the user picks a file or cancels — fine here because file dialogs are modal and expected to pause interaction, but a reminder that not every blocking call is a threading bug; the event loop is still running underneath a modal dialog, it just isn’t processing your window’s events. And in.readAll() loads the entire file into memory as a QString, which is a reasonable default for a text editor but would be the wrong choice for anything approaching log-file sizes — that’s where you’d reach for chunked reads or a dedicated text-buffer data structure instead.

A calculator with one slot for all buttons

#include <QWidget>
#include <QLineEdit>
#include <QPushButton>
#include <QGridLayout>

class Calculator : public QWidget {
    Q_OBJECT

private:
    QLineEdit *display;
    double currentValue;
    char currentOp;

public:
    Calculator() : currentValue(0), currentOp('\0') {
        display = new QLineEdit("0", this);
        display->setReadOnly(true);
        display->setAlignment(Qt::AlignRight);

        QGridLayout *layout = new QGridLayout(this);
        layout->addWidget(display, 0, 0, 1, 4);

        // Number buttons
        for (int i = 0; i < 10; i++) {
            QPushButton *btn = new QPushButton(QString::number(i), this);
            connect(btn, &QPushButton::clicked, this, &Calculator::digitClicked);
            int row = (9 - i) / 3 + 1;
            int col = (i - 1) % 3;
            if (i == 0) {
                layout->addWidget(btn, 4, 1);
            } else {
                layout->addWidget(btn, row, col);
            }
        }

        // Operator buttons
        QPushButton *addBtn = new QPushButton("+", this);
        connect(addBtn, &QPushButton::clicked, this, &Calculator::operatorClicked);
        layout->addWidget(addBtn, 1, 3);

        QPushButton *equalBtn = new QPushButton("=", this);
        connect(equalBtn, &QPushButton::clicked, this, &Calculator::equalClicked);
        layout->addWidget(equalBtn, 4, 3);

        resize(300, 400);
    }

private slots:
    void digitClicked() {
        QPushButton *btn = qobject_cast<QPushButton*>(sender());
        QString digit = btn->text();

        if (display->text() == "0") {
            display->setText(digit);
        } else {
            display->setText(display->text() + digit);
        }
    }

    void operatorClicked() {
        QPushButton *btn = qobject_cast<QPushButton*>(sender());
        currentOp = btn->text().at(0).toLatin1();
        currentValue = display->text().toDouble();
        display->setText("0");
    }

    void equalClicked() {
        double secondValue = display->text().toDouble();
        double result = 0;

        switch (currentOp) {
            case '+': result = currentValue + secondValue; break;
            case '-': result = currentValue - secondValue; break;
            case '*': result = currentValue * secondValue; break;
            case '/': result = currentValue / secondValue; break;
        }

        display->setText(QString::number(result));
        currentOp = '\0';
    }
};

The interesting design choice here is qobject_cast<QPushButton*>(sender()) — routing ten separate button clicks into a single slot and identifying the caller through sender(), instead of writing ten near-identical lambda slots. This is a legitimate pattern for a fixed grid of visually identical, behaviorally identical widgets, and it keeps the connection code compact. The trade-off is that sender() returns nullptr if the slot is ever invoked outside of a direct signal emission (for example, called manually from other code), so it’s a pattern best reserved for slots that only ever fire from connect(), not general-purpose member functions. It’s also worth noting the missing division-by-zero guard in equalClicked() — currentValue / secondValue with secondValue == 0.0 produces IEEE-754 infinity for a double rather than crashing, which is technically “correct” floating-point behavior but will display inf in the UI with no user-facing explanation, exactly the kind of edge case that’s invisible in a demo and embarrassing in front of a real user.

An image viewer in a scroll area

#include <QMainWindow>
#include <QLabel>
#include <QMenuBar>
#include <QFileDialog>
#include <QPixmap>
#include <QScrollArea>

class ImageViewer : public QMainWindow {
    Q_OBJECT

private:
    QLabel *imageLabel;
    QScrollArea *scrollArea;

public:
    ImageViewer() {
        imageLabel = new QLabel;
        imageLabel->setScaledContents(true);

        scrollArea = new QScrollArea;
        scrollArea->setWidget(imageLabel);
        setCentralWidget(scrollArea);

        createMenus();

        setWindowTitle("Image viewer");
        resize(800, 600);
    }

private:
    void createMenus() {
        QMenu *fileMenu = menuBar()->addMenu("File");

        QAction *openAction = fileMenu->addAction("Open");
        connect(openAction, &QAction::triggered, this, &ImageViewer::openImage);
    }

private slots:
    void openImage() {
        QString filename = QFileDialog::getOpenFileName(
            this, "Open image", "", "Images (*.png *.jpg *.bmp)");

        if (!filename.isEmpty()) {
            QPixmap image(filename);
            imageLabel->setPixmap(image);
            imageLabel->resize(image.size());
        }
    }
};

scrollArea->setWidget(imageLabel) transfers ownership of imageLabel to scrollArea even though imageLabel was constructed without an explicit parent argument — QScrollArea::setWidget() reparents the widget internally. This is a general Qt convention worth internalizing: many container widgets (setWidget, setLayout, setCentralWidget, addWidget on a layout) silently take ownership as a side effect of the call, not just as a documented parameter. It’s convenient once you know it, and a source of “wait, who owns this?” confusion until you do. The other thing to flag: QPixmap image(filename) loading a full-resolution image and calling imageLabel->resize(image.size()) works fine for reasonably sized photos, but for very large images (raw camera output, multi-gigapixel scans) this will allocate the entire decoded bitmap in memory and can stall the UI during load — a real image viewer would decode a scaled preview first and load full resolution on demand or in a worker thread.

moc and the build system: where the pain actually comes from

Every class that declares Q_OBJECT — every example above — needs a second, Qt-specific compilation step before your normal C++ compiler ever sees it. The Meta-Object Compiler (moc) scans the header for Q_OBJECT, signals, slots, and Q_PROPERTY declarations and generates an additional .cpp file containing the runtime type information, signal implementations, and qobject_cast support that make signals/slots and Qt’s property system work at all. This is not a template trick or a macro — moc is a separate program that runs as a build step, and if it doesn’t run, the linker fails with undefined reference to vtable for YourClass or similar, which is a genuinely confusing error message if you don’t already know moc exists.

This has concrete consequences for how you structure a project. Q_OBJECT classes are almost always declared in their own header, because moc operates on headers, not on inline class definitions buried inside a .cpp file — you can work around this with #include "file.moc" at the bottom of a translation unit, but it’s a pattern reserved for specific cases (single-file main.cpp demos, generated code) rather than something to reach for by default. It’s also why CMAKE_AUTOMOC exists: rather than manually listing every header that needs moc’d, CMake’s automoc scans your sources at configure/build time and wires up the generated files transparently. When people say “CMake and Qt fight each other,” it’s almost always one of: AUTOMOC not enabled on the target, a Q_OBJECT class declared in a .cpp file with no corresponding header for moc to process, or a stale build directory holding onto moc_*.cpp files generated against an older version of the header (deleting the build directory and reconfiguring fixes more Qt+CMake “impossible” errors than any other single action).

moc errors, silent connections, and ownership bugs

Q_OBJECT and missing moc output

Symptom: Errors mentioning moc, often undefined reference to vtable for X or undefined reference to X::staticMetaObject.

Cause: A class using Q_OBJECT was not processed by moc — usually because it’s declared somewhere moc’s discovery doesn’t reach (a .cpp file with no header, AUTOMOC disabled, or a stale generated-files cache).

Fix: Use CMAKE_AUTOMOC ON (CMake) or qmake’s default moc integration, put Q_OBJECT classes in headers, and when in doubt, delete the build directory and reconfigure from scratch before assuming the code itself is wrong.

A signal-slot connection that does nothing

Symptom: Clicks or other signals produce no visible effect, with no compile error and no runtime warning (or a runtime warning easy to miss in console noise).

Cause: Wrong signal/slot names, a mismatched connection style, or — with the string-based SIGNAL()/SLOT() macros — a typo that isn’t caught until runtime.

Fix: Prefer the compile-time checked connect syntax; it turns a silent runtime no-op into a compiler error.

// Runtime string-based check — typos fail silently at runtime
connect(button, SIGNAL(clicked()), this, SLOT(onClicked()));

// Compile-time checked — a typo here is a build error, not a mystery
connect(button, &QPushButton::clicked, this, &MyClass::onClicked);

Leaks and double-frees from the parent-child tree

Symptom: Growing memory use over the application’s lifetime, or a crash on shutdown that doesn’t reproduce consistently.

Cause: Either widgets constructed with no parent (so nothing ever calls delete on them), or the opposite mistake — calling delete manually on a widget that does have a parent, producing a double-free when the parent’s destructor runs.

Fix: Always construct child widgets with a parent so Qt’s ownership tree handles destruction, and never call delete directly on a parented QObject — use deleteLater() if early removal is genuinely needed.

// With a parent, destruction is automatic and safe
QPushButton *button = new QPushButton("Button", parentWidget);

// Never do this if `button` has a parent — the parent will delete it again later
// delete button;

FAQ

Q1: Qt vs other GUI frameworks?

A:

  • Qt: Cross-platform, large feature set, mature signal/slot and layout systems, dual-licensed (LGPL/commercial)
  • wxWidgets: Wraps native platform widgets directly, so it inherits native look-and-feel more closely
  • GTK: Common on Linux desktop environments, C-based core with C++ bindings available

Q2: Which Qt version?

A: Prefer the latest Qt 6 for new projects — better high-DPI handling, a more modern rendering pipeline, and active development. Qt 5 is still widely used in existing production codebases and isn’t going away soon, but new work should default to Qt 6 unless a specific dependency forces otherwise.

Q3: Qt Creator vs Visual Studio?

A: Qt Creator is purpose-built for Qt — it understands .ui files, moc output, and Qt Quick/QML tooling out of the box. Visual Studio works fine with the Qt Visual Studio Tools extension installed, and is the more natural choice if the rest of your codebase (or your team) is already anchored to the MSVC toolchain.

Q4: Do I need a commercial license?

A: Open-source projects can use Qt under LGPL/GPL terms in most cases; check Qt licensing carefully if you’re shipping a closed-source commercial product, since LGPL compliance for static linking has specific obligations.

Q5: Qt Quick vs Qt Widgets?

A:

  • Qt Widgets: Traditional desktop UI, imperative C++ construction, mature and stable, the subject of this guide
  • Qt Quick: Declarative UI described in QML, better suited to fluid animations, touch interfaces, and mobile-style UX

Q6: Where to learn Qt?

A:

  • Official docs: doc.qt.io
  • The example projects bundled with the Qt SDK — they cover real, non-trivial use cases beyond “Hello World”
  • Books such as C++ GUI Programming with Qt