系列:鸿蒙 HarmonyOS 6.1 新特性实战 · 第 44 篇
ArkUI 提供两种菜单绑定方式:bindMenu用于点击触发的下拉菜单,bindContextMenu用于长按或右键触发的上下文菜单。两者均支持简单数组配置和自定义@Builder布局两种写法,满足从快速原型到精细定制的不同需求。本篇通过操作日志区域直观记录每次菜单点击事件。
运行效果
初始状态(两个按钮 + 长按区域 + 操作日志):
菜单弹出展示(自定义菜单含分组标题):
bindMenu:数组写法
最简写法是直接传入MenuItem[]数组,每项包含value(显示文字)和action(点击回调)。适合菜单项固定、结构简单的场景。
Button('点击菜单') .bindMenu([ { value: '复制', action: () => { this.addLog('执行:复制') } }, { value: '粘贴', action: () => { this.addLog('执行:粘贴') } }, { value: '删除', action: () => { this.addLog('执行:删除') } }, ])数组写法无需额外定义组件,代码最为简洁。菜单点击后自动关闭,无需手动处理。
bindMenu:自定义 @Builder
当需要分组标题、图标或自定义样式时,改用@Builder函数构建Menu:
@Builder customMenuBuilder() { Menu() { MenuItem({ content: '分享' }) .onClick(() => { this.addLog('点击了:分享') }) MenuItem({ content: '收藏' }) .onClick(() => { this.addLog('点击了:收藏') }) MenuItemGroup({ header: '更多操作' }) { MenuItem({ content: '举报' }) .onClick(() => { this.addLog('点击了:举报') }) MenuItem({ content: '屏蔽此内容' }) .onClick(() => { this.addLog('点击了:屏蔽此内容') }) } } } // 绑定到按钮 Button('更多操作').bindMenu(this.customMenuBuilder)MenuItemGroup接受header参数作为分组标题,内部嵌套MenuItem,系统会自动渲染分割线与标题样式。
bindContextMenu:长按上下文菜单
bindContextMenu接受两个参数:@Builder函数和触发类型(ResponseType.LongPress或ResponseType.RightClick):
Text('长按此区域唤出菜单') .width('100%') .padding(20) .textAlign(TextAlign.Center) .fontSize(14) .fontColor('#666666') .backgroundColor('#f0f4ff') .borderRadius(8) .bindContextMenu(this.customMenuBuilder, ResponseType.LongPress)在触控屏设备上使用LongPress;在鼠标/触控板场景下也可同时支持RightClick。
MenuItem 图标配置
MenuItem支持startIcon和endIcon参数,用于在菜单项前后添加图标:
@Builder iconMenuBuilder() { Menu() { MenuItem({ startIcon: $r('app.media.icon_share'), content: '分享', endIcon: $r('app.media.icon_arrow') }) .onClick(() => { this.addLog('点击了:分享') }) MenuItem({ startIcon: $r('app.media.icon_star'), content: '收藏' }) .onClick(() => { this.addLog('点击了:收藏') }) } }操作日志区域
使用滚动列表记录每次菜单操作,帮助调试和演示:
@State logs: string[] = [] addLog(msg: string): void { const time = new Date().toLocaleTimeString() this.logs = [`[${time}] ${msg}`, ...this.logs].slice(0, 20) } // 日志渲染 Column({ space: 0 }) { Row() { Text('操作日志').fontSize(13).fontColor('#999999').layoutWeight(1) Text('清空').fontSize(13).fontColor('#0066ff') .onClick(() => { this.logs = [] }) } .padding({ left: 12, right: 12, top: 10, bottom: 8 }) if (this.logs.length === 0) { Text('暂无操作记录').fontSize(13).fontColor('#cccccc') .padding(16).width('100%').textAlign(TextAlign.Center) } else { List() { ForEach(this.logs, (log: string) => { ListItem() { Text(log).fontSize(12).fontColor('#444444') .padding({ left: 12, right: 12, top: 6, bottom: 6 }) } }) } .divider({ strokeWidth: 0.5, color: '#f0f0f0' }) } } .backgroundColor('#ffffff') .borderRadius(8)完整代码
@Entry @Component struct MenuContextMenuPage { @State logs: string[] = [] addLog(msg: string): void { const now = new Date() const time = `${now.getHours()}:${String(now.getMinutes()).padStart(2, '0')}:${String(now.getSeconds()).padStart(2, '0')}` this.logs = [`[${time}] ${msg}`, ...this.logs].slice(0, 20) } @Builder simpleMenuBuilder() { Menu() { MenuItem({ content: '分享' }).onClick(() => { this.addLog('点击了:分享') }) MenuItem({ content: '收藏' }).onClick(() => { this.addLog('点击了:收藏') }) MenuItemGroup({ header: '更多操作' }) { MenuItem({ content: '举报' }).onClick(() => { this.addLog('点击了:举报') }) MenuItem({ content: '屏蔽此内容' }).onClick(() => { this.addLog('点击了:屏蔽此内容') }) } } } build() { Column({ space: 16 }) { Text('菜单组件演示').fontSize(18).fontWeight(FontWeight.Bold).fontColor('#333333') .width('100%').padding({ left: 16 }) // 数组写法菜单 Column({ space: 8 }) { Text('bindMenu 数组写法').fontSize(13).fontColor('#999999') Button('点击菜单(数组写法)') .width('100%') .bindMenu([ { value: '复制', action: () => { this.addLog('执行:复制') } }, { value: '粘贴', action: () => { this.addLog('执行:粘贴') } }, { value: '删除', action: () => { this.addLog('执行:删除') } }, ]) } .padding({ left: 16, right: 16 }) .alignItems(HorizontalAlign.Start) // 自定义 Builder 菜单 Column({ space: 8 }) { Text('bindMenu 自定义 Builder').fontSize(13).fontColor('#999999') Button('更多操作(含分组标题)') .width('100%') .bindMenu(this.simpleMenuBuilder) } .padding({ left: 16, right: 16 }) .alignItems(HorizontalAlign.Start) // 长按上下文菜单 Column({ space: 8 }) { Text('bindContextMenu 长按触发').fontSize(13).fontColor('#999999') Text('长按此区域唤出菜单') .width('100%') .padding(20) .textAlign(TextAlign.Center) .fontSize(14) .fontColor('#0066ff') .backgroundColor('#f0f4ff') .borderRadius(8) .bindContextMenu(this.simpleMenuBuilder, ResponseType.LongPress) } .padding({ left: 16, right: 16 }) .alignItems(HorizontalAlign.Start) Divider().strokeWidth(1).color('#eeeeee').margin({ left: 16, right: 16 }) // 操作日志 Column({ space: 0 }) { Row() { Text('操作日志').fontSize(13).fontColor('#999999').layoutWeight(1) Text('清空').fontSize(13).fontColor('#0066ff') .onClick(() => { this.logs = [] }) } .padding({ left: 12, right: 12, top: 10, bottom: 8 }) if (this.logs.length === 0) { Text('暂无操作记录').fontSize(13).fontColor('#cccccc') .padding(16).width('100%').textAlign(TextAlign.Center) } else { List() { ForEach(this.logs, (log: string) => { ListItem() { Text(log) .fontSize(12) .fontColor('#444444') .width('100%') .padding({ left: 12, right: 12, top: 6, bottom: 6 }) } }) } .width('100%') .divider({ strokeWidth: 0.5, color: '#f0f0f0' }) } } .width('100%') .backgroundColor('#ffffff') .borderRadius(8) .margin({ left: 16, right: 16 }) .layoutWeight(1) } .width('100%') .height('100%') .backgroundColor('#f8f8f8') .padding({ top: 20, bottom: 20 }) } }API 速查
| 属性/方法 | 说明 |
|---|---|
.bindMenu([{ value, action }]) | 数组写法,点击触发简单菜单 |
.bindMenu(builder) | 自定义 Builder,支持复杂布局 |
.bindContextMenu(builder, type) | 长按/右键触发上下文菜单 |
ResponseType.LongPress | 长按触发类型 |
ResponseType.RightClick | 右键触发类型 |
Menu() | 菜单容器组件 |
MenuItem({ startIcon, content, endIcon }) | 单个菜单项,支持前后图标 |
MenuItemGroup({ header }) | 带标题的菜单分组 |
@Builder | 装饰器,定义可复用 UI 构建函数 |
小结
- 数组写法
{ value, action }最简洁,适合固定的三到五项操作;超过五项或需要分组时改用@Builder @Builder函数绑定给bindMenu时写this.builderFn,不加括号(否则会立即执行)bindContextMenu第二个参数决定触发方式,触控设备用LongPress,支持鼠标的场景可叠加RightClick- 菜单点击后系统自动关闭,无需在回调中手动处理关闭逻辑
MenuItemGroup的header文字由系统统一渲染样式,保持与系统 UI 风格一致MenuItem的onClick在菜单关闭前触发,回调中的状态更新会正常生效
上一篇:Marquee 跑马灯与 Gauge 仪表盘 | 下一篇:Toast、AlertDialog 与 CustomDialog 弹窗全解