Use this guide for any new or modified Qt graphical interface. Canonical rules:
GUI-*, plus ARC-*, NAM-*, SYN-*, RES-*, and TST-*.
When the task includes an application icon, launcher icon, store icon, or
in-application brand mark, also read
APP_ICONS_AND_BRANDING.md. Native packaging
artifacts remain platform resources; only the in-application mark belongs under
the ui/ visual boundary.
Generated projects should begin with the combined ordering in
PROJECT_CMAKE_BASELINE.md, which keeps project
modules, Qt policy setup, and generated-type include paths aligned.
The default stack is Qt 6, Qt Quick, QML, and Qt Quick Controls. Qt Widgets is a compatibility technology, not the default for new interfaces.
Primary Qt references:
- Qt Quick Controls customization: https://doc.qt.io/qt-6/qtquickcontrols-customize.html
- Qt Quick Controls styles and
QQuickStyle: https://doc.qt.io/qt-6/qtquickcontrols-styles.html and https://doc.qt.io/qt-6/qquickstyle.html - QML lint tooling: https://doc.qt.io/qt-6/qtqml-tooling-qmllint.html
Classify the product surface before designing screens or targets:
| Request or inspected context | Primary surface |
|---|---|
| Explicit graphical, desktop, Qt, or QML application | Qt Quick |
| User-facing interactive application with no interface specified | Qt Quick |
| Explicit CLI tool, service, library, daemon, or headless process | Requested non-graphical surface |
| Existing compatible UI under a constrained change | Preserve it and document any GUI-002 exception |
Do not silently interpret an unspecified user-facing interactive application as
CLI-only. Under GUI-015, Qt Quick is the primary interface. If automation,
testing, or headless use has real value, a CLI may be added as a secondary
adapter under GUI-016; it is not a substitute for the graphical interface.
Both adapters call the same C++ application and domain modules. QML and CLI argument parsing must not contain duplicated domain decisions. When the request is explicitly non-graphical, do not add Qt merely to follow a default that does not apply.
Do not begin with Main.qml. Define the interaction model first:
Audience and usage context:
Primary user goal:
Product-specific visual direction:
Information hierarchy and content density:
Screens and navigation:
User actions:
Authoritative state:
Loading, empty, success, and failure states:
Affordances, immediate feedback, and error prevention/recovery:
Keyboard path and focus order:
Reusable components:
Design tokens:
Application icon and in-application brand-mark ownership, when in scope:
Outer content bounds and maximum task width:
Columns, gutters, and shared alignment lines:
Repeated-control size and gap invariants:
Responsive breakpoints or layout behavior:
Grow, shrink, wrap, overflow, and scrolling rules by region:
Accessibility labels and announcements:
Localization requirements:
A visually attractive screen that omits failure, focus, resizing, or ownership states is incomplete.
Consistency is not repetition. Tokens and reusable components should make the product coherent, but every screen composition must follow its task and information hierarchy. Before drawing components, explain:
- who uses the product, in which environment, and what they need to notice first;
- which action is primary, which actions are secondary, and which actions are destructive or reversible;
- how the interface makes system status, selection, progress, validation, and recovery visible without requiring recall;
- how progressive disclosure controls complexity and preserves an appropriate content density;
- which visual qualities make the product recognizable without weakening usability or accessibility.
Do not default every product to the same centered hero, rounded-card grid, gratuitous gradient, glass panel, oversized empty spacing, or dashboard shell. Do not copy a reference screenshot as a component recipe. Extract useful interaction principles, then adapt hierarchy, density, typography, motion, and component composition to the actual product.
Use established UX principles deliberately: recognition over recall, visible system status, clear affordances, immediate feedback, error prevention, actionable recovery, consistent terminology, keyboard efficiency, and accessible contrast. A distinctive visual direction is successful only when the main task becomes easier to understand and complete.
flowchart LR
QML["Qt Quick presentation"] --> ViewModel["C++ presentation adapter"]
ViewModel --> Application["Application module"]
CLI["Optional CLI adapter"] --> Application
Application --> Domain["Pure C++ domain modules"]
Adapter["Platform and persistence adapters"] --> Application
Dependency rules:
- Domain modules do not import Qt Quick, QML, or visual types.
- Application modules own use cases and authoritative behavior.
- A presentation adapter translates typed C++ state into a minimal QML-facing contract.
- QML owns layout, transitions, visual state, and input forwarding.
- The composition root creates concrete dependencies and registers UI types.
| Responsibility | Owner |
|---|---|
| Domain decisions, validation, business rules | C++ domain module |
| Use-case orchestration and authoritative state | C++ application module |
| QML properties, signals, commands, list models | C++ presentation adapter |
| Layout, controls, animation, visual feedback | QML |
| Filesystem, network, settings, OS APIs | C++ adapter/platform module |
Small QML expressions for visibility, formatting, and visual state are acceptable. Parsing, persistence, validation, and domain decisions are not.
src/
domain/
app_domain.cppm
app_domain.cpp
application/
app.cppm
app.cpp
presentation/
app_view_model.hpp
app_view_model.cpp
adapters/
bootstrap/
main.cpp
cli/
main.cpp # optional secondary adapter
ui/
Main.qml
pages/
components/
PrimaryActionButton.qml
StatusPanel.qml
theme/
Theme.qml
assets/
tests/
domain/
application/
presentation/
ui/
tst_AppShell.qml
Create only directories that own real behavior or assets. The structure may be
smaller for a small product, but a new Qt Quick project uses ui/ as its visual
boundary rather than a top-level qml/ technology bucket. pages/,
components/, theme/, and assets/ are responsibility-based subdivisions,
not mandatory empty ceremony.
The .hpp presentation file is permitted only when Qt MOC requires a textual
meta-object boundary. It is an external-tool adapter under MOD-007, not a
reason to replace domain modules with headers.
Expose the smallest contract QML needs:
- typed
Q_PROPERTYstate withNOTIFYor bindable semantics; - clearly named invokable user intents;
- signals for observable events, not hidden command channels;
QAbstractItemModelderivatives for structured collections;- immutable value snapshots where practical;
- explicit busy, error, empty, and disabled states.
Do not expose a large service object or raw domain graph to QML.
QML_ELEMENT makes a class creatable from QML unless another registration
policy says otherwise. Qt generates an internal wrapper derived from that C++
type. Therefore a type instantiated like this:
AppViewModel {
id: appViewModel
}must not be declared final:
class AppViewModel : public QObject {
Q_OBJECT
QML_ELEMENT
};This is a framework extension point, not an invitation for project code to
subclass the adapter. A presentation type may remain final only when QML does
not instantiate it and the selected singleton, uncreatable, context-property,
or factory ownership strategy has been verified not to require Qt-generated
subclassing.
Generated projects copy cmake/AimcppProjectChecks.cmake from this repository
and call aimcpp_reject_final_qml_creatable_types for every project-owned QML
registration header before qt_add_qml_module. This turns the common
QML_ELEMENT plus final contradiction into a configure-time GUI-021
diagnostic. It is a fast preflight, not proof of Qt integration; the generated
registration sources and graphical executable still must complete a clean full
build.
When an additional CLI is justified, keep it as a thin composition and input/output adapter:
add_executable(MyAppCli src/cli/main.cpp)
target_link_libraries(MyAppCli PRIVATE app_core)
target_compile_features(MyAppCli PRIVATE cxx_std_26)The CLI forwards user intent to app_core; it does not reimplement validation,
state transitions, persistence policy, or other authoritative behavior.
- Keep QML, visual tokens, and presentation assets under the top-level
ui/boundary for new repositories. - QML component files and exported QML types use PascalCase.
id, property, signal, handler, and function names use lowerCamelCase.- Reusable components describe a UI role:
PrimaryActionButton, notBlueButton. - Keep pages responsible for composition; move reusable visuals into focused components.
- Avoid giant
Main.qmlfiles that own the whole product.
Using RowLayout, ColumnLayout, or GridLayout does not by itself create a
good layout. Define the relationships those containers must preserve.
Before QML implementation, record:
| Decision | Required evidence |
|---|---|
| Content bounds | Outer insets and the maximum useful task width |
| Columns | Column purpose, width policy, gutter, and collapse order |
| Alignment lines | Shared left/right edges, centers, text baselines, and numeric edges |
| Spacing scale | Named increments and where each tier is used |
| Repeated controls | Equal width/height, row/column gaps, radius, icon box, and label baseline |
| Region sizing | Which regions fill, stay intrinsic, cap, reflow, scroll, or hide |
| Breakpoints | Compact, standard, and wide compositions with transition criteria |
| Safe bounds | Bottom/top insets and containment for footers, overlays, focus rings, and shadows |
Use one authoritative spacing scale and semantic tokens. A value should come from content, a token, or a documented constraint—not from nudging one element until a single screenshot looks acceptable.
- Establish a small set of visible alignment lines per screen. Titles, header actions, dividers, panels, displays, grids, and footers should resolve to those lines instead of drifting independently.
- Repeated peers must share geometry. Keypads, toolbars, list rows, and card groups require consistent sizes and gaps unless hierarchy deliberately marks an exception.
- Align text by the appropriate metric. Use baselines for related labels, optical centering for icons and glyphs, and a stable right edge with tabular figures for changing numeric values when the product benefits from it.
- Preserve nested padding relationships. A child surface should not appear arbitrarily closer to one parent edge than another.
- Keep peripheral content inside its owning layout and safe inset. A status message, keyboard hint, or trailing icon must not float near the window edge because it was anchored outside the main content hierarchy.
Empty space is useful only when it supports hierarchy. For every large empty region, identify whether it provides focus, future content capacity, or a deliberate visual pause. Otherwise rebalance the composition.
- Do not let one child remain at a small fixed width on the left while its parent expands indefinitely and leaves unused space on the right.
- Related elements should normally share a bounded content width or a deliberate alignment relationship. For example, a display and keypad should not imply different grids without a product reason.
- At wide sizes, cap and center the task area, redistribute columns, or reveal a justified secondary region. Do not scale every control indefinitely.
- At compact sizes, reflow or collapse secondary panels before primary controls become clipped, crowded, or unreachable.
- Define reusable spacing, radius, typography, color, icon-size, control-height, border, and motion tokens.
- Let task hierarchy and content density determine composition; do not place every piece of content in an identical card merely for visual consistency.
- Support light/dark appearance through tokens rather than scattered colors.
- Preserve readable content under resizing, long values, and text expansion.
- Ensure icons share a coherent visual weight, bounding box, and baseline with adjacent text; geometric centering may still require optical adjustment.
- Make destructive, primary, and secondary actions visually distinct without breaking their placement and alignment contracts.
- Use animation to explain state changes, not delay interaction.
- Avoid magic pixels repeated across components.
Choose the Controls strategy before creating reusable controls:
| Strategy | Required behavior |
|---|---|
| Product-owned visual system | Select one customizable style such as Basic, Fusion, Imagine, Material, or Universal before loading QML; custom background, contentItem, indicator, delegates, and popups are then allowed and verified under that style |
| Native platform controls | Keep the platform style and customize only its documented surface; do not replace visual delegates that the native style rejects |
Do not let the workstation choose this architecture implicitly. macOS and Windows native styles may reject delegate replacement even when the same QML appears to work under another style. The application composition root, tests, lint environment, screenshots, and packaged application must agree on the effective style.
For a product-owned system, select the style before any QML importing Qt Quick Controls is loaded:
#include <QQuickStyle>
QQuickStyle::setStyle(QStringLiteral("Basic"));
QQmlApplicationEngine engine;The full entry point still owns QGuiApplication, object creation failure,
dependency composition, and smoke-test readiness. This excerpt shows ordering,
not a complete main function.
Validate QML against the exact instantiated type and the declared minimum Qt
version. Similar controls do not necessarily expose the same API. A property on
Text is not automatically a property on TextEdit or TextArea, and a newer
Qt documentation page does not expand the project's minimum-version contract.
Required checks:
- inspect the exact type documentation and inherited-member list;
- keep imports compatible with the declared minimum Qt version;
- run the generated module lint target and strict
qmllintwith zero allowed project warnings; - create every component at runtime, including lazy dialogs, popups, delegates, and alternate responsive branches;
- treat
Type ... unavailable,Cannot assign to non-existent property, and unresolved import/type diagnostics as causal failures.
Deleting a failing property until the window opens is not verification. Confirm the intended typography or behavior still exists through a supported API.
Every size relationship needs one owner. A parent may constrain a child, or a child's intrinsic content may inform a parent, but both directions must not be coupled through implicit size.
Incorrect
ScrollView {
id: viewport
TextArea {
width: viewport.availableWidth
implicitHeight: Math.max(contentHeight, viewport.availableHeight)
}
}The viewport can derive content size from the editor while the editor derives its implicit size from the viewport, producing a binding loop.
Correct direction
ScrollView {
id: viewport
clip: true
TextArea {
width: viewport.availableWidth
height: Math.max(contentHeight, viewport.height)
background: null
}
}This shape is appropriate only when the surrounding layout gives the viewport an explicit height. Other valid designs may let content own height and place the editor inside a separately constrained flickable. In either case, document viewport ownership, content ownership, minimums, maximums, and overflow.
- Primary, destructive, and confirmation action labels remain fully readable in the reference locale. Size from content plus padding, set a justified minimum, or reflow actions; do not silently elide the action meaning.
- Test translated expansion and realistic longest labels. Fixed control widths require evidence, not a convenient initial screenshot.
- Popup width must account for both anchors and delegate content while remaining inside safe window bounds. Long language names, native names, model names, and RTL text must not be clipped at the trailing edge.
- Delegate rows define how leading text, trailing text, icons, and indicators
divide space. Do not compute both text widths from an unstable
parent.widthor rely on coincidental remaining space. - Dialog header, body, and footer share alignment anchors. Footer actions may wrap or widen before their labels truncate.
- Instantiate open popups, open dialogs, long editor content, empty/error/loading states, and compact/wide branches during verification; closed lazy controls cannot be judged from startup alone.
Prefer Qt's resolved application/system fonts and consistent typography tokens.
Name a specific family only when it is bundled with a documented license or
verified on every supported platform with a deliberate fallback. Do not assume
that strings such as Monospace or monospace resolve to a portable family.
For code-like content, use a verified fixed-pitch resolution strategy and test
the resulting metrics on each supported platform.
Every interactive flow must be usable without a mouse:
- deliberate tab/focus order;
- visible focus indicators;
- keyboard activation and shortcuts where appropriate;
- accessible names, descriptions, roles, and state;
- adequate contrast and target size;
- no color-only communication;
- screen-reader announcement for important result or error changes.
User-visible strings must be translation-ready. Layouts must tolerate longer translations, different number formats, and right-to-left presentation when the product scope requires it. Do not concatenate translated sentence fragments.
- Never block the GUI thread with I/O or expensive computation.
- Model asynchronous progress, cancellation, failure, and object lifetime.
- Avoid bindings that form loops or repeatedly perform expensive work.
- Load large or optional UI regions deliberately.
- Test representative minimum, normal, and expanded window sizes.
find_package(Qt6 REQUIRED COMPONENTS Quick Qml QuickControls2 Test)
set(QT_QML_OUTPUT_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/qml")
set(my_app_qml_files
ui/Main.qml
ui/pages/HomePage.qml
ui/components/PrimaryActionButton.qml
ui/components/StatusPanel.qml
ui/theme/Theme.qml
)
foreach(qmlFile IN LISTS my_app_qml_files)
string(REGEX REPLACE "^ui/" "" qmlResourceAlias "${qmlFile}")
set_source_files_properties(
"${qmlFile}"
PROPERTIES QT_RESOURCE_ALIAS "${qmlResourceAlias}"
)
endforeach()
if(QT_KNOWN_POLICY_QTP0004)
qt_policy(SET QTP0004 NEW)
endif()
qt_add_executable(MyApp
src/bootstrap/main.cpp
)
set_target_properties(MyApp PROPERTIES
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/bin"
)
target_compile_definitions(MyApp
PRIVATE
MY_APP_QT_QUICK_CONTROLS_STYLE="Basic"
)
qt_add_qml_module(MyApp
URI MyApp
VERSION 1.0
QML_FILES
${my_app_qml_files}
SOURCES
src/presentation/app_view_model.cpp
src/presentation/app_view_model.hpp
)
target_include_directories(MyApp
PRIVATE
"${CMAKE_CURRENT_SOURCE_DIR}/src/presentation"
)
target_link_libraries(MyApp
PRIVATE
app_core
Qt6::Quick
Qt6::Qml
Qt6::QuickControls2
)Keep QML sources project-relative and set their aliases before
qt_add_qml_module. The source ui/Main.qml aliases to Main.qml, while
ui/components/PrimaryActionButton.qml aliases to
components/PrimaryActionButton.qml. This makes ui/ an architectural source
boundary without inserting it into the module's runtime resource namespace.
The alias, URI, and loadFromModule call are one contract: the root alias must
remain Main.qml for loadFromModule("MyApp", "Main").
Keep the QML artifact root and runtime target root separate. It is normal for
the executable target and URI to share the approved name MyApp; with explicit
roots, the module is generated under qml/MyApp/ and the executable under
bin/ instead of both competing for <binary-dir>/MyApp. The
QT_QML_OUTPUT_DIRECTORY variable is the deliberate project-wide root for Qt
QML modules. RUNTIME_OUTPUT_DIRECTORY remains target-local.
Set guarded Qt policies after find_package(Qt6 ...) and before
qt_add_qml_module. QTP0004 requires qmldir metadata for extra QML
directories; guarding it with QT_KNOWN_POLICY_QTP0004 preserves compatibility
when the project's declared minimum Qt predates that policy. Do not claim a
higher Qt minimum merely to silence the warning.
QML type metadata such as MyApp.qmltypes is generated during a successful
CMake Generate/build workflow. If generation first fails for an unrelated
toolchain property, a missing .qmltypes diagnostic from the IDE is a
cascading symptom. Fix the first CMake failure, clear the stale CMake
configuration, and regenerate before diagnosing QML registration.
Qt-generated QML registration code may include a QML_ELEMENT adapter header
by basename. When that header lives under src/presentation/, add the directory
to the QML target with target_include_directories. Register the adapter under
the SOURCES section of qt_add_qml_module, keep the include path target-local,
and never edit the generated *_qmltyperegistrations.cpp file.
Do not link Qt6::Widgets unless GUI-002 has a documented exception.
The composition root must call QQuickStyle::setStyle with
MY_APP_QT_QUICK_CONTROLS_STYLE before loading QML. Keep the compile-time
selection, test environment, and packaged application consistent. A custom
Controls design must not fall back silently to the host's native default style.
Strict lint must import the configured QT_QML_OUTPUT_DIRECTORY, the current
binary directory, and the active Qt installation's QML directory. Prefer module
mode (qmllint -M MyApp) after the target has generated its qmldir and
.qmltypes; it validates C++-registered types, singleton metadata, aliases, and
nested components as one module. Run it from the source root. --bare and -M
are available at the declared Qt 6.6 minimum. Add --max-warnings 0 only for
Qt 6.8 or newer; Qt 6.6/6.7 already fail when warnings are emitted and do not
recognize that option.
At minimum verify:
- Pure C++ domain behavior, invalid input, and boundary values.
- Presentation adapter state transitions and signals.
- QML component creation and primary interactions.
- Keyboard-only primary flow and focus visibility.
- Resizing, long translations, empty/error/loading states, and theme contrast.
- The generated QML lint target plus strict
qmllintwith zero project warnings. Verify the exact type/minimum-version API rather than assuming a property exists on a similar control. - Configure, build, CTest, and relevant QML test runner results separately.
- When a CLI adapter exists, verify it calls the shared application/domain behavior and does not replace graphical interaction coverage.
- Inspect the main flow at representative window sizes and confirm that the product-specific hierarchy, affordances, feedback, and recovery behavior are clear rather than merely visually consistent.
- Configure a clean build with the GUI enabled, build the full default target, and confirm that MOC, QML type registration, resources, QML cache sources, and the graphical executable all compile and link.
- Run a deterministic QML creation/interaction smoke check under the selected Controls style with project-owned Qt/QML warnings treated as failures. The test must reach an explicit ready state and exercise the primary path, including lazy popups, dialogs, delegates, or editors that path uses. A fixed-delay launch does not provide this evidence. Core-only tests do not validate the graphical product.
- Capture and inspect rendered screenshots at minimum, standard, and wide viewport sizes. Include light/dark modes where supported and empty, populated, error, focus, and long-content states that materially change the composition.
- Perform a detail pass for shared edges, text baselines, control metrics, gaps, optical centering, contrast, clipping, truncation, overlap, safe insets, and unexplained dead space. Fix visible defects before completion.
- Add deterministic geometry assertions for critical containment, non-overlap, breakpoint, repeated-size, and alignment invariants where the QML test environment can measure them reliably.
- Confirm zero component-load errors, invalid-property diagnostics, unsupported-style customization warnings, binding loops, missing-font warnings, clipped popup rows, and truncated primary actions.
- Verify source-to-resource mappings:
ui/Main.qmlis the module-rootMain.qml, logical subdirectories remain present, andloadFromModule(uri, "Main")reaches explicit readiness. - Verify the produced runtime target is under
bin/andqmldirplus.qmltypesare under the configuredqml/<URI path>/tree. The full final link must pass when target name and URI root are identical.
If Qt or another required GUI dependency is unavailable, report the Qt surface
as NOT VERIFIED. Do not describe the application or downloadable archive as
ready until the Qt-enabled build and smoke evidence exist.
Record evidence in a form such as:
| Viewport | Appearance | State | Inspected details | Result |
|---|---|---|---|---|
| Minimum supported | Light | Empty + focus | containment, reflow, focus, labels | PASS/FAIL |
| Standard | Light + dark | Populated + error | hierarchy, spacing, contrast | PASS/FAIL |
| Wide | Light + dark | Long content/history | max width, balance, alignment | PASS/FAIL |
The exact sizes belong to the product's layout contract. A screenshot at one convenient desktop size cannot prove responsiveness or detail quality.
- Choosing
QWidgetbecause the request merely says "Qt". - Delivering only a CLI for an unspecified user-facing interactive application.
- Duplicating application or domain behavior between QML and a CLI adapter.
- Implementing domain decisions or validation in button-handler JavaScript.
- Registering a global mutable service object for convenient QML access.
- Hard-coding every position and size for one screenshot.
- Reusing a generic card/gradient/dashboard recipe without a product-specific UX rationale.
- Copying a reference design without adapting hierarchy and interaction to the product.
- Creating a top-level
qml/dumping directory in a new repository instead of an explicitui/boundary. - Ignoring QTP0004 for QML files in extra directories or requiring a newer Qt release solely to avoid a guarded policy check.
- Diagnosing a missing generated
.qmltypesfile before fixing an earlier failed CMake Generate step. - Omitting the target-local include directory for a nested
QML_ELEMENTheader or editing generated QML type registration source to compensate. - Declaring a QML-creatable
QML_ELEMENTQObjectfinaleven though Qt's generated registration wrapper must derive from it. - Claiming GUI completion from a core-only, GUI-disabled, or headless test build.
- Delivering a final archive when the requested Qt target or QML smoke flow was not verified in an environment with Qt installed.
- Blocking the GUI thread during file, network, or expensive domain work.
- Linking all Qt modules rather than the required target-local components.
- Claiming a polished interface without keyboard and accessibility verification.
- Treating layout-container usage as proof of alignment, balance, or responsive quality without defining and reviewing geometry invariants.
- Stretching a wide panel while leaving primary controls pinned to one edge and accidental dead space on the other.
- Letting header actions, dividers, content cards, grids, or footers drift from their intended shared alignment lines.
- Leaving status text, hints, icons, focus rings, or overlays clipped, detached from their owning region, or too close to a viewport edge.
- Calling a UI polished after inspecting only one viewport, appearance mode, or content state.
- Replacing
background,contentItem,indicator, delegates, or popups while inheriting an unspecified native Controls style. - Assigning a property because another text or control type exposes it without checking the exact QML type and minimum Qt version.
- Coupling a child's implicit size to a viewport size that is itself derived from the child.
- Naming an unbundled, unverified font family and accepting platform substitution warnings.
- Using fixed action or popup widths that clip primary labels, translations, bilingual rows, RTL content, icons, or focus rings.
- Passing a GUI smoke test that only waits briefly at startup, never opens lazy controls, and ignores project-owned Qt/QML warnings.
- Passing absolute project QML paths to
qt_add_qml_moduleand relying on Qt to infer stable resource names. - Letting the architectural
ui/directory become an accidental runtime module path segment or assigning aliases that moveMain.qmlaway from the module root. - Leaving QML module output and runtime executable output in the same filesystem location when the target and URI share a name.
- Renaming the approved product or QML URI, disabling cache generation, or deleting the colliding directory as a substitute for separating output roots.