前言前面我们用 onTouch/onHover/onKeyEvent 覆盖了手势/悬停/按键——但有种「弹出菜单」交互还没讲右键菜单PC、长按菜单手机/TV、上下文操作菜单。玩家在「猫猫大作战」里长按某列可能想选猫等级、清空该列、提示推荐位置——这种「上下文相关操作菜单」HarmonyOS 提供了bindContextMenu绑定 Builder构建菜单内容。本篇以「猫猫大作战」长按列出菜单选猫等级/清空列为预演场景把bindContextMenu 绑定触发、ContextMenu 用 IMenu 接口、响应式菜单参数化 Builder、与自定义 Stack 弹层的取舍四大要点讲透。提示本系列不讲 ArkTS 基础语法与环境搭建假设你已跟完第 1–53 篇。本篇是阶段三第四篇。一、场景拆解长按列出菜单「猫猫大作战」玩家在某列长按 800ms第 51 篇 onTouch 预演提过想看选项选猫等级临时指定下一投放的等级小/中/大清空该列移除该列所有猫扣分惩罚提示推荐高亮引擎推荐的最佳投放列用bindContextMenu// 预演列点击层加 bindContextMenu ForEach(this.cols, (col: number) { Column() .width(GameConfig.CELL_SIZE) .height(GameConfig.BOARD_HEIGHT * GameConfig.CELL_SIZE) .backgroundColor(rgba(0,0,0,0)) .bindContextMenu(this.ColumnMenuBuilder(col), ContextMenuOptions) // ← 绑菜单 .onClick(() { this.handleColumnClick(col); }) }, (col: number) click_${col}) // 菜单 Builder参数化按 col 显示不同菜单 Builder ColumnMenuBuilder(col: number) { Menu() { MenuItem(选猫等级) .onClick(() { this.showLevelPicker(col); }) MenuItem(清空该列) .onClick(() { this.clearColumn(col); }) MenuItem(提示推荐) .onClick(() { this.showHint(); }) } }核心问题bindContextMenu何时触发右键长按MenuMenuItem怎么构建菜单内容参数化 Builder 怎么让菜单知道是哪列何时用bindContextMenu何时自定义 Stack 弹层二、bindContextMenu 绑定触发2.1 触发时机bindContextMenu在以下场景自动触发设备触发方式PC 端鼠标右键点击组件手机端手指长按组件约 500msTV 端遥控器菜单键如果有关键经验bindContextMenu 自动适配多设备触发——PC 右键、手机长按、TV 菜单键不用自己写 onTouch setTimeout第 51 篇预演的 DIY 方式。2.2 基本绑定Column() .bindContextMenu(this.MyMenuBuilder(), this.options)拆解片段含义bindContextMenu(builder, options)绑定方法参数是菜单 Builder 和选项this.MyMenuBuilder()菜单内容构造器Builder 方法this.options菜单选项可选控制弹层位置/样式2.3 菜单选项private menuOptions: ContextMenuOptions { // 呕弹层位置、样式等可选 }; Column() .bindContextMenu(this.MyMenuBuilder(), this.menuOptions)实战经验options 通常可省——默认弹层在触发点附近样式跟随系统主题。三、Menu MenuItem 构建菜单3.1 基本菜单结构Builder MyMenuBuilder() { Menu() { MenuItem(选项一) .onClick(() { console.info(选了选项一); }) MenuItem(选项二) .onClick(() { console.info(选了选项二); }) MenuItem(选项三) .onClick(() { console.info(选了选项三); }) } }拆解组件作用Menu菜单容器MenuItem(文本)菜单项参数是显示文本.onClick点击该项的回调3.2 分隔符和图标Builder MyMenuBuilder() { Menu() { MenuItem(选猫等级) .onClick(() { this.showLevelPicker(); }) MenuItem(清空该列) .onClick(() { this.clearColumn(); }) // 分隔符 MenuDivider() MenuItem(提示推荐) .onClick(() { this.showHint(); }) MenuItem(设置) .onClick(() { this.showSettings(); }) } }关键经验MenuDivider 分组菜单项——视觉上把「操作」和「辅助」分组提升可读性。3.3 子菜单嵌套Builder MyMenuBuilder() { Menu() { MenuItem(选猫等级) .onClick(() { /* 直接触发或展开子菜单 */ }) // 子菜单嵌套 Menu() { MenuItem(小猫).onClick(() { this.setLevel(CatLevel.SMALL); }) MenuItem(中猫).onClick(() { this.setLevel(CatLevel.MEDIUM); }) MenuItem(大猫).onClick(() { this.setLevel(CatLevel.BIG); }) } .title(选猫等级) // 子菜单标题 MenuItem(清空该列).onClick(() { /* ... */ }) } }实战经验子菜单适合「多级选择」——选猫等级有多个值嵌套子菜单比平铺更清晰。四、参数化 Builder让菜单知道是哪列4.1 参数化 Builder 的挑战bindContextMenu接受一个Builder方法但ForEach里每个列都要绑菜单——菜单内容要随 col 变清空第 3 列 vs 清空第 5 列不同。4.2 闭包捕获 colForEach(this.cols, (col: number) { Column() .bindContextMenu(() { // 箭头闭包捕获 col this.ColumnMenuBuilder(col); }, this.menuOptions) }, (col: number) click_${col}) Builder ColumnMenuBuilder(col: number) { Menu() { MenuItem(清空第 ${col 1} 列) // 显示具体列号 .onClick(() { this.clearColumn(col); }) // 清指定列 MenuItem(选猫等级) .onClick(() { this.showLevelPicker(col); }) } }关键经验用箭头闭包捕获 col——() { this.ColumnMenuBuilder(col); }每列绑自己的菜单。第 37、38 篇讲过闭包和箭头函数。4.2 ArkUI 的 Builder 参数约束提示ArkUI 的 Builder 方法支持参数——Builder ColumnMenuBuilder(col: number) {...}可在闭包里调this.ColumnMenuBuilder(col)传参。具体支持版本以 SDK 为准老版本可能要手动闭包构建。五、实战长按列出菜单5.1 改造 Index 加 bindContextMenu// 预演Index 列点击层加 bindContextMenu Entry Component struct Index { State gameState: GameState GameState.IDLE; State cats: Cat[] []; State nextCatLevel: CatLevel CatLevel.SMALL; /* ... 其他 state */ private gameEngine: GameEngine new GameEngine(); private readonly cols: number[] [0, 1, 2, 3, 4]; private menuOptions: ContextMenuOptions {}; /* startGame / pauseGame / resumeGame / endGame / clearTimers / formatTime / aboutToDisappear 等略 */ handleColumnClick(column: number) { if (this.gameState ! GameState.PLAYING) return; if (this.gameEngine.dropCat(column)) { this.cats this.gameEngine.getAllCats(); this.nextCatLevel this.gameEngine.getNextCatLevel(); } } // 菜单操作清空某列 clearColumn(col: number): void { if (this.gameState ! GameState.PLAYING) return; this.gameEngine.clearColumn(col); // 引擎清列扣分等 this.cats this.gameEngine.getAllCats(); this.score this.gameEngine.getScore(); } // 菜单操作选猫等级 showLevelPicker(col: number): void { // 简化直接设小猫实际应弹选择器 this.gameEngine.setNextCatLevel(CatLevel.MEDIUM); this.nextCatLevel CatLevel.MEDIUM; } // 菜单操作提示推荐 showHint(): void { // 简化高亮引擎推荐列 const recommend this.gameEngine.getRecommendedColumn(); console.info(推荐列: ${recommend 1}); } // 参数化菜单 Builder本篇重点 Builder ColumnMenuBuilder(col: number) { Menu() { MenuItem(清空第 ${col 1} 列) .onClick(() { this.clearColumn(col); }) Menu() { MenuItem(小猫).onClick(() { this.gameEngine.setNextCatLevel(CatLevel.SMALL); this.nextCatLevel CatLevel.SMALL; }) MenuItem(中猫).onClick(() { this.gameEngine.setNextCatLevel(CatLevel.MEDIUM); this.nextCatLevel CatLevel.MEDIUM; }) MenuItem(大猫).onClick(() { this.gameEngine.setNextCatLevel(CatLevel.BIG); this.nextCatLevel CatLevel.BIG; }) } .title(选猫等级) MenuDivider() MenuItem(提示推荐) .onClick(() { this.showHint(); }) } } Builder GameView() { Column() { this.GameHUD() Column() { Row() { /* 预告区 */ } Stack() { /* 棋盘背景 */ ForEach(this.cats, (cat: Cat) { /* ... */ }, (cat: Cat) cat.id) // 列点击层onClick bindContextMenu本篇重点 Row() { ForEach(this.cols, (col: number) { Column() .width(GameConfig.CELL_SIZE) .height(GameConfig.BOARD_HEIGHT * GameConfig.CELL_SIZE) .backgroundColor(rgba(0,0,0,0)) // 绑上下文菜单右键/长按触发 .bindContextMenu(() { this.ColumnMenuBuilder(col); // 闭包捕获 col }, this.menuOptions) .onClick(() { this.handleColumnClick(col); }) }, (col: number) click_${col}) } } .width(GameConfig.BOARD_WIDTH * GameConfig.CELL_SIZE) .height(GameConfig.BOARD_HEIGHT * GameConfig.CELL_SIZE) .borderRadius(12).clip(true).backgroundColor(#D6EEF5) }.alignItems(HorizontalAlign.Center) Spacer() Row() { /* 底部控制栏 */ } .width(100%).padding({ left: 24, right: 24, bottom: 24, top: 12 }) } .width(100%).height(100%) .linearGradient({ direction: GradientDirection.Bottom, colors: [[#E8F4F8, 0.0], [#D6EEF5, 0.5], [#C9E8F2, 1.0]] }) .alignItems(HorizontalAlign.Center) } /* GameHUD / MainMenuView / PauseOverlay / GameOverOverlay / StatItem 等略 */ }5.2 触发流程PC 鼠标场景鼠标移到第 3 列。鼠标右键 →bindContextMenu触发 → 弹出「清空第 3 列/选猫等级/提示推荐」菜单。点击「清空第 3 列」→clearColumn(2)→ 清第 3 列。手机手指场景手指按在第 3 列长按 500ms。bindContextMenu触发 → 弹出菜单。点选某项。TV 遥控场景方向键选中第 3 列。按遥控器菜单键如有→bindContextMenu触发。方向键选菜单项确认键触发。六、bindContextMenu vs 自定义 Stack 弹层6.1 自定义 Stack 弹层State showMenu: boolean false; State menuCol: number -1; Column() .onTouch((event) { if (event.type TouchType.Down) { this.startLongPressTimer(col); // DIY 长按计时 } else if (event.type TouchType.Up) { this.clearLongPressTimer(); } }) Stack() { /* 棋盘等内容 */ if (this.showMenu) { Column() { MenuItem(清空该列).onClick(() { this.clearColumn(this.menuCol); this.showMenu false; }) MenuItem(选猫等级).onClick(() { /* ... */ }) } .backgroundColor(#FFFFFF) .borderRadius(12) .position({ x: 100, y: 200 }) } }6.2 对比维度bindContextMenu自定义 Stack 弹层触发自动右键/长按/菜单键DIY onTouch setTimeout多设备✅ 自动适配❌ 手动适配菜单样式系统主题统风格自定义位置自动在触发点手动 position可控性中系统统风格✅ 高全自定义代码量少多6.3 取舍要「统风格、自动多设备」菜单 ├─ 是 → bindContextMenu推荐 └─ 要「高度自定义样式/动画」菜单 └─ 自定义 Stack 弹层关键经验统风格菜单用 bindContextMenu自定义弹层用 Stack——前者省心多设备适配后者样式可控。七、踩坑提示7.1 Builder 忘加 Builder// ❌ 错误没 Builder 装饰bindContextMenu 不识别 ColumnMenuBuilder(col: number) { /* ... */ } // ✅ 正确Builder 装饰 Builder ColumnMenuBuilder(col: number) { /* ... */ }7.2 闭包丢 col// ❌ 错误ForEach 里共享 col所有列绑同一个菜单 ForEach(this.cols, (col: number) { Column() .bindContextMenu(this.ColumnMenuBuilder(col), this.menuOptions) }) // col 可能都指向最后一个值 // ✅ 正确箭头闭包捕获当前 col ForEach(this.cols, (col: number) { Column() .bindContextMenu(() { this.ColumnMenuBuilder(col); // 箭头闭包 }, this.menuOptions) }, (col: number) click_${col})7.3 MenuItem onClick 用普通函数丢 this// ❌ 错误普通函数 this 不指向组件 MenuItem(清空).onClick(function () { this.clearColumn(); }) // ✅ 正确箭头函数保留 this第 38 篇讲过 MenuItem(清空).onClick(() { this.clearColumn(col); })7.4 忘关闭菜单后状态// ⚠️ bindContextMenu 系统菜单点击后自动关闭但自定义 Stack 要手动关 MenuItem(清空).onClick(() { this.clearColumn(col); this.showMenu false; // 自定义 Stack 要手动关 })7.5 菜单操作忘守卫 gameState// ❌ 错误暂停态菜单还能清列 clearColumn(col: number): void { this.gameEngine.clearColumn(col); // 暂停态也执行 } // ✅ 正确守卫 clearColumn(col: number): void { if (this.gameState ! GameState.PLAYING) return; this.gameEngine.clearColumn(col); this.cats this.gameEngine.getAllCats(); }八、调试技巧console.info在 MenuItem onClick追点击的菜单项。PC 端右键测试右键组件看菜单是否弹出。手机端长按测试长按 500ms 看菜单是否弹出。菜单不弹排查检查 bindContextMenu 是否绑定检查 Builder 是否 Builder检查 Menu 是否在 Builder 内。九、性能与最佳实践统风格菜单用 bindContextMenu——自动适配 PC 右键、手机长按、TV 菜单键。Builder 必加 Builder 装饰——bindContextMenu 才能识别。参数化用箭头闭包捕获——ForEach 里每列绑自己的菜单。MenuItem onClick 用箭头函数——保留 this第 38 篇讲过。MenuDivider 分组——视觉分组提升可读性。子菜单嵌套多级选择——选猫等级等多个值嵌套比平铺清晰。自定义样式用 Stack 弹层——bindContextMenu 走系统风格要自定义得 DIY。十、阶段三进度51–55本篇是阶段三「交互与动画」第 4 篇篇主题核心要点51onTouch手势三阶段 Down/Move/Up52onHover悬停进入/离开53onKeyEvent键盘/遥控按键54本篇bindContextMenu右键/长按上下文菜单55animateTo显式动画触发总结本篇我们从 bindContextMenu 上下文菜单切入掌握了绑定触发PC 右键/手机长按/TV 菜单键自动适配、Menu MenuItem 构建含 MenuDivider 和子菜单、参数化 Builder箭头闭包捕获 col、与自定义 Stack 弹层的取舍四大要点并给出了长按列出菜单的完整代码。核心要点bindContextMenu 自动多设备触发Builder Menu MenuItem箭头闭包参数化统风格用 bindContextMenu 自定义用 Stack。下一篇我们将拆解 animateTo——显式动画触发。如果这篇文章对你有帮助欢迎点赞、收藏⭐、关注你的支持是我持续创作的动力相关资源「猫猫大作战」项目源码本仓库entry/src/main/ets/pages/Index.ets、entry/src/main/ets/components/ArkUI bindContextMenu 上下文菜单官方指南Menu 菜单组件官方文档HarmonyOS 多模态输入与菜单最佳实践开源鸿蒙跨平台社区HarmonyOS 开发者官方文档首页系列索引本仓库articles/INDEX.md