【WPF学習中】Stackを使ったサンプルプログラム

C# コンピュータ
C#
プログラミングでStackというとpushで保存popで取り出すLast In First Outでメモリへのアクセスする方法だったように記憶しています。

使い道としてサブルーチンを呼び出す前に壊れて欲しくないデータをStackに入れて(push)サブルーチンから戻ったら取り出し(pop)てプログラムを再開する、ような使われ方をしていたような気がします。

実際その機能を必要とするプログラミング言語を使う機会はなかったですし、今現在の扱っているC#などのプログラミング言語ではサブルーチンを呼び出しでデータを破壊されないようにするためには、データを入れる変数をサブルーチン側からアクセスできないようにしたり、サブルーチンに引き渡すデータをオリジナルではなくコピーを引き渡す方法があります。

 それでは、Stackがどういった場面で使われるのか考えてみて、アプリケーションで作業手順を一つ戻る機能に使えるのではないかと思いましたので、サンプルプログラムを作成してみたいと思います。

プロジェクトの作成

PowerShellで実行。要dotnet.exe

mkdir プロジェクト名
cd プロジェクト名
dotnet new wpf
dotnet add package Microsoft.Xaml.Behaviors.Wpf
dotnet add package ReactiveProperty.WPF
dotnet add package OpenCvSharp4.Windows
dotnet add package System.Drawing.Common
code .

ソースコード

ファイル名:MainWindow.xaml

<Window x:Class="GridSample.MainWindow"
        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:local="clr-namespace:GridSample"
        mc:Ignorable="d"
        xmlns:i="clr-namespace:Microsoft.Xaml.Behaviors;assembly=Microsoft.Xaml.Behaviors"
        xmlns:interactivity="clr-namespace:Reactive.Bindings.Interactivity;assembly=ReactiveProperty.WPF"
        Title="MainWindow" Height="450" Width="800">
    <Window.DataContext>
        <local:MainWindowViewModel />
    </Window.DataContext>
    <Window.InputBindings>
        <KeyBinding Gesture="CTRL+Z" Command="{Binding PreviouseBitmapSouceCommand}"/>
    </Window.InputBindings>
        <i:Interaction.Triggers>
        <i:EventTrigger EventName="Closed">
            <interactivity:EventToReactiveCommand Command="{Binding WindowClosedCommand}" />
        </i:EventTrigger>
    </i:Interaction.Triggers>
    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="100*" />
            <ColumnDefinition Width="5" />
            <ColumnDefinition Width="400*" />
        </Grid.ColumnDefinitions>
        <StackPanel
            Margin="10"
            Grid.Column="0">
            <Expander
                Header = "グレースケール化"
                IsExpanded="True">
                <StackPanel>
                    <Button
                        Content="実行"
                        Command="{Binding GrayScaleCommand}" />
                </StackPanel>
            </Expander>
            <Expander
                Header = "ラプラシアンフィルタ"
                IsExpanded="True">
                <StackPanel>
                    <Button
                        Content="実行"
                        Command="{Binding LaplacianFilterCommand}" />
                </StackPanel>
            </Expander>
            <Expander
                Header = "反転"
                IsExpanded="True">
                <StackPanel>
                    <Button
                        Content="実行"
                        Command="{Binding InvertCommand}" />
                </StackPanel>
            </Expander>
            <Expander
                Header = "2値化"
                IsExpanded="True">
                <StackPanel>
                    <Button
                        Content="実行"
                        Command="{Binding ThresholdCommand}" />
                </StackPanel>
            </Expander>
        </StackPanel>
        <GridSplitter
            Grid.Column="1"
            HorizontalAlignment="Stretch"
            Background="LightGray"/>
        <ScrollViewer
            HorizontalScrollBarVisibility="Visible"
            Background="LightGray"
            Margin="10"
            AllowDrop="True"
            Grid.Column="2">
            <i:Interaction.Triggers>
                <i:EventTrigger EventName="DragOver">
                    <interactivity:EventToReactiveCommand Command="{Binding DragOverCommand}" />
                </i:EventTrigger>
                <i:EventTrigger EventName="Drop">
                    <interactivity:EventToReactiveCommand Command="{Binding DropCommand}" />
                </i:EventTrigger>
            </i:Interaction.Triggers>
            <Canvas
                Width="{Binding CanvasWidth.Value}"
                Height="{Binding CanvasHeight.Value}"
                Background="Navy" >
                <Image
                    Stretch="None"
                    Source="{Binding PictureView.Value}">
                    <i:Interaction.Triggers>
                        <i:EventTrigger EventName="SizeChanged">
                            <interactivity:EventToReactiveCommand Command="{Binding ImageSizeChangedCommand}" />
                        </i:EventTrigger>
                    </i:Interaction.Triggers>
                </Image>
            </Canvas>
        </ScrollViewer>
    </Grid>
</Window>

ファイル名:MainWindowViewModel.cs

using System.Diagnostics;
using System;
using System.ComponentModel;
using Reactive.Bindings;
using Reactive.Bindings.Extensions;
using System.Reactive.Disposables;
using System.Windows;
using System.Windows.Media.Imaging;
using System.IO;
using System.Threading.Tasks;

using System.Reactive.Linq;
using OpenCvSharp;
using OpenCvSharp.WpfExtensions;
using System.Collections.Generic;

namespace GridSample
{
    public class MainWindowViewModel : INotifyPropertyChanged, IDisposable
    {
#region Window関連プロパティ
        public event PropertyChangedEventHandler PropertyChanged;
        protected virtual void OnPropertyChanged(string name) =>
            PropertyChanged(this, new PropertyChangedEventArgs(name));
        private CompositeDisposable Disposable { get; } = new ();

        public ReactiveCommand<EventArgs> WindowClosedCommand { get; }
        public ReactiveCommand<SizeChangedEventArgs> Grid2SizeChangedCommand { get; }
        public ReactiveProperty<BitmapSource> PictureView { get; private set; }
        public ReactiveCommand<DragEventArgs> DragOverCommand { get; }
        public ReactiveCommand<DragEventArgs> DropCommand { get; }
        public ReactiveCommand<SizeChangedEventArgs> ImageSizeChangedCommand { get; }
        public ReactiveProperty<int> CanvasHeight { get; private set; } = new();
        public ReactiveProperty<int> CanvasWidth { get; private set; } = new();
#endregion Window関連プロパティ

        public ReactiveProperty<bool> Filtering { get; private set; } = new (false);
        public ReactiveCommand GrayScaleCommand { get; }

        public ReactiveCommand PreviouseBitmapSouceCommand { get; }

        Stack<BitmapSource> _bitmapSourceHistory = new();

        public ReactiveCommand LaplacianFilterCommand { get; }

        public ReactiveCommand InvertCommand { get; }
        public ReactiveCommand ThresholdCommand { get; }

        public MainWindowViewModel()
        {
#region Window関連プロパティの初期化

            PropertyChanged += (o, e) => {};
                        
            WindowClosedCommand = new ReactiveCommand<EventArgs>()
                .WithSubscribe(e =>this.Dispose()).AddTo(Disposable);
            
            PictureView = new ReactiveProperty<BitmapSource>().AddTo(Disposable);
            ImageSizeChangedCommand = new ReactiveCommand<SizeChangedEventArgs>()
                .WithSubscribe(e=>
                {
                    var sz = e.NewSize;
                    CanvasWidth.Value = (int)sz.Width;
                    CanvasHeight.Value = (int)sz.Height;
                }).AddTo(Disposable);
            
            DragOverCommand = new ReactiveCommand<DragEventArgs>()
                .WithSubscribe(e=>
                {
                    if (e.Data.GetDataPresent(DataFormats.FileDrop))
                    {
                        e.Effects = DragDropEffects.Copy;
                        e.Handled = true;
                    }
                    else
                    {
                        e.Effects = DragDropEffects.None;
                        e.Handled = false;
                    }
                }).AddTo(Disposable);
            
            var LoadPicture = new Func<string, BitmapImage> ((path)=>
            {
                BitmapImage bi = new();

                using (var fs = new FileStream(path, FileMode.Open, FileAccess.Read))
                using(var ms = new MemoryStream())
                {
                    fs.CopyTo(ms);
                    ms.Seek(0, SeekOrigin.Begin);
                    bi.BeginInit();
                    bi.CacheOption = BitmapCacheOption.OnLoad;
                    bi.StreamSource = ms;
                    bi.EndInit();
                    bi.Freeze();
                }

                return bi;                
            });

            Action<BitmapSource> SetNewBitmapSource = new Action<BitmapSource>((x)=>
            {
                if (PictureView.Value != null)
                    _bitmapSourceHistory.Push(PictureView.Value);
                
                PictureView.Value = x;
            });

            DropCommand = new ReactiveCommand<DragEventArgs>()
                .WithSubscribe(async e=>
                {
                    var files = (string[])e.Data.GetData(DataFormats.FileDrop);

                    Filtering.Value = false;

                    // PictureView.Value = null;
                    // _bitmapSourceHistory.Clear();
                    var bi = await Task.Run(() => LoadPicture(files[0]));


                    SetNewBitmapSource(bi);

                    Filtering.Value = true;
                }).AddTo(Disposable);
#endregion Window関連プロパティの初期化

            GrayScaleCommand = Filtering
                .ToReactiveCommand()
                .WithSubscribe(async ()=>
                {
                    Filtering.Value = false;
                    await Task.Run(()=>
                    {
                        using (var mat = BitmapSourceConverter.ToMat(PictureView.Value))
                        {
                            if (mat.Channels() == 1) return;

                            Cv2.CvtColor(mat, mat, ColorConversionCodes.RGB2GRAY);

                            var bi = BitmapSourceConverter.ToBitmapSource(mat);
                            bi.Freeze();
                            SetNewBitmapSource(bi);
                        }
                    });
                    Filtering.Value = true;
                }).AddTo(Disposable);
            
            PreviouseBitmapSouceCommand = Filtering
                .ToReactiveCommand()
                .WithSubscribe(()=>
                {
                    if (_bitmapSourceHistory.Count == 0)
                    {
                        MessageBox.Show("戻るべき過去が存在しない。");
                        return;
                    }

                    Filtering.Value = false;

                    var bi = _bitmapSourceHistory.Pop();
                    PictureView.Value = bi;

                    Filtering.Value = true;
                }
                ).AddTo(Disposable);

            LaplacianFilterCommand = Filtering
                .ToReactiveCommand()
                .WithSubscribe(async ()=>
                {
                    Filtering.Value = false;
                    await Task.Run(()=>
                    {
                        using (var mat = BitmapSourceConverter.ToMat(PictureView.Value))
                        {
                            if (mat.Channels() != 1) return;
                            
                            Cv2.Laplacian(mat, mat, MatType.CV_8U, 3);

                            var bi = BitmapSourceConverter.ToBitmapSource(mat);
                            bi.Freeze();
                            SetNewBitmapSource(bi);
                        }
                    });
                    Filtering.Value = true;
                }).AddTo(Disposable);

                InvertCommand = Filtering
                .ToReactiveCommand()
                .WithSubscribe(async ()=>
                {
                    Filtering.Value = false;
                    await Task.Run(()=>
                    {
                        using (var mat = BitmapSourceConverter.ToMat(PictureView.Value))
                        {
                            if (mat.Channels() != 1) return;

                            Cv2.BitwiseNot(mat, mat);

                            var bi = BitmapSourceConverter.ToBitmapSource(mat);
                            bi.Freeze();
                            SetNewBitmapSource(bi);
                        }
                    });
                    Filtering.Value = true;
                }).AddTo(Disposable);

                ThresholdCommand = Filtering
                .ToReactiveCommand()
                .WithSubscribe(async ()=>
                {
                    Filtering.Value = false;
                    await Task.Run(()=>
                    {
                        using (var mat = BitmapSourceConverter.ToMat(PictureView.Value))
                        {
                            if (mat.Channels() != 1) return;

                            Cv2.Threshold(mat, mat, 127d, 255d, ThresholdTypes.Binary);

                            var bi = BitmapSourceConverter.ToBitmapSource(mat);
                            bi.Freeze();
                            SetNewBitmapSource(bi);
                        }
                    });
                    Filtering.Value = true;
                }).AddTo(Disposable);
        }
#region Dispose()
        public void Dispose()
        {
            Debug.WriteLine("Dispose()");
            Disposable.Dispose();
        }
#endregion Dispose()
    }
}

実行

dotnet run

説明

画像ファイルをドラックアンドドラックすると画像が表示されます。
左側のボタンを押すとフィルター処理により画像が変化します。
CTRL-Zボタンを押すとひとつ前の画像に戻ります。

コメント