次の方法で共有


Xamarin.Mac のメニュー

この記事では、Xamarin.Mac アプリケーションでのメニューの使用について説明しています。 Xcode および Interface Builder でのメニュー項目とメニュー項目の作成と保守、およびプログラムによる操作について説明します。

Xamarin.Mac アプリケーションで C# と .NET を使用している場合、Objective-C と Xcode を使用する開発者が使用しているのと同じ Cocoa メニューにアクセスできます。 Xamarin.Mac は直接 Xcode と統合できるため、Xcode の Interface Builder を使用して、メニュー バー、メニュー、メニュー項目を作成および保守できます (または、必要に応じて C# コードで直接作成することも可能です)。

メニューは Mac アプリケーションのユーザー エクスペリエンスに不可欠な要素であり、一般的にユーザー インターフェイスのさまざまな部分に表示されます。

  • アプリケーションのメニュー バー - これは、すべての Mac アプリケーションの画面の上部に表示されるメイン メニューです。
  • コンテキスト メニュー - ユーザーがウィンドウ内の項目を右クリックする、または Ctrl キーを押しながらクリックすると表示されます。
  • ステータス バー - これは、画面の上部 (メニュー バーの時計の左側) に表示され、項目が追加されると左側に拡大する、アプリケーション メニュー バーの右端にある領域です。
  • Dock メニュー - ユーザーがアプリケーションのアイコンを右クリックするか、Ctrl キーを押しながらクリックする、またはユーザーがアイコンを左クリックしてマウス ボタンを押したままにすると表示される、ドック内の各アプリケーションのメニュー。
  • ポップアップ ボタンとプルダウン リスト - ポップアップ ボタンには、選択した項目が表示され、ユーザーがクリックしたときに選択できるオプションの一覧が表示されます。 プルダウン リストは、現在のタスクのコンテキストに特定のコマンドを選択する目的で主に使用されるポップアップ ボタンの一種です。 どちらもウィンドウ内の任意の場所に表示できます。

メニューの例

この記事では、Xamarin.Mac アプリケーションでの Cocoa メニュー バー、メニュー、メニュー項目の操作の基本について説明します。 この記事で使う主要な概念と手法については、まず Hello Mac の記事、特に「Xcode と Interface Builder の概要」と「Outlet と Action」のセクションを参照することを強くお勧めします。

Xamarin.Mac Internals ドキュメントの C# クラス/メソッドの Objective-C への公開に関するセクションも参照することをお勧めします。C# クラスを Objective-C オブジェクトと UI 要素に結び付けるために使われる Register および Export 属性について説明されています。

アプリケーションのメニュー バー

すべてのウィンドウに独自のメニュー バーをアタッチできる Windows OS 上で実行されているアプリケーションとは異なり、macOS で実行されているすべてのアプリケーションに、そのアプリケーション内のすべてのウィンドウに使用される画面上部に沿って並ぶ 1 つのメニュー バーがあります。

メニュー バー

このメニュー バーの項目は、現在のコンテキスト、またはアプリケーションの状態とそのユーザー インターフェイスに基づいて、任意の時点でアクティブ化または非アクティブ化されます。 たとえば、ユーザーがテキスト フィールドを選択した場合、[Edit] メニューの項目 ([Copy][Cut] など) が有効になります。

Apple によると、すべての macOS アプリケーションに、アプリケーションのメニュー バーに表示される既定のメニューとメニュー項目の標準セットがあります。

  • Apple メニュー - このメニューでは、実行中のアプリケーションに関係なく、ユーザーがいつでも使用できるシステム全体の項目にアクセスできます。 これらの項目は、開発者が変更することはできません。
  • アプリ メニュー - このメニューには、アプリケーションの名前が太字で表示され、ユーザーが現在実行中のアプリケーションを識別するのに役立ちます。 アプリケーション全体に適用される項目が含まれており、特定のドキュメントやプロセス (アプリケーションの終了など) は含まれません。
  • [File] メニュー - アプリケーションで使用するドキュメントを作成、開く、または保存するために使用する項目があります。 アプリケーションがドキュメント ベースでない場合は、このメニューの名前を変更または削除できます。
  • [Edit] メニュー - アプリケーションのユーザー インターフェイスで要素を編集または変更するために使用される [Cut][Copy][Paste] などのコマンドを保持します。
  • [Format] メニュー - アプリケーションがテキストを使用して動作する場合、このメニューには、そのテキストの書式設定を調整するコマンドが保持されます。
  • [View] メニュー - アプリケーションのユーザー インターフェイスでのコンテンツの表示方法に影響するコマンドを保持します。
  • アプリケーション固有のメニュー - アプリケーションに固有のメニュー (Web ブラウザーのブックマーク メニューなど) があります。 これらはバーの [View] メニューと [Window] メニューの間に表示されます。
  • [Window] メニュー - アプリケーションでウィンドウを操作するためのコマンドと、現在開いているウィンドウの一覧が含まれます。
  • ヘルプ メニュー - アプリケーションが画面上のヘルプを提供する場合は、ヘルプ メニューがバーの右端のメニューである必要があります。

アプリケーションメニューバーと標準メニューとメニュー項目の詳細については、Apple のヒューマン インターフェイス ガイドラインを参照してください。

既定のアプリケーション メニュー バー

前のセクションで説明したように、新しい Xamarin.Mac プロジェクトを作成するたびに、macOS アプリケーションが通常持つ一般的な項目を含む標準の既定のアプリケーション メニュー バーが自動的に表示されます。 アプリケーションの既定のメニュー バーは、アプリの UI のその他の部分に加えて、Solution Pad 内のプロジェクトの下の Main.storyboard ファイルで定義されます。

メイン ストーリーボードを選択する

Main.storyboard ファイルをダブルクリックして Xcode の Interface Builder で編集用に開くと、メニュー エディター インターフェイスが表示されます。

Xcode で UI を編集し、メイン ドット ストーリーボードを表示します。

ここから、[File] メニューの [Open] メニュー項目などの項目をクリックし、[Attributes Inspector] でそのプロパティを編集または調整できます。

メニューの属性を編集する

メニューと項目の追加、編集、削除については、この記事の後半で説明します。 ここでは、既定で使用できるメニューおよびメニュー項目と、定義済みのアウトレットとアクションのセットを介してこれらがコードに自動的に公開されている方法を確認するだけです (詳細については、「Outlet と Action」ドキュメントを参照してください)。

たとえば、[Open] メニュー項目の [Connection Inspector] をクリックすると、これは openDocument: アクションに自動的に関連付けられていることがわかります。

アタッチされたアクションを表示する

[Interface Hierarchy][First Responder] を選択し、[Connection Inspector] で下にスクロールすると、[Open] メニュー項目が接続されている openDocument: アクションの定義が表示されます (アプリケーションに対するその他のいくつかの既定のアクションと一緒に表示され、それらにはコントロールに自動的に接続されたものとされていないものがあります)。

アタッチされたすべてのアクションを表示する

これは、なぜ重要ですか。 次のセクションでは、これらの自動的に定義されたアクションが、他の Cocoa ユーザー インターフェイス要素と連携して、メニュー項目を自動的に有効または無効にする方法と、項目の組み込み機能を提供する方法について説明します。

後で、これらの組み込みアクションを使用して、コードの項目を有効または無効にし、選択されたときに独自の機能を提供します。

組み込みのメニュー機能

UI 項目やコードを追加する前に、新しく作成した Xamarin.Mac アプリケーションを実行した場合は、一部の項目が自動的に関連付けられ、完全な機能が自動的に組み込まれた状態で有効になっていることがわかります ([App] メニューの [Quit] 項目など)。

有効なメニュー項目

一方で、[Cut][Copy][Paste] などの他のメニュー項目は、そうではありません。

無効なメニュー項目

アプリケーションを停止し、Solution PadMain.storyboard ファイルをダブルクリックして、Xcode の Interface Builder で編集用に開きます。 次に、[Text View][Library] から [Interface Editor] のウィンドウのビュー コントローラーにドラッグします。

ライブラリからテキスト ビューを選択する

[Constraint Editor] で、テキスト ビューをウィンドウの端にピン留めし、ウィンドウと一緒に拡大縮小する場所を設定します。これを実行するには、エディターの上部にある 4 つの赤い I ビームをすべてクリックし、[Add 4 Constraints] ボタンをクリックします。

制約を編集する

変更をユーザー インターフェイス デザインに保存して Visual Studio for Mac に戻り、変更を Xamarin.Mac プロジェクトと同期します。 次に、アプリケーションを起動し、テキスト ビューにテキストを入力し、選択し、[Edit] メニューを開きます。

メニュー項目は自動的に有効または無効になります

[Cut][Copy][Paste] の各項目が、どれにも 1 行のコードも記述せずに、自動的に有効になり、完全に機能していることに注目してください。

どうなっているのでしょうか? 既定のメニュー項目に関連付けられた組み込みの定義済みアクション (上に示すとおり)、macOS の一部である Cocoa ユーザー インターフェイス要素のほとんどが、特定のアクション (copy: など) への組み込みフックを持つことを思い出してください。 そのため、それらがウィンドウに追加され、アクティブになり、選択されると、対応するメニュー項目またはそのアクションに関連付けられた項目が、自動的に有効になります。 ユーザーがそのメニュー項目を選択すると、UI 要素に組み込まれた機能が、すべて開発者の介入なしに呼び出されて実行されます。

メニューと項目の有効化と無効化

既定では、ユーザー イベントが発生するたびに、NSMenu は、アプリケーションのコンテキストに基づいて、表示される各メニューおよびメニュー項目を自動的に有効および無効にします。 項目を有効または無効にするには、次の 3 つの方法があります。

  • 自動メニューの有効化 - NSMenu が項目の関連付け先アクションに応答する適切なオブジェクトを見つけられると、メニュー項目が有効になります。 たとえば、上のテキスト ビューには、copy: アクションへの組み込みフックがあります。
  • カスタム アクションと validateMenuItem: - ウィンドウまたはビュー コントローラーのカスタム アクションにバインドされている任意のメニュー項目の場合は、validateMenuItem: アクションを追加し、メニュー項目を手動で有効または無効にできます。
  • 手動でのメニューの有効化 - メニューの各項目を個別に有効または無効にするように、各 NSMenuItemEnabled プロパティを手動で設定します。

システムを選択するには、NSMenuAutoEnablesItems プロパティを設定します。 true は自動 (既定の動作) であり、false は手動です。

重要

手動でのメニューの有効化を使用する場合、NSTextView などの AppKit クラスによって制御されるメニュー項目であっても、どのメニュー項目も自動的に更新されることはありません。 コード内のすべての項目を手動で有効または無効にする責任は、開発者にあります。

validateMenuItem の使用

前述のように、ウィンドウまたはビュー コントローラーのカスタム アクションにバインドされているメニュー項目に対し、validateMenuItem: アクションを追加し、メニュー項目を手動で有効または無効にすることができます。

次の例では、Tag プロパティが、NSTextView で選択したテキストの状態に基づいて、validateMenuItem: アクションで有効または無効にするメニュー項目の種類を決定するために使用されます。 Tag プロパティは、各メニュー項目に対して Interface Builder で設定されています。

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 でテキストが選択されていない場合、2 つのラップ メニュー項目は、ビュー コントローラー上のアクションに関連付けられていても無効になります。

無効なアイテムを表示する

テキストのセクションが選択され、メニューが再度開かれた場合、2 つのラップ メニュー項目が使用できるようになります。

有効な項目を表示する

コード内のメニュー項目の有効化と応答

前述のように、UI デザインに特定の Cocoa ユーザー インターフェイス要素 (テキスト フィールドなど) を追加するだけで、いくつかの既定のメニュー項目が有効になり、コードを記述しなくても自動的に機能します。 次に、Xamarin.Mac プロジェクトに独自の C# コードを追加してメニュー項目を有効にし、ユーザーが選択したときに機能を提供する方法を見てみましょう。

たとえば、ユーザーが [File] メニューの [Open] 項目を使用してフォルダーを選択できるようにするとします。 これをアプリケーション全体の機能にし、特定のウィンドウまたは UI 要素に限定されないようにするために、これを処理するコードをアプリケーション デリゲートに追加します。

Solution PadAppDelegate.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 ();
    }
}

ここでアプリケーションを実行し、[File] メニューを開きます。

[ファイル] メニュー

[Open] メニュー項目が有効になっていることに注意してください。 これを選択すると、開くダイアログが表示されます。

オープン ダイアログ

[Open] ボタンをクリックすると、アラート メッセージが表示されます。

ダイアログ メッセージの例

ここでの重要な行は [Export ("openDocument:")] であり、これは、AppDelegateopenDocument: アクションに応答する void OpenDialog (NSObject sender) メソッドがあることを NSMenu 示しています。 これまでの説明で、[Open] メニュー項目が、Interface Builder の既定でこのアクションに自動的に関連付けられていたことを思い出してください。

アタッチされたアクションを表示する

次に、独自のメニュー、メニュー項目、アクションを作成し、コードでそれらに応答する方法を見てみましょう。

最近開いたメニューの操作

既定では、[File] メニューには、ユーザーがアプリで開いた最後のいくつかのファイルを追跡する [Open Recent] 項目が含まれています。 NSDocument ベースの Xamarin.Mac アプリを作成する場合、このメニューは自動的に処理されます。 その他の種類の Xamarin.Mac アプリについては、このメニュー項目を手動で管理し、応答する必要があります。

[Open Recent] メニューを手動で処理するには、まずこのメニューに対し、次を使用して、新しいファイルが開かれたか保存されたことを通知する必要があります。

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

アプリで NSDocuments を使用していない場合でも、[Open Recent] メニューを維持するために NSDocumentController を使用します。これには、ファイルの場所と一緒に NSUrlSharedDocumentControllerNoteNewRecentDocumentURL メソッドに送信します。

次に、アプリ デリゲートの OpenFile メソッドをオーバーライドして、[Open Recent] メニューからユーザーが選択した任意のファイルを開く必要があります。 次に例を示します。

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 を返し、ファイルを開けなかったという組み込みの警告がユーザーに表示されます。

[Open Recent] メニューから返されるファイル名とパスにはスペースが含まれている可能性があるため、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
    }
}

アプリの要件に基づいて、ユーザーが同じファイルを複数のウィンドウで同時に開かないようにしたい場合があります。 このサンプル アプリでは、ユーザーが既に開いているファイルを選択した場合 ([Open Recent] または [Open] のいずれかのメニュー項目から)、そのファイルを含むウィンドウが前面に表示されます。

これを実現するために、ヘルパー メソッドで次のコードを使用しました。

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;
    }
}

Path プロパティ内のファイルへのパスを保持するように、ViewController クラスを設計しました。 次は、アプリで現在開いているすべてのウィンドウをループ処理します。 ファイルがいずれかのウィンドウで既に開いている場合は、次を使用すると、そのウィンドウが他のすべてのウィンドウの前面に表示されます。

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

一致するものが見つからない場合は、ファイルが読み込まれた新しいウィンドウが開き、このファイルは [Open Recent] メニューに含められます。

// 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");
}

次に、Solution Pad でアプリのストーリーボード ファイルをダブルクリックして、Xcode の Interface Builder で編集するために開きます。 [Application Scene][First Responder] を選択し、[Attributes Inspector] に切り替えます。

属性インスペクター

[Attributes Inspector] の下部にある + ボタンをクリックして、新しいカスタム アクションを追加します。

新しいアクションを追加する

これに、ウィンドウ コントローラーで作成したカスタム アクションと同じ名前を付けます。

アクション名を編集する

Ctrl キーを押しながらメニュー項目からクリックし、[Application Scene] の下にある [First Responder] にドラッグします。 ポップアップ リストから、先ほど作成した新しいアクション (この例の defineKeyword:) を選択します。

アクションをアタッチする

ストーリーボードへの変更内容を保存し、Visual Studio for Mac に戻って変更を同期します。 アプリを実行すると、カスタム アクションを接続したメニュー項目が、アクションが開いているウィンドウに基づいて自動的に有効または無効になり、メニュー項目を選択するとアクションが起動します。

新しいアクションをテストする

メニューの追加、編集、削除

前のセクションで説明したように、Xamarin.Mac アプリケーションには、特定の UI コントロールが自動的にアクティブにし、応答する既定のメニューとメニュー項目があらかじめ用意されています。 また、これらの既定の項目を有効にして応答するコードをアプリケーションに追加する方法についても説明しました。

このセクションでは、不要なメニュー項目の削除、メニューの再構成、新しいメニュー、メニュー項目、アクションの追加について説明します。

Solution PadMain.storyboard ファイルをダブルクリックし、編集するために開きます。

ストーリーボード ファイルをダブルクリックして Xcode で UI を編集します。

ここでの特定の Xamarin.Mac アプリケーションでは、既定の [View] メニューを使用しないため、削除します。 [Interface Hierarchy] で、メイン メニュー バーに属する [View] メニュー項目を選択します。

[表示] メニュー項目を選択する

Del キーまたは Backspace キーを押してメニューを削除します。 次に、[Format] メニュー内のすべての項目を使用するのではなく、これから使用する項目をサブメニューの下から移動します。 [Interface Hierarchy] で、次のメニュー項目を選択します。

複数のアイテムを強調表示する

[Menu] の下の項目を、現在それらが配置されているサブメニューからドラッグします。

メニュー項目を親メニューにドラッグする

メニューは次のようになります。

新しい場所の項目

次に、[Format] メニューの下から [Text] サブメニューをドラッグし、[Format] メニューと [Window] メニューの間のメイン メニュー バーに配置します。

[テキスト] メニュー

[Format] メニューの下に戻り、[Font] サブメニュー項目を削除します。 次に、[Format] メニューを選択し、「Font」という名前に変更します。

[フォント] メニュー

次に、定義済みのフレーズのカスタム メニューを作成し、このフレーズが、テキスト ビューのテキストが選択されたときに自動的に追加されるようにします。 [Library Inspector] の下部にある検索ボックスに、「メニュー」と入力します。これにより、すべてのメニュー UI 要素が簡単に見つかり操作しやすくなります。

ライブラリ インスペクター

次に、メニューを作成するために次の操作を行います。

  1. [Menu Item][Library Inspector] から [Text] メニューと [Window] メニューの間のメニュー バーにドラッグします。

    ライブラリで新しいメニュー項目を選択する

  2. 項目の名前を「Phrases」に変更します。

    メニュー名を設定する

  3. 次に、[Library Inspector] から [Menu] をドラッグします。

    ライブラリからメニューを選択する

  4. [Menu] を先ほど作成した新しい [Menu Item] にドロップし、その名前を「Phrases」に変更します。

    メニュー名を編集する

  5. 次に、既定の 3 つの [Menu Items] の名前を「Address」、「Date」、「Greeting」に変更します。

    [フレーズ] メニュー

  6. 4 番目の [Menu Item] を追加します。これには、[Menu Item][Library Inspector] からドラッグし、「Signature」という名前を付けます。

    メニュー項目名を編集する

  7. 変更をメニュー バーに保存します。

次に、新しいメニュー項目が C# コードに提示されるように、一連のカスタム アクションを作成します。 Xcode で [Assistant] ビューに切り替えます。

必要なアクションを作成する

次の操作を行います。

  1. Ctrl キーを押しながら [Address] メニュー項目を AppDelegate.h ファイルにドラッグします。

  2. [接続] の種類を [アクション] に切り替えます。

    アクションの種類を選択する

  3. [Name] に「phraseAddress」と入力し、[Connect] ボタンを押して新しいアクションを作成します。

    名前を入力してアクションを構成する。

  4. 上記の手順を [Date][Greeting][Signature] の各メニュー項目に対して繰り返します。

    完了したアクション

  5. 変更をメニュー バーに保存します。

次に、コードからコンテンツを調整できるように、テキスト ビューのアウトレットを作成する必要があります。 ViewController.h ファイルを [Assistant Editor] で選択し、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
    }
}

ここでは、Interface Builder で定義したアクションとアウトレットを使用できるように、AppDelegate を部分クラスにしました。 また、どのウィンドウが現在フォーカスされているかを追跡するために、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";
}

アプリケーションを実行すると、[Phrase] メニューのすべての項目がアクティブになり、選択されたときに、フレーズがテキスト ビューに追加されます。

実行中のアプリの例

アプリケーション メニュー バーの操作の基本を説明したので、次はカスタム コンテキスト メニューの作成を見てみましょう。

コードからのメニューの作成

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 関数にそのコレクション ([Format] メニューが基本として設定されている) が渡され、次の動的メニューとメニュー項目が作成されます。

実行中のアプリの新しいメニュー項目

メニューと項目の削除

アプリのユーザー インターフェイスからメニューまたはメニュー項目を削除する必要がある場合は、NSMenu クラスの RemoveItemAt メソッドを使用して、削除する項目の 0 から始まるインデックスをこれに指定するだけ実行できます。

たとえば、上記のルーチンによって作成されたメニューとメニュー項目を削除するには、次のコードを使用できます。

public void UnpopulateFormattingMenu(NSMenu menu) {

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

上記のコードの場合、最初の 4 つのメニュー項目は Xcode の Interface Builder で作成され、アプリで使用可能な場所に配置されるため、動的には削除されません。

コンテキスト メニュー

コンテキスト メニューは、ユーザーがウィンドウ内の項目を右クリックするか、Ctrl キーを押しながらクリックすると表示されます。 既定では、macOS に組み込まれているいくつかの UI 要素には、コンテキスト メニュー (テキスト ビューなど) が既にアタッチされています。 ただし、ウィンドウに追加した UI 要素に対し、独自のカスタム コンテキスト メニューを作成する場合があります。

Xcode で Main.storyboard ファイルを編集し、デザインに [Window] ウィンドウを追加し、[Identity Inspector] でその [Class] を "NSPanel" に設定し、新しい [Assistant] 項目を [Window] メニューに追加します。次に、[Show Segue] を使用して、これを新しいウィンドウにアタッチします。

メイン ドット ストーリーボード ファイルでセグエの種類を設定します。

次の操作を行います。

  1. [Library Inspector] から [Label][Panel] ウィンドウにドラッグし、そのテキストを「Property」に設定します。

    ラベルの値を編集する

  2. 次に、[Library Inspector] から [Menu] を [View Hierarchy] の [View Controller] にドラッグし、3 つのデフォルト メニュー項目の名前を [Document][Text][Font] に変更します。

    必要なメニュー項目

  3. 次に、[Property Label] から Ctrl キーを押しながら [Menu] にドラッグします。

    ドラッグしてセグエを作成する

  4. ポップアップ ダイアログから [Menu] を選択します。

    [ラベル] コンテキスト メニューの [Outlets] (アウトレット) からメニューを選択して、セグエの種類を設定します。

  5. [Identity Inspector] から、Identity Inspector のクラスを「PanelViewController」に設定します。

    セグエ クラスを設定する

  6. 同期するには Visual Studio for Mac に戻ってから、Interface Builder に戻ります。

  7. [Assistant Editor] に切り替え、PanelViewController.h ファイルを選択します。

  8. [Document] メニュー項目に propertyDocument という名前のアクションを作成します。

    propertyDocument という名前のアクションの構成。

  9. 残りのメニュー項目に対して、アクションの作成を繰り返します。

    残りのメニュー項目に対して、アクションの作成を繰り返します。

  10. 最後に、[Property Label]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); は、新しいステータス バー項目を作成します。 そこからメニューといくつかのメニュー項目を作成し、先ほど作成したステータス バー項目にメニューを添付します。

アプリケーションを実行すると、新しいステータス バー項目が表示されます。 メニューから項目を選択すると、テキスト ビューのテキストが変更されます。

実行中のステータス バー メニュー

次は、カスタム Dock メニュー項目の作成を見てみましょう。

カスタム Dock メニュー

ユーザーが Dock でアプリケーションのアイコンを右クリックするか、Ctrl キーを押しながらクリックすると、Mac アプリケーションの Dock メニューが表示されます。

カスタム ドック メニュー

次の手順を実行して、アプリケーションのカスタム Dock メニューを作成しましょう。

  1. Visual Studio for Mac で、アプリケーションのプロジェクトを右クリックし、[Add]>[New File] を選択します新しいファイル ダイアログで、[Xamarin.Mac]>[Empty Interface Definition] を選択し、[Name] に "DockMenu" を使用し、[New] ボタンをクリックして、新しい DockMenu.xib ファイルを作成します。

    空のインターフェイス定義を追加する

  2. Solution PadDockMenu.xib ファイルをダブルクリックし、Xcode で編集するために開きます。 新しい [Menu][Address][Date][Greeting][Signature] の各項目を持たせて作成します

    UI のレイアウト

  3. 次に、新しいメニュー項目を、上記の「メニューの追加、編集、削除」セクションでカスタム メニュー用に作成した既存のアクションに接続します。 [Connection Inspector] に切り替え、[Interface Hierarchy][First Responder] を選択します。 下にスクロールして phraseAddress: アクションを見つけます。 そのアクションの円から [Address] メニュー項目に線をドラッグします。

    行を [アドレス] メニュー項目にドラッグします。

  4. 他のすべてのメニュー項目に対して繰り返し、対応するアクションにアタッチします。

    他のすべてのメニュー項目に対して繰り返し、対応するアクションにアタッチします。

  5. 次に、[Interface Hierarchy][Application] を選択します。 [Connection Inspector] で、dockMenu アウトレットの円から先ほど作成したメニューに線をドラッグします。

    ドラッグしてアウトレットを結び付ける

  6. 変更内容を保存し、Visual Studio for Mac に戻って Xcode と同期します。

  7. Info.plist ファイルをダブルクリックして、編集用に開きます。

    Info.plist ファイルの編集

  8. 画面の下部にある [Source] タブをクリックします。

    ソース ビューを選択する

  9. [Add new entry] をクリックし、緑色のプラス ボタンをクリックし、プロパティ名を "AppleDockMenu" に設定し、値を "DockMenu" (拡張子のない新しい .xib ファイルの名前) に設定します。

    DockMenu 項目を追加する

ここで、アプリケーションを実行し、Dock でそのアイコンを右クリックすると、新しいメニュー項目が表示されます。

実行中のドック メニューの例

メニューからカスタム項目の 1 つを選択すると、テキスト ビューのテキストが変更されます。

ポップアップ ボタンとプルダウン リスト

ポップアップ ボタンに、選択した項目が表示され、ユーザーがボタンをクリックしたときに選択するオプションの一覧が表示されます。 プルダウン リストは、現在のタスクのコンテキストに特定のコマンドを選択する目的で主に使用されるポップアップ ボタンの一種です。 どちらもウィンドウ内の任意の場所に表示できます。

次の手順を実行して、アプリケーションのカスタム ポップアップ ボタンを作成しましょう。

  1. Xcode で Main.storyboard ファイルを編集し、[Library Inspector] から [Popup Button][Contextual Menus] セクションで作成した [Panel] ウィンドウにドラッグします。

    ポップアップ ボタンを追加する

  2. 新しいメニュー項目を追加し、[Popup] の [Items] のタイトルをそれぞれ [Address][Date][Greeting][Signature] に設定します

    メニュー項目を構成する

  3. 次に、新しいメニュー項目を、上記の「メニューの追加、編集、削除」セクションでカスタム メニュー用に作成した既存のアクションに接続します。 [Connection Inspector] に切り替え、[Interface Hierarchy][First Responder] を選択します。 下にスクロールして phraseAddress: アクションを見つけます。 そのアクションの円から [Address] メニュー項目に線をドラッグします。

    ドラッグしてアクションを結び付ける

  4. 他のすべてのメニュー項目に対して繰り返し、対応するアクションにアタッチします。

    必要なすべてのアクション

  5. 変更内容を保存し、Visual Studio for Mac に戻って Xcode と同期します。

ここで、アプリケーションを実行し、ポップアップから項目を選択すると、テキスト ビューのテキストが変更されます。

実行中のポップアップの例

プルダウン リストは、ポップアップ ボタンとまったく同じ方法で作成して操作できます。 既存のアクションにアタッチする代わりに、[Contextual Menus] セクションのコンテキスト メニューに対して実行したのと同様に、独自のカスタム アクションを作成できます。

まとめ

この記事では、Xamarin.Mac アプリケーションでのメニューとメニュー項目の操作について詳しく説明しました。 最初にアプリケーションのメニュー バーを調べ、次にコンテキスト メニューを作成し、次にステータス バーのメニューとカスタム Dock メニューについて調べました。 最後に、ポップアップ メニューとプルダウン リストについて説明しました。