Xamarin.Mac 中的菜单

本文介绍如何在 Xamarin.Mac 应用程序中使用菜单。 它介绍如何在 Xcode 和 Interface Builder 中创建和维护菜单和菜单项,以及如何以编程方式使用它们。

在 Xamarin.Mac 应用程序中使用 C# 和 .NET 时,可以访问与开发人员 Objective-C 和 Xcode 相同的 Cocoa 菜单。 由于 Xamarin.Mac 与 Xcode 直接集成,因此可以使用 Xcode 的 Interface Builder 创建和维护菜单栏、菜单和菜单项, (或选择直接在 C# 代码) 中创建菜单栏、菜单和菜单项。

菜单是 Mac 应用程序用户体验不可或缺的一部分,通常显示在用户界面的各个部分:

  • 应用程序的菜单栏 - 这是显示在每个 Mac 应用程序的屏幕顶部的main菜单。
  • 上下文菜单 - 当用户右键单击或控件单击窗口中的某个项时,会显示这些菜单。
  • 状态栏 - 这是应用程序菜单栏最右侧的区域,显示在屏幕顶部, (菜单栏时钟左侧) ,并在向其中添加项目时向左扩展。
  • 停靠菜单 - 当用户右键单击或控制单击应用程序的图标时,或者当用户左键单击图标并按住鼠标按钮时,扩展坞中每个应用程序的菜单。
  • 弹出按钮和下拉列表 - 弹出按钮显示所选项目,并显示用户单击时要从中进行选择的选项列表。 下拉列表是一种弹出按钮,通常用于选择特定于当前任务上下文的命令。 两者都可以显示在窗口中的任意位置。

示例菜单

本文介绍在 Xamarin.Mac 应用程序中使用 Cocoa 菜单栏、菜单和菜单项的基础知识。 强烈建议先完成 Hello, Mac 文章,特别是 Xcode 和接口生成器简介 以及 插座和操作 部分,因为它涵盖了我们将在本文中使用的关键概念和技术。

您可能还希望查看 Xamarin.Mac Internals 文档的公开 C# 类/方法Objective-C部分,其中介绍了Register用于将 C# 类Objective-C连接到对象和 UI 元素的 和 Export 属性。

应用程序的菜单栏

与 Windows OS 上运行的应用程序不同,其中每个窗口都可以附加其自己的菜单栏,在 macOS 上运行的每个应用程序都有一个菜单栏,该菜单栏沿屏幕顶部运行,用于该应用程序中的每个窗口:

菜单栏

此菜单栏上的项会根据应用程序的当前上下文或状态及其用户界面在任何给定时刻激活或停用。 例如:如果用户选择文本字段,将启用 “编辑 ”菜单上的项目,例如 “复制 ”和“ 剪切”。

根据 Apple 和默认情况下,所有 macOS 应用程序都有一组标准菜单和菜单项,这些菜单和菜单项显示在应用程序的菜单栏中:

  • Apple 菜单 - 此菜单提供对用户随时可用的系统范围项目的访问权限,而不管哪个应用程序正在运行。 开发人员无法修改这些项目。
  • 应用菜单 - 此菜单以粗体显示应用程序的名称,并帮助用户识别当前正在运行的应用程序。 它包含应用于整个应用程序而不是给定文档或进程(例如退出应用程序)的项目。
  • “文件”菜单 - 用于创建、打开或保存应用程序使用的文档的项。 如果应用程序不是基于文档的,则可以重命名或删除此菜单。
  • “编辑”菜单 - 保存用于编辑或修改应用程序用户界面中的元素的“ 剪切”、“ 复制”和“ 粘贴 ”等命令。
  • “格式”菜单 - 如果应用程序处理文本,则此菜单包含用于调整该文本格式的命令。
  • “视图”菜单 - 保留影响内容的显示方式的命令,这些命令 (在应用程序的用户界面中) 查看内容的方式。
  • 特定于应用程序的菜单 - 这些是特定于应用程序 (的任何菜单,例如 Web 浏览器) 的书签菜单。 它们应出现在栏上的 “视图 ”和“ 窗口 ”菜单之间。
  • 窗口菜单 - 包含用于在应用程序中使用窗口的命令,以及当前打开的窗口列表。
  • “帮助”菜单 - 如果应用程序提供屏幕帮助,则“帮助”菜单应为栏上最右侧的菜单。

有关应用程序菜单栏以及标准菜单和菜单项的详细信息,请参阅 Apple 的人机界面指南

默认应用程序菜单栏

每当创建新的 Xamarin.Mac 项目时,都会自动获得一个标准的默认应用程序菜单栏,其中包含 macOS 应用程序通常 (的典型项,如上一节) 所述。 应用程序的默认菜单栏在 Main.storyboard 文件 (以及解决方案 中的项目下的应用 UI) 中定义:

选择main情节提要

双击 Main.storyboard 文件以在 Xcode 的 Interface Builder 中打开该文件进行编辑,你将看到菜单编辑器界面:

在 Xcode 中编辑 UI,显示主点情节提要。

在此处,我们可以单击“文件”菜单中的“打开”菜单项等项,并在属性检查器中编辑或调整其属性:

编辑菜单的属性

本文稍后将介绍如何添加、编辑和删除菜单和项。 目前,我们只想了解哪些菜单和菜单项默认可用,以及它们如何通过一组预定义的插座和操作自动向代码公开 (有关详细信息,请参阅我们的 插座和操作 文档) 。

例如,如果单击“打开”菜单项的连接检查器,可以看到它会自动连接到操作openDocument:

查看附加操作

如果在“接口层次结构”中选择“第一响应方”,并在连接检查器中向下滚动,你将看到openDocument:打开”菜单项附加到 (操作的定义,以及应用程序的几个其他默认操作,这些操作是且不会自动连接到控件) :

查看所有附加的操作

为什么这很重要? 在下一部分中,将了解这些自动定义的操作如何与其他 Cocoa 用户界面元素协同工作,以自动启用和禁用菜单项,并为项提供内置功能。

稍后,我们将使用这些内置操作来启用和禁用代码中的项,并在选择项时提供我们自己的功能。

内置菜单功能

如果你是在添加任何 UI 项或代码之前运行新创建的 Xamarin.Mac 应用程序,你会注意到某些项目会自动连接并启用, (具有自动内置) 的完全功能,例如“应用”菜单中的“退出”项:

已启用的菜单项

而其他菜单项(如 剪切复制粘贴 )则不是:

禁用的菜单项

让我们停止应用程序,双击解决方案板中的 Main.storyboard 文件,将其打开以在 Xcode 的 Interface Builder 中编辑。 接下来,在“接口编辑器”中,将“文本视图”从“库”拖到窗口的视图控制器上:

从库中选择文本视图

“约束编辑器” 中,将文本视图固定到窗口的边缘,并通过单击编辑器顶部的所有四个红色 I 型束并单击“ 添加 4 个约束” 按钮,将其设置为随窗口增长和收缩的位置:

编辑禁忌

将更改保存到用户界面设计,并切换回Visual Studio for Mac,以将更改与 Xamarin.Mac 项目同步。 现在,启动应用程序,在文本视图中键入一些文本,将其选中,然后打开 “编辑” 菜单:

菜单项自动启用/禁用

请注意 剪切复制粘贴 项如何自动启用并完全正常运行,而无需编写一行代码。

这是怎么回事? 请记住,内置预定义操作连接到上面) 所示的默认菜单项 (,作为 macOS 的一部分的大多数 Cocoa 用户界面元素都内置了与特定操作 ((如) ) copy: 的挂钩。 因此,当它们添加到窗口、活动状态和选中状态时,会自动启用附加到该操作的相应菜单项。 如果用户选择该菜单项,则会调用并执行 UI 元素中内置的功能,这一切无需开发人员干预。

启用和禁用菜单和项

默认情况下,每次发生用户事件时, NSMenu 都会根据应用程序的上下文自动启用和禁用每个可见菜单和菜单项。 可通过三种方式启用/禁用项:

  • 自动菜单启用 - 如果 NSMenu 可以找到响应项目连接到的操作的相应对象,则启用菜单项。 例如,上面具有与操作的内置挂钩 copy: 的文本视图。
  • 自定义操作和 validateMenuItem: - 对于绑定到 窗口或视图控制器自定义操作的任何菜单项,可以添加操作 validateMenuItem: 并手动启用或禁用菜单项。
  • 手动菜单启用 - 手动设置 Enabled 每个 NSMenuItem 的 属性,以单独启用或禁用菜单中的每一项。

若要选择系统,请 AutoEnablesItems 设置 的 NSMenu属性。 true 是自动 (默认行为) , false 并且是手动的。

重要

如果选择使用手动菜单启用,则不会自动更新任何菜单项,即使是由 AppKit 类(如 NSTextView)控制的菜单项。 你将负责通过手动代码启用和禁用所有项。

使用 validateMenuItem

如上所述,对于绑定到 窗口或视图控制器自定义操作的任何菜单项,可以添加操作 validateMenuItem: 并手动启用或禁用菜单项。

在以下示例中, Tag 属性将用于根据 中NSTextView所选文本的状态确定由操作启用/禁用validateMenuItem:的菜单项的类型。 已在 Tag 接口生成器中为每个菜单项设置了 属性:

设置 Tag 属性

并将以下代码添加到视图控制器:

[Action("validateMenuItem:")]
public bool ValidateMenuItem (NSMenuItem item) {

    // Take action based on the menu item type
    // (As specified in its Tag)
    switch (item.Tag) {
    case 1:
        // Wrap menu items should only be available if
        // a range of text is selected
        return (TextEditor.SelectedRange.Length > 0);
    case 2:
        // Quote menu items should only be available if
        // a range is NOT selected.
        return (TextEditor.SelectedRange.Length == 0);
    }

    return true;
}

当运行此代码并且未在 中选择任何文本时 NSTextView,两个换行菜单项 (禁用,即使它们连接到视图控制器上的操作) :

显示禁用的项目

如果选择了一部分文本并重新打开菜单,则两个换行菜单项将可用:

显示已启用的项目

启用和响应代码中的菜单项

如上所述,只需将特定的 Cocoa 用户界面元素添加到 UI 设计 ((如文本字段) ),多个默认菜单项就会自动启用并运行,而无需编写任何代码。 接下来,让我们看看将我们自己的 C# 代码添加到 Xamarin.Mac 项目,以便在用户选择菜单项时启用菜单项并提供功能。

例如,假设我们希望用户能够使用“文件”菜单中的“打开”项来选择文件夹。 由于我们希望这是一个应用程序范围的函数,而不限于给定窗口或 UI 元素,因此我们将添加代码以将其处理到应用程序委托中。

“解决方案板”中,双击 AppDelegate.CS 文件以将其打开进行编辑:

选择应用委托

DidFinishLaunching 方法下面添加以下代码:

[Export ("openDocument:")]
void OpenDialog (NSObject sender)
{
    var dlg = NSOpenPanel.OpenPanel;
    dlg.CanChooseFiles = false;
    dlg.CanChooseDirectories = true;

    if (dlg.RunModal () == 1) {
        var alert = new NSAlert () {
            AlertStyle = NSAlertStyle.Informational,
            InformativeText = "At this point we should do something with the folder that the user just selected in the Open File Dialog box...",
            MessageText = "Folder Selected"
        };
        alert.RunModal ();
    }
}

现在,让我们运行应用程序并打开 “文件 ”菜单:

“文件”菜单

请注意,“ 打开 ”菜单项现已启用。 如果我们选择它,将显示打开的对话框:

打开对话框

如果单击“ 打开 ”按钮,将显示警报消息:

示例对话消息

此处的关键行是 [Export ("openDocument:")],它指示NSMenu我们的 AppDelegate 有一个响应openDocument:操作的方法void OpenDialog (NSObject sender)。 如果你还记得上面提到的内容,则默认情况下,“ 打开 ”菜单项会自动连接到接口生成器中的此操作:

查看附加的操作

接下来,让我们看看创建自己的菜单、菜单项和操作,并在代码中响应它们。

使用打开的“最近”菜单

默认情况下,“ 文件 ”菜单包含一个 “打开最近” 项,用于跟踪用户使用应用打开的最后几个文件。 如果要创建基于 NSDocument Xamarin.Mac 的应用,将自动处理此菜单。 对于任何其他类型的 Xamarin.Mac 应用,你将负责手动管理和响应此菜单项。

若要手动处理“ 打开最近打开 ”菜单,首先需要通过以下命令通知它已打开或保存新文件:

// Add document to the Open Recent menu
NSDocumentController.SharedDocumentController.NoteNewRecentDocumentURL(url);

即使你的应用未使用 NSDocuments,你仍使用 NSDocumentController 来维护“ 打开最近打开 ”菜单,方法是将 包含文件位置的 发送到 NSUrlNoteNewRecentDocumentURL 的 方法 SharedDocumentController

接下来,需要重写 OpenFile 应用委托的 方法来打开用户从“ 打开最近打开 ”菜单中选择的任何文件。 例如:

public override bool OpenFile (NSApplication sender, string filename)
{
    // Trap all errors
    try {
        filename = filename.Replace (" ", "%20");
        var url = new NSUrl ("file://"+filename);
        return OpenFile(url);
    } catch {
        return false;
    }
}

true如果文件可以打开,则返回 false ,然后向用户显示一条内置警告,指出无法打开该文件。

由于“ 打开最近打开 ”菜单返回的文件名和路径可能包含空格,因此我们需要在创建 之前正确转义此字符, NSUrl 否则我们将收到错误。 我们使用以下代码执行此操作:

filename = filename.Replace (" ", "%20");

最后,创建指向文件的 , NSUrl 并在应用委托中使用帮助程序方法打开一个新窗口,并将文件加载到其中:

var url = new NSUrl ("file://"+filename);
return OpenFile(url);

为了集中所有内容,让我们看一下 AppDelegate.cs 文件中的示例实现:

using AppKit;
using Foundation;
using System.IO;
using System;

namespace MacHyperlink
{
    [Register ("AppDelegate")]
    public class AppDelegate : NSApplicationDelegate
    {
        #region Computed Properties
        public int NewWindowNumber { get; set;} = -1;
        #endregion

        #region Constructors
        public AppDelegate ()
        {
        }
        #endregion

        #region Override Methods
        public override void DidFinishLaunching (NSNotification notification)
        {
            // Insert code here to initialize your application
        }

        public override void WillTerminate (NSNotification notification)
        {
            // Insert code here to tear down your application
        }

        public override bool OpenFile (NSApplication sender, string filename)
        {
            // Trap all errors
            try {
                filename = filename.Replace (" ", "%20");
                var url = new NSUrl ("file://"+filename);
                return OpenFile(url);
            } catch {
                return false;
            }
        }
        #endregion

        #region Private Methods
        private bool OpenFile(NSUrl url) {
            var good = false;

            // Trap all errors
            try {
                var path = url.Path;

                // Is the file already open?
                for(int n=0; n<NSApplication.SharedApplication.Windows.Length; ++n) {
                    var content = NSApplication.SharedApplication.Windows[n].ContentViewController as ViewController;
                    if (content != null && path == content.FilePath) {
                        // Bring window to front
                        NSApplication.SharedApplication.Windows[n].MakeKeyAndOrderFront(this);
                        return true;
                    }
                }

                // Get new window
                var storyboard = NSStoryboard.FromName ("Main", null);
                var controller = storyboard.InstantiateControllerWithIdentifier ("MainWindow") as NSWindowController;

                // Display
                controller.ShowWindow(this);

                // Load the text into the window
                var viewController = controller.Window.ContentViewController as ViewController;
                viewController.Text = File.ReadAllText(path);
                viewController.SetLanguageFromPath(path);
                viewController.View.Window.SetTitleWithRepresentedFilename (Path.GetFileName(path));
                viewController.View.Window.RepresentedUrl = url;

                // Add document to the Open Recent menu
                NSDocumentController.SharedDocumentController.NoteNewRecentDocumentURL(url);

                // Make as successful
                good = true;
            } catch {
                // Mark as bad file on error
                good = false;
            }

            // Return results
            return good;
        }
        #endregion

        #region actions
        [Export ("openDocument:")]
        void OpenDialog (NSObject sender)
        {
            var dlg = NSOpenPanel.OpenPanel;
            dlg.CanChooseFiles = true;
            dlg.CanChooseDirectories = false;

            if (dlg.RunModal () == 1) {
                // Nab the first file
                var url = dlg.Urls [0];

                if (url != null) {
                    // Open the document in a new window
                    OpenFile (url);
                }
            }
        }
        #endregion
    }
}

根据应用的要求,你可能不希望用户同时在多个窗口中打开同一个文件。 在我们的示例应用中,如果用户从“打开 最近 打开”或“打开”菜单项 (选择已 打开 的文件) ,则包含该文件的窗口将位于前面。

为此,我们在帮助程序方法中使用了以下代码:

var path = url.Path;

// Is the file already open?
for(int n=0; n<NSApplication.SharedApplication.Windows.Length; ++n) {
    var content = NSApplication.SharedApplication.Windows[n].ContentViewController as ViewController;
    if (content != null && path == content.FilePath) {
        // Bring window to front
        NSApplication.SharedApplication.Windows[n].MakeKeyAndOrderFront(this);
        return true;
    }
}

我们将类设计为 ViewController 在其 Path 属性中保存文件的路径。 接下来,我们将循环访问应用中所有当前打开的窗口。 如果文件已在其中一个窗口中打开,则使用将文件带到所有其他窗口的前面:

NSApplication.SharedApplication.Windows[n].MakeKeyAndOrderFront(this);

如果未找到匹配项,则会打开一个新窗口,其中加载了文件,并在“ 打开最近打开 ”菜单中记录该文件:

// Get new window
var storyboard = NSStoryboard.FromName ("Main", null);
var controller = storyboard.InstantiateControllerWithIdentifier ("MainWindow") as NSWindowController;

// Display
controller.ShowWindow(this);

// Load the text into the window
var viewController = controller.Window.ContentViewController as ViewController;
viewController.Text = File.ReadAllText(path);
viewController.SetLanguageFromPath(path);
viewController.View.Window.SetTitleWithRepresentedFilename (Path.GetFileName(path));
viewController.View.Window.RepresentedUrl = url;

// Add document to the Open Recent menu
NSDocumentController.SharedDocumentController.NoteNewRecentDocumentURL(url);

使用自定义窗口操作

与预先连接到标准菜单项的内置 “响应者 ”操作一样,可以创建新的自定义操作并将其连接到 Interface Builder 中的菜单项。

首先,在应用的某个窗口控制器上定义自定义操作。 例如:

[Action("defineKeyword:")]
public void defineKeyword (NSObject sender) {
    // Preform some action when the menu is selected
    Console.WriteLine ("Request to define keyword");
}

接下来,双击 解决方案板 中的应用情节提要文件,将其打开,以便在 Xcode 的 Interface Builder 中编辑。 在“应用程序场景”下选择“第一响应方”,然后切换到“属性检查器”:

属性检查器

+单击属性检查器底部的按钮以添加新的自定义操作:

添加新操作

为其指定与在窗口控制器上创建的自定义操作相同的名称:

编辑操作名称

按住 Control,然后从菜单项拖动到应用程序场景下的“第一响应者”。 从弹出列表中,选择刚刚在此示例中 (defineKeyword: 创建的新操作) :

附加操作

保存对情节提要所做的更改,并返回到Visual Studio for Mac以同步更改。 如果运行该应用,将基于窗口自动启用/禁用将自定义操作连接到的菜单项 () ,并且选择菜单项将触发该操作:

测试新操作

添加、编辑和删除菜单

正如我们在前面的部分中所看到的,Xamarin.Mac 应用程序附带了预设数量的默认菜单和菜单项,特定 UI 控件将自动激活和响应这些菜单和菜单项。 我们还了解了如何将代码添加到应用程序,这些代码还将启用和响应这些默认项。

在本部分中,我们将介绍如何删除不需要的菜单项、重新组织菜单以及添加新菜单、菜单项和操作。

双击解决方案板中的 Main.storyboard 文件,将其打开进行编辑:

双击情节提要文件以在 Xcode 中编辑 UI。

对于特定的 Xamarin.Mac 应用程序,我们不会使用默认的 “视图 ”菜单,因此我们将删除它。 在“接口层次结构”中,选择作为main菜单栏一部分的“视图”菜单项:

选择“视图”菜单项

按 delete 或 backspace 删除菜单。 接下来,我们不会使用“ 格式 ”菜单中的所有项,我们希望将要使用的项从子菜单下移出。 在 “接口层次结构 ”中选择以下菜单项:

突出显示多个项

将父 菜单 下的项从当前所在的子菜单中拖动:

将菜单项拖到父菜单

菜单现在应如下所示:

新位置中的项 新

接下来,将“文本”子菜单从“格式”菜单下拖出,并将其放置在“格式”菜单和“窗口”菜单之间的main菜单栏上:

“文本”菜单

让我们返回到“ 格式 ”菜单下,删除 “字体 ”子菜单项。 接下来,选择“ 格式 ”菜单并将其重命名为“字体”:

“字体”菜单

接下来,让我们创建一个预定义短语的自定义菜单,当选定短语时,这些短语将自动追加到文本视图中的文本。 在 库检查器 底部的搜索框中,键入“菜单”。这将便于查找和使用所有菜单 UI 元素:

库检查器

现在,让我们执行以下操作来创建菜单:

  1. 菜单项库检查器 拖到 “文本 ”和“ 窗口 ”菜单之间的菜单栏上:

    在库中选择新菜单项

  2. 将项重命名为“Phrases”:

    设置菜单名称

  3. 接下来,从库检查器拖动菜单

    从“库”中选择菜单

  4. 在刚刚创建的新菜单项上放置菜单,将其名称更改为“短语”:

    编辑菜单名称

  5. 现在,让我们将三个默认 菜单项 重命名为“地址”、“日期”和“问候”:

    “短语”菜单

  6. 让我们通过从库检查器中拖动菜单并将其称为“签名”来添加第四个菜单项

    编辑菜单项名称

  7. 将更改保存到菜单栏。

现在,让我们创建一组自定义操作,以便向 C# 代码公开新菜单项。 在 Xcode 中,切换到 助手 视图:

创建所需的操作

让我们执行以下操作:

  1. 将 Control 从 “地址 ”菜单项拖动到 AppDelegate.h 文件。

  2. “连接类型” 切换为 “操作”:

    选择操作类型

  3. 输入 phraseAddress”的名称,然后按 “连接 ”按钮创建新操作:

    通过输入名称配置操作。

  4. “日期”、“ 问候”和“ 签名” 菜单项重复上述步骤:

    已完成的操作

  5. 将更改保存到菜单栏。

接下来,我们需要为文本视图创建一个输出口,以便可以从代码调整其内容。 在助手编辑器中选择 ViewController.h 文件,并创建名为 documentText的新输出口:

创建输出口

返回到 Visual Studio for Mac 以从 Xcode 同步更改。 接下来编辑 ViewController.cs 文件,使其如下所示:

using System;

using AppKit;
using Foundation;

namespace MacMenus
{
    public partial class ViewController : NSViewController
    {
        #region Application Access
        public static AppDelegate App {
            get { return (AppDelegate)NSApplication.SharedApplication.Delegate; }
        }
        #endregion

        #region Computed Properties
        public override NSObject RepresentedObject {
            get {
                return base.RepresentedObject;
            }
            set {
                base.RepresentedObject = value;
                // Update the view, if already loaded.
            }
        }

        public string Text {
            get { return documentText.Value; }
            set { documentText.Value = value; }
        } 
        #endregion

        #region Constructors
        public ViewController (IntPtr handle) : base (handle)
        {
        }
        #endregion

        #region Override Methods
        public override void ViewDidLoad ()
        {
            base.ViewDidLoad ();

            // Do any additional setup after loading the view.
        }

        public override void ViewWillAppear ()
        {
            base.ViewWillAppear ();

            App.textEditor = this;
        }

        public override void ViewWillDisappear ()
        {
            base.ViewDidDisappear ();

            App.textEditor = null;
        }
        #endregion
    }
}

这会在 类外部 ViewController 公开文本视图的文本,并在窗口获得或失去焦点时通知应用委托。 现在编辑 AppDelegate.cs 文件,使其如下所示:

using AppKit;
using Foundation;
using System;

namespace MacMenus
{
    [Register ("AppDelegate")]
    public partial class AppDelegate : NSApplicationDelegate
    {
        #region Computed Properties
        public ViewController textEditor { get; set;} = null;
        #endregion

        #region Constructors
        public AppDelegate ()
        {
        }
        #endregion

        #region Override Methods
        public override void DidFinishLaunching (NSNotification notification)
        {
            // Insert code here to initialize your application
        }

        public override void WillTerminate (NSNotification notification)
        {
            // Insert code here to tear down your application
        }
        #endregion

        #region Custom actions
        [Export ("openDocument:")]
        void OpenDialog (NSObject sender)
        {
            var dlg = NSOpenPanel.OpenPanel;
            dlg.CanChooseFiles = false;
            dlg.CanChooseDirectories = true;

            if (dlg.RunModal () == 1) {
                var alert = new NSAlert () {
                    AlertStyle = NSAlertStyle.Informational,
                    InformativeText = "At this point we should do something with the folder that the user just selected in the Open File Dialog box...",
                    MessageText = "Folder Selected"
                };
                alert.RunModal ();
            }
        }

        partial void phrasesAddress (Foundation.NSObject sender) {

            textEditor.Text += "Xamarin HQ\n394 Pacific Ave, 4th Floor\nSan Francisco CA 94111\n\n";
        }

        partial void phrasesDate (Foundation.NSObject sender) {

            textEditor.Text += DateTime.Now.ToString("D");
        }

        partial void phrasesGreeting (Foundation.NSObject sender) {

            textEditor.Text += "Dear Sirs,\n\n";
        }

        partial void phrasesSignature (Foundation.NSObject sender) {

            textEditor.Text += "Sincerely,\n\nKevin Mullins\nXamarin,Inc.\n";
        }
        #endregion
    }
}

此处我们制作 AppDelegate 了分部类,以便可以使用在 Interface Builder 中定义的操作和输出口。 我们还公开 , textEditor 以跟踪当前处于焦点的窗口。

以下方法用于处理自定义菜单和菜单项:

partial void phrasesAddress (Foundation.NSObject sender) {

    if (textEditor == null) return;
    textEditor.Text += "Xamarin HQ\n394 Pacific Ave, 4th Floor\nSan Francisco CA 94111\n\n";
}

partial void phrasesDate (Foundation.NSObject sender) {

    if (textEditor == null) return;
    textEditor.Text += DateTime.Now.ToString("D");
}

partial void phrasesGreeting (Foundation.NSObject sender) {

    if (textEditor == null) return;
    textEditor.Text += "Dear Sirs,\n\n";
}

partial void phrasesSignature (Foundation.NSObject sender) {

    if (textEditor == null) return;
    textEditor.Text += "Sincerely,\n\nKevin Mullins\nXamarin,Inc.\n";
}

现在,如果我们运行应用程序,“ 短语 ”菜单中的所有项都将处于活动状态,并在选中时将指定短语添加到文本视图:

运行的应用示例

现在我们已经了解了使用应用程序菜单栏的基础知识,接下来让我们来看看如何创建自定义上下文菜单。

从代码创建菜单

除了使用 Xcode 的 Interface Builder 创建菜单和菜单项外,有时 Xamarin.Mac 应用可能需要从代码中创建、修改或删除菜单、子菜单或菜单项。

在以下示例中,创建了一个类,用于保存有关将动态创建的菜单项和子菜单的信息:

using System;
using System.Collections.Generic;
using Foundation;
using AppKit;

namespace AppKit.TextKit.Formatter
{
    public class LanguageFormatCommand : NSObject
    {
        #region Computed Properties
        public string Title { get; set; } = "";
        public string Prefix { get; set; } = "";
        public string Postfix { get; set; } = "";
        public List<LanguageFormatCommand> SubCommands { get; set; } = new List<LanguageFormatCommand>();
        #endregion

        #region Constructors
        public LanguageFormatCommand () {

        }

        public LanguageFormatCommand (string title)
        {
            // Initialize
            this.Title = title;
        }

        public LanguageFormatCommand (string title, string prefix)
        {
            // Initialize
            this.Title = title;
            this.Prefix = prefix;
        }

        public LanguageFormatCommand (string title, string prefix, string postfix)
        {
            // Initialize
            this.Title = title;
            this.Prefix = prefix;
            this.Postfix = postfix;
        }
        #endregion
    }
}

添加菜单和项

定义此类后,以下例程将分析 对象的集合 LanguageFormatCommand,并通过将新菜单和菜单项追加到已传入的 Interface Builder) 中创建的现有菜单 (底部,以递归方式生成新菜单和菜单项:

private void AssembleMenu(NSMenu menu, List<LanguageFormatCommand> commands) {
    NSMenuItem menuItem;

    // Add any formatting commands to the Formatting menu
    foreach (LanguageFormatCommand command in commands) {
        // Add separator or item?
        if (command.Title == "") {
            menuItem = NSMenuItem.SeparatorItem;
        } else {
            menuItem = new NSMenuItem (command.Title);

            // Submenu?
            if (command.SubCommands.Count > 0) {
                // Yes, populate submenu
                menuItem.Submenu = new NSMenu (command.Title);
                AssembleMenu (menuItem.Submenu, command.SubCommands);
            } else {
                // No, add normal menu item
                menuItem.Activated += (sender, e) => {
                    // Apply the command on the selected text
                    TextEditor.PerformFormattingCommand (command);
                };
            }
        }
        menu.AddItem (menuItem);
    }
}

对于具有空白Title属性的任何LanguageFormatCommand对象,此例程在菜单部分之间创建一个分隔符菜单项 (细灰色线条) :

menuItem = NSMenuItem.SeparatorItem;

如果提供了标题,则会创建具有该标题的新菜单项:

menuItem = new NSMenuItem (command.Title);

LanguageFormatCommand如果对象包含子LanguageFormatCommand对象,则会创建一个子菜单,并AssembleMenu递归调用 方法以生成该菜单:

menuItem.Submenu = new NSMenu (command.Title);
AssembleMenu (menuItem.Submenu, command.SubCommands);

对于任何没有子菜单的新菜单项,将添加代码来处理用户选择的菜单项:

menuItem.Activated += (sender, e) => {
    // Do something when the menu item is selected
    ...
};

测试菜单创建

如果创建了以下对象集合 LanguageFormatCommand ,则使用上述所有代码:

// Define formatting commands
FormattingCommands.Add(new LanguageFormatCommand("Strong","**","**"));
FormattingCommands.Add(new LanguageFormatCommand("Emphasize","_","_"));
FormattingCommands.Add(new LanguageFormatCommand("Inline Code","`","`"));
FormattingCommands.Add(new LanguageFormatCommand("Code Block","```\n","\n```"));
FormattingCommands.Add(new LanguageFormatCommand("Comment","<!--","-->"));
FormattingCommands.Add (new LanguageFormatCommand ());
FormattingCommands.Add(new LanguageFormatCommand("Unordered List","* "));
FormattingCommands.Add(new LanguageFormatCommand("Ordered List","1. "));
FormattingCommands.Add(new LanguageFormatCommand("Block Quote","> "));
FormattingCommands.Add (new LanguageFormatCommand ());

var Headings = new LanguageFormatCommand ("Headings");
Headings.SubCommands.Add(new LanguageFormatCommand("Heading 1","# "));
Headings.SubCommands.Add(new LanguageFormatCommand("Heading 2","## "));
Headings.SubCommands.Add(new LanguageFormatCommand("Heading 3","### "));
Headings.SubCommands.Add(new LanguageFormatCommand("Heading 4","#### "));
Headings.SubCommands.Add(new LanguageFormatCommand("Heading 5","##### "));
Headings.SubCommands.Add(new LanguageFormatCommand("Heading 6","###### "));
FormattingCommands.Add (Headings);

FormattingCommands.Add(new LanguageFormatCommand ());
FormattingCommands.Add(new LanguageFormatCommand("Link","[","]()"));
FormattingCommands.Add(new LanguageFormatCommand("Image","![](",")"));
FormattingCommands.Add(new LanguageFormatCommand("Image Link","[![](",")](LinkImageHere)"));

该集合传递给 AssembleMenu 函数 (将 “格式 菜单”设置为基) ,将创建以下动态菜单和菜单项:

正在运行的应用中的新菜单项

删除菜单和项

如果需要从应用的用户界面中删除任何菜单或菜单项,只需为类提供要删除的项的从零开始的索引即可使用 RemoveItemAt 类的 方法 NSMenu

例如,若要删除上述例程创建的菜单和菜单项,可以使用以下代码:

public void UnpopulateFormattingMenu(NSMenu menu) {

    // Remove any additional items
    for (int n = (int)menu.Count - 1; n > 4; --n) {
        menu.RemoveItemAt (n);
    }
}

对于上述代码,前四个菜单项是在 Xcode 的 Interface Builder 中创建的,并在应用中可用,因此不会动态删除它们。

上下文菜单

当用户右键单击或控件单击窗口中的项时,将显示上下文菜单。 默认情况下,macOS 中内置的几个 UI 元素已附加上下文菜单, (如文本视图) 。 但是,有时我们想要为添加到窗口的 UI 元素创建自己的自定义上下文菜单。

让我们在 Xcode 中编辑 Main.storyboard 文件,并在设计中添加一个窗口窗口,在标识检查器中将其设置为“NSPanel”,向“窗口”菜单添加新的 Assistant 项,并使用 Show Segue 将其附加到新窗口:

在主点情节提要文件中设置 segue 类型。

让我们执行以下操作:

  1. “标签 ”从 “库检查器 ”拖到 “面板 ”窗口中,并将其文本设置为“属性”:

    编辑标签的值

  2. 接下来,将 “菜单”“库检查器 ”拖到“视图层次结构”中的“视图控制器”上,并重命名三个默认菜单项 “文档”、“ 文本” 和“ 字体”:

    所需菜单项

  3. 现在,将 Control 从 “属性标签” 拖动到 “菜单”上:

    拖动以创建 segue

  4. 在弹出对话框中,选择 “菜单”

    通过在“标签”上下文菜单中选择“输出口”菜单来设置 segue 类型。

  5. Identity Inspector 中,将视图控制器的类设置为“PanelViewController”:

    设置 segue 类

  6. 切换回Visual Studio for Mac进行同步,然后返回到 Interface Builder。

  7. 切换到 助手编辑器 并选择 PanelViewController.h 文件。

  8. 为名为 propertyDocument“文档”菜单项创建操作:

    配置名为 propertyDocument 的操作。

  9. 为剩余的菜单项重复创建操作:

    对剩余菜单项重复操作。

  10. 最后,为 名为 的属性标签 创建一个 propertyLabel出口:

    配置插座

  11. 保存更改并返回到 Visual Studio for Mac 以与 Xcode 同步。

编辑 PanelViewController.cs 文件并添加以下代码:

partial void propertyDocument (Foundation.NSObject sender) {
    propertyLabel.StringValue = "Document";
}

partial void propertyFont (Foundation.NSObject sender) {
    propertyLabel.StringValue = "Font";
}

partial void propertyText (Foundation.NSObject sender) {
    propertyLabel.StringValue = "Text";
}

现在,如果我们运行应用程序并右键单击面板中的属性标签,我们将看到自定义上下文菜单。 如果我们从菜单中选择 和 项,标签的值将更改:

运行的上下文菜单

接下来,让我们看一下创建状态栏菜单。

状态栏菜单

状态栏菜单显示状态菜单项的集合,这些菜单项向用户提供交互或反馈,例如反映应用程序状态的菜单或图像。 即使应用程序在后台运行,应用程序的状态栏菜单也会启用并处于活动状态。 系统范围内的状态栏位于应用程序菜单栏的右侧,是 macOS 中当前唯一可用的状态栏。

让我们编辑 AppDelegate.cs 文件, DidFinishLaunching 使 方法如下所示:

public override void DidFinishLaunching (NSNotification notification)
{
    // Create a status bar menu
    NSStatusBar statusBar = NSStatusBar.SystemStatusBar;

    var item = statusBar.CreateStatusItem (NSStatusItemLength.Variable);
    item.Title = "Text";
    item.HighlightMode = true;
    item.Menu = new NSMenu ("Text");

    var address = new NSMenuItem ("Address");
    address.Activated += (sender, e) => {
        PhraseAddress(address);
    };
    item.Menu.AddItem (address);

    var date = new NSMenuItem ("Date");
    date.Activated += (sender, e) => {
        PhraseDate(date);
    };
    item.Menu.AddItem (date);

    var greeting = new NSMenuItem ("Greeting");
    greeting.Activated += (sender, e) => {
        PhraseGreeting(greeting);
    };
    item.Menu.AddItem (greeting);

    var signature = new NSMenuItem ("Signature");
    signature.Activated += (sender, e) => {
        PhraseSignature(signature);
    };
    item.Menu.AddItem (signature);
}

NSStatusBar statusBar = NSStatusBar.SystemStatusBar; 允许我们访问系统范围的状态栏。 var item = statusBar.CreateStatusItem (NSStatusItemLength.Variable); 创建新的状态栏项。 从那里,我们创建一个菜单和多个菜单项,并将该菜单附加到刚刚创建的状态栏项。

如果运行应用程序,将显示新的状态栏项。 从菜单中选择某个项将更改文本视图中的文本:

运行的状态栏菜单

接下来,让我们看看创建自定义停靠菜单项。

自定义停靠菜单

当用户右键单击或按住 control 键单击扩展坞中的应用程序图标时,Mac 应用程序的扩展坞菜单将出现:

自定义扩展坞菜单

让我们通过执行以下操作为应用程序创建自定义停靠菜单:

  1. 在“Visual Studio for Mac”中,右键单击应用程序的项目,然后选择“添加新>文件...”在“新建文件”对话框中,选择“Xamarin.Mac>空接口定义”,使用“DockMenu”为名称,然后单击“新建”按钮创建新的 DockMenu.xib 文件:

    添加空接口定义

  2. Solution Pad 中,双击 DockMenu.xib 文件以将其打开以在 Xcode 中编辑。 创建包含以下项的新 菜单地址日期问候语和 签名

    设置 UI 布局

  3. 接下来,让我们将新菜单项连接到我们在上面的 添加、编辑和删除 菜单部分中为自定义菜单创建的现有操作。 切换到“连接检查器”,并在“接口层次结构”中选择“第一响应方”。 向下滚动并找到操作 phraseAddress: 。 将该操作上的圆圈中的一条线拖到 “地址 ”菜单项:

    将一行拖动到“地址”菜单项。

  4. 对附加到其相应操作的所有其他菜单项重复此操作:

    对附加到其相应操作的其他菜单项重复此操作。

  5. 接下来,在“接口层次结构”中选择“应用程序”。 在 “连接检查器”中,将一条线从出线口的 dockMenu 圆拖到我们刚刚创建的菜单中:

    向上拖动电线出口

  6. 保存更改并切换回 Visual Studio for Mac 以与 Xcode 同步。

  7. 双击 Info.plist 文件将其打开进行编辑:

    编辑 Info.plist 文件

  8. 单击屏幕底部的“ ”选项卡:

    选择“源”视图

  9. 单击“ 添加新条目”,单击绿色加号按钮,将属性名称设置为“AppleDockMenu”,将值设置为“DockMenu”, (不带扩展名) 的新 .xib 文件的名称:

    添加 DockMenu 项

现在,如果我们运行应用程序并在 Dock 中右键单击其图标,将显示新的菜单项:

运行停靠菜单的示例

如果我们从菜单中选择一个自定义项,将修改文本视图中的文本。

弹出按钮和下拉列表

弹出按钮显示所选项目,并显示用户单击时要从中进行选择的选项列表。 下拉列表是一种弹出按钮类型,通常用于选择特定于当前任务上下文的命令。 两者都可以显示在窗口中的任意位置。

让我们执行以下操作,为应用程序创建自定义弹出按钮:

  1. 在 Xcode 中编辑 Main.storyboard 文件,并将“库检查器”中的弹出按钮拖动到我们在“上下文菜单”部分中创建的“面板”窗口:

    添加弹出按钮

  2. 添加新菜单项,并将弹出窗口中项的标题设置为: 地址日期问候语和 签名

    配置菜单项

  3. 接下来,让我们将新菜单项连接到我们在上面的 添加、编辑和删除 菜单部分中为自定义菜单创建的现有操作。 切换到“连接检查器”,并在“接口层次结构”中选择“第一响应方”。 向下滚动并找到操作 phraseAddress: 。 将该操作上的圆圈中的一条线拖到 “地址 ”菜单项:

    拖动以连接操作

  4. 对附加到其相应操作的所有其他菜单项重复此操作:

    所有必需操作

  5. 保存更改并切换回 Visual Studio for Mac 以与 Xcode 同步。

现在,如果我们运行应用程序并从弹出窗口中选择一个项,文本视图中的文本将发生更改:

运行弹出窗口的示例

可以采用与弹出按钮完全相同的方式创建和使用下拉列表。 可以创建自己的自定义操作,而不是附加到现有操作,就像我们在上下文菜单部分中对上下文 菜单 执行的操作一样。

总结

本文详细介绍了如何使用 Xamarin.Mac 应用程序中的菜单和菜单项。 首先,我们检查了应用程序的菜单栏,然后查看了创建上下文菜单,接下来我们检查了状态栏菜单和自定义停靠菜单。 最后,我们介绍了弹出菜单和下拉列表。