概要
以前、パワーアップしたXamarin.Formsでクロスプラットフォームアプリ開発を体験してみた記事を書きました。
Xamarin.FormsでWPF・GTKアプリも開発してみたかった
しかし、ごく単純なサンプルですので、どうにもインパクトが弱いんじゃないかと考えました。そこで、より実用的なアプリ開発を例に、詳細な解説を行っていきます。
※今回使用したコードは、GiuHubで公開しています。
https://github.com/YSRKEN/CalcExpPoint
※Linux向けのバイナリは、その性質上Monoを使ったものになります。ざっくり言えば、
・Windowsだとexeをクリックするだけで動作する
・LinuxだとMonoのランタイムをインストール後、exeをMono runtimeで開けば動作する
となります。Monoのインストール方法はこちら。
※Xamarin.MacやXamarin.iOSをスルーしているのは、単にハードウェアを持っていないからです。Xamarin Live Playerを使えばWindows+iPhoneで表示をプレビューできますが、やはり両者をセットで購入しておくのが理想でしょう。いちいち専用ハードを要求するからAppleは嫌いですわ
作成手順
Step1. リポジトリを作成する
別に必須ではありませんが、プログラミングする際にGit等でバージョン管理することは息をするように行いますよね?
Step2. プロジェクトを作成する
上記記事などを参考にしながら、プロジェクトを構築します。この際、下の画像のようなフォルダ構成にしようと頑張りますと、
共有プロジェクトで指定できるソリューションが見つからず詰むというサイテーな結果になることがありますので注意しましょう。
Step3. お約束
Mac持ち勢はこれにXamarin.MacとXamarin.iOSを並べられるんですよね? 羨ましい……
Step4. UI設計
なまじ「様々なウィンドウサイズに対応」するのがスマホアプリですので、WPFなどととは色々と勝手が違います。基本的にXAMLなので知見が生かせないわけではないですが……。
たぶんLive PlayerでXAMLをプレビューしながら組むのが一番楽なのですが、重要なのは「プロジェクト名.Android」ではなく「プロジェクト名.WPF」や「プロジェクト名.GTK」の方を選択して編集することです(Shared ProjectのXAMLを弄っている場合、どれを編集しても同じ結果になります)。なぜかAndroid側のソースではIntelliSenseが効きませんので……
そんなこんなで組み上げたGUIがこちらです。1つのXAMLで3種類のプラットフォームに対するGUIが構築できていることが分かります。ただ、スマホ向けにUI設計すると他がかなり不自然になるのは否めませんので、現実的にはXAMLをそれぞれのプラットフォーム向けに書き分けるのがベストでしょう(やり方は知らない)
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:CalcExpPoint"
x:Class="CalcExpPoint.MainPage">
<Grid Margin="10,10,10,10">
<Grid.RowDefinitions>
<RowDefinition Height="auto"/>
<RowDefinition Height="auto"/>
<RowDefinition Height="auto"/>
<RowDefinition Height="auto"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<Label Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="2"
Text="経験値計算機"
VerticalOptions="Center"
HorizontalOptions="Center"
FontSize="Large"/>
<Label Grid.Row="1" Grid.Column="0" Margin="5,5,5,5"
Text="現在のレベル"
VerticalOptions="Center"
HorizontalOptions="End"/>
<Entry Grid.Row="1" Grid.Column="1" Margin="5,5,5,5"
Text="1" WidthRequest="50"
VerticalOptions="Center"
HorizontalOptions="Start"/>
<Label Grid.Row="2" Grid.Column="0" Margin="5,5,5,5"
Text="目標のレベル"
VerticalOptions="Center"
HorizontalOptions="End"/>
<Entry Grid.Row="2" Grid.Column="1" Margin="5,5,5,5"
Text="165" WidthRequest="50"
VerticalOptions="Center"
HorizontalOptions="Start"/>
<Button Grid.Row="3" Grid.Column="0" Grid.ColumnSpan="2"
Text="計算!"
VerticalOptions="Center"
HorizontalOptions="Center"/>
</Grid>
</ContentPage>
Step5. ViewModelの構築
Data Bindingのため、ReactivePropertyを活用します。どういったライブラリなのかについては、次のページなどを参考にしてください。
【C#】ReactiveProperty全然分からねぇ!って人向けのFAQ集【修正済】 - Qiita
まず、ViewModelを構築します。ReactiveProperty
型およびReactiveCommand
型はちゃんとpublicなPropertyにする必要がある必要があることと、とりあえずINotifyPropertyChanged
を実装する必要があることに注意しましょう。
using Reactive.Bindings;
using System;
using System.ComponentModel;
namespace CalcExpPoint
{
class MainViewModel : INotifyPropertyChanged
{
public ReactiveProperty<int> NowLevel { get; } = new ReactiveProperty<int>(1);
public ReactiveProperty<int> EndLevel { get; } = new ReactiveProperty<int>(165);
public ReactiveProperty<string> OutputMessage { get; } = new ReactiveProperty<string>("");
public ReactiveCommand CalcCommand { get; } = new ReactiveCommand();
//
public MainViewModel()
{
CalcCommand.Subscribe(_ => {
// コマンドを定義する
});
}
public event PropertyChangedEventHandler PropertyChanged;
}
}
次に、そのViewModelをBindingContext
に代入します。
using Xamarin.Forms;
namespace CalcExpPoint
{
public partial class MainPage : ContentPage
{
public MainPage()
{
InitializeComponent();
this.BindingContext = new MainViewModel();
}
}
}
最後に、それらをXAMLに関連付けます。ReactiveProperty Hoge
をHoge.Value
のようにBindingすることと、<ContentPage.BindingContext>
で紐づけすることが重要です。
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:CalcExpPoint"
x:Class="CalcExpPoint.MainPage">
<ContentPage.BindingContext>
<local:MainViewModel/>
</ContentPage.BindingContext>
<Grid Margin="10,10,10,10">
<Grid.RowDefinitions>
<RowDefinition Height="auto"/>
<RowDefinition Height="auto"/>
<RowDefinition Height="auto"/>
<RowDefinition Height="auto"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<Label Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="2"
Text="経験値計算機"
VerticalOptions="Center"
HorizontalOptions="Center"
FontSize="Large"/>
<Label Grid.Row="1" Grid.Column="0" Margin="5,5,5,5"
Text="現在のレベル"
VerticalOptions="Center"
HorizontalOptions="End"/>
<Entry Grid.Row="1" Grid.Column="1" Margin="5,5,5,5"
Text="{Binding NowLevel.Value, Mode=TwoWay}"
WidthRequest="50"
VerticalOptions="Center"
HorizontalOptions="Start"/>
<Label Grid.Row="2" Grid.Column="0" Margin="5,5,5,5"
Text="目標のレベル"
VerticalOptions="Center"
HorizontalOptions="End"/>
<Entry Grid.Row="2" Grid.Column="1" Margin="5,5,5,5"
Text="{Binding EndLevel.Value, Mode=TwoWay}"
WidthRequest="50"
VerticalOptions="Center"
HorizontalOptions="Start"/>
<Button Grid.Row="3" Grid.Column="0" Grid.ColumnSpan="2"
Text="計算!"
VerticalOptions="Center"
HorizontalOptions="Center"
Command="{Binding CalcCommand}"/>
<Label Grid.Row="4" Grid.Column="0" Grid.ColumnSpan="2"
Text="{Binding OutputMessage.Value, Mode=OneWay}"
HorizontalOptions="Center"
VerticalOptions="Start"/>
</Grid>
</ContentPage>
2018/03/19追記:
現在、Xamarin Live Playerで上記コードを実行すると、次のようなエラーが出て実行できません。
Visualization Error
Failed to resolve type {Name = "T";
GenericArgs = [||];
ArrayRank = 0;} (NInterpretException)
ググっても原因は見つからず、私としても困り果てています。Androidエミュレーターでは正常に起動したので、ReactivePropertyとXamarin Live Playerの相性が悪いのだと思わr……エミュも正常に動くんだからXamarin Live Playerのバグに決まってるだろ常考。
確かにXamarin Live Playerは一部Reflectionが動作しないですが、ReactiveProperty無しでコードを書くのは今では考えられないので辛いorz
Step6. ロジックを実装
後は思い思いにロジックを実装します。今回は必要経験値を計算したいので、エラー対策を含めこんなコードでいいんじゃないでしょうか。
using System.Collections.Generic;
namespace CalcExpPoint
{
static class Library
{
// 達成に必要な経験値(Lv.1~Lv.165)
private static List<int> levelExpList = new List<int> {
0,
0,
100,
300,
//(中略)
7320000,
7820000,
};
// あるレベルにおける総経験値を返す
private static int LevelExp(int level) => levelExpList[level];
// レベルA→レベルBになるために必要な経験値を返す
public static int WantExpByLevelDifference(int levelA, int levelB)
=> LevelExp(levelB) - LevelExp(levelA);
}
}
public MainViewModel()
{
CalcCommand.Subscribe(_ => {
if(NowLevel.Value <= 0 || NowLevel.Value > 165)
{
OutputMessage.Value = "エラー:現在のレベルは1以上165以下の整数で入力してください";
}else if (EndLevel.Value <= 0 || EndLevel.Value > 165)
{
OutputMessage.Value = "エラー:目標のレベルは1以上165以下の整数で入力してください";
}
else {
OutputMessage.Value = $"必要経験値:{Library.WantExpByLevelDifference(NowLevel.Value, EndLevel.Value)}";
}
});
}