C#
WPF
VisualStudio
ズンドコキヨシ
テッテーテレッテー

WPFでズンドコキヨシ&テッテーテレッテーしてみた

皆様ご存じズンドコキヨシをWPFでやってみた

ついでに最近ずっと聞き続けているGYARIさんのテッテーテレッテーも同じようにやってみた('ω')

18/07/17 MVVMらしく改修


作ったもの

ZundokoSample.gif

入力パターン(カンマ区切り)、完成パターン(カンマ区切り)と出力文字を指定してStartを押すと入力パターンからランダムに文字を出力して、完成パターンと一致したら出力文字を末尾につけてダイアログを出して終了するよ(=゚ω゚)ノ

また、文字出力中にStopを押すと停止できる(ΦωΦ)


作り方

ライブラリは

Prism

ReactiveProperty

MahApps.Metro

を使用


Model

Modelといっても、今回は入力パターンと完成パターンを指定して完成パターンに一致するまでランダムに文字列を出力するメソッドを作るだけ


StreamCreater.cs

public static class StreamCreater

{
static readonly private Queue<string> queue = new Queue<string>();
static private Random random = new Random();
/// <summary>
/// 入力したパターンからランダムに出力し、完成パターンと一致すれば出力を終了する
/// </summary>
/// <param name="inputPattern">入力パターン</param>
/// <param name="finishedPattern">完成パターン</param>
/// <returns></returns>
public static IEnumerable<string> Create(string[] inputPattern, string[] finishedPattern)
{
queue.Clear();
while (!queue.SequenceEqual(finishedPattern))
{
queue.Enqueue(inputPattern[random.Next(inputPattern.Count())]);
if (queue.Count == finishedPattern.Count() + 1)
{
queue.Dequeue();
}
yield return queue.Last();
}
}
}

戻り値の型をIEnumerableにしてyield returnすることでforeachで一個づつ値が取れるようにしている


ViewModel


ShellViewModel.cs

public class ShellViewModel : BindableBase

{
public ReactiveProperty<string> LeftInputPattern { get; private set; } = new ReactiveProperty<string>("");
public ReactiveProperty<string> LeftFinishedPattern { get; private set; } = new ReactiveProperty<string>("");
public ReactiveProperty<string> LeftOutput { get; private set; } = new ReactiveProperty<string>("");
public ReactiveProperty<bool> IsLeftChecked { get; private set; } = new ReactiveProperty<bool>(true);

public ReactiveProperty<string> RightInputPattern { get; private set; } = new ReactiveProperty<string>("");
public ReactiveProperty<string> RightFinishedPattern { get; private set; } = new ReactiveProperty<string>("");
public ReactiveProperty<string> RightOutput { get; private set; } = new ReactiveProperty<string>("");
public ReactiveProperty<bool> IsRightChecked { get; private set; } = new ReactiveProperty<bool>(false);

public ReactiveProperty<bool> IsStarted { get; private set; } = new ReactiveProperty<bool>(false);
public ReactiveProperty<int> StreamCount { get; private set; } = new ReactiveProperty<int>();
public ReactiveProperty<string> ResultText { get; private set; } = new ReactiveProperty<string>();
public ReactiveProperty<string> StartStopButtonText { get; private set; } = new ReactiveProperty<string>("Start");

public InteractionRequest<Notification> Notification { get; } = new InteractionRequest<Notification>();

public ReactiveCommand StreamStartCommand { get; private set; } = new ReactiveCommand();

private CancellationTokenSource cancellationTokenSource;

public ShellViewModel()
{
StreamStartCommand.Subscribe(async () =>
{
if (!this.IsStarted.Value)
{
if (this.IsLeftChecked.Value) await this.StreamStart(this.LeftInputPattern.Value, this.LeftFinishedPattern.Value, this.LeftOutput.Value);
else if (this.IsRightChecked.Value) await this.StreamStart(this.RightInputPattern.Value, this.RightFinishedPattern.Value, this.RightOutput.Value);
}
else this.StreamStop();
});
}

/// <summary>
/// Streamを開始する
/// </summary>
/// <param name="input"></param>
/// <param name="finished"></param>
/// <param name="output"></param>
private async Task StreamStart(string input, string finished, string output)
{
if (this.IsStarted.Value) return;
if (string.IsNullOrWhiteSpace(input) || string.IsNullOrWhiteSpace(finished)) return;

var inputPattern = input.Split(',');
var finishedPattern = finished.Split(',');
//パターンが一致可能か確認
if (finishedPattern.Except(inputPattern).Any())
{
this.Notification.RaiseEx("エラー", "完成パターン内に、入力パターンに存在しないものがあります。");
return;
}
//初期化
this.StartStopButtonText.Value = "Stop";
this.IsStarted.Value = true;
this.StreamCount.Value = 0;
this.ResultText.Value = "";
this.cancellationTokenSource = new CancellationTokenSource();

//Streamを非同期で生成
var res = await Task.Run(async () =>
{
foreach (var item in StreamCreater.Create(inputPattern, finishedPattern))
{
if (cancellationTokenSource.IsCancellationRequested) return false;
this.ResultText.Value += item + ',';
this.StreamCount.Value++;
await Task.Delay(1);
}
this.ResultText.Value += output;
this.IsStarted.Value = false;
return true;
}, this.cancellationTokenSource.Token);
//キャンセルされなければダイアログ表示
if (res)
{
this.Notification.RaiseEx("パターン一致", $"{finished},{output}\r\n{this.StreamCount.Value}回で完成パターンと一致しました。");
this.StartStopButtonText.Value = "Start";
}
}
/// <summary>
/// 動作中のStreamを停止する
/// </summary>
private void StreamStop()
{
if (!this.IsStarted.Value) return;
this.StartStopButtonText.Value = "Start";
this.IsStarted.Value = false;
this.cancellationTokenSource.Cancel();
}
}


ここではViewから入力パターンなどを受け取ったり、結果を表示したりするプロパティやコマンド、ダイアログ表示の為のInteractionRequest等を定義している

ここのコマンドで上記のStreamCreater.Create()している

また、ViewModelはINotifyPropertyChangedを実装していないとメモリリークするようなのでINotifyPropertyChangedを実装したPrismのBindableBaseを継承している

コードが少し汚いけどお許しを…

今回はViewModelに実行処理も書いているが、この処理もModel層に記述して呼び出しだけ行ったほうがよりMVVMらしいと思う


View


xml:Shell.xaml

<metro:MetroWindow x:Class="ZunDokoKIYOSHI.Views.Shell"

xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:prism="http://prismlibrary.com/"
xmlns:prismEx="clr-namespace:Prism.InteractivityExtension;assembly=Prism.InteractivityExtension"
prism:ViewModelLocator.AutoWireViewModel="True"
xmlns:metro="http://metro.mahapps.com/winfx/xaml/controls"
xmlns:vm="clr-namespace:ZunDokoKIYOSHI.ViewModels"
xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
d:DataContext="{d:DesignInstance {x:Type vm:ShellViewModel}}"
xmlns:local="clr-namespace:ZunDokoKIYOSHI.Views"
mc:Ignorable="d"
Title="Shell" Height="450" Width="800">
<i:Interaction.Triggers>
<prism:InteractionRequestTrigger SourceObject="{Binding Notification}">
<prismEx:PopupMetroWindowAction IsModal="True"/>
</prism:InteractionRequestTrigger>
</i:Interaction.Triggers>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition/>
</Grid.RowDefinitions>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<StackPanel
Orientation="Vertical">
<RadioButton
HorizontalAlignment="Center"
GroupName="StreamSelect"
IsChecked="{Binding IsLeftChecked.Value}"/>
<TextBox
Text="{Binding LeftInputPattern.Value}"
metro:TextBoxHelper.Watermark="入力パターン"/>
<TextBox
Text="{Binding LeftFinishedPattern.Value}"
metro:TextBoxHelper.Watermark="完成パターン"/>
<TextBox
Text="{Binding LeftOutput.Value}"
metro:TextBoxHelper.Watermark="出力文字"/>
</StackPanel>
<StackPanel
Orientation="Vertical"
Grid.Column="1">
<RadioButton
HorizontalAlignment="Center"
GroupName="StreamSelect"
IsChecked="{Binding IsRightChecked.Value}"/>
<TextBox
Text="{Binding RightInputPattern.Value}"
metro:TextBoxHelper.Watermark="入力パターン"/>
<TextBox
Text="{Binding RightFinishedPattern.Value}"
metro:TextBoxHelper.Watermark="完成パターン"/>
<TextBox
Text="{Binding RightOutput.Value}"
metro:TextBoxHelper.Watermark="出力文字"/>
</StackPanel>
</Grid>
<Button
Grid.Row="1"
Content="{Binding StartStopButtonText.Value}"
Command="{Binding StreamStartCommand}"/>
<Grid Grid.Row="2">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition/>
</Grid.RowDefinitions>
<StackPanel
Orientation="Horizontal">
<TextBlock Text="Count:"/>
<TextBlock Text="{Binding StreamCount.Value}"/>
</StackPanel>
<TextBox
Grid.Row="1"
IsReadOnly="True"
TextWrapping="Wrap"
Text="{Binding ResultText.Value}"
ScrollViewer.VerticalScrollBarVisibility="Auto"/>
</Grid>
</Grid>
</metro:MetroWindow>


MahApps.Metroを使用している為、WindowはMetroWindowにしている

    <i:Interaction.Triggers>

<prism:InteractionRequestTrigger SourceObject="{Binding Notification}">
<prismEx:PopupMetroWindowAction IsModal="True"/>
</prism:InteractionRequestTrigger>
</i:Interaction.Triggers>

ここではダイアログを表示する為のInteractionRequestTriggerを定義している

Prismの標準ではMetroWindowのダイアログを出すことができない為、以前作ったPrism.InteractivityExtensionを使用している


まとめ

遊び半分で作った(゚∀゚)

無駄にDL先を置いておく

コードの全容はココ