📦 desktop_drop 用于接收拖入 Flutter 页面的文件。我用鸿蒙电脑双窗口 Example 复现了放置框下半区不响应、收到文件后无法读取的问题,并修正坐标换算与文件路径交付。
完整代码已提交 PR 至官方仓库 CPF-Flutter/fluttertpc_desktop_drop,有问题可以到仓库提 Issue,也可以来 鸿蒙系统Flutter开源库社区 交流。

前言

我做了一个鸿蒙电脑附件校验页面:左边的小窗口放几份本地文档,右边的 Flutter 窗口接收文件,显示大小和首行摘要。接入社区版 desktop_drop 后,我把文档拖到框的下半区,页面没有反应;往中间挪一些,回调来了,读取内容却报错。

我先查鼠标落点,再查收到的文件地址。原来,插件算坐标时漏掉了窗口标题栏的高度,读取时又把带协议头的 URI 当成了文件路径。改完坐标和路径转换后,我顺着交付链路继续补了异步复制、接收区二次判断与失败清理,再回到同一台电脑模拟器上重新跑完两次拖入,确认两份附件都能准确命中并读出摘要。

一、使用场景与原版问题

1.1 把附件拖进校验页面

用户把说明文档或配置文件拖进框里,就能查看文件名、字节数和开头内容,省去复制路径、再打开编辑器查看的操作。我把 Example 的功能停在预览这一步:文件格式是否符合业务要求、是否允许上传,留给接入插件的应用判断。

为了让每次拖入的内容和窗口位置都能对上,我在 Example 里放了两个窗口。左侧用 ArkUI 发起 UDMF 文件拖拽,右侧通过 Flutter 引擎把事件传给 desktop_drop。两个窗口属于同一个应用、同一个沙箱,文件也是 Example 自己生成的;这组操作没有经过系统文件管理器或另一款应用。

系统负责产生拖拽事件、提供窗口位置和处理文件授权,desktop_drop 把坐标和文件标识转成 Dart 能用的数据,Example 再据此改变边框颜色、读取摘要。这次补丁集中在插件的坐标换算和文件交付上,电脑设备声明仍由宿主配置,JSON 字段校验则由业务处理。

我检查这次拖入是否成功,先看松手位置:框内接收,框外不开始读取。收到文件后,再调用 length() 或 openRead() 确认内容可读。边框变色、列表多出一条,只说明页面收到了事件,还不能保证预览成功。

同一台鸿蒙电脑中从样本窗口拖入附件预览,经系统事件、desktop_drop 与 Flutter 页面显示摘要

图1 附件拖入流程

1.2 检查已有库,再确定修复基线

我先检查了上游 MixinNetwork/flutter-plugins 的 desktop_drop 子目录和 pub.dev 发布页,又查了 oh-Flutter、CPF-Flutter 中与这个包对应的仓库入口。上游提供通用 Dart 接口和各平台实现,CPF-Flutter 的适配分支里已经有 ohos 原生代码。我便沿着这份实现继续排查,在已有包里修正问题。

MixinNetwork 仓库中的 desktop_drop 目录与 0.9.2 发布提交

图2 上游源码

查库时,上游发布版是 0.9.2,最低要求 Flutter 3.47.7、Dart 3.13.5,拖拽 API 也已经换过一套。我使用的是 Flutter 3.41.9 的鸿蒙工具链,社区适配包则基于 0.6.1,所以没有直接切到上游最新版。下文的 onDragDone、DropDoneDetails 和 DropItem 都是旧版接口,接入时要看对应版本的示例。

desktop_drop 的 pub.dev 发布页面

图3 包发布页

我逐文件对照了社区 br_3.27_dev 分支与稳定标签 desktop_drop-0.6.1-ohos.1.0.0,两个固定提交中的 packages/desktop_drop 源码一致。许可也要看子包:desktop_drop 的 LICENSE 是 Apache-2.0,多包仓库主页显示的是根目录许可。至于 oh-Flutter,我在这次检查的对应包名入口中没有找到另一份适配仓。

CPF-Flutter 仓库的 br_3.27_dev 分支和项目介绍

图4 社区适配仓

打开社区网页时,文件列表提示访问频次受限,我便用匿名 Git 取回固定提交,继续对照源码。图里的页面保留了仓库来源,具体实现以取回的源码为准。社区原始标签中还没有本文的修复补丁;主线合入前,完整的插件修订与双窗口 Example 保存在公开复现分支 fix/ohos-desktop-drop-window-offset 中。

1.3 框内松手,却漏接或读不出文件

我用上述社区 0.6.1 的鸿蒙实现运行 Example,把带标题栏的主窗口摆在桌面右侧。Flutter 放置框相对渲染视图的位置是横向 24~934、纵向 84~269 逻辑像素。在这个布局下,复现步骤如下:

  1. 从左侧样本窗拖动 release_notes.md,落到右侧框的下半区。预期显示高亮并读取文档;原版没有触发完成回调。
  2. 将 window_metrics.json 拖到框的中部。预期读出配置摘要;原版触发了回调,却在读取长度时抛出 PathNotFoundException。
  3. 在相同窗口布局和样本下更换插件实现,重复两个动作,核对落点、接收条目和读取结果。

第一份文件连完成回调都没来,我先看插件有没有把鼠标判到框外;第二份文件已经触发回调,就继续查 Dart 收到的地址,以及读取报错的位置。

检查工程时,我还遇到了另一处问题:仓库里留有未被入口导出的平台接口模板及模板测试,其中引用了未声明的依赖,静态分析曾报告 14 项问题。我清理了这些遗留文件,让全包检查能继续运行。它们与拖拽故障分开处理,鼠标下半区不响应仍要回到坐标代码里找原因。

二、同条件对照与验证范围

2.1 窗口、样本与工具链

我在 MateBook Pro 电脑模拟器上做对照,设备类型为 2in1,镜像是 7.0.0.107,系统属性回报 OpenHarmony-7.0.0.105,API 26。屏幕分辨率为 3120 × 2080 物理像素,像素密度比为 1.90。开发工具是 Flutter 3.41.9+ohos-1.0.2、Dart 3.11.5 和 DevEco Studio 26.0.0.865。宿主构建配置中的兼容版本、目标版本都设为 5.1.0(API 18),与运行镜像的版本不同。

两个窗口的位置由同一套 Example 布局代码设置。左侧样本窗的物理矩形为 [280, 340, 700, 1000],右侧主窗为 [1080, 260, 1820, 1260],四个数依次是左、上、宽、高。右窗离桌面原点有一段距离,标题栏在这次布局中占 36.84 逻辑像素。我分别用 com.example.pgc21base、com.example.pgc21fix 两个包名安装原版和修复版,窗口布局、页面与样本保持一致。

下面的截图、动图和日志都在这份包含坐标对齐、路径转换与双阶段交付校验的修订上采集,记录了两个小文件的落点与读取过程。跨应用外部 URI 授权、媒体和大文件等未测边界列在第四节。

2.2 两次拖入的结果

原版中下半区未接收,第二份文件读取失败

图5 原版拖入

修复版本接收并校验两份同沙箱文件

图6 修复拖入

两段动图分别约 4.3 秒和 6.5 秒,保留了电脑模拟器的设备外框。我先把文件拖到框的下半区,落点在 Flutter 视图中约为 (478.9, 247.4)。鼠标还在框内,原版拿去判断的纵坐标却是 284.2,已经超过下边界 269。扣掉标题栏偏移后,修复版收到的框内局部坐标约为 (454.9, 163.4),随后读出了 220 B 的说明文档。

接着拖入中部的 JSON 文件,原版能触发完成回调,但传给 Dart 的地址仍带着 file:// 协议头,读取就卡在这里。修复版把地址转成同一沙箱里的普通路径,读出了 70 B 的 JSON 内容,页面上也出现了摘要。这两个样本走的都是同沙箱路径转换,没有经过跨应用复制。

原版校验页面中的读取异常与偏移坐标

图7 原版结果

修复后页面列出两份可读样本

图8 修复结果

观测项目社区原版当前修复版
下半区释放后的完成回调0 次1 次 🟢↑
两次操作中可读文件数0 份2 份 🟢↑
中部落点的纵向偏差约 +36.8 逻辑像素与测得的布局换算一致
读取内容长度读取报错文档 220 B、JSON 70 B

表1 同条件结果

我这里只重复了两种固定动作,没有采集成功率、延迟分布或大文件吞吐量。两次操作中,鼠标的横向落点还有些轨迹差异,所以我没有拿它计算横向精度。纵向则能和窗口几何数据对上:漏掉的标题栏高度,正好解释了框内下半区为什么会被判成框外。

2.3 顺着日志继续排查

原版读取异常与修复版本的拖拽交付日志

图9 运行日志

我把窗口位置、进入事件坐标、文件路径转换和业务读取结果放在一起看。修复版的日志依次记下了进入坐标、同沙箱文件直通、Drop delivery finished: accepted=true 回执和页面的首行摘要。日志里还有个容易让人误判的细节:已经采集到的进入事件中,getDisplayX/Y() 和 getWindowX/Y() 打印出的数值相同。只看这一行,还分不清数值相同发生在哪一层。

于是我检查了对应 FlutterPage 的事件转发代码:它直接把原始 DragEvent 交给 FlutterManager,并没有改写坐标。我能确认的原因是,插件没有扣除渲染表面在窗口内的偏移,通道两端又混用了像素密度。修改时选用窗口坐标接口,是因为它明确以窗口为参照;不能把日志里的相同数值解释为引擎替插件换算过。

修改交付逻辑后,我用 Dart 测试检查框内、框外、禁用、销毁、缺少业务回调,以及复制期间出现新悬停位置的情况。原生逻辑测试则用替身文件系统和通道构造失败条件,检查复制失败、长度不符、目录拒绝、回执超时及生命周期变化。这样可以看到某条失败路径有没有清理文件,但 UDMF 的临时授权和系统结束动效仍需要实际拖拽来检查。

三、原理与核心代码

3.1 坐标必须落在同一个参照系

🔍 查坐标时,我先分清三个起点:整个桌面的左上角、应用窗口的左上角,以及 Flutter 内容的左上角。窗口可以挪动,窗口里面还可能有标题栏;Flutter 内容从标题栏下面开始。插件要拿落点和 Flutter 组件的范围比较,就得先把它们放到同一个坐标系里。

我从当前 Ability 的主窗口取得 UIContext,找到挂载的 FlutterView 节点,再调用 getPositionToWindow() 读取渲染表面相对窗口的偏移。窗口坐标减去这个偏移,才是 Dart 需要的视图坐标。DesktopDropPlugin.ets 中的完整换算函数如下;同文件的 validateCoordinate 会拒绝非有限数,其余数值原样保留。

function resolveFlutterLocalOffset(
  event: DragEvent,
  abilityBinding: AbilityPluginBinding | null
): number[] {
  const winX: number = event.getWindowX();
  const winY: number = event.getWindowY();
  let surfaceOffsetX: number = 0;
  let surfaceOffsetY: number = 0;

  if (abilityBinding != null) {
    try {
      const ability = abilityBinding.getAbility();
      const windowStage: window.WindowStage | undefined =
        FlutterManager.getInstance().getWindowStage(ability);
      const mainWindow: window.Window | undefined = windowStage?.getMainWindowSync();
      const uiContext: UIContext | undefined = mainWindow?.getUIContext();
      if (uiContext != undefined) {
        const viewMap: Map<String, FlutterView> = FlutterManager.getInstance().getFlutterViewList();
        viewMap.forEach((_view: FlutterView, viewIdKey: String) => {
          const node = uiContext.getAttachedFrameNodeById(viewIdKey.toString());
          if (node != null) {
            const pos = node.getPositionToWindow();
            surfaceOffsetX = Number(pos.x ?? 0);
            surfaceOffsetY = Number(pos.y ?? 0);
          }
        });
      }
    } catch (err) {
      hilog.warn(
        LOG_DOMAIN,
        LOG_TAG,
        'Failed to query FlutterSurface window offset, fallback to window origin: %{public}s',
        String(err)
      );
    }
  }

  // 扣除标题栏等窗口内偏移,保持逻辑像素单位传给 Dart。
  const localX: number = validateCoordinate(winX - surfaceOffsetX);
  const localY: number = validateCoordinate(winY - surfaceOffsetY);
  return [localX, localY];
}

这个函数用到了 AbilityPluginBinding、FlutterManager 和窗口模块,日志常量定义在同一文件顶部。若查询偏移时抛出异常,函数会记录警告并回退到窗口原点;带标题栏的页面在这种情况下仍可能错位。我会先检查 FlutterView 节点是否已经挂载,再看后面的坐标。当前实现只按主窗口中的单个 Flutter 视图处理,同一 UIContext 下的多引擎布局还没有测过。

接下来还要看 Dart 的 _scaleHoverPoint。依赖中的平台判断把未识别系统当成 Android,鸿蒙因此也走了除以像素密度的分支。原生先乘一次、Dart 再除一次,部分值虽然抵消了,原始监听器和组件回调看到的却不是同一套单位,而且标题栏那段偏移一直都在。我保留其他平台的处理,只让鸿蒙直接使用传入的逻辑坐标:

Offset _scaleHoverPoint(BuildContext context, Offset point) {
  // 鸿蒙通道已传入逻辑坐标,不再按 Android 的物理像素换算。
  if (defaultTargetPlatform != TargetPlatform.ohos &&
      (UniversalPlatform.isWindows || UniversalPlatform.isAndroid)) {
    return point.scale(
      1 / MediaQuery.of(context).devicePixelRatio,
      1 / MediaQuery.of(context).devicePixelRatio,
    );
  }
  return point;
}

到这里,原生发送的是 Flutter 视图内的位置,DropTarget 再把它换成相对自身左上角的位置。原始监听器看到的值和完成回调里的局部坐标仍可能不同,但这时差的是组件位置,不是像素密度。排查业务页面时,应先确认日志记录的是哪一个位置,再与框的边界比较。

我没有把标题栏高度写死为 36.84,而是读取节点的实际偏移。最大化窗口、隐藏标题栏时,偏移理论上也会变化。不过,这些切换动作,以及连接不同 DPI 副屏的情况,我还没有采集过运行结果。

3.2 URI 与路径分别处理

坐标对上以后,我继续看文件为什么读不出来。UDMF 文件记录给的是资源标识,而 Dart 的 File 接口需要一个当前应用能访问的本地路径。如果文件属于当前应用,插件就解析 FileUri,打开文件、确认它是普通文件,再把沙箱路径交给 Dart。前面修复版读到的两份小文件都走这条路径,内容本身不用复制。

换成其他应用提供的 URI,就不能照着同沙箱路径直接处理了。即使去掉协议头,接收方也没有因此拿到对方沙箱的访问权限。我在这个分支里保留原始授权 URI,通过鸿蒙文件接口打开只读句柄,再异步复制到接收应用的缓存目录。系统若拒绝打开,插件会记录错误并跳过该文件,继续处理其他记录。

复制部分我改用了系统异步接口,拖拽回调不再自己跑同步读写循环。每次先创建一个独立目录,再把副本按原文件名放进去,这样两份同名文件不会互相覆盖。复制完,我把副本长度与打开源文件时记下的长度比较;如果不一致,就按复制失败处理。

长度检查只能帮助发现截断。两个文件长度相同,内容仍可能不同;源文件若在复制期间被其他线程改写,也没有一致快照的保证。因此,这里的校验不能替代内容哈希。

同一原生文件中的复制函数如下。临时文件带着所属目录一起返回,后面的清理函数就能找到本次创建的副本。对于同沙箱直通的原文件,临时目录标记为空,清理时会跳过它,避免把用户交给页面预览的源文件一起删掉。

async function copyAuthorizedFile(
  source: fs.File,
  context: common.UIAbilityContext,
  name: string,
  expectedSize: number
): Promise<ReadableDropFile> {
  const root: string = `${context.cacheDir}/desktop_drop`;
  if (!await fs.access(root)) {
    try {
      await fs.mkdir(root);
    } catch (err) {
      // 两次拖拽同时首次建目录时,只接受目录已经存在的竞争结果。
      if (!await fs.access(root) || !(await fs.stat(root)).isDirectory()) {
        throw err as Error;
      }
    }
  }
  const directory: string = await fs.mkdtemp(`${root}/drop_`);
  const result: ReadableDropFile = { path: `${directory}/${name}`, temporaryDirectory: directory };
  try {
    // 系统异步复制处理完整写入,避免同步循环阻塞 ArkUI 线程。
    await fs.copyFile(source.fd, result.path);
    if ((await fs.stat(result.path)).size !== expectedSize) {
      throw new Error('Copied file size differs from the opened source');
    }
    return result;
  } catch (err) {
    await removeTemporaryFile(result);
    throw err as Error;
  }
}

打开的源句柄由外层 bridgeUriToReadablePath 在 finally 中关闭,复制成功或失败都会执行。文件名会过滤路径分隔符和控制字符;如果文件属性表明它是目录,插件直接拒绝交付。代码里也有图片、视频和音频记录的 URI 提取分支,不过这些类型还没有实际拖入成功的样本。

3.3 交付前后都核对接收区

把复制改为异步以后,文件准备期间的页面变化也得考虑进去。用户可能切走页面,也可能开始下一次拖拽。假如完成事件仍然读取全局保存的最后一次悬停坐标,上一份文件就会带上下一次拖拽的位置,业务收到的文件和落点便对不上了。

我保留 DropTarget 的公开用法,按下面的顺序调整鸿蒙内部通道:

  1. 松手时保存本次最终位置,并同步取出 UDMF 记录,后续不再借用全局悬停位置。
  2. 原生层先询问 Dart,该位置是否落在仍启用且提供完成回调的接收区内;拒绝时不复制文件。
  3. 文件准备好后,把路径列表和同一次位置一起送到 Dart,再检查组件是否存在、是否启用、是否命中。
  4. 只有收到明确的接收回执,才设置成功结果;没有可读文件或接收区拒绝时设置失败,并清理尚未交付的副本。

鸿蒙原生与Dart双阶段拖拽交付时序

图10 跨端交付时序

我也顺手去掉了坐标里的极小小数偏移。此前用它让整数按浮点类型传过通道,数值却跟着变了。现在 Dart 同时接受整数和浮点数,再统一转换为 double;坐标项数不对、包含无穷值或非数字,就直接拒绝。这样到了边界判断处,使用的仍是松手时记录的原始落点。

还有一种情况发生在原生侧:文件尚未准备好,Ability 或引擎已经分离。我给插件加了一个生命周期标识,分离时递增;异步任务恢复后,发现标识与开始时不同,就停止交付并清理。Dart 侧则在组件创建、更新和销毁时维护接收区状态,页面已经禁用,即使矩形位置没变,也不再接收。

这里的成功回执只表示 Dart 已经命中接收区,并发出了业务事件。旧版 onDragDone 是通知式回调,不会等应用保存、解析或上传完毕;业务里的异步读取出错,还需要页面自己处理。我显式设置结果,是为了通知系统插件是否接受了这次交付。原版的失败动效没有实际复现依据,官方文档也未规定不调用 setResult 就一定播放失败动画。

四、踩坑与边界条件

4.1 缓存何时能够删除

清理文件时,我把“明确拒绝”和“没有收到回信”分开处理。复制失败,或 Dart 明确拒绝接收,插件就删除自己创建的副本与目录。收到成功回执则保留文件,让业务继续读取。棘手的是最终回执超时:Dart 可能已经拿到路径,只是回信迟了。这时删文件会打断正在进行的读取,所以我选择保留副本,并在日志中记下回执不确定。

需要长期保留的附件,应用应另存到业务目录。清理临时副本时,范围限定在插件创建的缓存根目录,并避开仍在读取的文件;不能只看路径里是否含有某个字符串就递归删除。同沙箱直通的源文件不属于这些副本。

文件变大以后,磁盘剩余空间、源文件是否还能访问、复制花多久,以及多次拖拽之间的竞争,都可能影响结果。改用异步接口解决的是回调里同步复制的问题;大文件的吞吐量和内存曲线我还没有测,暂时无法判断它在这些负载下的表现。

4.2 被弹层遮住的页面仍可能接收文件

接入页面时还要留意弹层。一个 DropTarget 即使被新路由或弹层挡住,也可能仍挂在组件树上,矩形检测本身看不出业务页面是否允许接收。我会在页面不可接收时设置 enable: false。嵌套或重叠多个接收框则沿用旧版事件分发方式,并没有接入上游新版只选择最上层目标的行为。

如果原来只订阅原始事件、页面里没有 DropTarget,也要调整接入方式。这份鸿蒙修订会检查有效接收区和完成回调,两者缺一就不交付文件。公开方法签名虽然没改,原始监听器现在只能观察坐标,不能单独让文件被接受。

宿主中需要声明 "2in1",应用才能以电脑设备形态安装运行。声明之后,页面仍要自己适应窗口尺寸;Example 为了比较落点而固定的窗口矩形,不适合原样搬进业务布局。还有一点容易漏掉:修改原生插件后,需要重新构建和安装 HAP。只热重载 Dart,运行的仍可能是之前那份 ArkTS 文件桥接代码。

宿主 2in1 声明与鸿蒙逻辑坐标缩放条件

图11 关键配置

4.3 已测范围和接入限制

范围证据与适用边界
自由窗口中的两个小文本文件当前修订已完成模拟器双包同轨迹截图、动图与运行日志核验
框外、禁用、销毁后的拒绝有 7 项 Dart 回归测试覆盖接收区命中、禁用与销毁边界
异步复制、失败清理、回执异常有原生逻辑测试与 HAP 构建,不代表系统授权实测
目录、媒体与大文件目录拒绝;媒体和大文件没有运行成功结论
多引擎、外接副屏、其他平台未验证;不能从单模拟器结果外推

表2 验证边界

如果接入场景也是同应用中的文本附件预览,可以先参考前面的坐标与路径处理。若文件要从系统文件管理器或另一款应用拖过来,我建议再检查真实来源授予的 URI 权限,以及文件在业务读取期间是否仍然可用。单元测试里的虚拟 URI 能帮助检查处理顺序,系统是否授权还得由实际来源来确认。

五、结论与可直接复用的建议

对于开头的附件预览页面,坐标与路径交付修复后,用户已经可以把两份小文件拖进框里查看内容,业务仍通过 desktop_drop 的公开接口接收。我接入的这份源码基于社区 0.6.1,包名没有改;pub.dev 的同名版本、社区原始标签和带补丁的源码分支不是同一份实现,所以依赖来源也要固定。社区主仓发布新标签前,可以在 pubspec.yaml 中直接引用公开复现分支(若已克隆到本地同级目录,也可改用 path: ../desktop_drop):

dependencies:
  desktop_drop:
    git:
      url: https://atomgit.com/abaoge/fluttertpc_desktop_drop.git
      path: packages/desktop_drop
      ref: fix/ohos-desktop-drop-window-offset

页面继续导入 package:desktop_drop/desktop_drop.dart。把下面两个完整方法放进页面的 State,并准备两个状态成员:_hovering 初值为 false,_message 初值为“拖入文本附件”;同时导入 dart:convert 和 Flutter Material。这里先取文件总长度,再最多读前 4096 字节作为摘要,首行预览就不用把整个附件装进内存。

Future<void> _handleDrop(DropDoneDetails details) async {
  if (details.files.isEmpty) return;
  final item = details.files.first;
  try {
    final length = await item.length();
    final end = length < 4096 ? length : 4096;
    final bytes = await item.openRead(0, end).fold<List<int>>(
      <int>[], (buffer, chunk) => buffer..addAll(chunk),
    );
    final firstLine = utf8.decode(bytes, allowMalformed: true)
        .split('\n').first.trim();
    // 异步读取回来后,页面可能已经关闭。
    if (!mounted) return;
    setState(() {
      _hovering = false;
      _message = '${item.name}($length B):$firstLine';
    });
  } catch (error) {
    if (!mounted) return;
    setState(() {
      _hovering = false;
      _message = '读取失败:$error';
    });
  }
}


Widget build(BuildContext context) {
  final active = ModalRoute.of(context)?.isCurrent ?? true;
  return DropTarget(
    enable: active,
    onDragEntered: (_) => setState(() => _hovering = true),
    onDragExited: (_) => setState(() => _hovering = false),
    onDragDone: _handleDrop,
    child: ColoredBox(
      color: _hovering ? const Color(0xFFE0F2FE) : const Color(0xFFF8FAFC),
      child: SizedBox(height: 160, child: Center(child: Text(_message))),
    ),
  );
}

我在这段预览里按 UTF-8 解码,并允许截断处出现不完整字符,所以它只适合看摘要。要核对内容是否完整,需要另外做校验。一次拖入多份文件时,可以逐份处理 details.files;接收二进制内容时则直接使用字节流,再按业务要求检查大小和内容,不能只看扩展名或 UDMF 记录类型。

💡 接入业务页面时,我会让提示文字跟着读取结果走:读完再展示摘要,出错则显示失败提示。页面被遮挡时关闭 enable,需要长期保留的附件另存到业务目录。普通接入只需放置框和读取处理,Example 的固定窗口布局不用搬过来。

小结

两份附件终于能在页面里显示摘要,问题也落到了具体代码上:标题栏偏移影响命中判断,URI 与路径的混用阻断了读取。我排查时先看完成回调有没有到,再沿着文件地址往下查,这个顺序帮我把两处故障分开。加入异步复制后,还得考虑文件会被用到什么时候;最终回执不确定时先保留副本,是为了给可能已经开始的业务读取留出时间。

参考资料

  1. desktop_drop 上游文档与源码
  2. HarmonyOS 拖拽事件与 DragEvent
  3. HarmonyOS UnifiedDataChannel
  4. HarmonyOS 文件访问接口
Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐