鸿蒙PC迁移:Phototonic Qt 图片查看器鸿蒙适配全记录:一次从 Widgets 桌面应用到 HAP 的迁移

一、这次适配和 Electron 项目不太一样

欢迎加入鸿蒙PC开发者社区,共同打造开发者工具生态:鸿蒙PC开发者社区 :https://harmonypc.csdn.net/

项目开源地址(但是需要倒入qt for harmony sdk):https://atomgit.com/OpenHarmonyPCDeveloper/ohos_phototonic

欢迎在PC社区平台申请新建项目:https://atomgit.com/OpenHarmonyPCDeveloper

环境搭建文章:https://blog.csdn.net/weixin_52908342/article/details/161343743

Qt for harmony SDK 规则:

项目需要 "arguments": "-DQT_PREFIX=../qtforharmony",SDK 文件请到 https://atomgit.com/nutpi/qtforharmony_sdk 自取,并放到项目同级目录 qtforharmony 中。

这篇文章记录的是 Phototonic 在 HarmonyOS / OpenHarmony PC 环境中的一次适配过程。

先把项目性质说清楚:Phototonic 不是 Electron 应用,也不是前端项目。它是一个传统的 C++ / Qt Widgets 图片查看器,主界面、缩略图、文件树、图片预览、编辑菜单、快捷键、拖拽、文件操作都建立在 Qt Widgets 这一套桌面模型上。

所以这次适配遇到的问题和 Electron 项目明显不同。Electron 项目常见的核心工作是资源路径、主进程、renderer、Node 能力和 HAP 资源同步;而 Phototonic 的关键点在另一边:

  • 怎样让一个 Qt Widgets 程序进入鸿蒙 Stage 模型
  • 怎样让 libentry.so 作为 HAP native library 被启动
  • 怎样把 Qt 的窗口画到 ArkUI XComponent
  • 怎样把 Qt for OpenHarmony 的 QPA 插件和 Qt 动态库一起打包
  • 怎样处理图片文件 URI、公共目录权限和 Qt 文件模型之间的差异
  • 怎样修掉真实模拟器操作中暴露出来的 Qt 容器越界崩溃

这也是这篇文章和前面那些 Electron 适配记录最大的区别:这里不是把 Web 页面塞进 HAP,而是把一个老牌桌面 Qt 程序接进鸿蒙的 Ability、XComponent 和 native runtime。

本次适配后,Phototonic 已经可以在模拟器中启动、显示 Qt Widgets 主窗口、浏览图片、通过应用内"打开图片"入口选择图片,并且可以响应外部传入的图片 URI。过程中也保留了桌面版代码路径,鸿蒙相关改动尽量收敛在 harmony_pc/ 工程和 Q_OS_OPENHARMONY 条件编译中。

项目本体目录:

text 复制代码
phototonic-master/
├── main.cpp
├── Phototonic.cpp
├── FileSystemTree.cpp
├── Trashcan.cpp
├── phototonic.pro
└── harmony_pc/

鸿蒙适配工程目录:

text 复制代码
phototonic-master/harmony_pc/
├── AppScope/
├── build-profile.json5
├── entry/
│   ├── build-profile.json5
│   ├── libs/arm64-v8a/
│   └── src/main/
│       ├── cpp/CMakeLists.txt
│       ├── ets/
│       └── module.json5
└── oh-package.json5

二、项目起点:原始 Phototonic 是标准桌面 Qt 程序

Phototonic 原本的启动方式非常典型:

cpp 复制代码
int main(int argc, char *argv[]) {
    QApplication app(argc, argv);
    return runPhototonic(app);
}

桌面系统里这样没有问题。程序拿到命令行参数,解析文件或目录,创建 QApplication,再创建 Phototonic 主窗口。

但在鸿蒙上,应用生命周期不是普通桌面进程那套模型。HAP 入口首先进入 ArkTS 的 UIAbility,窗口由 Ability 创建,Qt 窗口需要通过 Qt for OpenHarmony 的平台插件挂到 ArkUI XComponent 上。

因此第一步不是修改业务逻辑,而是建立一个新的鸿蒙工程壳,让它能承载原来的 Qt 程序。

本次新增的 harmony_pc/ 不取代原来的桌面工程。原项目仍然可以继续使用:

bash 复制代码
qmake phototonic.pro
make

鸿蒙侧则通过 harmony_pc/entry/src/main/cpp/CMakeLists.txt 重新组织 C++ 源码,把原来的 Qt 代码编译成:

text 复制代码
libentry.so

这一步的核心思路是:

不复制一份业务代码,不重写 Qt 界面,只新增一个 HarmonyOS PC 外壳,让同一套 C++ / Qt Widgets 代码在鸿蒙工具链下生成 native library。


三、鸿蒙工程壳:Ability + XComponent + Qt QPA

3.1 ArkTS 侧只负责承载窗口

鸿蒙页面入口非常薄,核心就是一个 XComponent

ts 复制代码
XComponent({
  id: this.windowId,
  type: XComponentType.NODE,
  libraryname: 'plugins_platforms_qopenharmony'
})
  .width('100%')
  .height('100%');

它的意义是给 Qt for OpenHarmony 的 QPA 插件提供一个原生绘制承载点。Qt Widgets 主窗口不是用 ArkUI 重写出来的,而是由 Qt 平台插件接管输入、绘制和窗口生命周期。

EntryAbility.ets 中的关键链路是:

ts 复制代码
windowStage.loadContent(this.loadContentUrl, localStore).then(() => {
  qpa.handleJsTopWindowCreated(this.name, this);
});

this.startQtApplication();

startQtApplication() 再调用:

ts 复制代码
qpa.startQtApplication(this);

这里的 qpa 来自:

ts 复制代码
import qpa from 'libplugins_platforms_qopenharmony.so';

也就是说,ArkTS 层不是业务层,只是负责:

  1. 创建 Ability。
  2. 创建窗口。
  3. 加载 XComponent
  4. 把窗口交给 Qt OpenHarmony 平台插件。
  5. 启动 native 侧的 Qt 应用。

3.2 CMake 侧把原始 Qt 源码编进 libentry.so

harmony_pc/entry/src/main/cpp/CMakeLists.txt 中把 Phototonic 根目录的 C++ 文件全部纳入构建:

cmake 复制代码
set(PHOTOTONIC_SOURCES
    ${PHOTOTONIC_ROOT}/main.cpp
    ${PHOTOTONIC_ROOT}/Bookmarks.cpp
    ${PHOTOTONIC_ROOT}/ColorsDialog.cpp
    ${PHOTOTONIC_ROOT}/FileSystemTree.cpp
    ${PHOTOTONIC_ROOT}/ImageViewer.cpp
    ${PHOTOTONIC_ROOT}/Phototonic.cpp
    ${PHOTOTONIC_ROOT}/ThumbsViewer.cpp
    ${PHOTOTONIC_ROOT}/Trashcan.cpp
    ${PHOTOTONIC_ROOT}/phototonic.qrc
)

add_library(entry SHARED ${PHOTOTONIC_SOURCES})

然后链接 Qt for HarmonyOS 相关库:

cmake 复制代码
target_link_libraries(entry PRIVATE
    Qt5::Core
    Qt5::Gui
    Qt5::OhExtras
    Qt5::Svg
    Qt5::Widgets
    Qt5::QOpenHarmonyPlatformIntegrationPlugin
    Qt5::QGifPlugin
    Qt5::QICOPlugin
    Qt5::QJpegPlugin
    Qt5::QSvgPlugin
    Qt5::QTiffPlugin
    Qt5::QWebpPlugin
)

这一步有一个容易忽略的点:图片查看器不能只链接 Qt5::Widgets。如果没有把 JPEG、PNG、SVG、WebP、TIFF 等图片插件一起带进去,主窗口可能能打开,但图片格式支持会残缺。对 Phototonic 这种软件来说,图片格式插件本身就是核心能力的一部分。

3.3 Qt SDK 路径通过 build-profile 传入

entry/build-profile.json5 里通过 CMake 参数指定 Qt for HarmonyOS SDK:

json5 复制代码
"arguments": "-DQT_PREFIX=../../../qt适配借鉴/qtforharmony_sdk"

这样 CMake 可以找到:

text 复制代码
qtforharmony_sdk/lib/cmake/Qt5/Qt5Config.cmake

如果后续换机器或者移动 SDK,主要调整这个 QT_PREFIX 即可。


四、入口函数改造:让 Qt 程序适应 Ability 生命周期

桌面程序一般只会启动一次。但鸿蒙里可能出现几种情况:

  • 从桌面图标启动
  • 从文件打开入口启动
  • 应用已经在前台,再收到新的图片 Want
  • Qt 应用对象已经存在,但需要打开新的路径

因此 main.cpp 里对鸿蒙做了单独处理:

cpp 复制代码
#if defined(Q_OS_OPENHARMONY)
extern "C" int main(int argc, char *argv[]) {
    if (runtime.window) {
        QStringList inputPaths;
        for (int i = 1; i < argc && argv; ++i) {
            inputPaths << QString::fromLocal8Bit(argv[i]);
        }
        openInExistingWindow(harmonyNormalizeInputPaths(inputPaths));
        return 0;
    }

    QApplication *app = qobject_cast<QApplication *>(QCoreApplication::instance());
    ...
    initializePhototonic(*app);
    ...
}

extern "C" int qtmain(int argc, char *argv[]) {
    return main(argc, argv);
}
#endif

这里做了几件事。

第一,导出 extern "C" int main(...)qtmain(...),适配不同 Qt OpenHarmony 示例或插件查找入口的习惯。

第二,如果窗口已经存在,不再重复创建 QApplication 和主窗口,而是把新的路径交给已有窗口:

cpp 复制代码
openInExistingWindow(harmonyNormalizeInputPaths(inputPaths));

第三,鸿蒙传入的路径可能不是普通本地路径,而是:

text 复制代码
file://...
content://...

因此增加了 URI 归一化:

cpp 复制代码
QString harmonyPathFromArgument(const QString &argument) {
    if (!argument.startsWith("file://") && !argument.startsWith("content://")) {
        return argument;
    }

    const QString resolvedPath = QtOh::pathFromUri(argument);
    if (!resolvedPath.isEmpty()) {
        return resolvedPath;
    }

    const QUrl url(argument);
    if (url.isLocalFile()) {
        return url.toLocalFile();
    }

    return argument;
}

这段逻辑看起来不大,但很关键。因为 Qt 原来的 QFileInfoQDirQImageReader 都更习惯处理普通本地路径;如果把 content:// 原样丢进去,很多桌面端逻辑会直接失效。


五、依赖取舍:先让图片浏览主流程稳定,再谈 Exiv2

Phototonic 桌面版包含一部分 Exif/IPTC/XMP 元数据能力,这依赖 Exiv2。桌面 Linux/macOS 上装好库就能用,但鸿蒙 arm64-v8a 下不能假设系统已经提供。

这次适配没有在第一阶段强行把 Exiv2 搬进去,而是在 CMake 里做了开关:

cmake 复制代码
option(PHOTOTONIC_ENABLE_EXIV2 "Enable Exiv2 metadata support" OFF)

默认关闭时,核心图片浏览、目录浏览、缩略图、查看、编辑、保存等流程先跑起来;元数据相关功能做降级提示或隐藏:

cpp 复制代码
#if !defined(PHOTOTONIC_ENABLE_EXIV2)
    MessageBox msgBox(this);
    msgBox.warning(tr("Remove Metadata"),
                   tr("Removing Exif metadata is not available in this build."));
    return;
#else
    ...
#endif

这个选择是务实的。图片查看器的第一优先级是能打开图片、能浏览、能切换、不卡死、不崩溃。Exif 元数据属于增强能力,应该等基础链路稳定后再单独准备鸿蒙侧 libexiv2.so 和依赖库。


六、启动目录问题:不要让用户看到 bundle/libs

第一次跑起来时,有一个很不桌面的现象:应用默认目录可能落在 HAP native library 所在位置,比如:

text 复制代码
/data/storage/el1/bundle/libs/arm64

这对 Qt 程序来说并不奇怪,因为进程启动环境和桌面不一样。但对用户来说,这个路径没有意义,也不应该作为图片查看器默认目录。

因此在 Phototonic.cpp 中增加了 HarmonyOS 专用默认目录判断:

cpp 复制代码
#if defined(Q_OS_OPENHARMONY)
bool isHarmonyBundleLibPath(const QString &path) {
    return path.startsWith(QLatin1String("/data/storage/el1/bundle/libs"));
}

QString harmonyDefaultStartDirectory() {
    const QStringList candidateDirs = {
        QStandardPaths::writableLocation(QStandardPaths::PicturesLocation),
        QStandardPaths::writableLocation(QStandardPaths::HomeLocation),
        QDir::homePath()
    };

    for (const QString &candidateDir : candidateDirs) {
        if (!candidateDir.isEmpty() && QDir(candidateDir).exists()) {
            return candidateDir;
        }
    }

    return QDir::rootPath();
}
#endif

然后在处理启动参数后兜底:

cpp 复制代码
#if defined(Q_OS_OPENHARMONY)
if (Settings::currentDirectory.isEmpty() || isHarmonyBundleLibPath(Settings::currentDirectory)) {
    Settings::currentDirectory = harmonyDefaultStartDirectory();
}
#endif

这样至少不会让应用默认停在打包库目录里。后面真实测试又发现,鸿蒙公共下载目录不是简单的桌面路径映射,这个问题会在文件打开部分继续处理。


七、最真实的坑:输入 /storage/Users/currentUser/Download 并不是正确用法

在模拟器里测试图片打开时,最开始很容易按桌面思维去找下载目录,比如输入:

text 复制代码
/storage/Users/currentUser/Download

但实际模拟器里的文件管理器下载目录对应位置是:

text 复制代码
/mnt/user/100/currentUser/filemgr/Download

其中测试图片是:

text 复制代码
/mnt/user/100/currentUser/filemgr/Download/img3.png

这时会出现一个非常典型的迁移误区:开发者知道真实路径,但普通用户不应该知道,也不应该让用户手输。更重要的是,第三方应用并不能像传统桌面应用一样随意扫描公共目录。

所以这里的结论很明确:

对用户来说,正确流程不是在图片查看器里手输系统路径,而应该是从应用内打开系统文件选择器,或者由系统文件打开 Want 把 URI 传给应用。

这也是后续补"打开图片"入口的原因。

八、外部图片打开:URI 先复制到应用缓存,再交给 Qt

鸿蒙文件管理器或系统能力传进来的可能是 URI,而不是普通文件路径。Phototonic 的 C++ 逻辑更适合处理本地路径,所以 ArkTS 侧先把外部 URI 复制到应用 cache 目录:

ts 复制代码
private async cacheExternalUri(uri: string): Promise<string> {
  if (uri === '' || (!uri.startsWith('file://') && !uri.startsWith('content://'))) {
    return uri;
  }

  let destinationPath = this.cachedOpenPath(uri);
  let destinationUri = fileUri.getUriFromPath(destinationPath);

  try {
    await fs.copy(uri, destinationUri);
    hilog.info(this.domain, this.tag, 'Cached external URI to %{public}s', destinationPath);
    return destinationPath;
  } catch (copyError) {
    hilog.warn(this.domain, this.tag, 'fs.copy failed for URI %{public}s: %{public}s',
      uri, JSON.stringify(copyError));
  }

  try {
    await fs.copyFile(uri, destinationPath, 0);
    hilog.info(this.domain, this.tag, 'Copied external URI to %{public}s', destinationPath);
    return destinationPath;
  } catch (copyFileError) {
    hilog.warn(this.domain, this.tag, 'fs.copyFile failed for URI %{public}s: %{public}s',
      uri, JSON.stringify(copyFileError));
  }

  return uri;
}

启动参数组装时:

ts 复制代码
async launchParamSet() {
  let params: string[] = [];
  if (this.launchWant.uri !== undefined && this.launchWant.uri !== '') {
    params.push(await this.cacheExternalUri(this.launchWant.uri));
  }

  this.launchParams = params.join(' ');
}

应用已经启动后,再收到新的 Want,也走同样的缓存逻辑:

ts 复制代码
private async prepareNewWant(want: Want, launchParam: AbilityConstant.LaunchParam) {
  if (want.uri !== undefined && want.uri !== '') {
    want.uri = await this.cacheExternalUri(want.uri);
  }
  qpa.handleOnNewWant(want, launchParam);
}

这条链路验证过:使用 aa start 显式传入下载目录中的图片 URI,可以启动 Phototonic 并显示图片。

示例命令:

bash 复制代码
hdc shell aa start \
  -a FileOpenAbility \
  -b com.phototonic.viewer \
  -m entry \
  -U 'file://docs/storage/Users/currentUser/Download/img3.png' \
  -A ohos.want.action.viewData \
  -t general.png \
  -e entity.system.default

这里有一个现阶段限制:模拟器文件管理器的"双击图片"默认仍然会走系统预览器,打开方式 中也不一定列出 Phototonic。也就是说,manifest 里声明图片 UTD/MIME 是必要工作,但文件管理器是否把第三方应用列入候选,还受到系统文件管理器实现影响。

因此对当前版本更可靠的用户入口,是应用内主动打开图片。

九、补一个真正给用户用的入口:文件 -> Open Images...

Phototonic 原本更偏桌面目录浏览模式。迁移到鸿蒙后,真实用户不应该被要求记路径、输路径、猜公共目录。因此补了一个直接入口:

cpp 复制代码
void Phototonic::openImages() {
    if (Settings::slideShowActive) {
        toggleSlideShow();
    }

    QStringList imageFiles = QFileDialog::getOpenFileNames(this, tr("Open Images"), QString(),
        tr("Images") + " (*.jpg *.jpeg *.png *.gif *.bmp *.webp *.tif *.tiff *.svg);;" +
        tr("All Files") + " (*)");

    if (imageFiles.isEmpty()) {
        return;
    }

    openPaths(imageFiles);
}

菜单和工具栏增加:

cpp 复制代码
openImagesAction = new QAction(tr("Open Images..."), this);
openImagesAction->setObjectName("openImages");
openImagesAction->setIcon(QIcon::fromTheme("document-open", QIcon(":/images/open.png")));
connect(openImagesAction, SIGNAL(triggered()), this, SLOT(openImages()));

同时注册快捷键:

cpp 复制代码
openImagesAction->setShortcut(QKeySequence("Ctrl+O"));

这一步看起来像一个小功能,但它解决的是"用户怎样真实打开图片"的问题。适配不是把应用强行跑起来就结束,入口设计也要符合目标系统的权限模型。

后续如果 Qt for OpenHarmony 的 QFileDialog 在某些系统版本中不能接到原生 picker,可以继续把这个入口替换为 ArkTS PhotoViewPicker / DocumentViewPicker,再复用上面已经打通的 cacheExternalUri() 逻辑,把 URI 转成本地缓存文件后交给 Qt。

十、崩溃定位:QList::operator\[\] index out of range

这次模拟器测试中遇到过一次真实崩溃。日志核心信息是:

text 复制代码
ASSERT failure in QList<T>::operator[]: "index out of range"
QList<QModelIndex>::operator[](int)
Phototonic::createSubDirectory()

调用链说明用户点击了菜单中的"创建子目录"之类的目录操作,但当前文件树没有有效选中项。原代码直接取 selectedRows()[0],在没有选中目录时就会越界。

桌面环境下这个问题可能不容易被触发,因为焦点、右键菜单和文件树选择状态更符合原作者预期。但在鸿蒙 Qt 窗口、菜单弹窗、XComponent 输入事件协作下,菜单触发时不一定还保留有效选中行。

修复方式不是简单加一个 if,而是把目录选择逻辑集中起来:

cpp 复制代码
QModelIndex selectedDirectoryIndex(FileSystemTree *fileSystemTree) {
    if (!fileSystemTree || !fileSystemTree->selectionModel()) {
        return QModelIndex();
    }

    QModelIndexList selectedDirs = fileSystemTree->selectionModel()->selectedRows();
    if (!selectedDirs.isEmpty() && selectedDirs.first().isValid()) {
        return selectedDirs.first();
    }

    QModelIndex currentDir = fileSystemTree->currentIndex();
    if (!currentDir.isValid()) {
        return QModelIndex();
    }

    return currentDir.sibling(currentDir.row(), 0);
}

然后在目录重命名、删除、新建子目录、获取当前路径等地方统一使用它。

createSubDirectory() 的处理变成:

cpp 复制代码
QModelIndex selectedDir = selectedDirectoryIndex(fileSystemTree);
QString parentDir = selectedDir.isValid()
                    ? fileSystemTree->fileSystemModel->filePath(selectedDir)
                    : Settings::currentDirectory;

if (parentDir.isEmpty() || !QFileInfo(parentDir).isDir()) {
    setStatus(tr("No directory selected"));
    return;
}

同时 FileSystemTree::getCurrentIndex() 也做了兜底:

cpp 复制代码
QModelIndex FileSystemTree::getCurrentIndex() {
    const QModelIndexList indexes = selectedIndexes();
    if (!indexes.isEmpty()) {
        return indexes.first();
    }

    return currentIndex();
}

这个问题给人的提醒很直接:桌面 Qt 程序移到鸿蒙以后,不能默认菜单触发、焦点状态、文件树选择状态完全和传统桌面一致。凡是从 Qt model/view 里取 first()[0]selectedRows()[0] 的地方,都应该重新看一遍。

十一、文件操作能力也要按鸿蒙环境降级

Phototonic 桌面版支持把文件移动到回收站。Linux 桌面上可以走 freedesktop trash spec,但鸿蒙上不能照搬这套路径和规范。

因此 Trashcan.cpp 中对鸿蒙做了明确降级:

cpp 复制代码
#if defined(Q_OS_OPENHARMONY)

Trash::Result Trash::moveToTrash(const QString &path, QString &error, Trash::Options trashOptions)
{
    Q_UNUSED(path);
    Q_UNUSED(trashOptions);
    error = "Putting files into trashcan is not supported on HarmonyOS yet";
    return Trash::Error;
}

#endif

这类处理不华丽,但必要。适配时最怕的是保留一个看似可用的按钮,实际执行到一半破坏用户文件。对于文件删除、移动、回收站这种高风险操作,宁愿先明确提示不支持,也不要假装兼容。


十二、module.json5:图片打开声明做了,但不能迷信它

为了让系统知道 Phototonic 是图片查看器,module.json5 中给 Ability 增加了 viewData 和图片类型声明,例如:

json5 复制代码
{
  "actions": [
    "ohos.want.action.viewData"
  ],
  "entities": [
    "entity.system.default"
  ],
  "uris": [
    {
      "scheme": "file",
      "type": "general.image",
      "linkFeature": "FileOpen"
    },
    {
      "scheme": "content",
      "type": "general.png",
      "linkFeature": "FileOpen"
    },
    {
      "scheme": "file",
      "type": "image/jpeg",
      "linkFeature": "FileOpen"
    }
  ]
}

同时单独配置了 FileOpenAbility,用于处理文件打开场景。

但实际测试表明,仅靠这些声明,并不能保证模拟器文件管理器双击图片时一定出现 Phototonic。系统文件管理器默认可能仍然走系统图片预览器。

所以这里的经验是:

  1. module.json5 的文件类型声明必须做,它是系统识别的前提。
  2. 但不要把"文件管理器一定列出我的应用"当成已完成事实。
  3. 要准备应用内打开入口,作为用户真实可达的主流程。
  4. 外部 Want 打开可以通过命令和系统能力验证,但最终还要看目标系统文件管理器策略。

十三、构建与安装命令

本次命令行构建使用 DevEco 自带 Node 和 Hvigor:

bash 复制代码
cd ~/XM/phototonic-master/harmony_pc

DEVECO_SDK_HOME=/Applications/DevEco-Studio.app/Contents/sdk \
/Applications/DevEco-Studio.app/Contents/tools/node/bin/node \
/Applications/DevEco-Studio.app/Contents/tools/hvigor/bin/hvigorw.js \
--mode module \
-p module=entry@default \
-p product=default \
-p requiredDeviceType=2in1 \
assembleHap \
--analyze=normal \
--parallel \
--incremental \
--no-daemon

构建产物:

text 复制代码
harmony_pc/entry/build/default/outputs/default/entry-default-unsigned.hap

安装到模拟器:

bash 复制代码
/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/toolchains/hdc uninstall com.phototonic.viewer

/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/toolchains/hdc install \
  entry/build/default/outputs/default/entry-default-unsigned.hap

启动:

bash 复制代码
/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/toolchains/hdc shell aa start \
  -a EntryAbility \
  -b com.phototonic.viewer \
  -m entry

十四、模拟器验证过程

这次不是只停留在编译通过,而是在模拟器里做了多轮真实操作。

14.1 基础启动

验证点:

  • HAP 可以安装
  • EntryAbility 可以启动
  • Qt 主窗口可以显示
  • 菜单栏、工具栏、文件树、缩略图区可以渲染
  • 应用不会立即退出

14.2 图片 URI 打开

模拟器下载目录中准备了图片:

text 复制代码
/mnt/user/100/currentUser/filemgr/Download/img3.png

通过 aa start 传入图片 URI 后,Phototonic 能够打开图片。这说明:

  • FileOpenAbility 能启动
  • ArkTS 能拿到 want.uri
  • fs.copy / copyFile 能把外部 URI 复制到 cache
  • Qt 侧能拿到本地缓存路径
  • openPaths() 能加载图片

实际运行效果可以看到,Qt Widgets 主窗口、文件树、缩略图区域、预览区域和图片信息面板都已经显示出来,测试图片 img3.png 也进入了 Phototonic 的图片列表。

14.3 菜单操作崩溃

触发过一次 createSubDirectory() 越界崩溃。修复后重新构建安装,目录操作不再因为空选择直接崩溃,而是走状态提示或当前目录兜底。

14.4 用户打开图片路径

测试中确认,手输 /storage/Users/currentUser/Download 不是合适方式。真实用户流程应当是:

text 复制代码
文件 -> Open Images... -> 选择图片 -> 打开

这也是最终给应用补入口的原因。


十五、当前可用状态

目前 Phototonic 鸿蒙适配版已经具备这些能力:

  • 在 HarmonyOS / OpenHarmony PC 模拟器中作为 HAP 安装启动
  • 显示 Qt Widgets 主窗口
  • 支持菜单栏、工具栏、缩略图区、图片查看区
  • 支持常见图片格式插件随包进入运行环境
  • 支持从外部 Want 接收图片 URI
  • 支持把外部图片 URI 复制到应用 cache 后打开
  • 支持应用内"Open Images..."入口打开图片
  • 支持应用已启动时继续接收新的打开路径
  • 避免启动目录落到 /data/storage/el1/bundle/libs/arm64
  • 修复文件树无选中项时创建目录导致的 QList 越界崩溃
  • 对鸿蒙暂不支持的回收站能力做明确降级
  • Exiv2 元数据能力暂时通过编译开关隔离,主流程不被阻塞

还有一些后续可以继续完善的地方:

  • 构建鸿蒙 arm64-v8a 版本 Exiv2,恢复完整 Exif/IPTC/XMP 能力
  • 如果目标设备上的 QFileDialog 原生体验不足,接入 ArkTS PhotoViewPicker
  • 深入验证文件管理器 打开方式 候选策略
  • 对公共目录图片编辑后的保存策略做更细权限设计
  • 补充多选图片、保存副本、批量操作在鸿蒙公共文件体系下的体验

十六、这次适配最大的经验

Phototonic 这类 Qt Widgets 应用迁移到鸿蒙,最关键的不是"能不能编译过",而是要同时处理三层边界。

第一层是运行时边界:Qt 程序要通过 Ability、XComponent 和 Qt OpenHarmony QPA 插件进入鸿蒙窗口系统。

第二层是文件边界:桌面应用习惯直接读路径,鸿蒙更强调 URI、picker、授权和应用沙箱。图片查看器尤其容易踩这个坑,因为它天然依赖用户文件。

第三层是交互边界:Qt 桌面端菜单、焦点、文件树选择状态,在鸿蒙窗口环境下可能和原桌面行为不完全一致。空 selection、无效 index、弹窗焦点变化,都可能把老代码里的隐含假设暴露出来。

这次适配最后能进入可用状态,靠的不是一次性"大改",而是把问题拆开:

  1. 先让 Qt Widgets 主窗口进入 HAP。
  2. 再让图片格式插件和 native library 正确打包。
  3. 然后解决启动路径和 URI 打开。
  4. 接着修真实操作触发的崩溃。
  5. 最后补一个用户真的会用的打开图片入口。

这种顺序比较适合 Phototonic:它不是一个需要重做 UI 的应用,Qt 原来的图片查看体验可以保留;真正要改的是它和鸿蒙系统之间的连接方式。

十七、结语

Phototonic 的适配过程说明了一件事:传统 C++ / Qt 桌面软件迁移到鸿蒙,不一定要推倒重写,但也不能只把 .so 打进包里就算结束。

对图片查看器来说,"能启动"只是第一步。"能让用户打开自己的图片,并且不因为目录、权限、菜单状态崩溃",才算真正接近可用。

这次 Phototonic 的鸿蒙适配,把 Qt Widgets 主体保留下来,把鸿蒙差异集中处理在工程壳、入口函数、URI 缓存、目录选择和平台降级中。这样既保住了原项目的成熟功能,也让后续继续补 Exiv2、系统 picker、文件管理器联动时有了清晰的落点。

相关推荐
伶俜661 小时前
鸿蒙原生应用实战(十五)ArkUI 健康计步器:加速度传感器 + 峰值检测 + SQLite 存储 + 周报统计
华为·harmonyos
knighthood20011 小时前
鸿蒙PC迁移:KeePassXC Qt 密码管理器鸿蒙PC适配全记录
qt·华为·harmonyos
Swift社区1 小时前
鸿蒙 PC 正在诞生“第二操作系统”:Agent Runtime 架构揭秘
华为·架构·harmonyos
luoyayun3611 小时前
Qt 中使用 QtConcurrent::run + QFutureWatcher 实现异步处理
qt·异步·qtconcurrent
不良使1 小时前
鸿蒙PC迁移_LocalSend 迁移到鸿蒙 PC:一次 Flutter + Rust + 三方库适配的完整记录
flutter·rust·harmonyos
小鹏linux2 小时前
鸿蒙PC使用 Electron 迁移:LX Music 桌面版适配全记录
华为·electron·harmonyos
古德new2 小时前
鸿蒙PC迁移:使用Electron`yesplaymusic-ohos` 鸿蒙迁移实战与适配全记录
华为·electron·harmonyos
鸽芷咕2 小时前
鸿蒙PC迁移:Minitube Qt YouTube 客户端鸿蒙PC适配全记录
qt·华为·harmonyos