共用方式為


Xamarin.Mac 中的功能表

本文涵蓋在 Xamarin.Mac 應用程式中使用功能表。 它描述如何在 Xcode 和 Interface Builder 中建立和維護功能表和功能表項,並以程式設計方式加以使用。

在 Xamarin.Mac 應用程式中使用 C# 和 .NET 時,您可以存取開發人員在 和 Xcode 中 Objective-C 運作的相同 Cocoa 功能表。 由於 Xamarin.Mac 直接與 Xcode 整合,因此您可以使用 Xcode 的介面產生器來建立和維護功能表欄、功能表和功能表項(或選擇性地直接在 C# 程式代碼中建立它們)。

選單是 Mac 應用程式用戶體驗不可或缺的一部分,通常會出現在使用者介面的各個部分:

  • 應用程式的功能表列 - 這是出現在每個 Mac 應用程式畫面頂端的主要選單。
  • 關係型功能表 - 當使用者以滑鼠右鍵按兩下或按下視窗中的專案時,就會顯示這些功能表。
  • 狀態列 - 這是應用程式功能表列 最右邊的區域,出現在畫面頂端(功能表欄時鐘左側),並在專案新增時向左成長。
  • 停駐功能表 - 當使用者以滑鼠右鍵按鍵按下或控件按鍵圖示時,或使用者以滑鼠左鍵按一下圖示,並按住滑鼠按鈕時,即會出現停駐區中每個應用程式的功能表。
  • 快顯按鈕和下拉式清單 - 彈出視窗按鈕會顯示選取的專案,並在使用者按兩下時顯示要從中選取的選項清單。 下拉式清單是一種快顯按鈕類型,通常用於選取目前工作內容特定的命令。 兩者都可以出現在視窗中的任何位置。

範例功能表

在本文中,我們將討論在 Xamarin.Mac 應用程式中使用 Cocoa 功能表欄、功能表和功能表項的基本概念。 強烈建議您先完成 Hello,Mac 文章,特別是 Xcode 和 Interface Builder 和 Outlets 和 Actions 簡介小節,因為它涵蓋我們將在本文中使用的重要概念和技術。

您可能也想要查看 Xamarin.Mac Internals 檔的公開 C# 類別/方法Objective-C一節,它也會說明 Register 用來將 C# 類別連接至Objective-C物件和 UI 元素的 和 Export 屬性。

應用程式的功能表列

不同於在 Windows OS 上執行的應用程式,其中每個視窗都可以附加自己的功能表欄,在macOS上執行的每個應用程式都會有單一功能表欄,沿著該應用程式中每個視窗使用的畫面頂端執行:

功能表欄

此功能表列上的項目會根據應用程式及其使用者介面的目前內容或狀態,隨時啟動或停用。 例如:如果使用者選取文字欄位,則會啟用 [編輯] 選單上的專案,例如 [複製] 和 [剪下]。

根據 Apple 和預設,所有 macOS 應用程式都有一組標準功能表和功能表項,這些功能表和功能表項會出現在應用程式的功能表列中:

  • Apple 功能表 - 此功能表可讓您隨時存取使用者隨時可用的系統範圍專案,而不論應用程式執行什麼。 開發人員無法修改這些專案。
  • 應用程式選單 - 此功能表以粗體顯示應用程式的名稱,並協助使用者識別目前正在執行的應用程式。 它包含套用至整個應用程式的專案,而不是指定的檔或程式,例如結束應用程式。
  • 檔案選單 - 用來建立、開啟或儲存應用程式使用的文件的專案。 如果您的應用程式不是以檔為基礎,則可以重新命名或移除此功能表。
  • 編輯選單 - 保留命令,例如 剪下複製貼上 ,用來編輯或修改應用程式使用者介面中的元素。
  • 格式選單 - 如果應用程式使用文字,此功能表會保留命令來調整該文字的格式。
  • [檢視] 功能表 - 保留會影響應用程式使用者介面中內容顯示方式(已檢視)的命令。
  • 應用程式特定的功能表 - 這些是應用程式特有的任何功能表(例如網頁瀏覽器的書籤功能表)。 它們應該會出現在列上的 [ 檢視 ] 和 [視窗 ] 功能表之間。
  • 視窗選單 - 包含在應用程式中使用視窗的命令,以及目前開啟的視窗清單。
  • 說明功能表 - 如果您的應用程式提供螢幕上的說明,[說明] 選單應該是列上最右邊的功能表。

如需應用程式功能表欄和標準功能表和功能表項的詳細資訊,請參閱 Apple 的 Human Interface Guidelines

默認應用程式功能表列

每當您建立新的 Xamarin.Mac 專案時,您都會自動取得標準的預設應用程式選單欄,其中包含 macOS 應用程式通常會擁有的一般專案(如上一節所述)。 應用程式的預設功能表欄定義於 Main.storyboard 檔案中(以及您應用程式 UI 的其餘部分)的 Solution Pad 底下

選取主分鏡腳本

按兩下 Main.storyboard 檔案,以開啟它以在 Xcode 的 Interface Builder 中編輯,您將會看到功能表編輯器介面:

在 Xcode 中編輯 UI,顯示主點分鏡腳本。

從這裡,我們可以按下 [檔案] 選單中的 [開啟] 功能表項等專案,然後在 [屬性偵測器] 中編輯或調整其屬性:

編輯功能表的屬性

我們將在本文稍後新增、編輯和刪除功能表和專案。 現在,我們只想查看預設可用的功能表和功能表項,以及它們如何透過一組預先定義的出口和動作自動公開至程式代碼(如需詳細資訊,請參閱我們的 出口和動作 檔)。

例如,如果我們按兩下 [開啟] 功能表項的 [連線 ion Inspector],我們可以看到它會自動連接到openDocument:動作:

檢視附加動作

如果您在 [介面階層] 中選取 [第一個回應程式],並在 連線 ion Inspector 中向下捲動,您會看到 [開啟] 功能表項所附加的動作定義openDocument:(以及應用程式的數個其他預設動作,且不會自動連接到控件):

檢視所有附加動作

這為什麼很重要? 在下一節中,您將瞭解如何使用這些自動定義的動作與其他Cocoa使用者介面元素搭配運作,以自動啟用和停用功能表項,以及為專案提供內建功能。

稍後,我們將使用這些內建動作來啟用和停用程式代碼中的專案,並在選取專案時提供自己的功能。

內建功能

如果您是在新增任何 UI 專案或程式代碼之前執行新建立的 Xamarin.Mac 應用程式,您會發現某些專案會自動連線並為您啟用(自動內建功能),例如 [應用程式] 功能表中的 [結束] 專案:

已啟用的功能表項

雖然其他功能表項,例如 剪下複製貼上 不是:

停用的功能表項

讓我們停止應用程式,然後按兩下SolutionPad中的Main.storyboard檔案,以開啟它以在 Xcode 的 Interface Builder 中編輯。 接下來,將文字檢視從 [連結庫] 拖曳至 [介面編輯器] 中的窗口檢視控制器:

從文檔庫選取文字檢視

在 [ 條件約束編輯器] 中,讓我們將文字檢視釘選到視窗的邊緣,並藉由按兩下編輯器頂端的四個紅色 I 型梁,然後按兩下 [新增 4 條件約束 ] 按鈕,將它設定為視窗成長和縮小的位置:

編輯限制

將變更儲存至使用者介面設計,並切換回 Visual Studio for Mac,以同步處理 Xamarin.Mac 項目的變更。 現在啟動應用程式,在文字檢視中輸入一些文字、加以選取,然後開啟 [ 編輯 ] 功能表:

功能表項會自動啟用/停用

請注意如何自動啟用剪下複製貼上專案並完全正常運作,而不需要撰寫單行程序代碼。

這其中發生了什麼狀況? 請記住內建的預先定義動作,這些動作會連接到預設功能表項(如上所示),大部分屬於macOS的Cocoa用戶介面元素都內建了特定動作的勾點(例如 copy:)。 因此,當它們新增至視窗、使用中和選取時,會自動啟用附加至該動作的對應功能表項或專案。 如果使用者選取該功能表項,則會呼叫並執行UI元素內建的功能,而不需要開發人員介入。

啟用和停用功能表和專案

根據預設,每次發生使用者事件時, NSMenu 都會根據應用程式的內容自動啟用和停用每個可見的功能表和功能表項。 有三種方式可以啟用/停用專案:

  • 自動選單啟用 - 如果 NSMenu 可以找到回應專案所連線動作的適當物件,則會啟用功能表項。 例如,上述文字檢視具有動作的內建勾點 copy:
  • 自定義動作和 validateMenuItem: - 對於系結至 視窗或檢視控制器自定義動作的任何功能表項,您可以新增 validateMenuItem: 動作,並手動啟用或停用功能表項。
  • 手動功能表啟用 - 您可以手動設定 Enabled 每個 NSMenuItem 的 屬性,以個別啟用或停用功能表中的每個專案。

若要選擇系統,請設定 AutoEnablesItemsNSMenu屬性。 true 是自動的(預設行為),而且 false 是手動的。

重要

如果您選擇使用手動功能表啟用,則任何功能表項,即使是由 類似 NSTextView的 AppKit 類別所控制的功能表項,也會自動更新。 您將負責透過手動程式代碼啟用和停用所有專案。

使用 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 元素,因此我們會將程式代碼新增至應用程式委派。

在 Solution Pad,按兩下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);

使用自訂視窗動作

就像預先連接到標準功能表項的內 建 First Responder 動作一樣,您可以建立新的自定義動作,並將其連線到 Interface Builder 中的功能表項。

首先,在應用程式的其中一個視窗控制器上定義自定義動作。 例如:

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

接下來,按兩下SolutionPad中的應用程式分鏡腳本檔案,以開啟它以在 Xcode 的 Interface Builder 中編輯。 選取 [應用程式場景] 底下的 [第一個回應者],然後切換至 [屬性偵測器]:

屬性偵測器

+點選單擊 [屬性偵測器] 底部的按鈕以新增自訂動作:

新增動作

指定與您在視窗控制器上建立的自訂動作相同的名稱:

編輯動作名稱

從功能表項單擊控件,然後拖曳到應用程式場景下的 [第一個回應者]。 從快顯清單中,選取您剛才建立的新動作(defineKeyword: 在此範例中):

附加動作

將變更儲存至分鏡腳本,並返回 Visual Studio for Mac 以同步處理變更。 如果您執行應用程式,將自訂動作連線至 的功能表項將會自動啟用/停用(根據開啟動作的視窗),然後選取選單項將會關閉動作:

測試新動作

新增、編輯和刪除功能表

如前幾節所見,Xamarin.Mac 應用程式隨附預設數目的預設功能表和功能表項,特定 UI 控制件會自動啟動和回應。 我們也已瞭解如何將程式代碼新增至應用程式,以啟用和回應這些預設專案。

在本節中,我們將探討移除不需要的功能表項、重新組織功能表,以及新增功能表、功能表項和動作。

按兩下 Solution Pad 中的 Main.storyboard 檔案,以開啟它以進行編輯:

按兩下分鏡文稿檔案,以在 Xcode 中編輯 UI。

針對我們特定的 Xamarin.Mac 應用程式,我們不會使用預設 的 [檢視 ] 功能表,因此我們會將其移除。 在 [ 介面階層 ] 中,選取 屬於主功能表欄一部分的 [檢視 ] 功能表項:

選取 [檢視] 功能選單項

按 delete 或 backspace 以刪除功能表。 接下來,我們不會使用 [格式] 功能表中的所有專案,而我們想要從子功能表下移動要用到的專案。 在 [ 介面階層] 中,選取下列功能表項:

醒目提示多個專案

將專案從目前所在的子選單拖曳到父功能表下方:

將功能表項拖曳至父功能表

您的功能表現在看起來應該像這樣:

新位置中的專案

接下來,讓我們將 [文字] 子功能表從 [格式] 功能表下方曳出來,並將它放在 [格式] 和 [視窗] 功能表之間的主功能表欄上:

[文字] 功能表

讓我們回到 [ 格式] 功能表下,並刪除 [字型 ] 子功能表項。 接下來,選取 [ 格式] 選單,並將它重新命名為 “Font”:

[字型] 功能表

接下來,讓我們建立預先定義片語的自定義功能表,以在選取文字檢視時自動附加至文字檢視中的文字。 在 [鏈接庫偵測器] 底部的搜尋方塊中,輸入 「功能表」。這可讓您更輕鬆地尋找和使用所有功能表 UI 元素:

連結庫偵測器

現在讓我們執行下列動作來建立我們的功能表:

  1. 將選單項目從 [連結庫偵測器] 拖曳到 [文字] 和 [視窗] 選單之間的選單列:

    在文檔庫中選取新的功能表項

  2. 重新命名專案 「Phrases」

    設定功能表名稱

  3. 接下來,從連結庫偵測器拖曳功能表

    從文檔庫選取功能表

  4. 在剛建立的新功能表項上,卸除 [功能表] ,並將其名稱變更為 “Phrases”:

    編輯功能表名稱

  5. 現在讓我們重新命名三個默認 功能表項 「Address」、」Date“ 和 「Greeting」:

    [片語] 選單

  6. 讓我們從連結庫偵測器拖曳功能表項,並將其呼叫為「簽章」,以新增第四功能表項

    編輯功能表項名稱

  7. 將變更儲存至功能表欄。

現在讓我們建立一組自定義動作,讓我們的新功能表項公開至 C# 程式代碼。 在 Xcode 中,讓我們切換至 小幫手 檢視:

建立必要的動作

讓我們執行下列動作:

  1. 將 Control-drag 從 [位址 ] 功能表項拖曳至 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);
    }
}

對於任何 LanguageFormatCommand 具有空白 Title 屬性的物件,此例程會在 功能表區段之間建立分隔符功能表項 (細灰色線):

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 檔案,並將視窗視窗新增至設計、將其類別設定為 Identity Inspector 中的 “NSPanel”、將新的 Assistant 專案新增至 [視窗] 功能表,然後使用 [顯示 Segue] 將它附加至新視窗:

在主要點分鏡腳本檔案中設定segue類型。

讓我們執行下列動作:

  1. 將標籤從 [連結庫偵測器] 拖曳到 [面板] 視窗,並將其文字設定為 “Property”:

    編輯標籤的值

  2. 接下來,將功能表從文檔庫偵測器拖曳到檢視階層中的檢視控制器,然後重新命名三個默認功能表項[檔]、[文字] 和 [字]:

    必要的功能表項

  3. 現在,從 屬性標籤 將控件拖曳到 功能表

    拖曳以建立 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); 會建立新的狀態列專案。 從該處,我們會建立功能表和一些功能表項,並將功能表附加至我們剛才建立的狀態欄專案。

如果我們執行應用程式,則會顯示新的狀態列專案。 從選單中選取專案將會變更文字檢視中的文字:

正在執行的狀態列功能表

接下來,讓我們來看看如何建立自定義擴充座功能表項。

自定義停駐功能表

當使用者以滑鼠右鍵按下或按下停駐區中的應用程式圖示時,Mac 應用程式會出現停駐功能表:

自定義停駐功能表

讓我們執行下列動作,為應用程式建立自定義擴充座功能表:

  1. 在 Visual Studio for Mac 中,以滑鼠右鍵按鍵的項目,然後選取 [新增>檔案...從 [新增檔案] 對話框中,選取 [Xamarin.Mac>空白介面定義],使用 “DockMenu” 作為 [名稱],然後按兩下 [新增] 按鈕以建立新的 DockMenu.xib 檔案:

    新增空白介面定義

  2. 在 Solution Pad,按兩下 DockMenu.xib 檔案以開啟它以在 Xcode 中編輯。 使用下列專案建立新的選單:位址、日期、問候語簽章

    配置 UI

  3. 接下來,讓我們將新的功能表項連線到我們在上方的 [新增、編輯和刪除功能表 ] 區段中為自定義功能表建立的現有動作。 切換至 連線 ion Inspector,然後選取介面階層中的第一個回應程式。 向下捲動並尋找 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. 接下來,讓我們將新的功能表項連線到我們在上方的 新增、編輯和刪除功能表 一節中為自定義功能表建立的現有動作。 切換至 [連線 ion Inspector],然後選取 [介面階層] 中的 [第一個回應者]。 向下捲動並尋找 phraseAddress: 動作。 將該動作上的圓形拖曳一行至 [位址 ] 選單項:

    拖曳以連接動作

  4. 針對附加至其對應動作的所有其他選單項重複:

    所有必要的動作

  5. 儲存變更並切換回 Visual Studio for Mac 以與 Xcode 同步。

現在,如果我們執行應用程式並從彈出視窗中選取專案,文字檢視中的文字將會變更:

執行中的快顯範例

您可以使用與快顯按鈕完全相同的方式來建立及使用下拉式清單。 您可以建立自己的自定義動作,而不是附加至現有的動作,就像我們在關係型功能表一節中針對關係型功能表所做的一樣。

摘要

本文已詳細探討在 Xamarin.Mac 應用程式中使用功能表和功能表項。 首先,我們檢查了應用程式的功能表欄,然後我們查看了建立關係型功能表,接下來我們檢查了狀態欄功能表和自定義停駐功能表。 最後,我們已涵蓋快捷功能表和下拉式清單。