flutter 桌面应用之右键菜单

news2025/4/16 6:10:23

​在 Flutter 桌面应用开发中,context_menucontextual_menu 是两款常用的右键菜单插件,各有特色。以下是对它们的对比分析:​


context_menu

  • 集成方式:​通过 ContextMenuArea 组件包裹目标组件,定义菜单项。​掘金

  • 菜单定义:​使用 builder 返回一个 List<Widget>,通常为 ListTile,支持图标、文字和点击事件。​掘金

  • 适用场景:​适合需要快速实现简单右键菜单的场景,集成方便,适用于大多数桌面应用。​掘金


contextual_menu

  • 集成方式:​需要手动监听鼠标右键事件,并调用 popUpContextualMenu() 方法显示菜单。​掘金

  • 菜单定义:​使用 MenuMenuItem 结构,支持普通项、复选框、分隔符和子菜单等多种类型。​掘金

  • 适用场景:​适合需要复杂菜单结构(如多级菜单、复选项)的应用,提供更高的自定义能力。​


总结对比

特性context_menucontextual_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: "粘贴"),
                ];
              },
            )

运行效果

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2334928.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Cygwin编译安装Acise

本文记录Windows下使用Cygwin编译安装Acise的流程。 零、环境 操作系统Windows11Visual Studio CodeVisual Studio Code 1.92.0Cygwin 一、工具及依赖 1.1 Visual Studio Code 下载并安装Visual Studio Code, 同时安装以下插件&#xff0c; Task Explorer Output Colorizer …

pyqtgraph.opengl.items.GLSurfacePlotItem.GLSurfacePlotItem 报了一个错

1. 需求是这个样子的 有一个 pyqtgraph.opengl.GLViewWidget &#xff0c;在应用启动时存在QMainWindow中&#xff0c;即父对象是QMainWindow&#xff0c;当业务需要时&#xff0c;修改它的父对象变为一个QDialog&#xff0c;可以让它从QMainWindow中弹出显示在QDialog里&#…

【C++初学】课后作业汇总复习(六) 函数模板

1、函数模板 思考&#xff1a;如果重载的函数&#xff0c;其解决问题的逻辑是一致的、函数体语句相同&#xff0c;只是处理的数据类型不同&#xff0c;那么写多个相同的函数体&#xff0c;是重复劳动&#xff0c;而且还可能因为代码的冗余造成不一致性。 解决&#xff1a;使用…

【第16届蓝桥杯C++C组】--- 数位倍数

Hello呀&#xff0c;小伙伴们&#xff0c;第16届蓝桥杯也完美结束了&#xff0c;无论大家考的如何&#xff0c;都要放平心态&#xff0c;今年我刚上大一&#xff0c;也第一次参加蓝桥杯&#xff0c;刷的算法题也只有200来道&#xff0c;但是还是考的不咋滴&#xff0c;但是拿不…

Numpy和OpenCV库匹配查询,安装OpenCV ABI错误

文章目录 地址opencv-python&#xff1a;4.x版本的对应numpyopencv-python&#xff1a;5.x版本的对应numpy方法2 ps&#xff1a;装个opencv遇到ABI错误无语了&#xff0c;翻了官网&#xff0c;github文档啥都没&#xff0c;记录下 地址 opencv-python&#xff1a;4.x版本的对应…

ubuntu18.04安装miniforge3

1.下载安装文件 略&#xff08;注&#xff1a;从同事哪里拖来的安装包&#xff09; 2.修改安装文件权限 chmod x Miniforge3-Linux-x86_64.sh 3.将它安装到指定位置 micromamba activate /home/xxx/fxp/fromDukto/miniforge3 4.激活 /home/xxx/fxp/fromDukto/miniforge3…

智能手机功耗测试

随着智能手机发展,用户体验对手机的续航功耗要求越来越高。需要对手机进行功耗测试及分解优化,将手机的性能与功耗平衡。低功耗技术推动了手机的用户体验。手机功耗测试可以采用powermonitor或者NI仪表在功耗版上进行测试与优化。作为一个多功能的智能终端,手机的功耗组成极…

使用U盘安装 ubuntu 系统

1. 准备U 盘制作镜像 1.1 下载 ubuntu iso https://ubuntu.com/download/ 这里有多个版本以供下载&#xff0c;本文选择桌面版。 1.2 下载rufus https://rufus.ie/downloads/ 1.3 以管理员身份运行 rufus 设备选择你用来制作启动项的U盘&#xff0c;不能选错了&#xff1b;点…

oracle 并行度(Parallel Degree)

在Oracle数据库中&#xff0c;并行度&#xff08;Parallel Degree&#xff09; 是用于控制并行处理任务的关键配置&#xff0c;旨在通过多进程协作加速大规模数据处 一、并行度的核心概念 并行度&#xff08;DOP, Degree of Parallelism&#xff09; 表示一个操作同时使用的并…

Redis-场景缓存+秒杀+管道+消息队列

缓存一致性 1.两次更新 先更新数据库&#xff0c;再更新缓存&#xff1b;先更新缓存&#xff0c;再更新数据库&#xff1b; 出现不一致问题场景&#xff1a; 先更新数据库&#xff0c;再更新缓存&#xff1b; 先更新缓存&#xff0c;再更新数据库&#xff1b; 两次更新的适…

系统的安全及应用

仓库做了哪些优化 仓库源换成国内源不使用root用户登录将不必要的开机启动项关闭内核的调优 系统做了哪些安全加固 禁止使用root禁止使用弱命令将常见的 远程连接端口换掉 系统安全及应用 Cpu负载高 java程序 运行异常中病毒&#xff1f; ps aux - - sort %cpu %mem Cpu …

PostgreSQL内幕探索—基础知识

PostgreSQL内幕探索—基础知识 PostgreSQL&#xff08;以下简称PG&#xff09; 起源于 1986 年加州大学伯克利分校的 ‌POSTGRES 项目‌&#xff0c;最初以对象关系模型为核心&#xff0c;支持高级数据类型和复杂查询功能‌。 1996 年更名为 PostgreSQL 并开源&#xff0c;逐…

WPS复制粘贴错误 ,文件未找到 mathpage.wll

文章目录 1.错误提示图片2.解决方案1.找到MathType.wll文件和MathType Commands 2016.dotm文件并复制2.找到wps安装地址并拷贝上述两个文件到指定目录 3.重启WPS 1.错误提示图片 2.解决方案 1.找到MathType.wll文件和MathType Commands 2016.dotm文件并复制 MathType.wll地址如…

驱动开发硬核特训 · Day 6 : 深入解析设备模型的数据流与匹配机制 —— 以 i.MX8M 与树莓派为例的实战对比

&#x1f50d; B站相应的视屏教程&#xff1a; &#x1f4cc; 内核&#xff1a;博文视频 - 从静态绑定驱动模型到现代设备模型 主题&#xff1a;深入解析设备模型的数据流与匹配机制 —— 以 i.MX8M 与树莓派为例的实战对比 在上一节中&#xff0c;我们从驱动框架的历史演进出…

【UE5 C++课程系列笔记】35——HTTP基础——HTTP客户端异步请求API接口并解析响应的JSON

目录 前言 步骤 一、 搭建异步蓝图节点框架 二、异步蓝图节点嵌入到引擎的执行流程 三、获取本地时间并异步返回 四、获取网络时间并异步返回 五、源码 前言 本文以请求网络/本地时间API为例&#xff0c;介绍如何实现HTTP异步请求。 步骤 一、 搭建异步蓝图节点框架 …

手机静态ip地址怎么获取?方法与解析‌

而在某些特定情境下&#xff0c;我们可能需要为手机设置一个静态IP地址。本文将详细介绍手机静态IP地址详解及获取方法 一、什么是静态IP地址&#xff1f; 静态IP&#xff1a;由用户手动设置的固定IP地址&#xff0c;不会因网络重启或设备重连而改变。 动态IP&#xff1a;由路…

Python 基础语法汇总

Python 语法 │ ├── 基本结构 │ ├── 语句&#xff08;Statements&#xff09; │ │ ├── 表达式语句&#xff08;如赋值、算术运算&#xff09; │ │ ├── 控制流语句&#xff08;if, for, while&#xff09; │ │ ├── 定义语句&#xff08;def…

Linux上位机开发实践(OpenCV算法硬件加速)

【 声明&#xff1a;版权所有&#xff0c;欢迎转载&#xff0c;请勿用于商业用途。 联系信箱&#xff1a;feixiaoxing 163.com】 图像处理里面&#xff0c;opencv基本是一个标准模块。但是由于图像处理的特点&#xff0c;如果所有的算法都是cpu来做的话&#xff0c;效率会很低。…

Spring Boot MongoDB自定义连接池配置

手打不易&#xff0c;如果转摘&#xff0c;请注明出处&#xff01; 注明原文&#xff1a;http://zhangxiaofan.blog.csdn.net/article/details/144341407 一、引言 在 Spring Boot 应用中使用 MongoDB 时&#xff0c;合理配置连接池可以显著提升数据库访问的性能和稳定性。默…

游戏引擎学习第223天

回顾 今天我们正在进行过场动画序列的制作&#xff0c;因此我想深入探讨这个部分。昨天&#xff0c;我们暂时停止了过场动画的制作&#xff0c;距离最终结局还有一些内容没有完成。今天的目标是继续完成这些内容。 我们已经制作了一个过场动画的系列&#xff0c;并把它们集中…