在 Xamarin.Mac 中不使用 .storyboard/.xib 的用户界面设计

本文介绍如何直接通过 C# 代码创建 Xamarin.Mac 应用程序的用户界面,而无需使用情节提要文件、.xib 文件或 Interface Builder。

概述

在 Xamarin.Mac 应用程序中使用 C# 和 .NET 时,你可以访问的用户界面元素和工具与使用 Objective-CXcode 的开发人员访问的元素和工具相同。 通常,创建 Xamarin.Mac 应用程序时,会将 Xcode 的 Interface Builder 与 .storyboard 或 .xib 文件配合使用来创建和维护应用程序的用户界面。

也可以直接使用 C# 代码创建部分或全部的 Xamarin.Mac 应用程序用户界面。 本文将会介绍使用 C# 代码创建用户界面和 UI 元素的基础知识。

Visual Studio for Mac 代码编辑器

切换窗口以使用代码

创建新的 Xamarin.Mac Cocoa 应用程序时,默认情况下会获得标准空白窗口。 此窗口自动在包含在项目中的 Main.storyboard(或过去的 MainWindow.xib)文件中定义。 其中还包括一个 ViewController.cs 文件,用于管理应用程序的主视图(或过去的 MainWindow.csMainWindowController.cs 文件)。

若要切换到应用程序的 Xibless 窗口,请执行以下操作:

  1. 打开想要停止使用 .storyboard 或 .xib 文件的应用程序,在 Visual Studio for Mac 中定义用户界面。

  2. Solution Pad 中,右键单击 Main.storyboardMainWindow.xib 文件,然后选择“删除”:

    删除主情节提要或窗口

  3. 在“删除对话框”中,单击“删除”按钮,彻底删除项目中的 .storyboard 或 .xib 文件:

    确认删除

接下来,我们需要修改 MainWindow.cs 文件来定义窗口的布局,并修改 ViewController.csMainWindowController.cs 文件以创建 MainWindow 类的实例,因为我们不再使用 .storyboard 或 .xib 文件。

用户界面使用情节提要的新式 Xamarin.Mac 应用程序可能不会自动包含 MainWindow.csViewController.csMainWindowController.cs 文件。 根据需要,只需向项目添加一个新的空的 C# 类(添加>新建文件…>常规>空类),并将其命名为与缺少的文件相同的名称。

在代码中定义窗口

接下来,编辑 MainWindow.cs 文件,使其如下所示:

using System;
using Foundation;
using AppKit;
using CoreGraphics;

namespace MacXibless
{
    public partial class MainWindow : NSWindow
    {
        #region Private Variables
        private int NumberOfTimesClicked = 0;
        #endregion

        #region Computed Properties
        public NSButton ClickMeButton { get; set;}
        public NSTextField ClickMeLabel { get ; set;}
        #endregion

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

        [Export ("initWithCoder:")]
        public MainWindow (NSCoder coder) : base (coder)
        {
        }

        public MainWindow(CGRect contentRect, NSWindowStyle aStyle, NSBackingStore bufferingType, bool deferCreation): base (contentRect, aStyle,bufferingType,deferCreation) {
            // Define the user interface of the window here
            Title = "Window From Code";

            // Create the content view for the window and make it fill the window
            ContentView = new NSView (Frame);

            // Add UI elements to window
            ClickMeButton = new NSButton (new CGRect (10, Frame.Height-70, 100, 30)){
                AutoresizingMask = NSViewResizingMask.MinYMargin
            };
            ContentView.AddSubview (ClickMeButton);

            ClickMeLabel = new NSTextField (new CGRect (120, Frame.Height - 65, Frame.Width - 130, 20)) {
                BackgroundColor = NSColor.Clear,
                TextColor = NSColor.Black,
                Editable = false,
                Bezeled = false,
                AutoresizingMask = NSViewResizingMask.WidthSizable | NSViewResizingMask.MinYMargin,
                StringValue = "Button has not been clicked yet."
            };
            ContentView.AddSubview (ClickMeLabel);
        }
        #endregion

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

            // Wireup events
            ClickMeButton.Activated += (sender, e) => {
                // Update count
                ClickMeLabel.StringValue = (++NumberOfTimesClicked == 1) ? "Button clicked one time." : string.Format("Button clicked {0} times.",NumberOfTimesClicked);
            };
        }
        #endregion

    }
}

让我们来介绍几个关键元素。

首先,我们添加了几个计算属性,它们的作用类似于插座(如同该窗口是在 .storyboard 或 .xib 文件中创建的一样):

public NSButton ClickMeButton { get; set;}
public NSTextField ClickMeLabel { get ; set;}

这样,我们就能访问想要在窗口上显示的 UI 元素。 由于该窗口并不是从 .storyboard 或 .xib 文件放大的,因此我们需要将其实例化(如我们之后会在 MainWindowController 类中看到的那样)。 这就是这个新的构造函数的作用:

public MainWindow(CGRect contentRect, NSWindowStyle aStyle, NSBackingStore bufferingType, bool deferCreation): base (contentRect, aStyle,bufferingType,deferCreation) {
    ...
}

在这里,我们会设计窗口的布局,并放置创建用户界面所需的任何 UI 元素。 需要一个内容视图来包含这些元素,然后才能将任何 UI 元素添加到窗口:

ContentView = new NSView (Frame);

这会创建一个填充该窗口的内容视图。 接下来,我们将第一个 UI 元素 NSButton 添加到窗口:

ClickMeButton = new NSButton (new CGRect (10, Frame.Height-70, 100, 30)){
    AutoresizingMask = NSViewResizingMask.MinYMargin
};
ContentView.AddSubview (ClickMeButton);

这里首先要注意的是,与 iOS 不同,macOS 使用数学表示法来定义其窗口坐标系。 因此,原点位于窗口的左下角,值向着窗口的右侧和右上角不断增加。 创建新的 NSButton 时,我们会考虑到这一点,因为我们在屏幕上定义其位置和大小。

AutoresizingMask = NSViewResizingMask.MinYMargin 属性告诉按钮,当窗口垂直调整大小时,我们希望保持窗口顶部的位置不变。 这同样也是必需的,因为 (0,0) 位于窗口的左下角。

最后,ContentView.AddSubview (ClickMeButton) 方法将 NSButton 添加到内容视图。这样,当应用程序正在运行且窗口显示时,它会显示在屏幕上。

接下来,向会显示 NSButton 单击次数的窗口添加一个标签:

ClickMeLabel = new NSTextField (new CGRect (120, Frame.Height - 65, Frame.Width - 130, 20)) {
    BackgroundColor = NSColor.Clear,
    TextColor = NSColor.Black,
    Editable = false,
    Bezeled = false,
    AutoresizingMask = NSViewResizingMask.WidthSizable | NSViewResizingMask.MinYMargin,
    StringValue = "Button has not been clicked yet."
};
ContentView.AddSubview (ClickMeLabel);

由于 macOS 没有特定的标签 UI 元素,因此我们添加了一个特殊样式且不可编辑的 NSTextField 来作为标签使用。 就像之前的按钮一样,考虑到大小和位置,(0,0) 位于窗口的左下角。 AutoresizingMask = NSViewResizingMask.WidthSizable | NSViewResizingMask.MinYMargin 属性使用 or 运算符合并两个 NSViewResizingMask 功能。 这会让窗口垂直调整大小,以及横向调整大小导致宽度缩小和扩大时,该标签留在窗口顶部的相同位置。

再次调整,ContentView.AddSubview (ClickMeLabel) 方法将 NSTextField 添加到内容视图。这样,当应用程序正在运行且窗口打开时,它会显示在屏幕上。

调整窗口控制器

由于 MainWindow 的设计不再从 .storyboard 或 .xib 文件加载,我们将需要对窗口控制器进行一些调整。 编辑 MainWindowController.cs 文件,使其如下所示:

using System;

using Foundation;
using AppKit;
using CoreGraphics;

namespace MacXibless
{
    public partial class MainWindowController : NSWindowController
    {
        public MainWindowController (IntPtr handle) : base (handle)
        {
        }

        [Export ("initWithCoder:")]
        public MainWindowController (NSCoder coder) : base (coder)
        {
        }

        public MainWindowController () : base ("MainWindow")
        {
            // Construct the window from code here
            CGRect contentRect = new CGRect (0, 0, 1000, 500);
            base.Window = new MainWindow(contentRect, (NSWindowStyle.Titled | NSWindowStyle.Closable | NSWindowStyle.Miniaturizable | NSWindowStyle.Resizable), NSBackingStore.Buffered, false);

            // Simulate Awaking from Nib
            Window.AwakeFromNib ();
        }

        public override void AwakeFromNib ()
        {
            base.AwakeFromNib ();
        }

        public new MainWindow Window {
            get { return (MainWindow)base.Window; }
        }
    }
}

让我们介绍此修改的关键元素。

首先,我们定义 MainWindow 类的一个新的实例,并将其分配给基础窗口的 Window 属性:

CGRect contentRect = new CGRect (0, 0, 1000, 500);
base.Window = new MainWindow(contentRect, (NSWindowStyle.Titled | NSWindowStyle.Closable | NSWindowStyle.Miniaturizable | NSWindowStyle.Resizable), NSBackingStore.Buffered, false);

使用 CGRect 定义屏幕的窗口的位置。 与窗口的坐标系类似,屏幕将 (0,0) 定义为左下方的最低点。 接下来,我们使用 Or 运算符定义窗口的样式,以结合两个或多个 NSWindowStyle 功能:

... (NSWindowStyle.Titled | NSWindowStyle.Closable | NSWindowStyle.Miniaturizable | NSWindowStyle.Resizable) ...

可以使用以下 NSWindowStyle 功能:

  • 无边框 - 窗口将不会有边框。
  • 有标题 - 窗口将会有标题栏。
  • 可关闭 - 窗口有一个最小化按钮,可以缩小到最小化。
  • 可最小化 - 窗口有一个最小化按钮,可以缩小到最小化。
  • 可重设大小 - 窗口将会有一个重设大小按钮,可以重设大小。
  • 实用工具 - 窗口是实用工具样式窗口(面板)。
  • DocModal - 如果窗口是面板,将会是文档模式而不是系统模式。
  • NonactivatingPanel - 如果窗口是面板,则不会将其设置为主窗口。
  • TexturedBackground - 窗口将会具有纹理背景。
  • 未缩放 - 窗口不会缩放。
  • UnifiedTitleAndToolbar - 将联接窗口的标题和工具栏区域。
  • Hud - 窗口将显示为抬头显示面板。
  • FullScreenWindow - 窗口可以进入全屏模式。
  • FullSizeContentView - 窗口的内容视图位于标题和工具栏区域后面。

最后两个属性定义窗口的缓冲类型,以及是否会延迟绘制窗口。 有关 NSWindows 的详细信息,请参阅 Apple 的窗口简介文档。

最后,由于窗口并不是从 .storyboard 或 .xib 文件放大的,因此我们需要通过调用窗口 AwakeFromNib 方法,在 MainWindowController.cs 中进行模拟。

Window.AwakeFromNib ();

这样就可以像从 .storyboard 或 .xib 文件加载的标准窗口一样针对窗口编写代码。

显示窗口

在删除了 .storyboard 或 .xib 文件,并修改了 MainWindow.csMainWindowController.cs 文件之后,你将会像在 Xcode 的 Interface Builder 中使用 .xib 文件创建的任何普通窗口一样使用该窗口。

下面会创建窗口的新实例及其控制器,并在屏幕上显示该窗口:

private MainWindowController mainWindowController;
...

mainWindowController = new MainWindowController ();
mainWindowController.Window.MakeKeyAndOrderFront (this);

此时,如果应用程序正在运行,并且单击了几次按钮,刚会显示以下内容:

示例应用运行

添加仅代码窗口

如果希望将仅包含代码的 xibless 窗口添加到现有的 Xamarin.Mac 应用程序,请在 Solution Pad 中右键单击项目,然后依次选择“添加”>“新建文件…”。在“新建文件”对话框中,选择“Xamarin.Mac”>“带有控制器的 Cocoa 窗口”,如下所示:

添加新的窗口控制器

像之前一样,我们将会从项目中删除默认的 .storyboard 或 .xib 文件(在本例中为 SecondWindow.xib),并按照上面切换窗口以使用代码部分中的步骤进行操作,覆盖要编写代码的窗口的定义。

使用代码将 UI 元素添加到窗口

无论是使用代码创建窗口,还是从 .storyboard 或 .xib 文件加载窗口,有时都有可能希望从代码将 UI 元素添加到窗口。 例如:

var ClickMeButton = new NSButton (new CGRect (10, 10, 100, 30)){
    AutoresizingMask = NSViewResizingMask.MinYMargin
};
MyWindow.ContentView.AddSubview (ClickMeButton);

以上这段代码会创建一个新的 NSButton,并将其添加到 MyWindow 窗口实例进行显示。 可以在 Xcode 的 Interface Builder 中以 .storyboard 或 .xib 文件形式进行定义的任何 UI 元素,基本上都可以使用在代码中创建,并显示在窗口中。

在代码中定义菜单栏

由于 Xamarin.Mac 中当前存在的限制,不建议使用 NSMenuBar 代码创建 Xamarin.Mac 应用程序的菜单,而是建议继续使用 Main.storyboardMainMenu.xib 文件来定义它。 也就是说,可以在 C# 代码中添加和删除菜单和菜单项。

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

public override void DidFinishLaunching (NSNotification notification)
{
    mainWindowController = new MainWindowController ();
    mainWindowController.Window.MakeKeyAndOrderFront (this);

    // Create a Status Bar Menu
    NSStatusBar statusBar = NSStatusBar.SystemStatusBar;

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

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

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

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

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

上面从代码创建状态栏菜单,并在应用程序启动时显示它。 有关使用菜单的详细信息,请参阅我们的菜单文档。

总结

本文详细介绍了如何在 C# 代码中创建 Xamarin.Mac 应用程序的用户界面,而不是将 Xcode 的 Interface Builder 与 .storyboard 或 .xib 文件配合使用。