Posted at
XamarinDay 6

Xamarin.Mac First Step (Hard Mode)


あらまし

さてさて始まりましたXamarin Advent Calendar 2018の6日目として、生Xamarin.Macでのアプリケーション作成をお送りしたいと思います。

ただ、StoryboardInterface Builderを使用した例は、Web検索すればいくらでも見つかります。

なのでハードモードと称して、Web検索してもあまりサンプルが見つからない、コードだけでの作成(Nibless Application)をお送りしたいかと思います。


前提

Xamarin.Macによるアプリケーション作成のため、MacOS環境での.NET上の開発となります。

あと私は日和っているので、Visual Studio for Macでの解説となりますがご了承をば。

ここでは、Xamarin.Mac込みでVisual Studio for Macがインストール済みで話を進めます。

記憶が定かではないですが、インストーラー回せばいい感じに勝手にいい感じにセットアップしてくれたように思えます。


プロジェクト準備


プロジェクト作成

プロジェクトテンプレートとして、


Mac > アプリ > Cocoa アプリ


の順に選択し、プロジェクトを作成します。

2-project.png


不要ファイルの削除

Niblessアプリを作成するための準備として、



  • Main.storyboardを削除します。


  • ViewController.csを削除します。


info.plistの編集

info.plistを開き、ソースを選択します。

3-info_plist.png

NSApplicationStoryBoardFileの行を削除します。


空ウインドウ表示まで

MainWindowController.csを新規ファイルとして追加します。

アイテムテンプレートとして、


General > 空のクラス


の順に選択します。名前は、MainWindowControllerとします。5-new-container.png


MainWindowController.csの編集

親クラスとして、AppKit.NSWindowControllerを指定します。

public partial class MainWindow : AppKit.NSWindowController {

// 省略...
}

次いで、コンストラクタを作成します。

サイズを指定してウインドウを初期表示したいため、コンストラクタでサイズを受け取れるようにします。

以降、using指定は、適宜追加してください。

public partial class MainWindowController : AppKit.NSWindowController {

public MainWindowController(CGRect rect) {
var w = new NSWindow();

w.Title = "Nibless App";
w.StyleMask |= NSWindowStyle.Titled | NSWindowStyle.Closable | NSWindowStyle.Resizable;
w.BackingType = NSBackingStore.Buffered;
w.SetFrame(rect, true);
this.Window = w;
}
}


AppDelegate.csの編集

ウインドウコントローラーのインスタンス化、および表示するコードを追加します。

[Register("AppDelegate")]

public class AppDelegate : NSApplicationDelegate {
MainWindowController controller;

public AppDelegate() {
}

//
// メインウインドウのインスタンスを作成
//
public override void WillFinishLaunching(NSNotification notification) {
var contentSize = new CGRect(0, 0, 800, 600);
this.controller = new MainWindowController(contentSize);
}

//
// メインウインドウの表示
//
public override void DidFinishLaunching(NSNotification notification) {
this.controller.ShowWindow(null);
}

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


Main.csの編集

明示的にAppDelidateをインスタンス化し引き渡します。

static class MainClass {

static void Main(string[] args) {
NSApplication.Init();

using (var pool = new NSAutoreleasePool()) {
NSApplication.SharedApplication.Delegate = new AppDelegate();
NSApplication.Main(args);
}
}
}


実行

とりあえず、空のウインドウが表示されたかと思います。

11-empty-window.png

ただ、MacOS村に旧くから受け継がれている慣習により、メインウインドウを閉じてもアプリケーションは終了しません1

あえて風習に逆らうのもどうかと思いますので、次章で、メインメニューからの終了の選択でアプリケーションを終わらせるよう修正します。


メインメニューの実装


AppDelegate.csの編集

AppDelegate#WillFinishLaunchingにメインメニュー初期化のコードを追加します。

    public override void WillFinishLaunching(NSNotification notification) {

var contentSize = new CGRect(0, 0, 800, 600);
this.controller = new MainWindowController(contentSize);
this.PopulateMainMenu(); // 追加
}

private void PopulateMainMenu() {
var mainMenu = new NSMenu();
var appMenu = new NSMenu();
appMenu.Title = "whatever";
appMenu.AutoEnablesItems = false;

var appMenuMi = new NSMenuItem(appMenu.Title);
appMenuMi.Submenu = appMenu;
mainMenu.AddItem(appMenuMi);

var exitSubMenuMi = new NSMenuItem("Quit");
exitSubMenuMi.Activated += this.NotifyOnQuitMenuClicked;

appMenu.AddItem(exitSubMenuMi);

NSApplication.SharedApplication.Menu = mainMenu;
}

private void NotifyOnQuitMenuClicked(object sender, EventArgs e) {
NSApplication.SharedApplication.Terminate(this);
}

Quitメニューアイテムに選択された場合のイベントハンドラを追加しています。

このイベントハンドラで、有無を言わさずアプリを終了させています。


実行

メニュー > Quitの選択で、アプリケーションの終了が確認できます。

空のウインドウを表示しただけでは面白くないので、次章で何か部品を置けるようにしてみたいと思います。


コンテナの準備

NSWindowはただの枠なので、部品を置けるようにするためにはコンテナを用意してあげる必要があります。

ContainerViweController.csを追加します。

アイテムテンプレートとして、


General > 空のクラス


の順に選択します。名前は、ContainerViweControllerとします。

5-new-container.png

親クラスとして、AppKit.NSViewControllerを指定します。

public class ContainerViewController: AppKit.NSViewController {

public ContainerViewController() {
}
}

Macアプリは、nibリソースが存在することを前提としている節があります。

そのため、nibのないViewControllerは、実行時にエラー吐いて落ちます。

niblessでコンテナを扱えるようにするため、NSViewController#LoadViewメソッドが用意されています。

これをオーバーライドすることで、niblessなコンテナを作成することができます。

public class ContainerViewController: AppKit.NSViewController {

// 省略...

public override void LoadView() {
this.View = new NSView();
}

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

this.View.WantsLayer = true;
this.View.Layer.BackgroundColor = new CGColor(0, 0, 255, 0.8);
}
}

コンテナが割り当てられたことがわかるよう背景色をつけています。



  1. MainWindowControllerのコンストラクタで、コンテナをインスタンス化し、ウインドウに割り当てます。

public class MainWindowController: AppKit.NSWindowController {

public MainWindowController(CGRect rect) {
var w = new NSWindow();

w.Title = "Nibless App";
w.StyleMask |= NSWindowStyle.Titled | NSWindowStyle.Closable | NSWindowStyle.Resizable;
w.BackingType = NSBackingStore.Buffered;
w.ContentViewController = new ContainerViewController(); // 追加
w.SetFrame(rect, true);
this.Window = w;
}
}


実行

領域がうっすら色づいたウインドウが表示されます。

6-with-conteiner.png

次章で、このコンテナにボタンを貼り付け、クリックでメッセージを表示させます。


コンテナアイテムの割り当て

ContainerViewController#ViewDidLoadで、ボタンをインスタンス化し、コンテナに追加します。

ボタンには、クリックされた場合のイベントハンドラも割り当てておきます。

public class ContainerViewController: AppKit.NSViewController {

private NSButton button;

// 省略...

public override void ViewDidLoad() {
// 省略...

//
// 以下のコードを追加する
//
this.button = new NSButton(new CGRect(0, 0, 80, 30));
this.button.Title = "Click ME !!";
this.button.Activated += this.NotifyOnBittonClicked;
this.View.AddSubview(this.button);
}

//
// イベントハンドラで、メッセージ表示
//
private void NotifyOnBittonClicked(object sender, EventArgs e) {
var alert = new NSAlert();
alert.MessageText = "Hello Nibless World !";
alert.RunSheetModal(this.View.Window);
}
}

実行すると、ウインドウ内にボタンが表示され、クリックするとメッセージが表示されます。

7-show-message.png

ただ、場所がなんというか・・・。

使う側視点として、上から下に配置するため、できれば座標も上から下に向けて大きくなって欲しいところ。

左上が原点に指定する方法がXamarin.mac (cocoa)には用意されてはいます。

がしかし、実はそんなことをしなくても、別の方法で解決することができます。

次章で、その方法を説明します。


左上原点で部品を配置する

Xamarin.mac (cocoa)は、Mac は OS X Lion以降、AutoLayoutというレイアウトシステムが用意されています。

これは、親子間の相対的な位置や、もしくはサイズ指定により柔軟に配置を行えるようになるものです。

日本語で書かれた解説やチュートリアルが既に多く存在しているため、ここでは詳細については述べません。

AutoLayoutによる配置を行わせるために、NSViewController#UpdateViewConstraintsメソッドをオーバーライドします。

public class ContainerViewController: AppKit.NSViewController {

// 省略...

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

このメソッド内で、コンテナとボタンとの相対位置関係を定義します。

ここでは、ボタンをコンテナの左上から(100, 50)の位置に配置させます。

public class ContainerViewController: AppKit.NSViewController {

// 省略...

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

this.View.AddConstraints(
this.CreateConstraint(this.View, this.button).ToArray()
);
}

private IEnumerable<NSLayoutConstraint> CreateConstraint(NSView parent, NSView child) {
yield return NSLayoutConstraint.Create(
child, NSLayoutAttribute.Left, NSLayoutRelation.Equal,
parent, NSLayoutAttribute.Left, 1.0f, new nfloat(100)
);
yield return NSLayoutConstraint.Create(
child, NSLayoutAttribute.Top, NSLayoutRelation.Equal,
parent, NSLayoutAttribute.Top, 1.0f, new nfloat(50)
);
yield return NSLayoutConstraint.Create(
child, NSLayoutAttribute.Width, NSLayoutRelation.Equal,
null, NSLayoutAttribute.Width, 1.0f, new nfloat(80)
);
yield return NSLayoutConstraint.Create(
child, NSLayoutAttribute.Height, NSLayoutRelation.Equal,
null, NSLayoutAttribute.Height, 1.0f, new nfloat(30)
);
}
}

実行すると・・・・、あれボタンが表示されません><

8-lost-button.png

実はAutoLayoutを有効にするのに、これだけでは不十分で、以下の2点を行う必要があります。


  • コンテナを含むすべての部品に対して、NSView#TranslatesAutoresizingMaskIntoConstraintsプロパティの値をfalseに変更する。


  • AutoLayoutベースで、配置することを宣言する。


前者は、単にプロパティを変更するだけです。

AutoLayoutベースを予定しているため、ボタンのインスタンス化の際のサイズ指定も取り除いています。

public class ContainerViewController: AppKit.NSViewController {

// 省略...

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

this.View.TranslatesAutoresizingMaskIntoConstraints = false; // 追加
this.View.WantsLayer = true;
this.View.Layer.BackgroundColor = new CGColor(0, 0, 255, 0.8f);

this.button = new NSButton(); // サイズの指定はもはや不要
this.button.TranslatesAutoresizingMaskIntoConstraints = false; // 追加
this.button.Title = "Click ME !!";
this.button.Activated += this.NotifyOnBittonClicked;
this.View.AddSubview(this.button);
}
}

後者は、NSView#RequiresConstraintBasedLayout()メソッドの戻り値がtrueの場合、AutoLayoutが有効になります。

staticメソッドであるため、new修飾子をつけて、メソッドを置き換えます。

また、ExportAttributeで置き換えの意思があることも宣言しておきます。忘れると有効になりません(1敗)。

今回は、ContainerViweController.csNSViewの派生クラスを用意します。

    class ContainerView: NSView {

[Export("requiresConstraintBasedLayout")]
public static new bool RequiresConstraintBasedLayout() {
return true;
}
}

ContainerViewController#ViewDidLoadで、利用するViewを差し替えます。

public class ContainerViewController: AppKit.NSViewController {

// 省略...

public override void LoadView() {
// this.View = new NSView();
this.View = new ContainerView(); // 差し替える
}

// 省略...
}

実行すると、ボタンの配置が変わったかと思います。

9-autolayout-aware.png

ボタンをコンテナ中央に配置したい場合は、以下のようにします。

public class ContainerViewController: AppKit.NSViewController {

// 省略...

private IEnumerable<NSLayoutConstraint> CreateConstraint(NSView parent, NSView child) {
// X座標を親の中心を軸に配置
yield return NSLayoutConstraint.Create(
child, NSLayoutAttribute.CenterX, NSLayoutRelation.Equal,
parent, NSLayoutAttribute.CenterX, 1.0f, new nfloat(0)
);
// Y座標を親の中心を軸に配置
yield return NSLayoutConstraint.Create(
child, NSLayoutAttribute.CenterY, NSLayoutRelation.Equal,
parent, NSLayoutAttribute.CenterY, 1.0f, new nfloat(0)
);
yield return NSLayoutConstraint.Create(
child, NSLayoutAttribute.Width, NSLayoutRelation.Equal,
null, NSLayoutAttribute.Width, 1.0f, new nfloat(80)
);
yield return NSLayoutConstraint.Create(
child, NSLayoutAttribute.Height, NSLayoutRelation.Equal,
null, NSLayoutAttribute.Height, 1.0f, new nfloat(30)
);
}
}

実行すると、ボタンがウインドウの中央に配置されます。

10-autolayout-center.png


まとめ

Xamarin.Macを用いたNibless Applicationの作成について、プロジェクトの作成から、またハマりどころについて解説しました。

もっとも、全宇宙の99.999999999%の人類は、Storyboardを用いて、アプリケーション作成を行うと思われますので、この記事が役に立つこときっとないでしょう(なぜ書いたし)。





  1. NSApplicationDelegate#ApplicationShouldTerminateAfterLastWindowClosed()メソッドをオーバーライドし、trueを返させることで、Windowsっぽい挙動にできます。