在 Xamarin.Mac 中创建自定义控件

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

虽然 macOS 提供了大量内置用户控件,但有时可能需要创建自定义控件来提供未现成提供的功能或匹配自定义 UI 主题(例如游戏界面)。

自定义 UI 控件的示例

本文将介绍有关在 Xamarin.Mac 应用程序中创建可重用的自定义用户界面控件的基础知识。 强烈建议先阅读 Hello, Mac 一文,特别是 Xcode 和 Interface Builder 简介输出口和操作部分,因为其中介绍了我们将在本文中使用的关键概念和技术。

你可能还需要查看 Xamarin.Mac 内部机制文档的向 Objective-C 公开 C# 类/方法部分,因为其中介绍了用于将 C# 类连接到 Objective-C 对象和 UI 元素的 RegisterExport 命令。

自定义控件简介

如上所述,有时可能需要创建可重用的自定义用户界面控件来为 Xamarin.Mac 应用的 UI 提供独特的功能或创建自定义 UI 主题(例如游戏界面)。

在这种情况下,可以轻松从 NSControl 继承并创建自定义工具,可以通过 C# 代码或通过 Xcode 的 Interface Builder 将该工具添加到应用的 UI。 通过从 NSControl 继承,自定义控件将自动获得内置用户界面控件具有的所有标准功能(例如 NSButton)。

如果自定义用户界面控件仅显示信息(例如自定义图表和图形工具),你可能需要从 NSView 而不是 NSControl 继承。

无论使用哪个基类,创建自定义控件的基本步骤都是相同的。

本文将创建一个自定义翻转开关组件,该组件提供独特的用户界面主题,以及生成功能齐全的自定义用户界面控件的示例。

生成自定义控件

由于我们创建的自定义控件将响应用户输入(单击鼠标左键),因此我们将从 NSControl 继承。 这样,我们的自定义控件将自动获得内置用户界面控件具有的所有标准功能,并像标准 macOS 控件一样做出响应。

在 Visual Studio for Mac 中,打开要为其创建自定义用户界面控件(或创建新控件)的 Xamarin.Mac 项目。 添加一个新类并将其命名为 NSFlipSwitch

添加新类

接下来,编辑 NSFlipSwitch.cs 类,使其如下所示:

using Foundation;
using System;
using System.CodeDom.Compiler;
using AppKit;
using CoreGraphics;

namespace MacCustomControl
{
    [Register("NSFlipSwitch")]
    public class NSFlipSwitch : NSControl
    {
        #region Private Variables
        private bool _value = false;
        #endregion

        #region Computed Properties
        public bool Value {
            get { return _value; }
            set {
                // Save value and force a redraw
                _value = value;
                NeedsDisplay = true;
            }
        }
        #endregion

        #region Constructors
        public NSFlipSwitch ()
        {
            // Init
            Initialize();
        }

        public NSFlipSwitch (IntPtr handle) : base (handle)
        {
            // Init
            Initialize();
        }

        [Export ("initWithFrame:")]
        public NSFlipSwitch (CGRect frameRect) : base(frameRect) {
            // Init
            Initialize();
        }

        private void Initialize() {
            this.WantsLayer = true;
            this.LayerContentsRedrawPolicy = NSViewLayerContentsRedrawPolicy.OnSetNeedsDisplay;
        }
        #endregion

        #region Draw Methods
        public override void DrawRect (CGRect dirtyRect)
        {
            base.DrawRect (dirtyRect);

            // Use Core Graphic routines to draw our UI
            ...

        }
        #endregion

        #region Private Methods
        private void FlipSwitchState() {
            // Update state
            Value = !Value;
        }
        #endregion

    }
}

对于我们的自定义类,首先要注意的是,我们将从 NSControl 继承,并使用 Register 命令将此类公开给 Objective-C 和 Xcode 的 Interface Builder:

[Register("NSFlipSwitch")]
public class NSFlipSwitch : NSControl

在以下部分,我们将详细了解上述代码的其他组成部分。

跟踪控件的状态

由于我们的自定义控件是一个开关,因此我们需要通过一种方式来跟踪该开关的开/关状态。 我们使用 NSFlipSwitch 中的以下代码进行跟踪:

private bool _value = false;
...

public bool Value {
    get { return _value; }
    set {
        // Save value and force a redraw
        _value = value;
        NeedsDisplay = true;
    }
}

当开关的状态发生变化时,我们需要通过一种方式来更新 UI。 为此,我们将通过 NeedsDisplay = true 强制控件重绘其 UI。

如果控件需要的不仅仅是单个开/关状态(例如具有 3 个位置的多状态开关),我们可以使用枚举来跟踪状态。 对于我们的示例,一个简单的布尔值就可以了

我们还添加了一个帮助器方法在“开”和“关”之间交换开关状态:

private void FlipSwitchState() {
    // Update state
    Value = !Value;
}

稍后,我们将扩展此帮助器类,以便在开关状态发生变化时通知调用方。

绘制控件的界面

在运行时,我们将使用 Core Graphic 绘图例程绘制自定义控件的用户界面。 在执行此操作之前,我们需要打开控件的层。 我们使用以下专用方法执行此操作:

private void Initialize() {
    this.WantsLayer = true;
    this.LayerContentsRedrawPolicy = NSViewLayerContentsRedrawPolicy.OnSetNeedsDisplay;
}

从每个控件的构造函数调用此方法,以确保正确配置控件。 例如:

public NSFlipSwitch (IntPtr handle) : base (handle)
{
    // Init
    Initialize();
}

接下来,我们需要重写 DrawRect 方法并添加 Core Graphic 例程来绘制控件:

public override void DrawRect (CGRect dirtyRect)
{
    base.DrawRect (dirtyRect);

    // Use Core Graphic routines to draw our UI
    ...

}

当控件的状态发生变化(例如从“开”变为“关”)时,我们将调整控件的视觉表示形式。 每当状态发生变化时,我们都可以使用 NeedsDisplay = true 命令来强制根据新状态重绘控件。

响应用户输入

可以通过两种基本方式将用户输入添加到自定义控件:“重写鼠标处理例程”或“手势识别器”。 使用哪种方法取决于控件所需的功能。

重要

对于你创建的任何自定义控件,应使用替代方法手势识别器,但不能同时使用它们彼此冲突。

使用重写方法处理用户输入

NSControl(或 NSView)继承的对象具有多个用于处理鼠标或键盘输入的重写方法。 对于我们的示例控件,当用户用鼠标左键单击控件时,我们希望在“开”和“关”之间切换开关状态。 我们可以向 NSFlipSwitch 类添加以下重写方法来处理此操作:

#region Mouse Handling Methods
// --------------------------------------------------------------------------------
// Handle mouse with Override Methods.
// NOTE: Use either this method or Gesture Recognizers, NOT both!
// --------------------------------------------------------------------------------
public override void MouseDown (NSEvent theEvent)
{
    base.MouseDown (theEvent);

    FlipSwitchState ();
}

public override void MouseDragged (NSEvent theEvent)
{
    base.MouseDragged (theEvent);
}

public override void MouseUp (NSEvent theEvent)
{
    base.MouseUp (theEvent);
}

public override void MouseMoved (NSEvent theEvent)
{
    base.MouseMoved (theEvent);
}
## endregion

在上面的代码中,我们调用了 FlipSwitchState 方法(上面已定义)来翻转 MouseDown 方法中开关的开/关状态。 这也会强制重绘控件以反映当前状态。

使用手势识别器处理用户输入

(可选)可以使用手势识别器来处理用户与控件的交互。 删除上面添加的重写,编辑 Initialize 方法,使其如下所示:

private void Initialize() {
    this.WantsLayer = true;
    this.LayerContentsRedrawPolicy = NSViewLayerContentsRedrawPolicy.OnSetNeedsDisplay;

    // --------------------------------------------------------------------------------
    // Handle mouse with Gesture Recognizers.
    // NOTE: Use either this method or the Override Methods, NOT both!
    // --------------------------------------------------------------------------------
    var click = new NSClickGestureRecognizer (() => {
        FlipSwitchState();
    });
    AddGestureRecognizer (click);
}

在此处,我们将创建新的 NSClickGestureRecognizer 并调用 FlipSwitchState 方法,以便在用户用鼠标左键单击开关时更改开关的状态。 AddGestureRecognizer (click) 方法将手势识别器添加到控件。

同样,使用哪种方法取决于我们尝试通过自定义控件实现的目标。 如果需要对用户交互进行低级别访问,请使用“重写方法”。 如果需要预定义的功能,例如鼠标单击,请使用“手势识别器”。

响应状态更改事件

当用户更改自定义控件的状态时,我们需要通过一种方式来响应代码中的状态更改(例如在单击自定义按钮时执行某种操作)。

若要提供此功能,请编辑 NSFlipSwitch 类并添加以下代码:

#region Events
public event EventHandler ValueChanged;

internal void RaiseValueChanged() {
    if (this.ValueChanged != null)
        this.ValueChanged (this, EventArgs.Empty);

    // Perform any action bound to the control from Interface Builder
    // via an Action.
    if (this.Action !=null)
        NSApplication.SharedApplication.SendAction (this.Action, this.Target, this);
}
## endregion

接下来,编辑 FlipSwitchState 方法,使其如下所示:

private void FlipSwitchState() {
    // Update state
    Value = !Value;
    RaiseValueChanged ();
}

首先,提供一个 ValueChanged 事件,我们可以在 C# 代码中向其添加处理程序,以便可以在用户更改开关状态时执行某个操作。

其次,由于我们的自定义控件继承自 NSControl,因此它自动获得了可在 Xcode 的 Interface Builder 中分配的操作。 为了在状态更改时调用此操作,我们使用了以下代码

if (this.Action !=null)
    NSApplication.SharedApplication.SendAction (this.Action, this.Target, this);

首先,检查是否已将操作分配到控件。 接下来,调用操作(如果已定义)

使用自定义控件

完全定义自定义控件后,我们可以使用 C# 代码或 Xcode 的 Interface Builder 将其添加到 Xamarin.Mac 应用的 UI。

若要使用 Interface Builder 添加控件,请首先对 Xamarin.Mac 项目进行全新生成,然后双击 Main.storyboard 文件以在 Interface Builder 中将其打开进行编辑:

在 Xcode 中编辑情节提要

接下来,将 Custom View 拖放到用户界面设计中:

从库中选择自定义视图

在仍然选择了“自定义视图”的情况下,切换到“标识检查器”并将视图的“类”更改为 NSFlipSwitch

设置视图的类

切换到“助手编辑器”并为自定义控件创建一个出口(确保将其绑定到 ViewController.h 文件而不是 .m 文件中)

配置新的输出口

保存更改,返回 Visual Studio for Mac 并允许更改同步。编辑 ViewController.cs 文件,使 ViewDidLoad 方法如下所示:

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

    // Do any additional setup after loading the view.
    OptionTwo.ValueChanged += (sender, e) => {
        // Display the state of the option switch
        Console.WriteLine("Option Two: {0}", OptionTwo.Value);
    };
}

在此处,我们将响应前面在 NSFlipSwitch 类中定义的 ValueChanged 事件,并在用户单击控件时写出当前值

(可选)我们可以返回 Interface Builder 并在控件上定义一个操作

配置新操作

同样,请编辑 ViewController.cs 文件并添加以下方法:

partial void OptionTwoFlipped (Foundation.NSObject sender) {
    // Display the state of the option switch
    Console.WriteLine("Option Two: {0}", OptionTwo.Value);
}

重要

应在 Interface Builder 中使用事件或定义操作,但不应同时使用这两种方法,否则它们可能会相互冲突

总结

本文详细介绍了如何在 Xamarin.Mac 应用程序中创建可重用的自定义用户界面控件。 我们了解了如何绘制自定义控件 UI、响应鼠标和用户输入的两种主要方式,以及如何在 Xcode 的 Interface Builder 中向操作公开新控件。