Building Cross-Platform Desktop Apps with Qt: Lessons Learned
After shipping Qt applications on Windows, macOS, and Linux for the past three years, I’ve learned that “write once, run anywhere” is more nuanced than it sounds. Here are the practical lessons that will save you weeks of debugging.
Lesson 1: File Paths Are Platform-Specific
The Problem:
// This breaks on Windows
QString path = "/home/user/config.ini";
// This breaks on Linux/macOS
QString path = "C:\\Users\\user\\config.ini";
The Solution:
// Use QStandardPaths for system directories
QString configPath = QStandardPaths::writableLocation(
QStandardPaths::AppConfigLocation
);
// Use QDir for path manipulation
QDir configDir(configPath);
QString filePath = configDir.filePath("config.ini");
// Or use forward slashes (Qt converts automatically)
QString path = QDir::homePath() + "/myapp/config.ini";
Key Insight: Qt’s QDir and QStandardPaths handle platform differences automatically. Never hardcode paths.
Lesson 2: High-DPI Scaling Requires Explicit Support
Modern displays (Retina, 4K) need special handling:
int main(int argc, char *argv[]) {
// MUST be called before QApplication
QApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
QApplication::setAttribute(Qt::AA_UseHighDpiPixmaps);
QApplication app(argc, argv);
// ... rest of your code
}
For custom painting:
void MyWidget::paintEvent(QPaintEvent *event) {
QPainter painter(this);
// Get device pixel ratio
qreal dpr = devicePixelRatioF();
// Scale your coordinates
painter.scale(dpr, dpr);
// Now draw normally
painter.drawRect(10, 10, 100, 100);
}
Lesson 3: Native Menu Bars on macOS
macOS expects the menu bar at the top of the screen, not in the window:
QMenuBar *menuBar = new QMenuBar(nullptr); // No parent!
QMenu *fileMenu = menuBar->addMenu("File");
fileMenu->addAction("New", this, &MainWindow::newFile, QKeySequence::New);
fileMenu->addAction("Open", this, &MainWindow::openFile, QKeySequence::Open);
#ifdef Q_OS_MAC
// On macOS, this creates a global menu bar
setMenuBar(menuBar);
#else
// On other platforms, it's part of the window
setMenuBar(menuBar);
#endif
Bonus: Handle the “About” menu correctly:
QAction *aboutAction = new QAction("About MyApp", this);
aboutAction->setMenuRole(QAction::AboutRole); // macOS moves this automatically
Lesson 4: Deployment is Platform-Specific
Windows
# Use windeployqt to bundle dependencies
windeployqt.exe --release --no-translations MyApp.exe
# Create installer with NSIS or Inno Setup
makensis installer.nsi
Gotcha: You need to ship Visual C++ Redistributables.
macOS
# Create app bundle
macdeployqt MyApp.app -dmg
# Sign the app (required for distribution)
codesign --deep --force --verify --verbose \
--sign "Developer ID Application: Your Name" \
MyApp.app
# Notarize for Gatekeeper
xcrun altool --notarize-app \
--primary-bundle-id "com.yourcompany.myapp" \
--username "your@email.com" \
--password "@keychain:AC_PASSWORD" \
--file MyApp.dmg
Linux
# Use linuxdeployqt or AppImage
linuxdeployqt MyApp -appimage
# Or create a .deb package
dpkg-deb --build myapp-1.0
Lesson 5: Platform-Specific Code is Sometimes Necessary
#ifdef Q_OS_WIN
// Windows-specific: Set window icon
setWindowIcon(QIcon(":/icons/app.ico"));
#elif defined(Q_OS_MAC)
// macOS-specific: Set dock icon
QApplication::setAttribute(Qt::AA_DontShowIconsInMenus);
#elif defined(Q_OS_LINUX)
// Linux-specific: Set window class for desktop integration
QApplication::setDesktopFileName("com.yourcompany.myapp.desktop");
#endif
For native APIs:
#ifdef Q_OS_WIN
#include <windows.h>
void setWindowsTaskbarProgress(int percent) {
// Use Windows COM APIs
ITaskbarList3 *taskbar;
CoCreateInstance(CLSID_TaskbarList, NULL, CLSCTX_INPROC_SERVER,
IID_ITaskbarList3, (void**)&taskbar);
taskbar->SetProgressValue((HWND)winId(), percent, 100);
}
#endif
Lesson 6: Testing on All Platforms is Non-Negotiable
Set up CI/CD:
# GitHub Actions example
name: Build
on: [push, pull_request]
jobs:
build-windows:
runs-on: windows-latest
steps:
- uses: actions/checkout@v2
- name: Install Qt
uses: jurplel/install-qt-action@v2
- name: Build
run: |
qmake
nmake
build-macos:
runs-on: macos-latest
steps:
- uses: actions/checkout@v2
- name: Install Qt
run: brew install qt
- name: Build
run: |
qmake
make
build-linux:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Install Qt
run: sudo apt-get install qt5-default
- name: Build
run: |
qmake
make
Lesson 7: Resource Files Simplify Asset Management
<!-- resources.qrc -->
<RCC>
<qresource prefix="/">
<file>icons/app.png</file>
<file>icons/open.png</file>
<file>styles/dark.qss</file>
</qresource>
</RCC>
// Access resources with :/
QPixmap icon(":/icons/app.png");
QFile styleFile(":/styles/dark.qss");
Benefits:
- Assets are compiled into the executable
- No missing file errors
- Works identically on all platforms
Lesson 8: Keyboard Shortcuts Must Be Platform-Aware
// Qt handles this automatically
QAction *saveAction = new QAction("Save", this);
saveAction->setShortcut(QKeySequence::Save); // Ctrl+S on Win/Linux, Cmd+S on macOS
// For custom shortcuts
#ifdef Q_OS_MAC
QKeySequence customShortcut(Qt::CTRL + Qt::Key_K); // Cmd+K
#else
QKeySequence customShortcut(Qt::CTRL + Qt::Key_K); // Ctrl+K
#endif
Lesson 9: Settings Storage Varies by Platform
// Qt handles platform-specific locations automatically
QSettings settings("MyCompany", "MyApp");
// Windows: HKEY_CURRENT_USER\Software\MyCompany\MyApp
// macOS: ~/Library/Preferences/com.MyCompany.MyApp.plist
// Linux: ~/.config/MyCompany/MyApp.conf
settings.setValue("window/geometry", saveGeometry());
settings.setValue("theme", "dark");
// Read back
QByteArray geometry = settings.value("window/geometry").toByteArray();
restoreGeometry(geometry);
Lesson 10: Performance Profiling is Platform-Specific
// Use Qt's built-in profiler
QElapsedTimer timer;
timer.start();
// Your code here
qDebug() << "Elapsed:" << timer.elapsed() << "ms";
Platform-specific tools:
- Windows: Visual Studio Profiler
- macOS: Instruments
- Linux: Valgrind, perf
Common Pitfalls
1. Assuming Case-Insensitive File Systems
// Works on Windows, breaks on Linux
QFile file("MyFile.txt"); // Actual file: myfile.txt
// Solution: Always match case exactly
2. Hardcoding Font Sizes
// Bad: Looks different on each platform
QFont font("Arial", 12);
// Good: Use system defaults
QFont font = QApplication::font();
font.setPointSize(font.pointSize() + 2); // Relative sizing
3. Ignoring Window Decorations
// Account for title bar height
int contentHeight = height() - menuBar()->height() - statusBar()->height();
Conclusion
Cross-platform Qt development is achievable, but requires attention to:
- Platform-specific paths and conventions
- High-DPI support
- Deployment tooling
- Continuous testing on all targets
Key Takeaways:
- Use Qt’s cross-platform APIs (
QDir,QStandardPaths,QSettings) - Test on all platforms regularly
- Embrace platform-specific code when necessary
- Automate deployment with CI/CD
What cross-platform challenges have you faced? Share your experiences!