放浪軍師のXamarin.Formsによるアプリ開発。
今回は乱ちゃんProjectを進めます。
まず、初めて訪問された方は以下をお読みください。
www.gunshi.info
乱屍の挙動が完成したよ!
www.gunshi.info
上記の記事で勉強した技術をフルに生かして乱屍の挙動部分がひとまず完成したのでお披露目しようと思います。
いやぁここまで長かったねぇ~!教えてくださったみなさんに感謝しかないですね。
ToggleButton.cs
Button
を継承してIsSelected
プロパティとIsHited
プロパティを追加したToggleButton
クラスです。Controls
というフォルダ内に置いてみました。
IsSelected
は未使用選択を示し、True
でランダム選択から除外されている事を示します。
IsHited
はルーレットによってランダム選択された状態を示し、True
の場合はランダム選択された事を示します。
using Xamarin.Forms; namespace RanCyan.Controls { public class ToggleButton : Button { public static readonly BindableProperty IsSelectedProperty = BindableProperty.Create( "IsSelected", typeof(bool), typeof(ToggleButton), false); public object IsSelected { get => GetValue(IsSelectedProperty); set => SetValue(IsSelectedProperty, value); } public static readonly BindableProperty IsHitedProperty = BindableProperty.Create( "IsHited", typeof(bool), typeof(ToggleButton), false); public object IsHited { get => GetValue(IsHitedProperty); set => SetValue(IsHitedProperty, value); } } }
RanShikaKoushinPage.xaml
View
に該当します。
Style.Triggers
を用いてToggleButton
のIsSelected
プロパティでTextColor
が、IsHited
プロパティでBackgroundColor
が変化するようにしてみました。
<?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:prism="clr-namespace:Prism.Mvvm;assembly=Prism.Forms" xmlns:controls="clr-namespace:RanCyan.Controls" prism:ViewModelLocator.AutowireViewModel="True" x:Name="Base" x:Class="RanCyan.Views.RanShikaKoushinPage" Title="交神ランダムの呪い"> <ContentPage.Resources> <ResourceDictionary> <Style TargetType="controls:ToggleButton"> <Setter Property="TextColor" Value="Black"/> <Setter Property="BackgroundColor" Value="AliceBlue"/> <Style.Triggers> <Trigger TargetType="controls:ToggleButton" Property="IsSelected" Value="True"> <Setter Property="TextColor" Value="Gainsboro"/> </Trigger> <Trigger TargetType="controls:ToggleButton" Property="IsHited" Value="True"> <Setter Property="BackgroundColor" Value="Red"/> </Trigger> </Style.Triggers> </Style> </ResourceDictionary> </ContentPage.Resources> <Grid> <Grid.RowDefinitions> <RowDefinition Height="4*" /> <RowDefinition Height="*" /> </Grid.RowDefinitions> <ListView ItemsSource="{Binding Items}" Grid.Row="0"> <ListView.ItemTemplate> <DataTemplate> <ViewCell> <controls:ToggleButton Command="{Binding BindingContext.ItemTapped, Source={x:Reference Base}}" CommandParameter="{Binding}" Text="{Binding Name}" IsSelected="{Binding IsSelected}" IsHited="{Binding IsHited}"/> </ViewCell> </DataTemplate> </ListView.ItemTemplate> </ListView> <Button Text="交神ランダム" Command="{Binding RanCommand}" Grid.Row="1"/> </Grid> </ContentPage>
RandomList.cs
Model
に該当します。
ちょっと悩んだんですけどList
用のItem
クラスはここに配置。RandomList
クラスではこのアプリの主となるランダム選択を実行。IsHited
がTrue
になったりFalse
になったりして最終的にどこかで止まります。これが変わる度にバインディングによってView
のToggleButton
のBackgroundColor
が変化します。
using Prism.Mvvm; using Reactive.Bindings; using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; using System.Threading.Tasks; namespace RanCyan.Models { public class Item : BindableBase { /// <summary> /// ラベル名称 /// </summary> public string Name { get => name; set => SetProperty(ref name, value); } private string name; /// <summary> /// 除外状態 /// </summary> public bool IsSelected { get => isSelected; set => SetProperty(ref isSelected, value); } private bool isSelected; /// <summary> /// 割合 /// </summary> public int Ratio { get => ratio; set => SetProperty(ref ratio, value); } private int ratio; /// <summary> /// ランダム選択された /// </summary> public bool IsHited { get => isHited; set => SetProperty(ref isHited, value); } private bool isHited; } public class RandomList : BindableBase { /// <summary> /// リスト /// </summary> public ObservableCollection<Item> Items { get; } /// <summary> /// ループする回数(回) /// </summary> public int LoopNo { get => loopNo; set => SetProperty(ref loopNo, value); } private int loopNo; /// <summary> /// 全ループ合計時間(msec) /// </summary> public int LoopTime { get => loopTime; set => SetProperty(ref loopTime, value); } private int loopTime; /// <summary> /// コンストラクタ /// </summary> /// <param name="items">ループリスト</param> /// <param name="loopNo">ループする回数(回)</param> /// <param name="loopTime">全ループ合計時間(msec)</param> public RandomList(List<Item> items, int loopNo = 10, int loopTime = 1000) { Items = new ObservableCollection<Item>(items); LoopNo = loopNo; LoopTime = loopTime; } public async void RandomAction() { //シード値を取得(乱数固定化の阻止) int seed = Environment.TickCount; // Randomクラスのインスタンス生成 Random rnd = new System.Random(seed); //基準となるウェイト時間(msec) float oneWaitTime = loopTime / (((loopNo - 1) * loopNo) / 2); var RaitoSum = Items.Where(x => !x.IsSelected) //選択している奴は除外した .Sum(x => x.Ratio); //Ratioの合計値 Items.Where(x => x.IsSelected).Select(x => x.IsHited = false).ToList();//IsHitedは全部Falseにする if (RaitoSum == 0) return; //0の場合抽選を回避する for (int i = 1; i <= loopNo; i++) { // listの数からダンダムで値を取得 var hitCount = rnd.Next(1, RaitoSum + 1); // 得た数値に該当するIsHitをTrueに。それ以外はFalseにする。 int lastCount = 0; int count = 0; foreach (var x in Items) { if (!x.IsSelected) { count = count + x.Ratio; x.IsHited = (lastCount < hitCount && hitCount <= count); lastCount = count; } } if (i < loopNo) { //少しずつウェイト時間を長くする await Task.Delay((int)(oneWaitTime * i)); } } //最後に点滅させる foreach (var x in Items) { if (x.IsHited) { for (int i = 0; i < 10; i++) { x.IsHited = !x.IsHited; await Task.Delay(50); } } } } } }
RanShikaKoushinPageViewModel.cs
ViewModel
に該当。
ルーレットの対象となるList
をここに記述してnew RandomList
で引き渡す…としてるんですけどなーんか違和感があります。
ToReadOnlyReactiveCollection
はReactiveCollection
のViewModel←Model
片方向バインディングですが、これって双方向の方が良い気もする。ちょっとやり方が見つからなかったですね。まぁちゃんと動きはするんですけど…あってんのかな?
using Prism.Mvvm; using RanCyan.Models; using Reactive.Bindings; using Reactive.Bindings.Extensions; using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Reactive.Disposables; namespace RanCyan.ViewModels { public class RanShikaKoushinPageViewModel : BindableBase, IDisposable { private CompositeDisposable Disposable { get; } = new CompositeDisposable(); public ReadOnlyReactiveCollection<Item> Items { get; } public ReactiveCommand<Item> ItemTapped { get; } = new ReactiveCommand<Item>(); public ReactiveCommand RanCommand { get; } = new ReactiveCommand(); public RandomList randomList { get; } public RanShikaKoushinPageViewModel() { var items = new List<Item>() { new Item { Name = "火" , Ratio=1 }, new Item { Name = "水" , Ratio=1 }, new Item { Name = "風" , Ratio=1 }, new Item { Name = "土" , Ratio=1 }, }; RandomList randomList = new RandomList(items, 20, 5000); //ViewModel←Model this.Items = randomList.Items.ToReadOnlyReactiveCollection().AddTo(this.Disposable); //Button ItemTapped.Subscribe(x => x.IsSelected = !x.IsSelected); RanCommand.Subscribe(_ => randomList.RandomAction()); } public void Dispose() { this.Disposable.Dispose(); } } }
RandomListクラスは流用できる!
この作りの良いところは、ViewModel
の
var items = new List<Item>() { new Item { Name = "火" , Ratio=1 }, new Item { Name = "水" , Ratio=1 }, new Item { Name = "風" , Ratio=1 }, new Item { Name = "土" , Ratio=1 }, };
の部分を変更するだけでModelを流用できるとこです。例えば職業ランダムなら
var items = new List<Item>() { new Item { Name = "剣士" , Ratio=1 }, new Item { Name = "薙刀士" , Ratio=1 }, new Item { Name = "弓使い" , Ratio=1 }, new Item { Name = "槍使い" , Ratio=1 }, new Item { Name = "拳法家" , Ratio=1 }, new Item { Name = "壊し屋" , Ratio=1 }, new Item { Name = "大筒士" , Ratio=1 }, new Item { Name = "踊り屋" , Ratio=1 }, };
とすればいいだけです。ちなみにRatio
は選択率を示していて、2を指定した場合は他の2倍、5を指定すれば他の5倍選ばれやすくなります。今回はすべて1を指定していますが、今後の為の布石です。そう、勘が良い方なら次に何を作りたいかわかりますよね?