Различные статьи по Qt и c++

Кросс-платформенный Qt-проект: общий код для Windows, Linux и macOS — подводные камни

Одно из главных преимуществ Qt — возможность писать единую базу кода для Windows, Linux и macOS. На практике, однако, «однажды написал — везде работает» срабатывает не всегда: различия в файловых системах, поведении компиляторов и системных API требуют аккуратной архитектуры. Разберём типичные проблемы и способы их обхода.

Базовая структура кросс-платформенного проекта

cmake_minimum_required(VERSION 3.16)
project(CrossPlatformApp LANGUAGES CXX)

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_AUTOMOC ON)

find_package(Qt6 REQUIRED COMPONENTS Core Widgets)

add_executable(CrossPlatformApp main.cpp)

# Платформо-зависимый код подключаем отдельными файлами
if (WIN32)
    target_sources(CrossPlatformApp PRIVATE platform_win.cpp)
elseif (APPLE)
    target_sources(CrossPlatformApp PRIVATE platform_mac.mm)
elseif (UNIX)
    target_sources(CrossPlatformApp PRIVATE platform_linux.cpp)
endif()

target_link_libraries(CrossPlatformApp PRIVATE Qt6::Core Qt6::Widgets)

Здесь главная идея — разделить общий код и платформо-специфичный, не размазывая #ifdef по всему проекту, а локализуя его в отдельных файлах.

Пример: абстракция над платформенным кодом

// platform.h — общий интерфейс
#pragma once
#include <QString>

namespace Platform {
    QString configDirectory();
    void showNativeNotification(const QString &title, const QString &message);
}
// platform_win.cpp
#include "platform.h"
#include <QStandardPaths>

namespace Platform {
QString configDirectory() {
    return QStandardPaths::writableLocation(QStandardPaths::AppConfigLocation);
}

void showNativeNotification(const QString &title, const QString &message) {
    // На Windows можно использовать системные toast-уведомления через WinRT API
    qDebug() << "[Windows]" << title << message;
}
}
// platform_linux.cpp
#include "platform.h"
#include <QProcess>

namespace Platform {
QString configDirectory() {
    return QStandardPaths::writableLocation(QStandardPaths::AppConfigLocation);
}

void showNativeNotification(const QString &title, const QString &message) {
    QProcess::startDetached("notify-send", {title, message});
}
}

В большинстве случаев QStandardPaths уже абстрагирует различия в путях между ОС, и платформо-зависимый код нужен только для специфичных вещей — системных уведомлений, интеграции с Dock/Taskbar и т.п.

Подводные камни

1. Различия в путях файловой системы.

Windows использует обратный слэш \, разделители дисков C:\, а пути могут быть длиной больше 260 символов только при специальном префиксе. Qt внутри всегда использует /, и QDir::toNativeSeparators() нужно вызывать только при выводе пути пользователю или при вызове внешних не-Qt API:

QString path = "C:/Users/user/file.txt";
QString nativePath = QDir::toNativeSeparators(path); // "C:\Users\user\file.txt" на Windows

2. Регистронезависимость файловой системы.

На Windows и (по умолчанию) macOS файловая система регистронезависима, на Linux — регистрозависима. Код, который работает на Windows с файлом «Photo.JPG», может не найти файл «photo.jpg» на Linux. Всегда явно контролируйте регистр при сравнении и поиске файлов.

3. Переводы строк (line endings).

Windows использует \r\n, Unix-системы — \n. При работе с текстовыми файлами используйте QIODevice::Text флаг при открытии, чтобы Qt сам нормализовал переводы строк:

QFile file("data.txt");
if (file.open(QIODevice::ReadOnly | QIODevice::Text)) {
    QTextStream in(&file);
    // \r\n и \n обрабатываются одинаково
}

4. Различия в системе шрифтов и DPI.

На macOS интерфейс рассчитан на Retina-дисплеи «из коробки», тогда как на Windows и Linux пользователь может вручную менять масштаб. Тестируйте интерфейс при масштабах 100%, 150%, 200%.

5. Поведение модальных диалогов и меню.

На macOS системное меню приложения находится наверху экрана, а не в окне. Используйте QMenuBar без явного родителя — Qt сам разместит меню правильно на каждой платформе:

QMenuBar *menuBar = new QMenuBar(nullptr); // без родителя — Qt сам решит, где разместить
QMenu *fileMenu = menuBar->addMenu("Файл");

6. Различия компиляторов.

MSVC, GCC и Clang по-разному трактуют некоторые тонкости стандарта C++ (например, порядок инициализации статических членов в разных единицах компиляции). Включайте предупреждения максимально строго на всех трёх платформах в CI:

if (MSVC)
    target_compile_options(CrossPlatformApp PRIVATE /W4)
else()
    target_compile_options(CrossPlatformApp PRIVATE -Wall -Wextra -Wpedantic)
endif()

7. Динамические библиотеки и deployment.

На Windows нужно явно копировать Qt DLL рядом с .exe (или использовать windeployqt), на Linux — настраивать RPATH или AppImage, на macOS — использовать macdeployqt для формирования .app-бандла с правильными @rpath-ссылками.

# Windows
windeployqt.exe MyApp.exe

# macOS
macdeployqt MyApp.app -dmg

Совет

Настройте CI с тремя параллельными джобами (Windows/Linux/macOS) с самого начала проекта, а не в конце разработки. Платформо-специфичные ошибки компиляции и поведения проще находить и фиксить, когда между их появлением и обнаружением проходят минуты, а не недели.

================================================================================