This document explains the architecture and design patterns used in the whereami application, providing a blueprint for creating robust Go + QML applications using MIQT (Modern Qt bindings for Go).
- Overview
- Project Structure
- Resource Management
- Go Backend Architecture
- QML Frontend Architecture
- Communication Patterns
- Theme and Styling
- JavaScript Libraries
- Development Workflow
- Best Practices
- Testing and Debugging
- Font Handling & Modular Typography
Consistent typography is implemented through a centralized singleton: Fonts.qml (compiled into resources and imported via import "qrc:/themes" as Themes, then accessed as Themes.Fonts). This replaces ad‑hoc font size constants scattered across themes and components.
- Base size & ratio:
Fonts.minFontSize(default base size, e.g. 12px)Fonts.fontScaleRatio(geometric progression ratio, e.g. 1.15)
- Scale function:
Fonts.scale(step, overrideMin, overrideRatio)- Definition:
scale(1) = base,scale(n) = base * ratio^(n-1)(rounded to integer pixels)
- Semantic helpers (optional):
Fonts.small(),Fonts.body(),Fonts.heading(), etc.
Each *Theme.qml declares (or can override):
property int minFontSize: 12
property real fontScaleRatio: 1.15 // or a custom ratio (e.g. 1.20 for larger progression)
Theme files do NOT duplicate the scaling math. Instead, size bindings reference:
Themes.Fonts.scale(3, minFontSize, fontScaleRatio)
The active theme is loaded by ThemeLoader.qml, which forwards a unified API:
theme.scale(n) // Internally: Themes.Fonts.scale(n, theme.minFontSize, theme.fontScaleRatio)
Components should prefer theme.scale(n) so that:
- Theme overrides are respected.
- Future user preferences (e.g. accessibility sizing) can be injected centrally.
- Open or create a theme file (e.g.
ui/themes/MyTheme.qml). - Set (or omit to inherit defaults):
property int minFontSize: 13 property real fontScaleRatio: 1.18 - Use consistent bindings for all size choices:
- Small metadata:
theme.scale(1) - Body text / common labels:
theme.scale(2) - Section titles / card headers:
theme.scale(3) - Prominent headers:
theme.scale(4)
- Small metadata:
- Avoid hard-coded numeric pixel sizes unless:
- A value must remain visually invariant (e.g. dense icon labels).
- It is a container / layout metric, not text.
| Goal | Change | Example |
|---|---|---|
| Slight global enlargement | Increase minFontSize |
12 → 13 |
| More dramatic step growth | Increase fontScaleRatio |
1.15 → 1.20 |
| Accessibility / large type | Both | 13 + 1.20 |
| Finer granularity | Decrease ratio | 1.15 → 1.10 |
(Keep ratio between ~1.10 and ~1.25 to avoid cramped or jumpy vertical rhythm.)
If a theme omits minFontSize or fontScaleRatio, the forwarding layer passes undefined and Fonts.scale() falls back to its internal defaults. For reliability (and to silence lint warnings), all shipped themes explicitly declare both properties.
A helper script ensures property surface consistency across themes:
./scripts/check_theme_knobs.sh --report-extras
While it currently focuses on color & structural knobs, it also verifies that typography knobs (minFontSize, fontScaleRatio, and dependent size properties like statusBarTextSize) are present.
- Always round: integer pixel sizes avoid blurry text on some platforms.
- Don’t mutate font sizes dynamically in many places (layout churn). Bind once to
theme.scale(n). - Keep maximum scale steps reasonable; the singleton clamps runaway step values.
- Avoid mixing raw pixel sizes with scale-based ones in the same component unless intentional (document exceptions inline).
Text {
text: model.label
font.pixelSize: theme.scale(2) // body
}
Label {
text: waypoint.name
font.pixelSize: theme.scale(3) // header
}
- User preference injection (persisted in settings, applied by
ThemeLoader). - Alternate scale curves (e.g. major/minor third blends) by swapping the implementation inside
Fonts.qml. - Dynamic accessibility mode that adjusts both base and ratio.
The whereami application demonstrates a clean separation between Go backend services and QML frontend UI, connected via HTTP APIs. This architecture provides:
- Clear separation of concerns: Business logic in Go, UI logic in QML
- Testable components: HTTP APIs can be tested independently
- Cross-platform compatibility: Qt handles platform differences
- Hot reloading during development: QML changes don't require Go recompilation
- Resource embedding: All QML files are compiled into the binary
your-app/
├── main.go # Application entry point
├── generate.go # Resource generation directive
├── resources_gen.go # Generated resource file (gitignored)
├── resources_gen.rcc # Generated resource file (gitignored)
├── api.go # HTTP API handlers
├── storage.go # Data persistence layer
├── go.mod # Go module definition
├── ui/ # QML frontend
│ ├── resources.qrc # Qt resource file
│ ├── MainView.qml # Root QML component
│ ├── components/ # Reusable UI components
│ │ ├── Theme.qml # Centralized styling
│ │ ├── SomeDialog.qml # Modal dialogs
│ │ ├── SomeCard.qml # Information cards
│ │ └── ...
│ ├── services/ # QML service objects
│ │ └── API.qml # HTTP API wrapper
│ └── lib/ # JavaScript libraries
│ └── SomeLibrary.js # Utility functions
└── .rules # Development guidelines
All QML files must be listed in the Qt resource file:
<RCC>
<qresource prefix="/">
<file>MainView.qml</file>
<file>components/Theme.qml</file>
<file>components/SomeDialog.qml</file>
<file>services/API.qml</file>
<file>lib/SomeLibrary.js</file>
</qresource>
</RCC>Use go:generate to compile QML into Go resources:
package main
//go:generate miqt-rcc -Qt6 -Input ui/resources.qrc -OutputGo resources_gen.go -OutputRcc resources_gen.rcc -Package mainLoad the main QML file from embedded resources:
func main() {
qt.NewQApplication(os.Args)
engine := qml.NewQQmlApplicationEngine()
// Load from embedded resources (qrc:/)
engine.Load(qt.NewQUrl3("qrc:/MainView.qml"))
if len(engine.RootObjects()) == 0 {
fmt.Fprintln(os.Stderr, "QML load failed")
os.Exit(2)
}
qt.QApplication_Exec()
}package main
import (
"net/http"
"os"
qt "github.com/mappu/miqt/qt6"
"github.com/mappu/miqt/qt6/qml"
)
func main() {
// 1. Setup data directories (XDG compliant)
dataDir := setupDataDirectory()
// 2. Register HTTP API handlers
RegisterAPI(http.DefaultServeMux, dataDir)
// 3. Start HTTP server in goroutine
go func() {
addr := "127.0.0.1:8080"
http.ListenAndServe(addr, nil)
}()
// 4. Initialize Qt application
qt.NewQApplication(os.Args)
engine := qml.NewQQmlApplicationEngine()
engine.Load(qt.NewQUrl3("qrc:/MainView.qml"))
qt.QApplication_Exec()
}Create a dedicated file for HTTP handlers:
// api.go
package main
func RegisterAPI(mux *http.ServeMux, dataDir string) {
// Use Go 1.22 method-aware routing patterns
mux.HandleFunc("GET /api/items", handleGetItems)
mux.HandleFunc("POST /api/items", handleCreateItem)
mux.HandleFunc("GET /api/items/{id}", handleGetItem)
mux.HandleFunc("PUT /api/items/{id}", handleUpdateItem)
mux.HandleFunc("DELETE /api/items/{id}", handleDeleteItem)
// CORS preflight
mux.HandleFunc("OPTIONS /api/items", handleItemsOptions)
mux.HandleFunc("OPTIONS /api/items/{id}", handleItemsOptions)
}
func handleGetItems(w http.ResponseWriter, r *http.Request) {
// List items - method is already guaranteed to be GET
items := getItems()
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(items)
}
func handleCreateItem(w http.ResponseWriter, r *http.Request) {
// Create item - method is already guaranteed to be POST
var item Item
if err := json.NewDecoder(r.Body).Decode(&item); err != nil {
http.Error(w, "Invalid JSON", http.StatusBadRequest)
return
}
savedItem, err := createItem(item)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(savedItem)
}
func handleGetItem(w http.ResponseWriter, r *http.Request) {
// Get path parameter using Go 1.22 pattern
id := r.PathValue("id")
item, err := getItemByID(id)
if err != nil {
http.Error(w, "Item not found", http.StatusNotFound)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(item)
}
func handleItemsOptions(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type")
w.WriteHeader(http.StatusNoContent)
}Separate data persistence concerns:
// storage.go
package main
type Item struct {
ID int `json:"id"`
Name string `json:"name"`
}
func LoadItems(dataDir string) ([]Item, error) {
// Load from file/database
}
func SaveItem(dataDir string, item Item) error {
// Save to file/database
}Use mutexes for shared data:
var (
items []Item
itemsMu sync.RWMutex
)
func GetItems() []Item {
itemsMu.RLock()
defer itemsMu.RUnlock()
return items
}
func AddItem(item Item) {
itemsMu.Lock()
defer itemsMu.Unlock()
items = append(items, item)
}Create a main ApplicationWindow as the root:
// MainView.qml
import QtQuick 2.15
import QtQuick.Controls 2.15
import "components"
import "services"
ApplicationWindow {
id: window
visible: true
width: 1200
height: 800
// Global theme
Theme {
id: theme
}
// API service
API {
id: api
apiPort: 8080
// Handle API responses
onItemsLoaded: function(items) {
// Update UI state
}
}
// Main content
SomeMainComponent {
anchors.fill: parent
api: api
theme: theme
}
}// components/SomeCard.qml
import QtQuick 2.15
import QtQuick.Controls 2.15
Rectangle {
id: root
// Public API
property var item: null
property var api: null
// Theme integration
Theme {
id: theme
}
color: theme.cardBackground
border.color: theme.cardBorder
// Internal implementation
Column {
// Card content
}
}The API service should be a comprehensive wrapper around ALL Go backend endpoints. No QML component should make direct XMLHttpRequest calls - everything must go through the API service.
// services/API.qml
import QtQuick 2.15
QtObject {
id: api
property int apiPort: 8080
property int requestTimeoutMs: 8000
// Generic signals for all operations
signal requestSucceeded(string kind, var result, var context)
signal requestFailed(string kind, string errorMessage, var context)
// Domain-specific signals - replace with your application's entities
signal dataLoaded(var data)
signal dataLoadFailed(string error)
signal entityCreated(var entity, var originalEntity)
signal entityCreateFailed(var entity, string error)
signal entityDeleted(var entity)
signal entityDeleteFailed(var entity, string error)
signal entityUpdated(var entity, var originalEntity)
signal entityUpdateFailed(var entity, string error)
// PUBLIC METHODS - One for each backend endpoint
function getData() {
_xhr("GET", "/api/your-endpoint", null, function(txt) {
var data = JSON.parse(txt || "[]");
dataLoaded(data);
requestSucceeded("GET /api/your-endpoint", data, null);
}, function(err) {
dataLoadFailed(err);
requestFailed("GET /api/your-endpoint", err, null);
});
}
function createEntity(entity) {
if (!entity) {
console.error("API.createEntity: invalid entity");
return;
}
_xhr("POST", "/api/your-endpoint", entity, function(txt) {
var saved = JSON.parse(txt || "{}");
entityCreated(saved, entity);
requestSucceeded("POST /api/your-endpoint", saved, entity);
}, function(err) {
entityCreateFailed(entity, err);
requestFailed("POST /api/your-endpoint", err, entity);
});
}
function deleteEntity(entity) {
var path = "/api/your-endpoint/" + entity.id;
_xhr("DELETE", path, null, function() {
entityDeleted(entity);
requestSucceeded("DELETE " + path, entity, entity);
}, function(err) {
entityDeleteFailed(entity, err);
requestFailed("DELETE " + path, err, entity);
});
}
function updateEntity(entity, updateData) {
var path = "/api/your-endpoint/" + entity.id;
_xhr("PATCH", path, updateData, function(txt) {
var updated = JSON.parse(txt || "{}");
entityUpdated(updated, entity);
requestSucceeded("PATCH " + path, updated, entity);
}, function(err) {
entityUpdateFailed(entity, err);
requestFailed("PATCH " + path, err, entity);
});
}
// Add more functions for each of your backend endpoints
// Follow the same pattern: validate input, make _xhr call, emit signals
// PRIVATE HELPER FUNCTIONS
function _xhr(method, path, body, success, failure) {
var xhr = new XMLHttpRequest();
xhr.open(method, "http://127.0.0.1:" + apiPort + path);
if (method === "POST" || method === "PUT" || method === "PATCH") {
xhr.setRequestHeader("Content-Type", "application/json");
}
xhr.onreadystatechange = function() {
if (xhr.readyState === XMLHttpRequest.DONE) {
if (xhr.status >= 200 && xhr.status < 300) {
if (success) success(xhr.responseText);
} else {
if (failure) failure("HTTP " + xhr.status);
}
}
};
xhr.send(body ? JSON.stringify(body) : null);
}
}Use properties and signals for state management:
ApplicationWindow {
// Application state
property var items: []
property var selectedItem: null
property bool loading: false
// State changes trigger UI updates automatically
onItemsChanged: {
console.log("Items updated:", items.length);
}
}Critical Architecture Rule: The API.qml service should be a comprehensive, self-contained wrapper around ALL Go backend endpoints. Every QML component must use this service exclusively—never make direct XMLHttpRequest calls.
// services/API.qml
import QtQuick 2.15
QtObject {
id: api
property int apiPort: 8080
property int requestTimeoutMs: 8000
// ========== SIGNALS ==========
// Generic signals
signal requestSucceeded(string kind, var result, var context)
signal requestFailed(string kind, string errorMessage, var context)
// Entity-specific signals (replace with your domain entities)
signal entitiesLoaded(var entities)
signal entitiesLoadFailed(string error)
signal entityAdded(var entity, var originalEntity)
signal entityAddFailed(var originalEntity, string error)
signal entityDeleted(var entity)
signal entityDeleteFailed(var entity, string error)
// ========== PUBLIC API METHODS ==========
// Wrap EVERY backend endpoint
function getEntities() {
if (api.apiPort < 0) {
entitiesLoaded([]);
return;
}
_xhr("GET", "/api/entities", null, function(response) {
var entities = JSON.parse(response);
entitiesLoaded(entities);
requestSucceeded("GET /api/entities", entities, null);
}, function(error) {
entitiesLoadFailed(error);
requestFailed("GET /api/entities", error, null);
});
}
function addEntity(entity) {
if (!entity || !entity.name) {
console.error("API.addEntity: invalid entity payload", entity);
return;
}
_xhr("POST", "/api/entities", entity, function(response) {
var savedEntity = JSON.parse(response);
entityAdded(savedEntity, entity);
requestSucceeded("POST /api/entities", savedEntity, entity);
}, function(error) {
entityAddFailed(entity, error);
requestFailed("POST /api/entities", error, entity);
});
}
function deleteEntity(entity) {
var path = "/api/entities/" + entity.id;
_xhr("DELETE", path, null, function() {
entityDeleted(entity);
requestSucceeded("DELETE " + path, entity, entity);
}, function(error) {
entityDeleteFailed(entity, error);
requestFailed("DELETE " + path, error, entity);
});
}
// Generic request method for future endpoints
function request(method, path, body, successCallback, errorCallback, context) {
_xhr(method, path, body, function(response) {
if (successCallback) successCallback(response);
requestSucceeded(method + " " + path, response, context);
}, function(error) {
if (errorCallback) errorCallback(error);
requestFailed(method + " " + path, error, context);
});
}
// ========== PRIVATE IMPLEMENTATION ==========
function _xhr(method, path, body, success, failure, timeoutOverride, context) {
var xhr = new XMLHttpRequest();
var timedOut = false;
var timeoutMs = timeoutOverride || api.requestTimeoutMs;
var timer = _startTimeout(timeoutMs, function() {
timedOut = true;
try { xhr.abort(); } catch(e) {}
if (failure) failure("timeout (" + timeoutMs + " ms)");
});
var url = _baseUrl() + path;
xhr.open(method, url);
if (method === "POST" || method === "PUT" || method === "PATCH") {
xhr.setRequestHeader("Content-Type", "application/json");
}
xhr.onreadystatechange = function() {
if (xhr.readyState === XMLHttpRequest.DONE) {
_clearTimeout(timer);
if (timedOut) return;
if (xhr.status >= 200 && xhr.status < 300) {
if (success) success(xhr.responseText);
} else {
var msg = "HTTP " + xhr.status;
if (xhr.responseText) {
msg += " " + xhr.responseText.substring(0, 160);
}
if (failure) failure(msg);
}
}
};
try {
if (body) {
xhr.send(JSON.stringify(body));
} else {
xhr.send();
}
} catch(e) {
_clearTimeout(timer);
if (failure) failure("send error: " + e);
}
}
function _baseUrl() {
return "http://127.0.0.1:" + api.apiPort;
}
// Timer management helpers
function _startTimeout(ms, callback) {
var timer = Qt.createQmlObject(
'import QtQuick 2.15; Timer { interval: ' + ms + '; repeat: false }',
api, "timeout_timer"
);
timer.timeout.connect(callback);
timer.start();
return timer;
}
function _clearTimeout(timer) {
if (timer) {
timer.stop();
timer.destroy();
}
}
}// components/EntityList.qml
Rectangle {
property var api: null // Injected from parent
property var entities: []
Component.onCompleted: {
// Never make direct XMLHttpRequest - always use API service
api.getEntities();
}
Connections {
target: api
function onEntitiesLoaded(loadedEntities) {
entities = loadedEntities;
}
function onEntitiesLoadFailed(error) {
console.error("Failed to load entities:", error);
}
}
}Use Qt's signal system for loose coupling between components:
// Parent component
SomeDialog {
id: dialog
api: root.api // Pass API service reference
onEntityCreated: function(entity) {
// Dialog handled the API call, just refresh UI
console.log("Entity created:", entity.name);
}
}
// Dialog component
Dialog {
property var api: null
signal entityCreated(var entity)
onAccepted: {
// Use API service, never direct XMLHttpRequest
api.addEntity({
name: nameField.text,
description: descField.text
});
}
Connections {
target: api
function onEntityAdded(savedEntity, originalEntity) {
entityCreated(savedEntity);
close();
}
function onEntityAddFailed(originalEntity, error) {
errorLabel.text = "Failed to create entity: " + error;
}
}
}This comprehensive API wrapper pattern provides:
- Single source of truth: All backend communication goes through one service
- Consistent error handling: Standardized error reporting across the application
- Offline mode support: Easy to implement offline fallbacks
- Testability: Mock the API service for testing UI components
- Maintainability: Backend API changes only require updates in one place
- Signal consistency: Uniform event-driven architecture throughout the app
The application uses a centralized theming system with multiple color variants that can be switched at compile time using the ThemeLoader component.
- Orange Theme (Default): Modern energetic style with orange accents
- Green Theme: Nature-inspired with green palette
- Purple Theme: Elegant with purple tones
All themes follow the same property structure to ensure consistency:
// components/Theme.qml (Orange - Default)
import QtQuick 2.15
QtObject {
// Base colors
property color background: "#222226"
property color accent: "#F97700"
property color primaryText: "#FF8226"
property color secondaryText: "#BDBDBD"
// Component-specific nested themes
property QtObject waypointInfoCard: QtObject {
property color background: Qt.rgba(0, 0, 0, 0.82)
property color border: "#444"
property color primaryText: "white"
property color secondaryText: "white"
property QtObject editButton: QtObject {
property color text: Qt.darker("DeepOrange", 1.3)
property color background: "White"
}
}
property QtObject mapControlButton: QtObject {
property color background: Qt.rgba(0, 0, 0, 0.6)
property color backgroundHover: Qt.rgba(0, 0, 0, 0.75)
property color border: "#444"
property color text: "white"
property color textHover: accent
property QtObject tooltip: QtObject {
property color background: Qt.rgba(0, 0, 0, 0.9)
property color border: "#555"
property color text: "white"
}
}
}The ThemeLoader provides a centralized way to switch between themes:
// components/ThemeLoader.qml
import QtQuick 2.15
Loader {
readonly property string THEME_VARIANT: "orange" // Change this to switch themes
source: {
switch (THEME_VARIANT) {
case "green": return "GreenTheme.qml"
case "purple": return "PurpleTheme.qml"
default: return "Theme.qml" // Orange default
}
}
}Components should use ThemeLoader instead of importing themes directly:
// In components
Rectangle {
ThemeLoader {
id: theme
}
color: theme.waypointInfoCard.background
border.color: theme.waypointInfoCard.border
Text {
color: theme.waypointInfoCard.primaryText
}
Button {
contentItem: Text {
color: theme.waypointInfoCard.editButton.text
}
background: Rectangle {
color: theme.waypointInfoCard.editButton.background
}
}
}To add a new theme variant:
-
Copy an existing theme:
cp ui/components/Theme.qml ui/components/BlueTheme.qml
-
Update color values in the new theme file:
// components/BlueTheme.qml import QtQuick 2.15 QtObject { property color background: "#1A1A2E" property color accent: "#2196F3" property color primaryText: "#42A5F5" // ... update all color properties }
-
Add to resources in
ui/resources.qrc:<file>components/BlueTheme.qml</file>
-
Update ThemeLoader to include the new option:
source: { switch (THEME_VARIANT) { case "green": return "GreenTheme.qml" case "purple": return "PurpleTheme.qml" case "blue": return "BlueTheme.qml" default: return "Theme.qml" } }
-
Regenerate resources:
go generate
To switch themes, edit ThemeLoader.qml:
readonly property string THEME_VARIANT: "green" // Changes entire app themeThen regenerate resources and rebuild:
go generate
go build -ldflags '-s -w' -o bin/whereami .Each theme provides these main property groups:
waypointInfoCard: Waypoint information display stylingsearchBox: Search interface colors and stylingaddWaypointDialog: Dialog box appearancemapControlButton: Map control button styling including tooltipswaypointTable: Table component stylingmapStatusBar: Status bar colorssnackBar: Notification styling
This nested structure keeps related colors organized and makes theme maintenance easier.
// lib/SomeLibrary.js
.pragma library
/**
* SomeLibrary.js - Utility functions for the application
*/
// Private data/functions
var PRIVATE_CONSTANT = "value";
function privateHelper(input) {
return input.toUpperCase();
}
// Public API
function publicFunction(input) {
return privateHelper(input);
}
function anotherFunction(data) {
// Implementation
}// Import the library
import "../lib/SomeLibrary.js" as SomeLibrary
Rectangle {
Component.onCompleted: {
var result = SomeLibrary.publicFunction("hello");
console.log(result); // "HELLO"
}
}- Prefer pure functions that take all needed data as parameters (easier to test).
- Use
.pragma libraryonly when you actually need shared cached state or want a single evaluated instance. For purely functional helpers it can be omitted for clarity. - Export only necessary functions; keep internal helpers unexported (file‑local).
- Keep each library focused on a single responsibility (e.g. string formatting, geometry utils, search logic).
- Document the public API (purpose, params, return shape, side effects) above each exported function.
- Avoid storing references to QML objects in global (library) scope; pass them in instead to reduce lifetime surprises.
- If you introduce shared mutable state (cache, throttling flags) add a clearly marked section:
// SHARED STATE:explaining invariants. - Keep normalization / parsing logic outside visual components so components stay declarative.
Add lightweight QML tests using Qt Quick Test:
- Directory layout:
- Put tests under
ui/tests/ - Name files
tst_<Something>.qmlsoqmltestrunnerauto-discovers them.
- Put tests under
- Import the library with a relative path:
import "../lib/SearchBoxLogic.js" as SearchBoxLogic
- Provide minimal mock objects for any expected properties a function mutates.
- Use
TestCase {}fromQtTest 1.2:- Functions starting with
test_are executed. - Use
verify(),compare(),fail()for assertions.
- Functions starting with
- Keep each test independent:
- Recreate mutable mock state in
init()(runs before every test).
- Recreate mutable mock state in
- Add a Make target (already present:
make qml-test) that runs:qmltestrunner-qt6 -import ui -input ui/tests(falls back toqmltestrunnerif needed).
Example minimal test file:
import QtQuick 2.15
import QtTest 1.2
import "../lib/SomeLibrary.js" as SomeLibrary
TestCase {
name: "SomeLibrary"
function test_uppercase() {
compare(SomeLibrary.publicFunction("abc"), "ABC")
}
}
Run tests:
make qml-test
# or directly:
qmltestrunner-qt6 -import ui -input ui/tests
Guidelines for adding tests to a new library:
- Start with edge cases first (empty input, symbol-only tags, extreme numeric bounds).
- Assert both the return value and any side-effect on passed-in objects.
- For asynchronous patterns (callbacks), wrap assertions in
wait()loops or redesign logic to be pure/ synchronous when feasible.
CI Integration (suggested):
- Add a job step after build/lint:
make qml-test
- Fail the pipeline on any QML test failure.
Keep UI logic (animations, visual geometry) out of JS libraries unless strictly necessary; the more deterministic the library, the simpler the tests.
# Initialize Go module
go mod init your-app
# Add dependencies
go get github.com/mappu/miqt
# Create directory structure
mkdir -p ui/{components,services,lib}# 1. Edit QML files
vim ui/components/SomeComponent.qml
# 2. Regenerate resources (if new files added)
go generate
# 3. Build and run
go build && ./your-app
# 4. For QML-only changes, just refresh the app
# (Resources are embedded, so changes need regeneration)# Regenerate resources after QML changes
go generate
# Check generated files (should be in .gitignore)
ls resources_gen.*
# Clean generated files
rm resources_gen.*- Separate concerns: Keep Go backend and QML frontend clearly separated
- Use meaningful names: Component names should reflect their purpose
- Group related files: Use directories to organize components, services, and libraries
- Version control: Ignore generated resource files
- Single responsibility: Each component should have one clear purpose
- Configurable: Use properties for customization
- Reusable: Avoid hardcoded values and dependencies
- Use ThemeLoader: Always use
ThemeLoaderfor consistent theming across theme variants - Documented: Include usage examples in comments
/*
SomeCard.qml
A reusable card component for displaying items.
Usage:
SomeCard {
item: myItem
onItemClicked: function(item) {
// Handle click
}
}
*/Theme Integration Example:
Rectangle {
ThemeLoader {
id: theme
}
// Good: Uses theme colors
color: theme.waypointInfoCard.background
border.color: theme.waypointInfoCard.border
// Bad: Hardcoded colors
// color: "#000000"
// border.color: "#444444"
}- RESTful patterns: Use standard HTTP methods and status codes
- JSON responses: Consistent data format
- Error handling: Meaningful error messages
- Validation: Validate input data
- Centralized state: Keep application state in the root component
- Reactive updates: Use property bindings for automatic updates
- Signal flow: Use signals for component communication
- Avoid global state: Pass data through component properties
- Lazy loading: Load components only when needed
- Image optimization: Use appropriate image formats and sizes
- Memory management: Avoid memory leaks in long-running applications
- Efficient updates: Minimize unnecessary re-renders
// api_test.go
func TestItemsAPI(t *testing.T) {
req := httptest.NewRequest("GET", "/api/items", nil)
w := httptest.NewRecorder()
handleItems(w, req)
if w.Code != 200 {
t.Errorf("Expected 200, got %d", w.Code)
}
}- Use
console.log()for debugging output - Enable QML debugging in Qt Creator
- Use
qmllintfor static analysis:
qmllint-qt6 ui/**/*.qml- Qt Creator: Full IDE with QML debugging
- qmllint: Static analysis tool for QML
- Browser DevTools: For HTTP API testing
func handleAPI(w http.ResponseWriter, r *http.Request) {
item, err := processItem(r)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
json.NewEncoder(w).Encode(item)
}function performAction() {
api.doSomething(function(result) {
// Success
handleSuccess(result);
}, function(error) {
// Failure
showErrorMessage(error);
});
}# Generate resources
go generate
# Build for current platform
go build -ldflags '-s -w' -o your-app
# Cross-compilation (if needed)
GOOS=windows GOARCH=amd64 go build -o your-app.exe- Single binary: All resources are embedded
- No external dependencies: Qt libraries may need to be bundled
- Cross-platform: Same codebase works on multiple platforms
This architecture provides a solid foundation for building maintainable, scalable QML applications with Go backends. The clear separation of concerns, consistent patterns, and robust tooling support make it suitable for both small utilities and large applications.