HarmonyOS PC 端适配实战:断点、分栏与键鼠交互
📱 鸿蒙 HarmonyOS 全栈专栏 · 第 36 篇 / 共 39 篇(第 5 季·收官季)
上一篇:HarmonyOS 游戏与图形渲染入门 | 下一篇:出海与国际化:资源方案与四道合规关
目录
上一篇讲了图形渲染,这一篇讲第二个投票方向:PC 端适配。鸿蒙生态正在向 PC 延伸,而 PC 端的应用数量远少于手机端 —— 这意味着对开发者来说它是一个流量洼地:同样的应用,PC 端竞争更小。
但要先泼一盆冷水:PC 适配不是「把窗口拉大」。布局适配大概只占三成工作量,剩下七成是键鼠交互 —— 手机上不存在的东西,PC 用户天天用。
📖 本篇导读
全文约 15 分钟,改造成本主要是交互细节。
- 断点系统:一套代码应对三种窗口宽度
- 栅格与分栏:GridRow 与 Navigation 的正确用法
- 键鼠交互:焦点、hover、右键、快捷键
- 交互模式抽象:触屏与鼠标一份代码
- 窗口管理:自由窗口与最小尺寸
- 一个真实的分栏页面:列表 + 详情
- 适配检查清单与坑表
一、断点系统:一套代码,三种窗口

图 1:断点系统 —— 窗口宽度决定布局形态
鸿蒙把窗口宽度划成三档:sm(手机)、md(折叠屏展开 / 小平板)、lg(平板 / PC)。注意这是窗口宽度而不是设备类型 —— PC 上你的应用可能是自由窗口,宽度随时被用户拖动。
监听断点
import window from '@ohos.window'
@Entry
@Component
struct Index {
@State bp: string = 'sm'
aboutToAppear() {
const win = window.getLastWindow(this.context)
win.then(w => {
// 监听窗口变化,运行时更新断点
w.on('windowSizeChange', (size) => {
const vp = size.width / display.getDefaultDisplaySync().densityPixels
this.bp = vp < 600 ? 'sm' : vp < 840 ? 'md' : 'lg'
})
})
}
build() {
if (this.bp === 'lg') {
this.listDetail() // 平板 / PC:列表 + 详情
} else {
this.singlePage() // 手机:单列
}
}
}
💡 实际项目里把断点放进 AppStorage 全局状态,所有页面共享一份 —— 不要每个页面各自监听一遍,那样改断点阈值时要全工程搜索。
二、栅格与分栏:两个官方组件就够了
GridRow / GridCol 是官方栅格组件,列数随断点自动变化:
栅格随断点变化
GridRow({ columns: { sm: 4, md: 8, lg: 12 } }) {
GridCol({ span: { sm: 4, md: 4, lg: 3 } }) {
CardA()
}
GridCol({ span: { sm: 4, md: 4, lg: 3 } }) {
CardB()
}
// sm 独占一行,lg 时四个卡片同行
}
页面结构上的适配,用 Navigation:它在手机上是普通的页面跳转,在 lg 下自动变成「列表 + 详情」分栏(mode 参数控制)。这正是第 14 篇推荐 Navigation 替代 router 的另一个理由 —— 它天生为多端形态设计。
Navigation 分栏
Navigation(this.pageMap) {
Column() { /* 列表页 */ }
}
.mode(NavigationMode.Stack) // sm/md 押栈;lg 可设 Split 分栏
.navBarWidth(360)
三、键鼠交互:真正的适配工作量在这

图 2:键鼠适配的五件事 —— 布局改完只是开始
五件事逐个说,配最小代码:
① 焦点系统。PC 用户会用 Tab 键导航。可聚焦组件要显式声明,默认顺序按声明顺序:
焦点
Button('搜索')
.focusable(true)
.onFocus(() => console.info('got focus'))
.defaultFocus(true) // 进入页面默认聚焦搜索框
② hover 态。手机没有悬停;没有 hover 反馈的按钮在 PC 上看起来像死的:
hover 态
@Component
struct HoverButton {
@State hovered: boolean = false
build() {
Button(this.label)
.backgroundColor(this.hovered ? '#0836B0' : '#0A59F7')
.onHover((isHovered: boolean) => { this.hovered = isHovered })
}
}
③ 右键菜单。手机上的长按菜单在 PC 上不可达,右键要单独绑:
右键菜单
Text(this.content)
.bindContextMenu(this.menuBuilder(), ResponseType.RightClick)
④ 快捷键。Ctrl+S 保存、Esc 关弹窗,用按键事件注册:
快捷键
.onKeyEvent((event: KeyEvent) => {
if (event.type !== KeyType.Down) return
if (event.keyCode === KeyCode.KEY_ESCAPE) this.close()
})
⑤ 滚轮与拖拽。长列表确认滚轮可用(Scroll 组件默认支持);拖拽行为用 onDragStart 系列 API 定义,和触摸手势分开调。
三点五、交互模式抽象:触屏与鼠标一份代码
上一篇分栏讲了「数据层一份、展示层两份策略」,交互也用同样的思路:把「用户在用触屏还是鼠标」抽象成一个全局状态,hover、右键这些 PC 专属行为只在鼠标态注册 —— 手机端零成本,PC 端一份配置生效。
按交互模式注册行为
// 全局:应用启动时判断(示例:按输入源类型判断,以官方 API 为准)
AppStorage.setOrCreate('isMouse', inputDevice.supportsMouse())
// 组件:按模式注册行为,而不是写两套组件
@Component
struct SmartItem {
private isMouse = AppStorage.get('isMouse')
build() {
Column() { ... }
.onHover(this.isMouse
? (h: boolean) => this.hovered = h
: undefined)
.bindContextMenu(this.isMouse ? this.menu : undefined,
ResponseType.RightClick)
// 长按菜单两种模式都保留
.bindContextMenu(this.menu, ResponseType.LongPress)
}
}
这样写的收益在维护期才显现:如果 hover、右键的逻辑散落在几十个组件里各自判断,后续改交互规范就是一次全工程搜索;收口到一个模式状态后,改一处生效全局。(inputDevice 的具体 API 名以官方文档为准,这里表达的是分层思路。)
四、窗口管理:自由窗口时代的最小尺寸
PC 上应用以自由窗口运行,用户可以拖小到很离谱的尺寸。两个必做动作:
窗口约束
// 1) 声明最小尺寸,防止用户拖出不可用布局
// module.json5 的 abilities 里:
"minWindowWidth": 480,
"minWindowHeight": 600
// 2) 竖屏手机布局在窄窗口下依然成立 —— 检查你的 sm 断点就是最小窗口
⚠️ 最小窗口宽度的意义是「保底可用」,不是「免检」。发布前把窗口拖到最小尺寸走一遍核心流程 —— 侧滑菜单、弹窗、键盘遮挡,问题都在这时暴露。
四点五、一个真实的分栏页面:列表 + 详情
把前面的东西拼起来看一个完整形态。分栏页面的结构是「导航容器 + 左列表 + 右详情」,断点切换时同一份数据流复用,只有容器形态变化:
分栏页面骨架
@Entry
@Component
struct MailPage {
@State bp: string = 'sm'
@State currentId: string = ''
@Builder listArea() {
List() {
ForEach(this.items, (item: Mail) => {
ListItem() {
MailItem({ mail: item })
.onClick(() => { this.currentId = item.id })
}
})
}.width(360)
}
@Builder detailArea() {
if (this.currentId) {
MailDetail({ id: this.currentId })
} else {
Column() { Text('选择一封邮件').fontColor('#999') }
.justifyContent(FlexAlign.Center).width('100%').height('100%')
}
}
build() {
if (this.bp === 'lg') {
Row() {
this.listArea()
Divider().vertical(true).height('100%')
this.detailArea()
}.width('100%')
} else {
// 手机:点击跳转详情页;分栏下:只是刷新右侧
this.listArea().width('100%')
}
}
}
实现时的关键差别:手机端「打开详情」是路由跳转,分栏端是刷新右侧区域。把「用户点了一条列表项」这个行为抽象成 onSelect(id),两种形态各自决定怎么呈现 —— 数据层一份代码,展示层两份策略,这是分栏适配的通用模式。
五、适配检查清单
| 项 | 手机 | 平板 / PC |
|---|---|---|
| 列表形态 | 单列全屏跳转 | lg 分栏:左列表右详情 |
| 悬停反馈 | 不需要 | 所有可点元素有 hover 态 |
| 右键 | 长按菜单 | 右键菜单 + 长按共存 |
| 键盘 | 软键盘遮挡处理 | Tab 焦点 + 快捷键 |
| 窗口 | 固定方向 | 自由尺寸 + 最小宽高声明 |
| 输入 | 软键盘 | 同时支持物理键盘与输入法 |
| 坑 | 现象 | 处理 |
|---|---|---|
| 断点写死按设备判断 | PC 小窗口按手机布局,反而正确;但平板大窗口漏判 | 按窗口宽度判,不按设备判 |
| 每个页面各自监听窗口 | 阈值改一处要改十处 | 断点放 AppStorage 全局 |
| 只适配了布局 | 「套壳感」:鼠标党没法高效操作 | 按本篇五件事补键鼠 |
| 忘记最小窗口 | 用户拖成 100px 宽,布局崩坏 | minWindowWidth 声明 + 最小尺寸自测 |
| 弹窗位置写死居中 | 分栏模式下遮住详情 | 相对宿主组件定位 |
这一篇你该记住:断点按窗口宽度算不按设备算;结构适配靠 GridRow + Navigation;键鼠五件事(焦点 / hover / 右键 / 快捷键 / 滚轮)才是工作量大头;自由窗口时代必须声明最小尺寸。
下一篇讲第三个方向:出海与国际化 —— 把你的应用带到全球市场,代码之外还有四道关。
如果这个专栏帮你把鸿蒙学明白了,点赞 + 收藏 + 关注 支持一下。
你还有哪个场景没跑通?评论区说清楚你的目标 + 现在怎么问的,我帮你改成能用的提示词。
更多推荐


所有评论(0)