Windows GUIプログラミング入門22 画像ビューア

# ■はじめに

今回は画像ビューアを作ります。
![00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/142879/1aa1e840-91aa-d5b4-8c4f-57940416a424.png)

キーワード：画像表示, 複数画面表示, ファイル選択ダイアログ, 子画面全終了, マウスホイール

[注意]
これまでの回で説明済みの操作方法等は、説明を省略したり簡略化している場合があります。

# ■開発環境

* Windows 10
* Visual Studio Community 2019 （Version 16.4.2）
* .NET Framework 4.5.2 / .NET Core 3.1

# ■作ってみる

## ◇プロジェクトの作成

WPFアプリの`.NET Framework`または`.NET Core`を選択してプロジェクトを作成します。
どちらを選ぶかは [こちらの記事](https://qiita.com/Kosen-amai/items/c5393aeeb092b26d0907 ".NET Frameworkと.NET Core、どちらを選ぶか") を参考に。
この記事では`.NET Framework`を選択しました。
※.NET Core版でも動作することは確認済みです。

![12.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/142879/6b7e6140-d762-5456-e0c7-1d7b8aa84e49.png)

## ◇メイン画面：レイアウト作成

メイン画面のレイアウト用コントロールを`Grid`から`Viewbox`に変更します。

![01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/142879/95100b3e-4cc7-cc5f-452a-20ce9a3f36dd.png)

`StackPanel`, `Button`を配置してプロパティやスタイルの設定をします。

![02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/142879/843c12b8-502d-aa9f-38e2-73b8bea2c262.png)

```Xml:MainWindow.xaml
<Window x:Class="ImageView.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:ImageView"
        mc:Ignorable="d"
        Title="画像ビューア" 
        Height="150" Width="450" 
        MinHeight="100" MinWidth="150"
        ResizeMode="CanResizeWithGrip">
    <Window.Resources>
        <!-- ボタンの既定スタイル -->
        <Style TargetType="Button">
            <Setter Property="Margin" Value="5"/>
            <Setter Property="Padding" Value="5"/>
        </Style>
    </Window.Resources>
    <Viewbox>
        <StackPanel Orientation="Horizontal">
            <Button Content="画像を開く" IsDefault="True"/>
            <Button Content="画像をすべて閉じる"/>
        </StackPanel>
    </Viewbox>
</Window>
```

## ◇画像表示用画面：レイアウト作成

ソリューションエクスプローラーでプロジェクトを右クリックし、`ウィンドウ`を追加します。
名前は`ImageWindow`にします。

![03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/142879/b5054671-7de5-c189-9351-63296190a32d.png)

`Image`コントロールやボタン等を配置、プロパティ設定をします。
`+`, `-`ボタンはリピートボタンを使います。ボタンを押し続けている間、Clickイベントが連続して発生します。
`RepeatButton`はツールボックスに無いのでタグを手打ちしてください。

![04_.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/142879/43dd039d-2cef-ef26-e3ea-dbd05cb0b978.png)

```Xml:ImageWindow.xaml
<Window x:Class="ImageView.ImageWindow"
        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:ImageView"
        mc:Ignorable="d"
        Title="ImageWindow" Height="220" Width="200" 
        MinHeight="100" MinWidth="100"
        ResizeMode="CanResizeWithGrip">
    <Window.Resources>
        <!-- リピートボタンの既定スタイル -->
        <Style TargetType="RepeatButton">
            <Setter Property="Margin" Value="2"/>
            <Setter Property="Width" Value="20"/>
        </Style>
    </Window.Resources>
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition/>
        </Grid.RowDefinitions>

        <StackPanel Orientation="Horizontal">
            <RepeatButton x:Name="plusButton" Content="+"/>
            <RepeatButton x:Name="minusButton" Content="-"/>
            <Button x:Name="closeButton" Content="閉じる" Margin="2" IsCancel="True"/>
        </StackPanel>
        
        <Image x:Name="img" Grid.Row="1"/>
    </Grid>
</Window>
```

## ◇画像表示用画面：ロジック作成

画像を指定して表示する処理、各ボタンクリック（`Click`イベント）、ウィンドウのマウスホイール回転（`MouseWheel`イベント）、ウィンドウのマウス左ボタンダウン処理（`MouseLeftButtonDown`イベント）を書きます。
マウスホイール回転時に拡大または縮小するようにします。
左ボタンダウン時の処理は、タイトルバーだけでなく画像部分をドラッグしてもウィンドウを移動できるようにします。

```CSharp:ImageWindow.xaml.cs
using System;
using System.Windows;
using System.Windows.Input;
using System.Windows.Media.Imaging;

namespace ImageView
{
    /// <summary>
    /// ImageWindow.xaml の相互作用ロジック
    /// </summary>
    public partial class ImageWindow : Window
    {
        /// <summary>
        /// コンストラクタ
        /// </summary>
        public ImageWindow()
        {
            InitializeComponent();
        }

        /// <summary>
        /// 画像設定処理（他の画面からこれを呼ぶ）
        /// </summary>
        /// <param name="filePath">画像ファイルパス</param>
        /// <returns>読み込み成功時true</returns>
        public bool SetImage(string filePath)
        {
            // パスが空
            if (string.IsNullOrEmpty(filePath))
            {
                return false;
            }

            var bmp = new BitmapImage();
            bmp.BeginInit();
            bmp.UriSource = new Uri(filePath);
            bmp.EndInit();
            // 画像設定
            img.Source = bmp;

            // タイトルバーにファイル名設定
            this.Title = System.IO.Path.GetFileName(filePath);

            return true;
        }

        /// <summary>
        /// ウィンドウサイズ拡大・縮小
        /// </summary>
        /// <param name="isZoom">true:拡大, false:縮小</param>
        private void ChangeWindowSize(bool isZoom)
        {
            int plusMinus = isZoom ? 1 : -1;

            // ウィンドウサイズを10%単位で変更
            this.Height += (this.Height * 0.1) * plusMinus;
            this.Width += (this.Width * 0.1) * plusMinus;

            // ウィンドウサイズが小さくなりすぎた場合、最小値に戻す
            if (this.Height < this.MinHeight ||
                this.Width < this.MinWidth)
            {
                this.Height = this.MinHeight;
                this.Width = this.MinWidth;
            }
        }

        /// <summary>
        /// 拡大ボタンクリック時
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void plusButton_Click(object sender, RoutedEventArgs e)
        {
            // ウィンドウサイズ拡大
            ChangeWindowSize(true);
        }

        /// <summary>
        /// 縮小ボタンクリック時
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void minusButton_Click(object sender, RoutedEventArgs e)
        {
            // ウィンドウサイズ縮小
            ChangeWindowSize(false);
        }

        /// <summary>
        /// 閉じるボタンクリック時
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void closeButton_Click(object sender, RoutedEventArgs e)
        {
            this.Close();
        }

        /// <summary>
        /// マウスホイール回転時
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void Window_MouseWheel(object sender, MouseWheelEventArgs e)
        {
            // ウィンドウサイズ拡大・縮小
            ChangeWindowSize(e.Delta > 0);
        }

        /// <summary>
        /// マウス左ボタンダウン時の処理
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void Window_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
        {
            // タイトルバー以外でもウィンドウ移動を可能にする
            DragMove();
        }
    }
}
```

最終的にXamlは以下のようになりました。

```Xml:ImageWindow.xaml
<Window x:Class="ImageView.ImageWindow"
        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:ImageView"
        mc:Ignorable="d"
        Title="ImageWindow" Height="220" Width="200" 
        MinHeight="100" MinWidth="100"
        ResizeMode="CanResizeWithGrip"
        MouseWheel="Window_MouseWheel"
        MouseLeftButtonDown="Window_MouseLeftButtonDown">
    <Window.Resources>
        <!-- リピートボタンの既定スタイル -->
        <Style TargetType="RepeatButton">
            <Setter Property="Margin" Value="2"/>
            <Setter Property="Width" Value="20"/>
        </Style>
    </Window.Resources>
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition/>
        </Grid.RowDefinitions>

        <StackPanel Orientation="Horizontal">
            <RepeatButton x:Name="plusButton" Content="+" Click="plusButton_Click"/>
            <RepeatButton x:Name="minusButton" Content="-" Click="minusButton_Click"/>
            <Button x:Name="closeButton" Content="閉じる" Margin="2" IsCancel="True" Click="closeButton_Click"/>
        </StackPanel>

        <Image x:Name="img" Grid.Row="1"/>
    </Grid>
</Window>
```

## ◇メイン画面：ロジック作成

画像表示用画面を使って画像表示する処理、画像表示用画面を一括で閉じる処理、ウィンドウ終了時の処理（`Closed`イベント）を書きます。

```CSharp:MainWindow.xaml.cs
using System;
using System.Windows;

namespace ImageView
{
    /// <summary>
    /// MainWindow.xaml の相互作用ロジック
    /// </summary>
    public partial class MainWindow : Window
    {
        /// <summary>
        /// コンストラクタ
        /// </summary>
        public MainWindow()
        {
            InitializeComponent();
        }

        /// <summary>
        /// 画像ファイルを選択し、表示
        /// </summary>
        private void OpenImageFile()
        {
            var dlg = new Microsoft.Win32.OpenFileDialog();
            dlg.Title = "画像を選択してください。";
            dlg.Filter = "画像ファイル|*.gif;*.png;*.jpeg;*.jpg;*.bmp|すべてのファイル|*.*";
            // ファイル選択ダイアログ表示
            if (dlg.ShowDialog() == true)
            {
                // 画像表示画面生成
                var window = new ImageWindow();

                // 画像表示画面に画像設定
                if (window.SetImage(dlg.FileName))
                {
                    // 画面表示
                    window.Show();
                }
            }
        }

        /// <summary>
        /// 画像を開くボタンクリック時
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void Button_Click(object sender, RoutedEventArgs e)
        {
            OpenImageFile();
        }

        /// <summary>
        /// 子画面全終了
        /// </summary>
        private void CloseSubWindows()
        {
            foreach (Window w in App.Current.Windows)
            {
                if (w != this)
                {
                    // 自身（メイン画面）以外なら終了させる
                    w.Close();
                }
            }
        }

        /// <summary>
        /// 画像をすべて閉じるボタンクリック時
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void Button_Click_1(object sender, RoutedEventArgs e)
        {
            // 子画面全終了
            CloseSubWindows();
        }

        /// <summary>
        /// ウィンドウ終了時
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void Window_Closed(object sender, EventArgs e)
        {
            // 開きっぱなしの子画面があったら終了
            CloseSubWindows();
        }
    }
}
```

最終的にXamlは以下のようになりました。

```Xml:MainWindow.xaml
<Window x:Class="ImageView.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:ImageView"
        mc:Ignorable="d"
        Title="画像ビューア" 
        Height="150" Width="450" 
        MinHeight="100" MinWidth="150"
        ResizeMode="CanResizeWithGrip"
        Closed="Window_Closed">
    <Window.Resources>
        <!-- ボタンの既定スタイル -->
        <Style TargetType="Button">
            <Setter Property="Margin" Value="5"/>
            <Setter Property="Padding" Value="5"/>
        </Style>
    </Window.Resources>
    <Viewbox>
        <StackPanel Orientation="Horizontal">
            <Button Content="画像を開く" IsDefault="True" Click="Button_Click"/>
            <Button Content="画像をすべて閉じる" Click="Button_Click_1"/>
        </StackPanel>
    </Viewbox>
</Window>
```

# ■動かしてみる

起動してみます。
![05.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/142879/7a6a27ae-9572-03ca-e1ec-983ab9bb538b.png)

ウィンドウサイズを小さくしてみます。
![06.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/142879/0e94d1b3-67e5-d23c-c402-060c6832f449.png)

ウィンドウサイズを大きくしてみます。
![07.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/142879/c41fd776-69aa-a28d-293b-4832187b407b.png)
Viewboxを使っているのでレイアウトはそのままに、きれいに拡大縮小されます。

「画像を開く」ボタンを押すと、ファイル選択ダイアログが表示されるので、画像を選択します。
![08.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/142879/38ff4ce7-14c8-57bd-8f88-2a9561e514d6.png)

表示されました。
![09.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/142879/194c3da2-b184-a923-517f-0d19f2bd68a6.png)

続けて他にも画像を開いてみます。
![10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/142879/e7095135-7744-89d0-6d3f-5c860f35f3e9.png)
画像の上でマウスホイールを奥に回すと拡大、手前に回すと縮小します。
`+`ボタン、`-`ボタンでも拡大、縮小できます。このボタンは長押しに対応しています。

「画像をすべて閉じる」ボタンを押すと、画像ウィンドウが一気に全部閉じます。
![11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/142879/9c89177f-8eec-bf6c-43a8-6c1859ca7dfc.png)

おしまい

---

[＜＜ 最初の記事](http://qiita.com/Kosen-amai/items/f9e3df2aa80363f5af5b "Windows GUIプログラミング入門1 メッセージボックス表示") 　 [＜ 前の記事](https://qiita.com/Kosen-amai/items/635897faa0c064d846c1 "Windows GUIプログラミング入門21 NuGet(3), ブラウザコントロール") 　 [次の記事 ＞](https://qiita.com/Kosen-amai/items/5712dc35d7eb8b7730fe "Windows GUIプログラミング入門23 NuGet(4), トースト通知")