プロフィール

髭山髭人(ひげひと)

自分の書いた記事が、一人でも誰かの役に立てば...
活動信条の一つとして「貴方のメモは、誰かのヒント」というのがあります。

このサイトについて

本家HP packetroom.net から切り離した いわゆる技術メモ用のブログで、無料レンタルサーバーにて運用しています。広告表示はその義務なのでご容赦。
XREA さんには長年お世話になっています

C# WPF 簡易ビューア Imageの疑似Stretch.Uniformを実装

概要など

シンプルな画像ビューアを作ってみようと色々調べたり弄ってた時の部分的メモ。
画面内に丁度収まるような 自動リサイズ・伸縮描写がメインです

  • 環境

    • C# WPF / .NET Framework 4.8
    • System.windows.Controls.Image を使用
    • 本記事例では、プロジェクト名を SampleViewer として作成
  • サンプル画像出典
    アイドルマスター シンデレラガールズ より 高森藍子 / ( モバマス時代のSR [こころに春を] 特訓前 )

一般的? Stretch.Uniform の場合

  • ソース(後述)の仕様
    <Border> を親 <Image> を子として配置。
    <Border> にイメージファイルが D&Dされたら BitmapImage として読み込んだのち、Image.Source に割り当てて描画。 System.Windows.Controls.Image
    System.Windows.Media.Imaging.BitmapImage

  • ポイント
    Image.Stretch"Uniform" を指定することで、常に親要素 ( Border ) にフィットしたリサイズ描画がなされる。
    ( ※ 本例はコードビハインド側ではなく、.xaml 側の記載とした )

    <Image x:Name="ImageArea" Stretch="Uniform"/>

    System.Windows.Controls.Image.Stretch

  • 余談 ( スケーリングアルゴリズム )
    .NET Learn - BitmapScalingMode 列挙型
    見栄え重視の BitmapScalingMode.Fant を指定。
    ( 拡大してもアンチエイリアスでぼやけないドット絵向けなら BitmapScalingMode.NearestNeighbor が良さそう )

  • 動作スクショ
    W:374 × H:469 の画像を表示させた。
    ちなみに Image.Stretch"None" を指定した場合、初期左上 ( 0 , 0 ) 起点の等倍配置となる ( ※ 画像右 )

主要ソース

<Window x:Class="SampleViewer.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:SampleViewer"
        mc:Ignorable="d"
        Title="SampleViewer" Height="250" Width="350">
    <Grid>
        <Border x:Name="CanvasArea" AllowDrop="True" Background="WhiteSmoke" ClipToBounds="True"
                DragOver="CanvasArea_DragOver" Drop="CanvasArea_Drop">
            <Image x:Name="ImageArea" Stretch="Uniform"/>
        </Border>
    </Grid>
</Window>
using System;
using System.Windows;
using System.Windows.Media;
using System.Windows.Media.Imaging;

namespace SampleViewer
{
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
        }

        private void CanvasArea_DragOver(object sender, DragEventArgs e)
        {
            if (e.Data.GetDataPresent(DataFormats.FileDrop))
            {
                e.Effects = DragDropEffects.Copy;
            }
            else
            {
                e.Effects = DragDropEffects.None;
            }
        }

        private void CanvasArea_Drop(object sender, DragEventArgs e)
        {
            if (e.Data.GetData(DataFormats.FileDrop) is string[] files)
            {
                var bmp = new System.Windows.Media.Imaging.BitmapImage();
                bmp.BeginInit();
                bmp.CacheOption = BitmapCacheOption.OnLoad;
                bmp.UriSource = new Uri(files[0]);
                bmp.EndInit();
                bmp.Freeze();
                RenderOptions.SetBitmapScalingMode(ImageArea, BitmapScalingMode.Fant); // リサイズ品質
                ImageArea.Source = bmp;
            }
        }
    }
}

疑似 Stretch.Uniform の実装

※ こっちが今回の主題

  • 上記からの変更点 & 趣旨

    • 親要素を <Border> から <Canvas> ( System.Windows.Controls.Canvas ) へ変更
    • SizeChanged イベント用リスナを追加
      ※ 描画リサイズ処理を手動にする為 ( 後述 )
    • リサイズ処理内容
      <Canvas> とオリジナル画像、互いのサイズを用いて 適切な表示倍率と位置を算出。
      算出結果は Matrix へ格納しておく。
      この MatrixImageArea.RenderTransform プロパティに割り当てる事で、リサイズ描写がなされる理屈。
      ( 本ケースでは 「領域フィット+中心揃え」 を常時維持出来ればOK なので、気持ち的に計算が楽 )

    RenderTransformMatrix に関する情報は .NET Learn - 変換の概要 あたりを参照

主要ソース

<Window x:Class="SampleViewer.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:SampleViewer"
        mc:Ignorable="d"
        Title="SampleViewer" Height="250" Width="350">
    <Grid>
        <Canvas x:Name="CanvasArea" AllowDrop="True" Background="WhiteSmoke" ClipToBounds="True"
                DragOver="CanvasArea_DragOver" Drop="CanvasArea_Drop" 
                SizeChanged="CanvasArea_SizeChanged">
            <Image x:Name="ImageArea" />
        </Canvas>
    </Grid>
</Window>
using System;
using System.Windows;
using System.Windows.Media;
using System.Windows.Media.Imaging;

namespace SampleViewer
{
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
        }

        private void CanvasArea_DragOver(object sender, DragEventArgs e)
        {
            if (e.Data.GetDataPresent(DataFormats.FileDrop))
            {
                e.Effects = DragDropEffects.Copy;
            }
            else
            {
                e.Effects = DragDropEffects.None;
            }
        }

        private void CanvasArea_Drop(object sender, DragEventArgs e)
        {
            if (e.Data.GetData(DataFormats.FileDrop) is string[] files)
            {
                var bmp = new System.Windows.Media.Imaging.BitmapImage();
                bmp.BeginInit();
                bmp.CacheOption = BitmapCacheOption.OnLoad;
                bmp.UriSource = new Uri(files[0]);
                bmp.EndInit();
                bmp.Freeze();
                RenderOptions.SetBitmapScalingMode(ImageArea, BitmapScalingMode.Fant);
                ImageArea.Source = bmp;

                var matrix = GetRenderMatrix();
                ImageArea.RenderTransform = new MatrixTransform(matrix);
            }
        }

        private void CanvasArea_SizeChanged(object sender, SizeChangedEventArgs e)
        {
            if (ImageArea.Source == null)
            {
                return;
            }
            var matrix = GetRenderMatrix();
            ImageArea.RenderTransform = new MatrixTransform(matrix);
        }

        private Matrix GetRenderMatrix()
        {
            BitmapImage bmp = (BitmapImage)ImageArea.Source;
            // 領域フィットに適した倍率を算出
            double useRenderScale = Math.Min(CanvasArea.ActualWidth / bmp.Width, CanvasArea.ActualHeight / bmp.Height);
            Size RenderSize = new Size(bmp.Width * useRenderScale, bmp.Height * useRenderScale);

            var matrix = new System.Windows.Media.Matrix();
            matrix.Scale(useRenderScale, useRenderScale);
            // 中央を維持する位置取り
            matrix.OffsetX = (CanvasArea.ActualWidth - RenderSize.Width) / 2;
            matrix.OffsetY = (CanvasArea.ActualHeight - RenderSize.Height) / 2;

            return matrix;
        }

    }
}

リサイズ計算

「横幅」 「高さ」の2パターンで ( 親領域の辺長 ÷ 画像の辺長 ) 式から算出のち、
どちらかの値が小さい方を画像リサイズ用倍率(係数)とする

  • 画像例
    横幅 ( ウィンドウ 234 ÷ 画像 375 ) = 0.624
    高さ ( ウィンドウ 111 ÷ 画像 469 ) = 0.236
    0.624 > 0.236 なので、 0.236 を倍率として採用

位置計算

画像の描画起点座標は デフォルトで 0,0 の左上 となっているので、
縦横それそれ ( 表示領域の辺長 - リサイズ後の画像辺長 ) ÷ 2 で計算すれば、中央配置の座標(距離)が得られる

  • 画像例 ( 横幅 )

個人的ハマりどころ

  • サイズ取得
    座標や位置計算の足がかりとして要素サイズを得る際、Image.Width.HeightNaN 扱いとなっていた為、
    代わりに .ActualWidth.ActualHeight から取得した

  • 描画位置(座標)の決定
    最初は Image.X やら .Left 的なプロパティを使うものと思い込んでいたが、そんなものはない。
    Image 要素そのものは動かさず、Matrix のプロパティ Matrix.OffsetX Matrix.OffsetY で事前調節しておく。
    .NET Learn > Matrix 構造体 > プロパティ
    .NET Learn > 変換の概要