在 Flutter 桌面应用开发中,context_menu
和 contextual_menu
是两款常用的右键菜单插件,各有特色。以下是对它们的对比分析:
context_menu
-
集成方式:通过
ContextMenuArea
组件包裹目标组件,定义菜单项。掘金 -
菜单定义:使用
builder
返回一个List<Widget>
,通常为ListTile
,支持图标、文字和点击事件。掘金 -
适用场景:适合需要快速实现简单右键菜单的场景,集成方便,适用于大多数桌面应用。掘金
contextual_menu
-
集成方式:需要手动监听鼠标右键事件,并调用
popUpContextualMenu()
方法显示菜单。掘金 -
菜单定义:使用
Menu
和MenuItem
结构,支持普通项、复选框、分隔符和子菜单等多种类型。掘金 -
适用场景:适合需要复杂菜单结构(如多级菜单、复选项)的应用,提供更高的自定义能力。
总结对比
特性 | context_menu | contextual_menu |
---|---|---|
集成方式 | 使用组件包裹目标组件,集成简单 | 手动监听事件,调用方法显示菜单,集成复杂 |
菜单结构 | 简单,适合基本菜单 | 复杂,支持多级菜单、复选项等 |
自定义能力 | 限制较多,主要通过 ListTile 实现 | 高度自定义,支持多种菜单项类型 |
适用场景 | 快速实现基本右键菜单 | 实现复杂、结构化的右键菜单 |
建议选择
-
选择
context_menu
:如果你需要快速集成一个简单的右键菜单,且菜单项较为基础,context_menu
是一个不错的选择。 -
选择
contextual_menu
:如果你的应用需要复杂的菜单结构,如多级菜单、复选项等,contextual_menu
提供了更强大的功能和灵活性。
contextmenu
hello word
引入依赖
contextmenu: ^3.0.0
import 'package:contextmenu/contextmenu.dart';
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
),
home: const MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key, required this.title});
final String title;
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
title: Text(widget.title),
),
body: ContextMenuArea(
child: Container(
color: Colors.grey,
padding: EdgeInsets.all(20),
child: Text("在这里右键"),
),
builder: (BuildContext context) {
return [
Container(padding: EdgeInsets.all(10), child: Text('自定义菜单')),
ListTile(
title: Text("点击"),
onTap: () {
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text("点击了")));
},
),
];
},
), // This trailing comma makes auto-formatting nicer for build methods.
);
}
}
自定义弹出位置

///完全自定义位置
GestureDetector(
onSecondaryTapDown:
(details) => showContextMenu(
details.globalPosition,
context,
(BuildContext context) {
return [
Container(
padding: EdgeInsets.all(10),
child: Text('自定义菜单'),
),
ListTile(
title: Text("点击"),
onTap: () {
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text("点击了")));
},
),
];
},
0.0,
200.0,
),
child: Container(
padding: EdgeInsets.all(15),
color: Colors.green,
child: Text('Tap!'),
),
),
contextual_menu
hello word
https://pub.dev/packages/contextual_menu
contextual_menu: ^0.1.2
GestureDetector(
onSecondaryTapDown: (details) {
Menu menu = Menu(
items: [
MenuItem(
label: 'Copy',
onClick: (_) {
print('Clicked Copy');
},
),
MenuItem(label: 'Disabled item', disabled: true),
MenuItem.checkbox(
key: 'checkbox1',
label: 'Checkbox1',
checked: true,
onClick: (menuItem) {
print('Clicked Checkbox1');
menuItem.checked = !(menuItem.checked == true);
},
),
MenuItem.separator(),
],
);
popUpContextualMenu(menu, placement: Placement.bottomLeft);
},
child: Container(
padding: EdgeInsets.all(15),
color: Colors.green,
child: Text('Tap!'),
),
),
popUpContextualMenu
可以看到我们本身没有传递position参数,那么他是怎么感知我鼠标点击的位置呢
他是通过window记录的鼠标点击位置来展示的
NSWindow.mouseLocationOutsideOfEventStream
是 macOS 平台(AppKit 框架)中的一个属性,用于获取当前鼠标在指定窗口中的坐标位置,而不是通过事件触发获取的。这个方法非常实用,尤其是在没有发生鼠标事件时,仍然需要获取当前鼠标位置的场景下。
var mouseLocationOutsideOfEventStream: NSPoint { get }
-
返回值:一个
NSPoint
,表示鼠标相对于窗口坐标系的位置。 -
类型:
NSWindow
的实例属性。
自定义ContextMenuArea
在contextmenu中我们看到ContextMenuArea包装使用非常简单,我们这里用contextual_menu也包装一个
import 'dart:ui';
import 'package:contextual_menu/contextual_menu.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/gestures.dart';
typedef ContextMenuBuilder = List<MenuItem> Function(BuildContext context);
class ContextualMenuArea extends StatefulWidget {
final Widget child;
final ContextMenuBuilder builder;
const ContextualMenuArea({
super.key,
required this.child,
required this.builder,
});
@override
State<StatefulWidget> createState() {
return ContextualMenuAreaState();
}
}
class ContextualMenuAreaState extends State<ContextualMenuArea> {
bool _shouldReact = false;
Offset? _position;
@override
Widget build(BuildContext context) {
return Listener(
child: widget.child,
onPointerDown: (details) {
///kSecondaryMouseButton 0x02 次键(一般是右键)
///PointerDeviceKind.mouse 输入来源是鼠标
_shouldReact =
details.kind == PointerDeviceKind.mouse &&
details.buttons == kSecondaryMouseButton;
},
onPointerUp: (details) {
if (!_shouldReact) return;
_position = details.position;
_handleClickPopUp();
},
);
}
void _handleClickPopUp() {
popUpContextualMenu(
Menu(items: widget.builder(context)),
position: _position,
placement: Placement.bottomRight,
);
}
}
使用方式
ContextualMenuArea(
child: Container(
padding: EdgeInsets.all(20),
color: Colors.grey,
child: Text("ContextualMenuArea"),
),
builder: (context) {
return [
MenuItem.submenu(
label: "复制",
submenu: Menu(
items: [
MenuItem.checkbox(label: "复制全部", checked: false),
MenuItem.checkbox(label: "复制当前", checked: true),
],
),
),
MenuItem.separator(),
MenuItem(label: "粘贴"),
];
},
)
运行效果