LoginSignup
12
13

More than 5 years have passed since last update.

Xamarin.FormsでAndroid・WPF・GTKアプリを開発した際のメモ

Last updated at Posted at 2018-03-17

概要

 以前、パワーアップした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. リポジトリを作成する

image.png

 別に必須ではありませんが、プログラミングする際にGit等でバージョン管理することは息をするように行いますよね?

Step2. プロジェクトを作成する

 上記記事などを参考にしながら、プロジェクトを構築します。この際、下の画像のようなフォルダ構成にしようと頑張りますと、

image.png

共有プロジェクトで指定できるソリューションが見つからず詰むというサイテーな結果になることがありますので注意しましょう。

image.png

Step3. お約束

 Mac持ち勢はこれにXamarin.MacとXamarin.iOSを並べられるんですよね? 羨ましい……

image.png

Step4. UI設計

 なまじ「様々なウィンドウサイズに対応」するのがスマホアプリですので、WPFなどととは色々と勝手が違います。基本的にXAMLなので知見が生かせないわけではないですが……。
 たぶんLive PlayerでXAMLをプレビューしながら組むのが一番楽なのですが、重要なのは「プロジェクト名.Android」ではなく「プロジェクト名.WPF」や「プロジェクト名.GTK」の方を選択して編集することです(Shared ProjectのXAMLを弄っている場合、どれを編集しても同じ結果になります)。なぜかAndroid側のソースではIntelliSenseが効きませんので……

image.png

image.png

image.png

 そんなこんなで組み上げたGUIがこちらです。1つのXAMLで3種類のプラットフォームに対するGUIが構築できていることが分かります。ただ、スマホ向けにUI設計すると他がかなり不自然になるのは否めませんので、現実的にはXAMLをそれぞれのプラットフォーム向けに書き分けるのがベストでしょう(やり方は知らない)

MainPage.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>

image.png

Step5. ViewModelの構築

 Data Bindingのため、ReactivePropertyを活用します。どういったライブラリなのかについては、次のページなどを参考にしてください。
  【C#】ReactiveProperty全然分からねぇ!って人向けのFAQ集【修正済】 - Qiita

 まず、ViewModelを構築します。ReactiveProperty型およびReactiveCommand型はちゃんとpublicなPropertyにする必要がある必要があることと、とりあえずINotifyPropertyChangedを実装する必要があることに注意しましょう。

MainViewModel.cs
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に代入します。

MainPage.xaml.cs
using Xamarin.Forms;

namespace CalcExpPoint
{
    public partial class MainPage : ContentPage
    {
        public MainPage()
        {
            InitializeComponent();
            this.BindingContext = new MainViewModel();
        }
    }
}

 最後に、それらをXAMLに関連付けます。ReactiveProperty HogeHoge.ValueのようにBindingすることと、<ContentPage.BindingContext>で紐づけすることが重要です。

MainPage.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">

    <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. ロジックを実装

 後は思い思いにロジックを実装します。今回は必要経験値を計算したいので、エラー対策を含めこんなコードでいいんじゃないでしょうか。

Library.cs
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);
    }
}
MainViewModel.cs
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)}";
        }
    });
}

Step7. 本番ビルド後にそれぞれの環境で動かす

 WPF版はこんな感じ(Windows 10 64bit版の上で動作)。
image.png

 GTK版はこんな感じ(同上・Monoで動作)。
image.png

 Android版はこんな感じ(Nox Player・Android 4.2の上で動作)。
image.png

12
13
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
12
13