一、这次适配和 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 层不是业务层,只是负责:
- 创建 Ability。
- 创建窗口。
- 加载
XComponent。 - 把窗口交给 Qt OpenHarmony 平台插件。
- 启动 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 原来的 QFileInfo、QDir、QImageReader 都更习惯处理普通本地路径;如果把 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。系统文件管理器默认可能仍然走系统图片预览器。
所以这里的经验是:
module.json5的文件类型声明必须做,它是系统识别的前提。- 但不要把"文件管理器一定列出我的应用"当成已完成事实。
- 要准备应用内打开入口,作为用户真实可达的主流程。
- 外部 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原生体验不足,接入 ArkTSPhotoViewPicker - 深入验证文件管理器
打开方式候选策略 - 对公共目录图片编辑后的保存策略做更细权限设计
- 补充多选图片、保存副本、批量操作在鸿蒙公共文件体系下的体验
十六、这次适配最大的经验
Phototonic 这类 Qt Widgets 应用迁移到鸿蒙,最关键的不是"能不能编译过",而是要同时处理三层边界。
第一层是运行时边界:Qt 程序要通过 Ability、XComponent 和 Qt OpenHarmony QPA 插件进入鸿蒙窗口系统。
第二层是文件边界:桌面应用习惯直接读路径,鸿蒙更强调 URI、picker、授权和应用沙箱。图片查看器尤其容易踩这个坑,因为它天然依赖用户文件。
第三层是交互边界:Qt 桌面端菜单、焦点、文件树选择状态,在鸿蒙窗口环境下可能和原桌面行为不完全一致。空 selection、无效 index、弹窗焦点变化,都可能把老代码里的隐含假设暴露出来。
这次适配最后能进入可用状态,靠的不是一次性"大改",而是把问题拆开:
- 先让 Qt Widgets 主窗口进入 HAP。
- 再让图片格式插件和 native library 正确打包。
- 然后解决启动路径和 URI 打开。
- 接着修真实操作触发的崩溃。
- 最后补一个用户真的会用的打开图片入口。
这种顺序比较适合 Phototonic:它不是一个需要重做 UI 的应用,Qt 原来的图片查看体验可以保留;真正要改的是它和鸿蒙系统之间的连接方式。
十七、结语
Phototonic 的适配过程说明了一件事:传统 C++ / Qt 桌面软件迁移到鸿蒙,不一定要推倒重写,但也不能只把 .so 打进包里就算结束。
对图片查看器来说,"能启动"只是第一步。"能让用户打开自己的图片,并且不因为目录、权限、菜单状态崩溃",才算真正接近可用。
这次 Phototonic 的鸿蒙适配,把 Qt Widgets 主体保留下来,把鸿蒙差异集中处理在工程壳、入口函数、URI 缓存、目录选择和平台降级中。这样既保住了原项目的成熟功能,也让后续继续补 Exiv2、系统 picker、文件管理器联动时有了清晰的落点。