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"/>
-
余談 ( スケーリングアルゴリズム )
.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
へ格納しておく。
このMatrix
をImageArea.RenderTransform
プロパティに割り当てる事で、リサイズ描写がなされる理屈。
( 本ケースでは 「領域フィット+中心揃え」 を常時維持出来ればOK なので、気持ち的に計算が楽 )
※
RenderTransform
やMatrix
に関する情報は .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
や.Height
はNaN
扱いとなっていた為、
代わりに.ActualWidth
と.ActualHeight
から取得した -
描画位置(座標)の決定
最初はImage
の.X
やら.Left
的なプロパティを使うものと思い込んでいたが、そんなものはない。
Image
要素そのものは動かさず、Matrix
のプロパティMatrix.OffsetX
Matrix.OffsetY
で事前調節しておく。
.NET Learn > Matrix 構造体 > プロパティ
.NET Learn > 変換の概要