WPF学習中「キー入力イベントとフルスクリーン切り替え」

C# コンピュータ
C#

以前作成しましたドラックアンドドロップで画像ファイルを表示する、簡易グラフィックビューアにF11キーでウィンドウをフルスクリーンに切り替える機能を乗せてみました。

プロジェクトの作成

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

ソースコード

ファイル名:MainWindow.xaml

<Window x:Class="Sample50FitPicture.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:i="clr-namespace:Microsoft.Xaml.Behaviors;assembly=Microsoft.Xaml.Behaviors"
        xmlns:interactivity="clr-namespace:Reactive.Bindings.Interactivity;assembly=ReactiveProperty.WPF"
        xmlns:local="clr-namespace:Sample50FitPicture"
        mc:Ignorable="d"
        WindowStyle="{Binding WinStyle.Value}"
        WindowState="{Binding WinState.Value}"
        Title="{Binding Titlebar.Value}" Height="450" Width="800">
    <Window.DataContext>
        <local:MainWindowViewModel />
    </Window.DataContext>
    <Window.InputBindings>
        <KeyBinding Gesture="F11" Command="{Binding FullScreenCommand}"/>
    </Window.InputBindings>
    <i:Interaction.Behaviors>
        <local:WindowCloseBehavior />
    </i:Interaction.Behaviors>
    <Grid>
        <Canvas
            Background="Gray"
            AllowDrop="True">
            <i:Interaction.Behaviors>
                <local:DragOverBehavior />
            </i:Interaction.Behaviors>
            <i:Interaction.Triggers>
                <i:EventTrigger EventName="Drop">
                    <interactivity:EventToReactiveCommand Command="{Binding DropCommand}" />
                </i:EventTrigger>
            </i:Interaction.Triggers>
            <Image
                Stretch="None"
                Source="{Binding PictureView.Value}">
            </Image>
        </Canvas>
    </Grid>
</Window>

ファイル名:MainWindowViewModel.cs

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

namespace Sample50FitPicture
{
    public class MainWindowViewModel : INotifyPropertyChanged, IDisposable
    {
        public event PropertyChangedEventHandler PropertyChanged;
        private CompositeDisposable Disposable { get; } = new CompositeDisposable();
        public ReactiveProperty<string> Titlebar { get; private set; }
        public AsyncReactiveCommand<DragEventArgs> DropCommand { get; }
        public ReactiveProperty<BitmapImage> PictureView { get; private set; }

        public ReactiveCommand FullScreenCommand { get; }

        public ReactiveProperty<WindowStyle> WinStyle { get; private set; }
        public ReactiveProperty<WindowState> WinState  { get; private set; }
        private BitmapImage LoadPicture(string path)
        {
            var uri = new Uri(path, UriKind.Absolute);
            var image = new BitmapImage(uri);
            image.Freeze();

            return image;
        }
        public MainWindowViewModel()
        {
            PictureView = new ReactiveProperty<BitmapImage>()
            .AddTo(this.Disposable);

            Titlebar = new ReactiveProperty<string>("MainWindow")
            .AddTo(this.Disposable);

            WinStyle = new ReactiveProperty<WindowStyle>(WindowStyle.SingleBorderWindow)
            .AddTo(this.Disposable);

            WinState = new ReactiveProperty<WindowState>(WindowState.Normal)
            .AddTo(this.Disposable);

            DropCommand = new AsyncReactiveCommand<DragEventArgs>()
            .WithSubscribe<DragEventArgs>(
                async e =>{
                    if (e.Data.GetDataPresent(DataFormats.FileDrop) == false) return;
                    var path = ((string[])e.Data.GetData(DataFormats.FileDrop))[0];
                    var ext = System.IO.Path.GetExtension(path).ToUpper();
                    var validExtensions = new List<string> {".PNG", ".JPEG", ".JPG", ".BMP"};
                    if (validExtensions.Contains(ext) == false) return;

                    var b = await Task.Run(() => LoadPicture(path));
                    PictureView.Value = b;
                })
            .AddTo(this.Disposable);

            FullScreenCommand = new ReactiveCommand()
            .WithSubscribe (
                () => {
                    if (WinStyle.Value == WindowStyle.SingleBorderWindow)
                    {
                        WinStyle.Value = WindowStyle.None;
                        WinState.Value = WindowState.Maximized;
                    }
                    else
                    {
                        WinStyle.Value = WindowStyle.SingleBorderWindow;
                        WinState.Value = WindowState.Normal;
                    }
                })
            .AddTo(this.Disposable);
        }
        public void Dispose()
        {
            Disposable.Dispose();
        }
    }
}

ファイル名:DragOverBehavior.cs

using System.Diagnostics;
using System.Xml.Serialization;
using System.Windows;
using Microsoft.Xaml.Behaviors;
using System;

namespace Sample50FitPicture
{
    public class DragOverBehavior : Behavior
    {
        protected override void OnAttached()
        {
            base.OnAttached();
            this.AssociatedObject.DragOver += this.DragOver;
        }

        private void DragOver(object sender, DragEventArgs e)
        {
            if (e.Data.GetDataPresent(DataFormats.FileDrop))
            {
                e.Effects = DragDropEffects.Copy;
                e.Handled = true;
                return;
            }

            e.Effects = DragDropEffects.None;
            e.Handled = false;
        }

        protected override void OnDetaching()
        {
            base.OnDetaching();
            this.AssociatedObject.DragOver -= this.DragOver;
        }
    }
}

ファイル名:WindowCloseBehavior.cs

using System.Diagnostics;
using System.Xml.Serialization;
using System.Windows;
using Microsoft.Xaml.Behaviors;
using System;

namespace Sample50FitPicture
{
    public class WindowCloseBehavior  : Behavior
    {
        protected override void OnAttached()
        {
            base.OnAttached();
            this.AssociatedObject.Closed += this.WindowClose;
        }

        private void WindowClose(object sender, EventArgs e)
        {
             (this.AssociatedObject.DataContext as IDisposable)?.Dispose();
        }

        protected override void OnDetaching()
        {
            base.OnDetaching();
            this.AssociatedObject.Closed -= this.WindowClose;
        }
    }
}

他のソースファイルに変更なし。

使い方

ウィンドウ上にpngやjpegなどの画像ファイルをドラックアンドドロップすると画像が表示されます。

F11キーを押すとノーマルウィンドウからフルスクリーン、フルスクリーンからノーマルウィンドウに切り替わります。※画像サイズは変更しません。

説明

XAMLのWindow.InputBindingsにキー入力とそれと連動して実行するコマンドをバインディングしています。

<

pre>
<Window.InputBindings>
<KeyBinding Gesture="F11" Command="{Binding FullScreenCommand}"/>
</Window.InputBindings>

<

pre>
MainWindowViewModel.csに呼び出されるコマンドを定義しています。


        public ReactiveCommand FullScreenCommand { get; }

            FullScreenCommand = new ReactiveCommand()
            .WithSubscribe (
                () => {
                    if (WinStyle.Value == WindowStyle.SingleBorderWindow)
                    {
                        WinStyle.Value = WindowStyle.None;
                        WinState.Value = WindowState.Maximized;
                    }
                    else
                    {
                        WinStyle.Value = WindowStyle.SingleBorderWindow;
                        WinState.Value = WindowState.Normal;
                    }
                })
            .AddTo(this.Disposable);

WinStyle.Valueの内容がSingleBorderWindowかNoneでフルスクリーン切替フラグとしています。

WPFのプログラミング学習していて感じたこと

MVVMパターンでプログラミングをする場合VMの部分をC#でコーディングするわけですが、View上のコントロールを直接触ることが出来ないようです。代わりにコントロールのプロパティをVM側の変数とバインディングしたり、コントロールのイベントでVM側のメソッドを呼び出されるようにすることで、特定コントロールに依存することがない作りになります。

たまにXAMLで表現できないコントロールのプロパティがあったりします。そうしたプロパティをバインディングするすべを知らないで、コードビハインドに頼ることとなります。場合によってはBehaviorを使うことでほかのソースに依存することなく、ほかのプログラムに流用しやすいコーディングが出来る場合もありますが、コードビハインドほど汎用性はなさそうです。

意味を理解していなくとも決まり事通りに作れば、それなりに見通しの良いプログラムが出来上がりそうな気がする点がMVVMの素晴らしいところなのではないでしょうか?

ふと気が付くとViewとViewModelは分離していましたがModelを分離していませんでいた。今後の課題にしたいと思います。

コメント