flutter 桌面应用之右键菜单

​在 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

引入依赖

Dart 复制代码
  contextmenu: ^3.0.0
Dart 复制代码
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.
    );
  }
}

自定义弹出位置

Dart 复制代码
          ///完全自定义位置
          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

Dart 复制代码
contextual_menu: ^0.1.2
Dart 复制代码
        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也包装一个

Dart 复制代码
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,
    );
  }
}

使用方式

Dart 复制代码
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: "粘贴"),
                ];
              },
            )

运行效果

相关推荐
Geek_Vison8 小时前
保险APP已经用了Flutter,还能接入小程序吗?
flutter·小程序·uni-app·mpaas·小程序容器
FungLeo8 小时前
成为全栈·Flutter App 篇·go_router 与四 Tab App Shell:保留导航状态、深链和登录回跳
flutter·移动开发·成为全栈·go_router·路由设计·深链接
m0_738185829 小时前
Flutter 鸿蒙化实战:screen_protector 适配 OpenHarmony,防截屏/防泄露
flutter·华为·harmonyos·鸿蒙
梦想的颜色14 小时前
【编程实战】AI 时代 APP 开发全栈硬核指南:技术选型 + AI 架构 + 模型落地全维度决策
python·flutter·react native·react.js·ai·桌面应用·milvus
yuanlaile1 天前
2026年全新Flutter 跨平台开发全栈学习资料整理
flutter·getx·鸿蒙 next·flutter开发鸿蒙·flutter入门实战·flutter全栈
恋猫de小郭2 天前
Genkit Dart 1.0 发布,Flutter 原生的 AI Agent 终于完整了
android·前端·flutter
第10086个小白2 天前
flutter桌面端打包方式(Inno Setup Compiler)版本
前端·flutter
陆断枫3 天前
Flutter 3.47 动画引擎盘点:Impeller 全平台,和那些"新特性"
flutter
程序员老刘3 天前
Flet 1.0 思路很好,可惜来晚了
flutter·ai编程·客户端
m0_738185823 天前
Flutter 鸿蒙化实战:qr_code_scanner_plus 适配 OpenHarmony,二维码扫描
flutter·华为·harmonyos·鸿蒙