XamarinでWindows / Mac OSX 両対応のデスクトップアプリを作る

  • 278
    いいね
  • 0
    コメント

 私はSSHクライアントPoderosaを開発・販売していますが、最近、従来のWindows版に加えてMac OSX版をXamarinを使って作成するという仕事をしましたので、そこでの知見をまとめておこうと思います。
 Windows/OSXの両対応アプリを作る際の定石のようなものも見えてきました。
 なおこの記事執筆時点ではOSX版Poderosaは開発終盤ですが未公開です。2017年4月に正式リリース予定です。

基礎知識おさらい

 Xamarin社は、源流はオープンソースの.NET環境ですが、現在はマイクロソフト傘下になっています。Xamarinの構成要素も多岐にわたり、C#コンパイラ、CLRランタイム、本家.NET互換の基本ライブラリ群、Xamarin FormsというiOS/android両対応のモバイルアプリを作る環境、Xamarin StudioというIDE、などいろいろありますが、この記事ではモバイルアプリは関係ありません。WindowsとOSXの両対応のデスクトップアプリのお話です。
 この周辺では、.NET coreという、MS本家の.NETの一部をマルチプラットフォーム&オープンソース化したものや、OSX版Visual Studioのようなツールも出始めており、状況は複雑です。

 なお、マルチプラットフォームのデスクトップアプリというとElectronというのもあります。これはnode.jsベースで、HTMLとJavaScriptで書くとデスクトップアプリをビルドしてくれるというやつです。cordovaの仲間ですね。あまり凝った動作は要らないから工数削減が最優先、というケースならElectronで良さそうですが、PoderosaはC#で書かれたオリジナルのWindows版が既にあったのでElectronは選択肢になりません。ただ、イチからお手軽にデスクトップアプリを作りたい場合は有力と思います。

シングルソースというわけにはいかない

 いきなり結論ですが、一つのソースで両対応アプリがビルドできる、というのは幻想です。さすがに無理筋。
 ライブラリに非互換な点があるから、とかの些末な問題でなく、WindowsとOSXの文化の違いが最大要因です。(もっとも、Windows版PoderosaはGUIにWPFを使っており、WPFはXamarinでは非対応であることから、シングルソースは最初からあきらめていましたが)

例1 ExcelとiTunes

 ExcelSS.png
iTunesSS.png

 これらはWindowsとOSXそれぞれの代表的なアプリケーションであるExcelとiTunesのものですが、

  • OK/Cancelボタンのうち、WindowsではOKボタンは左。OSXでは右。
  • Windowsではダイアログの項目にキーボードショートカットのニーモニック(Altキーを押しながら選択するもの)がある。OSXにはない。(というか、OSXではAltキーはまず使わないよね)

 といった違いがあります。

例2 ビットマップ上への描画

 メモリ上に確保したビットマップの上に文字を書く、という例で両方のAPI構造を確認してみます。

Windows版
            Font font = new Font(fontname, fontsize);
            Bitmap bmp = new Bitmap((int)width, (int)height, System.Drawing.Imaging.PixelFormat.Format32bppArgb);
            Graphics g = Graphics.FromImage(bmp);
            g.DrawString(text, font, Brushes.White, (bmp.Width-size.Width)/2, (bmp.Height-size.Height)/2);
            g.Dispose();
OSX版
            NSFont font = NSFont.FromFontName(k.ID, k.size);
            var attr = NSDictionary.FromObjectsAndKeys(
                new NSObject[] { font, NSColor.White },
                new NSObject[] { NSStringAttributeKey.Font, NSStringAttributeKey.ForegroundColor }
                );
            NSImage bmp = new NSImage(picsize);
            bmp.LockFocus();
            text.DrawInRect(new CGRect((picsize.Width - size.Width) / 2, (picsize.Height - size.Height) / 2, size.Width, size.Height), attr);
            bmp.UnlockFocus();

 もう全然違いますね。描画がStringのメソッド、というOSXの動作は違和感強いですが、NeXT時代からそうなっている歴史的理由があるので仕方ありません。

 こういう違いがある以上、1つのソースから両対応できるような統一的なAPIを設計し、十分なパフォーマンスを確保し、なおかつ間違った使い方をしたら適切なエラーメッセージとともに例外を投げるようにするのは確実に不可能といえます。これはXamarinの出来が悪いからとかではなく、誰がどう設計しても無理というものです。

 余談ですが、昔はSwingというJavaベースのマルチプラットフォームGUI環境がありました。ひょっとすると今は知らない人も多いかもしれませんが、これはマルチプラットフォームのGUIという魔境にがっぷり四つに組んでしまったため、パフォーマンスが劣悪なうえ各OSネイティブのGUIとは使用感が異なり、特定のOSでしか発生しないバグがどうしても残るなど悪夢そのものでした。
 WindowsアプリにはWindowsらしさが求められ、それはマイクロソフトがエクスプローラやExcelを通して「模範」を示してくれます。同様に、OSXアプリにはOSXらしさが求められ、それはAppleがFinderやiTunesを通して模範を示します。よほど強い理由がない限りその模範には従うべきなのです。
 もちろんアプリ開発の側からすれば同じにしてしまったほうが楽ですが、それぞれのOSの文化ごとに作るのはやむを得ないだろうと思います。郷に入れば郷に従え、です。

アプリ設計の定石

xamarin1.png

 ここに、Windows/OSX両対応アプリを作る場合のアセンブリの依存関係の典型パターンを示しました。キーポイントとしては以下です:

  • 自分のアプリを、「OSによらない共通パート(MyApp.Common)」「OSによる個別パート(MyApp.Windows / MyApp.OSX)」に分ける
  • 各モジュールの接続インタフェースをきっちり決める
  • 共通パートにできるだけ多くの機能が乗り、個別パートはできるだけスリムになるよう心掛けるが、やりすぎてインタフェースが複雑になるのは禁物。

 実際には、NuGetからもってきたライブラリが絡んだり(しかもそれがXamarinでは動かないとかで)して話はそう単純ではなくなりますが、経験ではグラフィックが絡まなければほぼすべての部分に十分な互換性があるようにXamarinはできているようです。ファイルIOやソケットまわりも安定しています。
 ですが、アプリ開発ではいつでも非互換の地雷を踏む可能性があるので、本家の.NETの互換、というフレコミをそのまま信用はできません。エンジニアたるもの、自分の目で見たコードとテスト結果以外は信用してはいけませんね。共通パートのユニットテストはWindows/OSXの両方で実行する必要がありますし、機能追加/変更時にも「この作業の結果Win/OSXの非互換な点が見つかるとしたらどういうものがあるだろうか?」とシミュレーションして、そのトラブル対応も入れたスケジュールをあらかじめ予測しておくべきです。(もっとも、この手の作業に事前のスケジュール見積もりは困難なので、完全な見通しが立ってからはじめて対外的なスケジュールを確約するのが実務上はトラブルがありません。

 さて、この分離方式において、アプリケーションの種類によっては、共通パートからOSごとに異なる実装になる機能を使いたい場合もあるでしょう。そういうときは共通パートに抽象クラスを用意し、個別パートで派生クラスを書いてOSによる差を隠蔽します。例えばビットマップを扱いたい場合、こういう具合ですね。
bitmap1.png

 共通パートからは抽象クラスだけにアクセスします。派生クラスのMyWindowsBitmap / MyOSXBitmapのそれぞれ単独でテストすれば動作も軽く済みます。
 もっとも、Windowsで動かすことしか考えずに書かれたコードから出発する場合、共通パートと個別パートに分離する作業からはじめないといけないのでこれがけっこう手間です。(Poderosaではこれだけで2週間かかりました)
 「今はWindowsだけだが、将来OSX版もやるかも」というアプリを新規開発する場合は最初からこういう構造に作っておいた方がいいでしょう。

WPFからの移植

 Windows版のGUIコードはWPFで書かれていましたが、これをOSXに移植するのはおおむね次のようにしました。
 細かく説明すればきりがなく、これだけで本1冊のボリュームになりかねない勢いなんですが、大胆に要約するとこんな具合です。

Windows OSX
WPFのXAMLのうち、基本レイアウト部分 完全手作業でxibに置き換え
WPFのXAMLのうち、スタイルとアニメーション部分 OSXではあきらめる(StoryBoardを駆使すれば何とかなるかもしれないが調査不足)
Button,CheckBox,RadioButton NSButton(クリック以外のイベントが欲しいときはNSButtonまたはNSViewからの派生クラスを)
Label,TextBox NSTextField
ComboBox NSComboBox(要素はSystem.Stringでは受け付けてくれず、Foundation.NSStringに変換しないといけないのが面倒)
ImageBox NSImageView

だいたいは、WPFのコントロールのプロパティに対応するものがCocoaにもあるので、ごく機械的に変換してやればできます。しかしイベントは殆ど用意されていないので、凝ったことをするにはいちいち派生クラスを書いてメソッドをoverrideしてやる必要があり、面倒です。
なお超難関はLayoutConstraint系で、自分が慣れていないせいもありますが、謎の挙動が多すぎて正直付き合いきれないと思いました。WPFならDockPanel, StackPanel, Gridの組み合わせでかなり柔軟にGUIのレイアウトができますが、OSXはこの点非常にキビシイ。可変サイズのダイアログには可能な限り手を出さない方がいいと思いました。

我らの味方! #ifdef

 ここまで説明した路線でだいたいなんとかなるのですが、そうはいっても手掛けるのは悪魔の潜むGUI
アプリ開発なので、そう綺麗ごとばかりで済むわけではありません。緊急対応やその場しのぎはどうしてもあります。
 そのときの強い味方は #ifdef です。Xamarin側のプロジェクト設定で適当にシンボルを決めておいて、

#if XAMARIN
(Xamarin固有コード)
#else
(Windows固有コード)
#endif

 みたいにやるわけです。
 スマートでないのは百も承知ですが、「いざとなったら#ifdefがあるもんね~」というのはコーディング時に心の余裕が生まれます。一旦#ifdefでしのいでおいて、後で時間に余裕ができたらスマートな解決法を改めて考える、としましょう。これはJavaにはできない芸当です。#ifdef XAMARINの中はWindowsではコンパイルも通らない(また、#elseの中がMacでコンパイルできない)、という構造でも許してくれるので。
 当初、OSX版Poderosaをやるかどうか悩んでいた時期にXamarinが使い物になるかどうかを実験していたのですが、正直に白状すると、この「#ifdefがある」というのが実は決断の決め手になりました。まあPoderosaは半分趣味・半分仕事のプロジェクトなので、#ifdefさえあれば、リリースが不可能になるような重大な問題にぶつかることはないだろうという判断です。

Xamarin.Macとは何か

 ある程度推測も入ってますが、Cocoaを含むOSXの'Framework'に収録されている型情報を材料に、C#からObjective-Cのメソッド呼び出しをブリッジするソースコードを自動生成し、それをビルドしたものをXamarin.Macとしている模様です。なので、OSXのAPIの「直訳」としてXamarin.Macが使えます。もっとも、OSXのAPIは非常にわかりづらいので、そこにさらにXamarin独自のレイヤーが挟まったら死ぬのは確実です。直訳にしたのは良い決定だったと思います。どのみちある程度複雑な機能に対するAPIは、StackOverflow等のサイトにQ&Aが十分蓄積されないと使い物にならないので仕方ないでしょう。
 なお、このあたりの機構は今後掘り下げて調べるつもり(主目的は、新しいMacBookに搭載されているTouchBarをXamarin経由でいじるため)なので、後日別記事でまとめようと思います。

Visual StudioとXamarin Studio

 Xamarin Studioはかなりよくできている(少なくともAndroid Studioよりは何倍もマシ)と思いますが、比較相手が天下のVisual Studioというのは相手が悪すぎです。私がVisual Studio使用歴20年で慣れ過ぎているから、というのも少なからずありますが、特にデバッガの機能と信頼性においてはVisual Studioのほうが明らかによくできています。
 なので、開発の「司令部」をWindows + Visual Studioにおき、「前線基地」をOSX + Xamarin Studioにするのがいいと思います。

 つまらないバグを取る、仕様未確定な機能の試作をする、リファクタリングをする、とかはふだんWindows上のみで行っておいて、ある程度落ち着いたらまとめてOSX版にもっていってテスト、という流れです。Xamarin StudioでデバッグするのはOSX固有の問題を追跡する場合に限る、としたほうが時間の効率がよいです。

Objective-Cについて

 Xamarinの宣伝文句を見ると、「使い慣れたC#でアプリ開発を!」みたいに書いてありますが、C#だけ知っていればいいわけではありません。OSXの厄介な問題に取り組むときは、参考情報はほぼ全部Objective-Cで書いてあるので、最低限読むことは必要です。また、Objective-Cのセレクタを重度に使う機能(そういうのは稀ですが)はXamarinではちゃんとサポートされてないので苦労します。ですがここの事情はおそらくXamarin Formsでも同じでしょう。
 あと、そもそもの話として、OSXデスクトップアプリを作っている人はWindowsに比べて少ないので、何をするにもWeb上の参考資料が見つかりにくいというのがあります。不便ですが仕方ないですね。

まとめ

 なんだかんだいっても、OSの全機能を使いつつクロスプラットフォームにできる点でXamarinはとても強力です。クロスプラットフォームのGUIというとSwingやらQtやらしかなかった時代と比べるとずいぶんハッピーです。
 とりわけ、C#で書かれたWindows用アプリをOSXに移植するにはほぼ唯一の選択肢といっていいと思います。今回開発したPoderosaの場合、共通パートに全体のコード量の70%ほどを収録することができました。
 ある程度以上の規模のアプリになると、型のない言語だといろいろ不便(...というか死ぬ)です。型のない言語でのアクロバティックな実装も好きですが、コンパイル時にエラーを出してくれるのはユニットテストの一部を代替してくれていると思ってきっちり利用してやりましょう!