当前位置: 首页 > news >正文

HarmonyOS ArkUI Menu 与 ContextMenu:点击菜单与长按上下文菜单

系列:鸿蒙 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.LongPressResponseType.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支持startIconendIcon参数,用于在菜单项前后添加图标:

@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
  • 菜单点击后系统自动关闭,无需在回调中手动处理关闭逻辑
  • MenuItemGroupheader文字由系统统一渲染样式,保持与系统 UI 风格一致
  • MenuItemonClick在菜单关闭前触发,回调中的状态更新会正常生效

上一篇:Marquee 跑马灯与 Gauge 仪表盘 | 下一篇:Toast、AlertDialog 与 CustomDialog 弹窗全解

http://www.jsqmd.com/news/1215591/

相关文章:

  • 2026/7/18
  • 2026年7月最新太原欧米茄官方售后客服电话及服务网点地址查询 - 欧米茄官方服务中心
  • 7-Zip官方原版,免费无广、极简、高效!
  • 为什么92%的初级开发者误用Gemini?:3个关键能力缺口+1套精准自测清单(限时公开)
  • 老年人居住厨卫适老化改造方案及牛牛快装服务分析
  • 2027年 Windows 系统必装的14款软件盘点(全部免费、无限制、体积超小)
  • 2026年广州GEO优化服务:本地企业GEO增长获客工具深度解析与服务商选型七维指南 - 企业新闻快传
  • 报表自动化工具怎么选?2026年四类方案对比与选型指南
  • iOS 企业开发者账号到期续费完整流程与避坑指南
  • 哪家团队有真实的GEO优化实测数据?
  • 玉环室内空气检测公司哪家靠谱?横向对比多家机构,甲醛检测治理避坑全攻略 - 专注室内空气检测治理
  • STM32F103+BMP180 气压温度传感器 + OLED 完整教程|软件 I2C 驱动、补偿算法、海拔计算全解析
  • 国产模型杀进全球前三,我劝你现在就注册 Kimi(附抽奖链接)
  • 足球数据分析怎么做?从球队指标到进球概率模型的完整思路
  • 如何高效配置yuzu模拟器实现Switch游戏最佳体验
  • Cursor配置即代码(Config-as-Code)实践手册:12个生产环境真实案例,含敏感字段自动脱敏逻辑
  • 搜yinheit-从 Vibe Coding 到 Harness × SDD 全栈开发实战-学习记录
  • 浪琴官方售后服务中心服务电话及24小时维修地址实地考察报告+多信源验证(2026年7月最新) - 浪琴服务中心
  • 食物链题解
  • 卡地亚中国官方售后服务中心|全部地址及热线电话权威信息声明(2026年7月最新) - 卡地亚服务中心
  • 投标人的时间,不该浪费在刷标上
  • 2026 年岳阳楼比较好的热收缩膜机批发厂家哪家可靠,别再浪费钱!揭秘膜机如何让生产成本骤降 - 行业推荐官【认证】
  • GIS 室专用 SF6 气体报警器布置点位设计方案
  • 一个普通的自我介绍
  • 基准测试工具之你不知道的两三事 | 9.如何模拟乱序写入
  • 劳力士中国官方售后服务中心|官方热线与门店地址权威信息声明(2026年7月最新) - 劳力士服务中心
  • 别再手写config了!Cursor最新v0.42配置生成引擎上线,仅限前500名开发者获取私有模板库
  • 新疆纯玩团多少钱?2026旺季真实价格表+费用拆解+避坑指南 - 旅行分享
  • 做了多年桌面打印,我为啥非要把它抽成 HTTP 本地服务
  • 海光 BW1000 DCU 初探:从硬件特性、软件栈到开发实践的初学者指南