C# BitmapImageの使い方完全ガイド|WPFで画像表示・Stream読み込み・ファイル占有エラーを解決

はじめに

C#のWPFアプリで画像を表示するとき、よく使われるクラスがBitmapImageです。Imageコントロールに画像を表示したり、ファイル・Stream・byte配列・Base64文字列から画像を読み込んだりする場面で利用されます。

一方で、BitmapImageは使い方を間違えると「画像が表示されない」「ファイルが使用中で削除できない」「Streamを閉じるとエラーになる」「大きな画像でメモリを圧迫する」といった問題が起きやすいクラスでもあります。

特にWPFでは、画像ファイルを読み込んだあとに上書き・削除しようとしてもファイルが占有されたままになるケースがよくあります。この問題は、CacheOptionの指定やStreamの扱いを正しく理解することで解決できます。

この記事では、C#のBitmapImageについて、WPFでの基本的な画像表示から、Stream読み込み、ファイルロック対策、MVVMでのバインド、パフォーマンス改善、変換・保存・加工まで実用的に解説します。

1. C#のBitmapImageとは?WPFで画像を扱う基本

1-1. BitmapImageの役割と使われる場面

BitmapImageは、WPFで画像データを読み込み、画面に表示するためによく使われる画像クラスです。主にPNG、JPEG、BMP、GIFなどの画像ファイルを読み込み、ImageコントロールのSourceに設定して表示します。

代表的な利用場面は次のとおりです。

  • ローカル画像ファイルをWPF画面に表示する

  • アプリケーション内のリソース画像を表示する

  • ファイル選択ダイアログで選んだ画像をプレビューする

  • Streamやbyte配列から画像を生成する

  • Webから取得した画像データを表示する

  • サムネイル画像を軽量に表示する

  • MVVMでViewModelから画像をバインドする

WPFでは、画像表示用のUI部品であるImageコントロールと、画像データを表すBitmapImageを組み合わせて使うのが基本です。

XML
<Image x:Name="PreviewImage" Width="300" Height="200" Stretch="Uniform"/>
C#
var bitmap = new BitmapImage(new Uri(@"C:\Images\sample.png"));
PreviewImage.Source = bitmap;

このように、BitmapImageは「画像ファイルや画像データをWPFで扱える形式にするためのクラス」と考えると理解しやすいです。

1-2. BitmapImage・BitmapSource・Bitmap・Imageコントロールの違い

C#で画像を扱っていると、BitmapImageBitmapSourceBitmapImageなど似た名前のクラスが出てきます。それぞれ役割が異なります。

BitmapImageは、WPFで画像を読み込むためのクラスです。画像ファイル、URI、Streamなどから画像を生成できます。

BitmapSourceは、WPF画像の基底的なクラスです。BitmapImageBitmapSourceを継承しているため、BitmapSourceとして扱うこともできます。画像の変換、トリミング、エンコードなどではBitmapSource型で処理することが多いです。

System.Drawing.Bitmapは、主にWindows FormsやGDI+で使われる画像クラスです。WPFのBitmapImageとは別物です。名前は似ていますが、直接Image.Sourceに設定することはできません。

Imageコントロールは、WPFの画面上に画像を表示するためのUI要素です。画像データそのものではなく、画像を表示するための部品です。Image.SourceBitmapImageBitmapSourceを設定して使います。

整理すると、次のようになります。

C#
// WPFで画像データを読み込む
BitmapImage bitmapImage;

// WPF画像データの基底型
BitmapSource bitmapSource;

// Windows Forms / GDI+系の画像
System.Drawing.Bitmap drawingBitmap;

// WPF画面に画像を表示するUI部品
System.Windows.Controls.Image imageControl;

WPFアプリで画像表示を行う場合は、基本的にBitmapImageまたはBitmapSourceを使います。System.Drawing.Bitmapは、既存ライブラリとの連携や画像加工の都合で必要な場合に使うものと考えるとよいでしょう。

1-3. WPFで画像表示にBitmapImageがよく使われる理由

WPFでBitmapImageがよく使われる理由は、Image.Sourceにそのまま設定でき、ファイル・リソース・Streamなど複数の読み込み方法に対応しているためです。

例えば、画像ファイルを表示するだけなら次のように簡単に書けます。

C#
ImageControl.Source = new BitmapImage(new Uri(@"C:\Images\photo.jpg"));

また、BitmapImageには画像読み込み時に便利な設定があります。

C#
var bitmap = new BitmapImage();
bitmap.BeginInit();
bitmap.UriSource = new Uri(@"C:\Images\photo.jpg");
bitmap.CacheOption = BitmapCacheOption.OnLoad;
bitmap.DecodePixelWidth = 300;
bitmap.EndInit();

この例では、画像ファイルを読み込むだけでなく、次のような制御も行っています。

  • CacheOptionで画像の読み込みタイミングを制御する

  • DecodePixelWidthで画像を縮小読み込みする

  • BeginInitEndInitの間で初期化プロパティをまとめて設定する

単に画像を表示するだけでなく、ファイルロック対策やメモリ対策まで行える点が、WPFでBitmapImageがよく使われる大きな理由です。

1-4. BitmapImageを使う前に必要なnamespaceと参照設定

BitmapImageを使うには、次の名前空間を使用します。

C#
using System;
using System.IO;
using System.Windows.Media.Imaging;

Imageコントロールをコードで扱う場合は、次の名前空間も使います。

C#
using System.Windows.Controls;

WPFアプリケーションであれば、通常はPresentationCorePresentationFrameworkWindowsBaseなどのWPF関連アセンブリが参照されています。

SDK形式のプロジェクトでWPFを使う場合は、.csprojに次のような設定が必要です。

XML
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net8.0-windows</TargetFramework>
<UseWPF>true</UseWPF>
</PropertyGroup>

</Project>

.NET FrameworkのWPFアプリや、Visual StudioのWPFアプリケーションテンプレートから作成したプロジェクトであれば、多くの場合は最初から必要な参照が設定されています。

2. C# BitmapImageで画像を表示する基本的な使い方

2-1. XAMLのImageコントロールに画像を表示する方法

WPFで画像を表示する基本は、XAMLにImageコントロールを配置し、Sourceに画像パスを指定する方法です。

XML
<Grid>
<Image Source="Images/sample.png"
Width="300"
Height="200"
Stretch="Uniform"/>
</Grid>

この例では、プロジェクト内のImages/sample.pngを表示しています。

画像ファイルをプロジェクトに含める場合は、Visual Studioで画像ファイルを選択し、プロパティを確認します。一般的には次のように設定します。

ビルド アクション: Resource
出力ディレクトリにコピー: コピーしない

WPFのリソース画像として扱う場合、Source="Images/sample.png"のような相対パスで表示できます。

Stretchは画像の拡大縮小方法を指定するプロパティです。

XML
<Image Source="Images/sample.png" Stretch="Uniform"/>

よく使う値は次のとおりです。

  • None: 元サイズで表示

  • Fill: 縦横比を無視して領域いっぱいに表示

  • Uniform: 縦横比を維持して領域内に収める

  • UniformToFill: 縦横比を維持して領域を埋める

画像の比率を崩したくない場合は、Uniformを指定するのが一般的です。

2-2. C#コードビハインドからBitmapImageを設定する方法

コードビハインドから画像を表示するには、BitmapImageを作成してImage.Sourceに設定します。

XAML側にImageコントロールを用意します。

XML
<Image x:Name="PreviewImage"
Width="400"
Height="300"
Stretch="Uniform"/>

C#側でBitmapImageを設定します。

C#
using System;
using System.Windows;
using System.Windows.Media.Imaging;

private void Window_Loaded(object sender, RoutedEventArgs e)
{
var bitmap = new BitmapImage();
bitmap.BeginInit();
bitmap.UriSource = new Uri(@"C:\Images\sample.jpg", UriKind.Absolute);
bitmap.EndInit();

PreviewImage.Source = bitmap;
}

より短く書く場合は、コンストラクタにUriを渡すこともできます。

C#
PreviewImage.Source = new BitmapImage(
new Uri(@"C:\Images\sample.jpg", UriKind.Absolute)
);

ただし、CacheOptionDecodePixelWidthなどを設定したい場合は、BeginInitEndInitを使う書き方が適しています。

C#
var bitmap = new BitmapImage();
bitmap.BeginInit();
bitmap.UriSource = new Uri(@"C:\Images\sample.jpg", UriKind.Absolute);
bitmap.CacheOption = BitmapCacheOption.OnLoad;
bitmap.DecodePixelWidth = 400;
bitmap.EndInit();

PreviewImage.Source = bitmap;

実用的には、この初期化形式を覚えておくと、ファイルロック対策やパフォーマンス対策にも対応しやすくなります。

2-3. UriSourceでローカル画像ファイルを読み込む方法

BitmapImageでローカル画像ファイルを読み込むには、UriSourceにファイルパスを指定します。

C#
var bitmap = new BitmapImage();
bitmap.BeginInit();
bitmap.UriSource = new Uri(@"C:\Images\photo.png", UriKind.Absolute);
bitmap.EndInit();

PreviewImage.Source = bitmap;

絶対パスを指定する場合は、UriKind.Absoluteを指定します。

C#
new Uri(@"C:\Images\photo.png", UriKind.Absolute)

相対パスを使う場合は、UriKind.RelativeまたはUriKind.RelativeOrAbsoluteを指定します。

C#
new Uri("Images/photo.png", UriKind.Relative)

ただし、相対パスは実行時の基準位置によって結果が変わることがあります。プロジェクト内のリソース画像なのか、出力フォルダにコピーされた画像なのかを明確にしておくことが大切です。

外部ファイルとして読み込みたい場合は、File.Existsで存在チェックをしておくと、原因調査がしやすくなります。

C#
string path = @"C:\Images\photo.png";

if (!File.Exists(path))
{
MessageBox.Show("画像ファイルが見つかりません。");
return;
}

var bitmap = new BitmapImage(new Uri(path, UriKind.Absolute));
PreviewImage.Source = bitmap;

画像が表示されない原因の多くは、BitmapImageそのものではなく、パス指定の誤りです。

2-4. 相対パス・絶対パス・pack URIの使い分け

WPFで画像を指定する方法には、主に相対パス、絶対パス、pack URIがあります。

相対パスは、アプリケーション内の画像を簡単に指定したいときに使います。

XML
<Image Source="Images/sample.png"/>

コードでは次のように指定できます。

C#
var bitmap = new BitmapImage(
new Uri("Images/sample.png", UriKind.Relative)
);

絶対パスは、ユーザーが選択した画像や任意の外部ファイルを読み込むときに使います。

C#
var bitmap = new BitmapImage(
new Uri(@"C:\Users\User\Pictures\sample.png", UriKind.Absolute)
);

pack URIは、WPFのリソースを明示的に指定したいときに使います。

C#
var bitmap = new BitmapImage(
new Uri("pack://application:,,,/Images/sample.png", UriKind.Absolute)
);

別アセンブリに含まれるリソースを参照する場合は、次のような形式になります。

C#
var bitmap = new BitmapImage(
new Uri("pack://application:,,,/MyLibrary;component/Images/sample.png", UriKind.Absolute)
);

使い分けの目安は次のとおりです。

プロジェクト内のリソース画像     → 相対パスまたはpack URI
ユーザーが選んだ外部画像 → 絶対パス
別アセンブリ内のリソース画像 → pack URI
出力フォルダにコピーした画像 → 実行ファイル基準の相対パスまたは絶対パス

WPFではリソース画像と外部ファイル画像で扱い方が変わるため、どの種類の画像を読み込んでいるのかを意識することが重要です。

2-5. 画像が表示されないときに確認すべきポイント

BitmapImageを設定しているのに画像が表示されない場合は、次の点を確認します。

まず、画像ファイルのパスが正しいか確認します。

C#
string path = @"C:\Images\sample.png";
MessageBox.Show(File.Exists(path).ToString());

Falseが表示される場合、パスが間違っているか、ファイルが存在しません。

次に、画像ファイルのビルドアクションを確認します。プロジェクト内の画像をXAMLから参照する場合は、通常Resourceにします。

ビルド アクション: Resource

出力フォルダにコピーして外部ファイルとして扱う場合は、次の設定にします。

ビルド アクション: Content
出力ディレクトリにコピー: 新しい場合はコピーする

また、Imageコントロールのサイズが0になっていないかも確認します。

XML
<Image Source="Images/sample.png"
Width="300"
Height="200"/>

GridStackPanelのレイアウトによっては、画像自体は読み込めていても表示領域が確保されていないことがあります。

コードで設定している場合は、例外が発生していないか確認します。

C#
try
{
var bitmap = new BitmapImage(new Uri(path, UriKind.Absolute));
PreviewImage.Source = bitmap;
}
catch (Exception ex)
{
MessageBox.Show(ex.Message);
}

Web画像や非同期処理で読み込んでいる場合は、UIスレッドでImage.Sourceを更新しているかも確認が必要です。

3. BitmapImageの初期化手順|BeginInit・EndInitの使い方

3-1. BitmapImageでBeginInitとEndInitが必要な理由

BitmapImageには、BeginInitEndInitを使って初期化する書き方があります。

C#
var bitmap = new BitmapImage();
bitmap.BeginInit();
bitmap.UriSource = new Uri(@"C:\Images\sample.jpg", UriKind.Absolute);
bitmap.CacheOption = BitmapCacheOption.OnLoad;
bitmap.EndInit();

この形式が必要になる理由は、BitmapImageの一部プロパティが初期化中にしか設定できないためです。

例えば、次のようなプロパティは基本的にBeginInitEndInitの間で設定します。

C#
bitmap.UriSource = ...
bitmap.StreamSource = ...
bitmap.CacheOption = ...
bitmap.CreateOptions = ...
bitmap.DecodePixelWidth = ...
bitmap.DecodePixelHeight = ...

EndInitを呼び出すと、画像の初期化が完了します。その後に初期化関連のプロパティを変更しても、期待どおりに反映されないことがあります。

特に、ファイル占有を防ぐためのCacheOption.OnLoadや、画像を軽量化するためのDecodePixelWidthは、画像を読み込む前に指定する必要があります。

C#
var bitmap = new BitmapImage();
bitmap.BeginInit();
bitmap.UriSource = new Uri(path);
bitmap.CacheOption = BitmapCacheOption.OnLoad;
bitmap.DecodePixelWidth = 300;
bitmap.EndInit();

安全で実用的なコードを書くなら、BitmapImageの生成時はこの初期化手順を基本形として使うのがおすすめです。

3-2. UriSourceを使った基本コード例

UriSourceを使った基本的なコードは次のとおりです。

C#
using System;
using System.Windows.Media.Imaging;

public BitmapImage LoadBitmapFromFile(string filePath)
{
var bitmap = new BitmapImage();
bitmap.BeginInit();
bitmap.UriSource = new Uri(filePath, UriKind.Absolute);
bitmap.EndInit();

return bitmap;
}

Imageコントロールに表示する場合は、次のように使います。

C#
PreviewImage.Source = LoadBitmapFromFile(@"C:\Images\sample.jpg");

ファイルロックを避けたい場合は、CacheOption.OnLoadを指定します。

C#
public BitmapImage LoadBitmapWithoutLock(string filePath)
{
var bitmap = new BitmapImage();
bitmap.BeginInit();
bitmap.UriSource = new Uri(filePath, UriKind.Absolute);
bitmap.CacheOption = BitmapCacheOption.OnLoad;
bitmap.EndInit();
bitmap.Freeze();

return bitmap;
}

Freezeは必須ではありませんが、画像を読み込み後に変更しない場合は呼び出しておくと扱いやすくなります。特に別スレッドで読み込んだ画像をUIに渡す場合に有効です。

3-3. CacheOption・CreateOptions・DecodePixelWidthの設定タイミング

BitmapImageで重要な設定は、EndInitの前に行います。

C#
var bitmap = new BitmapImage();
bitmap.BeginInit();

bitmap.UriSource = new Uri(filePath, UriKind.Absolute);
bitmap.CacheOption = BitmapCacheOption.OnLoad;
bitmap.CreateOptions = BitmapCreateOptions.IgnoreImageCache;
bitmap.DecodePixelWidth = 300;

bitmap.EndInit();

CacheOptionは、画像をどのタイミングで読み込むかを指定します。

C#
bitmap.CacheOption = BitmapCacheOption.OnLoad;

OnLoadを指定すると、EndInit時点で画像データを読み込みます。そのため、読み込み後に元ファイルやStreamを閉じやすくなります。

CreateOptionsは、画像作成時のオプションを指定します。

C#
bitmap.CreateOptions = BitmapCreateOptions.IgnoreImageCache;

IgnoreImageCacheを指定すると、同じURIの画像を再読み込みしたい場合にキャッシュの影響を避けやすくなります。画像差し替え処理で古い画像が表示される場合に役立ちます。

DecodePixelWidthは、読み込み時の横幅を指定します。

C#
bitmap.DecodePixelWidth = 300;

大きな画像を小さく表示するだけなら、元画像をそのまま読み込むより、読み込み時点で縮小したほうがメモリ使用量を抑えられます。

注意点として、DecodePixelWidthDecodePixelHeightを両方指定すると、画像の縦横比に影響する場合があります。通常はどちらか一方だけ指定し、縦横比を維持して縮小します。

3-4. 初期化後にプロパティ変更が反映されない原因

BitmapImageの初期化後にプロパティを変更しても反映されないことがあります。

例えば、次のようなコードは期待どおりに動かない可能性があります。

C#
var bitmap = new BitmapImage();
bitmap.BeginInit();
bitmap.UriSource = new Uri(filePath, UriKind.Absolute);
bitmap.EndInit();

// EndInit後に指定しても遅い
bitmap.CacheOption = BitmapCacheOption.OnLoad;
bitmap.DecodePixelWidth = 300;

CacheOptionDecodePixelWidthは、画像の読み込み処理に関係する設定です。そのため、画像が初期化されたあとに変更しても、すでに読み込まれた画像には反映されません。

正しくは、次のようにEndInit前に設定します。

C#
var bitmap = new BitmapImage();
bitmap.BeginInit();
bitmap.UriSource = new Uri(filePath, UriKind.Absolute);
bitmap.CacheOption = BitmapCacheOption.OnLoad;
bitmap.DecodePixelWidth = 300;
bitmap.EndInit();

また、Freezeした後はオブジェクトが変更不可になります。

C#
bitmap.Freeze();

// Freeze後は変更できない
// bitmap.DecodePixelWidth = 500; // エラー

BitmapImageは「作成時に必要な設定をすべて行い、初期化後は変更しない」ものとして扱うとトラブルを避けやすくなります。

4. StreamからBitmapImageを読み込む方法

4-1. FileStreamからBitmapImageを作成する方法

画像ファイルをFileStreamで開き、BitmapImageに読み込むこともできます。

C#
using System.IO;
using System.Windows.Media.Imaging;

public BitmapImage LoadFromFileStream(string filePath)
{
var bitmap = new BitmapImage();

using (var stream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read))
{
bitmap.BeginInit();
bitmap.StreamSource = stream;
bitmap.CacheOption = BitmapCacheOption.OnLoad;
bitmap.EndInit();
bitmap.Freeze();
}

return bitmap;
}

重要なのは、CacheOption.OnLoadを指定している点です。

C#
bitmap.CacheOption = BitmapCacheOption.OnLoad;

OnLoadを指定すると、EndInitの時点で画像データが読み込まれます。そのため、usingを抜けてStreamが閉じられても、画像を表示し続けることができます。

C#
PreviewImage.Source = LoadFromFileStream(@"C:\Images\sample.jpg");

この方法は、ファイルロックを避けたい場合にもよく使われます。

4-2. MemoryStreamからBitmapImageを作成する方法

MemoryStreamからBitmapImageを作成する場合も、基本的な流れはFileStreamと同じです。

C#
using System.IO;
using System.Windows.Media.Imaging;

public BitmapImage LoadFromMemoryStream(byte[] imageBytes)
{
var bitmap = new BitmapImage();

using (var stream = new MemoryStream(imageBytes))
{
bitmap.BeginInit();
bitmap.StreamSource = stream;
bitmap.CacheOption = BitmapCacheOption.OnLoad;
bitmap.EndInit();
bitmap.Freeze();
}

return bitmap;
}

MemoryStreamを閉じたあとも画像を使いたい場合は、必ずCacheOption.OnLoadを指定します。

C#
bitmap.CacheOption = BitmapCacheOption.OnLoad;

OnLoadを指定しない場合、画像の読み込みが遅延され、Streamが必要になったタイミングですでに閉じられているとエラーになることがあります。

画像データをAPIやデータベースからbyte配列として受け取る場合は、この方法が便利です。

4-3. byte配列からBitmapImageに変換する方法

byte配列からBitmapImageに変換するには、byte配列をMemoryStreamに渡して読み込みます。

C#
public BitmapImage ConvertBytesToBitmapImage(byte[] bytes)
{
if (bytes == null || bytes.Length == 0)
{
return null;
}

var bitmap = new BitmapImage();

using (var stream = new MemoryStream(bytes))
{
bitmap.BeginInit();
bitmap.CacheOption = BitmapCacheOption.OnLoad;
bitmap.StreamSource = stream;
bitmap.EndInit();
bitmap.Freeze();
}

return bitmap;
}

使い方は次のとおりです。

C#
byte[] imageBytes = File.ReadAllBytes(@"C:\Images\sample.png");
PreviewImage.Source = ConvertBytesToBitmapImage(imageBytes);

File.ReadAllBytesを使うと、ファイルの内容をすべてメモリに読み込みます。その後、BitmapImageはメモリ上のデータから生成されるため、画像ファイルを占有しにくくなります。

C#
byte[] bytes = File.ReadAllBytes(filePath);
var bitmap = ConvertBytesToBitmapImage(bytes);

ただし、大きな画像ファイルを大量に扱う場合は、byte配列として全体を保持する分だけメモリ使用量が増えます。サムネイル一覧などでは、DecodePixelWidthを組み合わせることを検討しましょう。

4-4. Base64文字列をBitmapImageに変換する方法

Base64文字列からBitmapImageに変換するには、まずConvert.FromBase64Stringでbyte配列に戻します。

C#
public BitmapImage ConvertBase64ToBitmapImage(string base64)
{
if (string.IsNullOrWhiteSpace(base64))
{
return null;
}

byte[] bytes = Convert.FromBase64String(base64);

var bitmap = new BitmapImage();

using (var stream = new MemoryStream(bytes))
{
bitmap.BeginInit();
bitmap.CacheOption = BitmapCacheOption.OnLoad;
bitmap.StreamSource = stream;
bitmap.EndInit();
bitmap.Freeze();
}

return bitmap;
}

使い方は次のとおりです。

C#
string base64 = File.ReadAllText(@"C:\Images\sample_base64.txt");
PreviewImage.Source = ConvertBase64ToBitmapImage(base64);

Web APIから画像がBase64形式で返ってくる場合や、データベースにBase64文字列として画像を保存している場合に使えます。

Base64文字列に次のようなプレフィックスが含まれる場合があります。

data:image/png;base64,

この場合は、カンマ以降だけを取り出してから変換します。

C#
public BitmapImage ConvertDataUriToBitmapImage(string dataUri)
{
int commaIndex = dataUri.IndexOf(',');
string base64 = commaIndex >= 0
? dataUri.Substring(commaIndex + 1)
: dataUri;

return ConvertBase64ToBitmapImage(base64);
}

Base64変換では、文字列が不正な場合にFormatExceptionが発生するため、必要に応じて例外処理を入れてください。

4-5. Streamを閉じても画像を表示し続けるための注意点

StreamからBitmapImageを作成する場合に最も重要なのは、CacheOption.OnLoadを指定することです。

悪い例は次のようなコードです。

C#
public BitmapImage LoadBad(string filePath)
{
var bitmap = new BitmapImage();

using (var stream = File.OpenRead(filePath))
{
bitmap.BeginInit();
bitmap.StreamSource = stream;
bitmap.EndInit();
}

return bitmap;
}

このコードでは、usingを抜けた時点でStreamが閉じられます。しかし、BitmapImageが画像データを遅延読み込みしようとすると、閉じられたStreamにアクセスしてエラーになる可能性があります。

正しくは、次のようにOnLoadを指定します。

C#
public BitmapImage LoadGood(string filePath)
{
var bitmap = new BitmapImage();

using (var stream = File.OpenRead(filePath))
{
bitmap.BeginInit();
bitmap.CacheOption = BitmapCacheOption.OnLoad;
bitmap.StreamSource = stream;
bitmap.EndInit();
bitmap.Freeze();
}

return bitmap;
}

OnLoadを指定すると、EndInitの時点で画像データを読み込みます。そのため、Streamを閉じても画像を表示し続けることができます。

Stream読み込みでは、次の3点をセットで覚えておくと安全です。

C#
bitmap.BeginInit();
bitmap.CacheOption = BitmapCacheOption.OnLoad;
bitmap.StreamSource = stream;
bitmap.EndInit();

さらに、読み込んだ画像を変更しない場合はFreezeしておくと、別スレッドに渡す処理でも扱いやすくなります。

5. ファイル占有エラーを解決するBitmapImageの読み込み方

5-1. BitmapImageで画像ファイルが削除・上書きできない原因

BitmapImageで画像を表示したあと、元の画像ファイルを削除・上書きしようとすると、次のようなエラーが発生することがあります。

プロセスはファイルにアクセスできません。
別のプロセスが使用中です。

原因は、BitmapImageが画像ファイルを遅延読み込みしており、ファイルへの参照を保持している場合があるためです。

例えば、次のようなコードではファイルがロックされることがあります。

C#
var bitmap = new BitmapImage(new Uri(filePath, UriKind.Absolute));
PreviewImage.Source = bitmap;

この書き方は簡単ですが、画像ファイルを表示後に削除・上書き・差し替えしたい場合には向いていません。

特に次のような処理では注意が必要です。

  • 表示中の画像を別画像で上書きする

  • 画像を表示したあとに削除する

  • 画像編集後に同じファイル名で保存し直す

  • サムネイル表示後に元ファイルを移動する

  • 一時ファイルとして画像を読み込む

この問題を避けるには、画像データを読み込み時に完全にメモリへロードし、ファイルやStreamを閉じる必要があります。

5-2. CacheOption.OnLoadでファイルロックを解除する方法

ファイル占有を防ぐ基本は、BitmapCacheOption.OnLoadを指定して画像を読み込むことです。

C#
public BitmapImage LoadBitmapWithoutFileLock(string filePath)
{
var bitmap = new BitmapImage();

bitmap.BeginInit();
bitmap.UriSource = new Uri(filePath, UriKind.Absolute);
bitmap.CacheOption = BitmapCacheOption.OnLoad;
bitmap.EndInit();
bitmap.Freeze();

return bitmap;
}

ただし、より確実にファイルロックを避けたい場合は、FileStreamを使ってusingで閉じる形がおすすめです。

C#
public BitmapImage LoadBitmapWithoutFileLock(string filePath)
{
var bitmap = new BitmapImage();

using (var stream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read))
{
bitmap.BeginInit();
bitmap.CacheOption = BitmapCacheOption.OnLoad;
bitmap.StreamSource = stream;
bitmap.EndInit();
bitmap.Freeze();
}

return bitmap;
}

この方法では、画像データはEndInit時に読み込まれ、usingを抜けるとStreamが閉じられます。そのため、読み込み後に元ファイルを削除・上書きしやすくなります。

C#
PreviewImage.Source = LoadBitmapWithoutFileLock(filePath);

// 読み込み後に削除できる
File.Delete(filePath);

画像ファイルの差し替えや一時ファイル処理を行うWPFアプリでは、この読み込み方を標準にしておくと安全です。

5-3. usingでStreamを安全に閉じるコード例

usingを使うと、Streamを確実に閉じることができます。BitmapImageで使う場合は、CacheOption.OnLoadと組み合わせることが重要です。

C#
public static BitmapImage LoadImage(string filePath)
{
if (!File.Exists(filePath))
{
throw new FileNotFoundException("画像ファイルが見つかりません。", filePath);
}

var bitmap = new BitmapImage();

using (var stream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read))
{
bitmap.BeginInit();
bitmap.CacheOption = BitmapCacheOption.OnLoad;
bitmap.CreateOptions = BitmapCreateOptions.IgnoreImageCache;
bitmap.StreamSource = stream;
bitmap.EndInit();
bitmap.Freeze();
}

return bitmap;
}

このコードのポイントは次のとおりです。

C#
bitmap.CacheOption = BitmapCacheOption.OnLoad;

これにより、Streamを閉じても画像を表示できます。

C#
bitmap.CreateOptions = BitmapCreateOptions.IgnoreImageCache;

これにより、同じファイルパスの画像を差し替えたときに、古いキャッシュ画像が表示される問題を避けやすくなります。

C#
bitmap.Freeze();

これにより、画像オブジェクトを変更不可にし、別スレッドからも扱いやすくなります。

このようなメソッドを共通化しておくと、アプリ全体でファイル占有トラブルを減らせます。

5-4. OnDemandとOnLoadの違い

BitmapImageCacheOptionには、主にOnDemandOnLoadがあります。

OnDemandは、必要になったタイミングで画像データを読み込む方式です。デフォルトではこの動作になることがあります。初期表示時の負荷を抑えられる場合がありますが、ファイルやStreamへの参照が残りやすく、ファイルロックの原因になることがあります。

OnLoadは、初期化時に画像データを読み込む方式です。EndInit時点で画像データを読み込むため、読み込み後にStreamを閉じたり、ファイル操作を行ったりしやすくなります。

ファイル占有を避けたい場合は、基本的にOnLoadを使います。

C#
bitmap.CacheOption = BitmapCacheOption.OnLoad;

使い分けの目安は次のとおりです。

画像ファイルを表示するだけで、元ファイルを操作しない → OnDemandでもよい
表示後に削除・上書き・移動する → OnLoadを使う
Streamをusingで閉じたい → OnLoadを使う
一時ファイルを読み込む → OnLoadを使う
画像差し替えを行う → OnLoad + IgnoreImageCacheを使う

WPFアプリで画像ファイルの編集・差し替え・削除を行うなら、OnLoadを基本にするとトラブルが少なくなります。

5-5. 画像差し替え時にファイル占有を防ぐ実装パターン

同じファイル名の画像を差し替える場合、古い画像が表示されたままになったり、ファイルが使用中で上書きできなかったりすることがあります。

安全な実装パターンは次のとおりです。

C#
private BitmapImage LoadImageForReplace(string filePath)
{
var bitmap = new BitmapImage();

using (var stream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read))
{
bitmap.BeginInit();
bitmap.CacheOption = BitmapCacheOption.OnLoad;
bitmap.CreateOptions = BitmapCreateOptions.IgnoreImageCache;
bitmap.StreamSource = stream;
bitmap.EndInit();
bitmap.Freeze();
}

return bitmap;
}

画像を差し替える前に、Image.Sourceをいったん解除する方法も有効です。

C#
PreviewImage.Source = null;

その後、新しい画像を読み込みます。

C#
PreviewImage.Source = LoadImageForReplace(filePath);

同じファイルに上書き保存する場合は、保存処理の前に表示中の画像参照を外すことを検討します。

C#
PreviewImage.Source = null;

// ここでファイルを上書き
File.Copy(newImagePath, targetPath, overwrite: true);

// 再読み込み
PreviewImage.Source = LoadImageForReplace(targetPath);

ただし、OnLoadで読み込んでStreamを閉じていれば、表示中であっても元ファイルのロックは残りにくくなります。

画像差し替え処理では、次の組み合わせが実用的です。

C#
BitmapCacheOption.OnLoad
BitmapCreateOptions.IgnoreImageCache
using FileStream
Freeze()

この組み合わせを共通メソッド化しておくと、ファイル占有エラーやキャッシュ問題をまとめて防ぎやすくなります。

6. BitmapImageの実用コード例

6-1. ファイル選択ダイアログで選んだ画像を表示する

ユーザーが選択した画像ファイルをWPF画面に表示する例です。

C#
using Microsoft.Win32;
using System.IO;
using System.Windows;
using System.Windows.Media.Imaging;

private void SelectImageButton_Click(object sender, RoutedEventArgs e)
{
var dialog = new OpenFileDialog
{
Title = "画像ファイルを選択してください",
Filter = "画像ファイル|*.png;*.jpg;*.jpeg;*.bmp;*.gif|すべてのファイル|*.*"
};

if (dialog.ShowDialog() == true)
{
PreviewImage.Source = LoadBitmapWithoutLock(dialog.FileName);
}
}

private BitmapImage LoadBitmapWithoutLock(string filePath)
{
var bitmap = new BitmapImage();

using (var stream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read))
{
bitmap.BeginInit();
bitmap.CacheOption = BitmapCacheOption.OnLoad;
bitmap.CreateOptions = BitmapCreateOptions.IgnoreImageCache;
bitmap.StreamSource = stream;
bitmap.EndInit();
bitmap.Freeze();
}

return bitmap;
}

XAMLは次のようにします。

XML
<StackPanel Margin="20">
<Button Content="画像を選択"
Click="SelectImageButton_Click"
Width="120"
Margin="0,0,0,10"/>

<Image x:Name="PreviewImage"
Width="400"
Height="300"
Stretch="Uniform"/>
</StackPanel>

このコードでは、選択した画像をFileStreamから読み込み、OnLoadでメモリに展開しています。そのため、表示後に元ファイルを削除・移動・上書きしやすくなります。

6-2. 画像ファイルをロックせずに読み込む

画像ファイルをロックせずに読み込む汎用メソッドは、WPFアプリで非常によく使います。

C#
public static BitmapImage LoadBitmapNoLock(string filePath)
{
if (string.IsNullOrWhiteSpace(filePath))
{
throw new ArgumentException("ファイルパスが空です。", nameof(filePath));
}

if (!File.Exists(filePath))
{
throw new FileNotFoundException("画像ファイルが見つかりません。", filePath);
}

var bitmap = new BitmapImage();

using (var stream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read))
{
bitmap.BeginInit();
bitmap.CacheOption = BitmapCacheOption.OnLoad;
bitmap.CreateOptions = BitmapCreateOptions.IgnoreImageCache;
bitmap.StreamSource = stream;
bitmap.EndInit();
bitmap.Freeze();
}

return bitmap;
}

使い方は次のとおりです。

C#
PreviewImage.Source = LoadBitmapNoLock(@"C:\Images\sample.jpg");

このメソッドを使うと、次のような処理がしやすくなります。

C#
var bitmap = LoadBitmapNoLock(filePath);
PreviewImage.Source = bitmap;

// 読み込み後にファイルを操作
File.Delete(filePath);

画像編集アプリ、画像管理アプリ、ファイル整理ツールなどでは、ファイルロック対策を最初から入れておくことが重要です。

6-3. サムネイル画像として軽量表示する

大きな画像を一覧表示する場合は、そのまま読み込むとメモリ使用量が大きくなります。サムネイル表示では、DecodePixelWidthまたはDecodePixelHeightを指定して縮小読み込みします。

C#
public static BitmapImage LoadThumbnail(string filePath, int decodePixelWidth)
{
var bitmap = new BitmapImage();

using (var stream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read))
{
bitmap.BeginInit();
bitmap.CacheOption = BitmapCacheOption.OnLoad;
bitmap.CreateOptions = BitmapCreateOptions.IgnoreImageCache;
bitmap.DecodePixelWidth = decodePixelWidth;
bitmap.StreamSource = stream;
bitmap.EndInit();
bitmap.Freeze();
}

return bitmap;
}

使い方は次のとおりです。

C#
ThumbnailImage.Source = LoadThumbnail(@"C:\Images\large.jpg", 200);

DecodePixelWidthを指定すると、指定した幅に近いサイズで画像をデコードできます。画面上で小さく表示するだけなら、元画像をフルサイズで読み込む必要はありません。

大量の画像を一覧表示する場合は、次のような設計が効果的です。

一覧表示     → DecodePixelWidthでサムネイル読み込み
詳細表示 → 必要になったときだけ高解像度で読み込み
非表示画像 → 参照を外してGC対象にする

サムネイル一覧では、見た目だけでなくメモリ使用量も意識することが大切です。

6-4. リソース画像をBitmapImageで表示する

プロジェクト内に含めたリソース画像をBitmapImageで表示するには、相対パスまたはpack URIを使います。

画像ファイルをImages/logo.pngとしてプロジェクトに追加し、ビルドアクションをResourceにします。

ビルド アクション: Resource

XAMLから表示する場合は、次のように書けます。

XML
<Image Source="Images/logo.png"
Width="200"
Height="100"
Stretch="Uniform"/>

C#コードから表示する場合は、次のようにします。

C#
var bitmap = new BitmapImage(
new Uri("pack://application:,,,/Images/logo.png", UriKind.Absolute)
);

LogoImage.Source = bitmap;

別アセンブリのリソース画像を参照する場合は、次のように指定します。

C#
var bitmap = new BitmapImage(
new Uri("pack://application:,,,/MyLibrary;component/Images/logo.png", UriKind.Absolute)
);

リソース画像はアプリケーションに埋め込まれるため、外部ファイルのようなファイルロック問題は基本的に起きにくいです。アイコン、ロゴ、固定の背景画像などに向いています。

一方で、ユーザーが差し替える画像や頻繁に更新する画像は、リソースではなく外部ファイルとして扱うほうが適しています。

6-5. MVVMでImage.SourceにBitmapImageをバインドする

MVVMで画像を表示する場合は、ViewModelにImageSourceまたはBitmapImage型のプロパティを用意し、XAMLのImage.Sourceにバインドします。

C#
using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Windows.Media;

public class MainViewModel : INotifyPropertyChanged
{
private ImageSource _previewImage;

public ImageSource PreviewImage
{
get => _previewImage;
set
{
_previewImage = value;
OnPropertyChanged();
}
}

public event PropertyChangedEventHandler PropertyChanged;

protected void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}

XAMLでは次のようにバインドします。

XML
<Image Source="{Binding PreviewImage}"
Width="400"
Height="300"
Stretch="Uniform"/>

ViewModelで画像を読み込む例です。

C#
public void LoadImage(string filePath)
{
PreviewImage = LoadBitmapNoLock(filePath);
}

private BitmapImage LoadBitmapNoLock(string filePath)
{
var bitmap = new BitmapImage();

using (var stream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read))
{
bitmap.BeginInit();
bitmap.CacheOption = BitmapCacheOption.OnLoad;
bitmap.CreateOptions = BitmapCreateOptions.IgnoreImageCache;
bitmap.StreamSource = stream;
bitmap.EndInit();
bitmap.Freeze();
}

return bitmap;
}

ViewModelのプロパティ型は、BitmapImageよりもImageSourceにしておくと柔軟です。BitmapImage以外のBitmapSourceや加工済み画像も設定しやすくなります。

C#
public ImageSource PreviewImage { get; set; }

MVVMでは、View側で画像読み込み処理を持たせるのではなく、ViewModelまたは画像読み込みサービスに切り出すとテストや保守がしやすくなります。

7. BitmapImageのパフォーマンスとメモリ対策

7-1. 大きな画像をそのまま読み込むリスク

BitmapImageで大きな画像をそのまま読み込むと、メモリ使用量が大きくなります。

例えば、スマートフォンやデジタルカメラで撮影した画像は、横幅が4000px以上あることも珍しくありません。その画像をWPF上で200pxのサムネイルとして表示するだけでも、何も指定しなければ大きな画像データとして読み込まれる可能性があります。

問題になりやすいケースは次のとおりです。

  • 高解像度画像を大量に一覧表示する

  • サムネイル一覧で元画像をそのまま読み込む

  • 画像を差し替えても古い参照が残っている

  • 非表示になった画像の参照を保持し続ける

  • byte配列とBitmapImageの両方を保持している

画像を表示するときは、画面上で必要なサイズを考えて読み込むことが重要です。

悪い例は次のようなコードです。

C#
PreviewImage.Source = new BitmapImage(new Uri(filePath));

このコードは簡単ですが、サイズ制御やファイルロック対策がありません。

実用的には、次のように読み込み時のサイズを指定します。

C#
bitmap.DecodePixelWidth = 300;

特に大量画像を扱うアプリでは、DecodePixelWidthDecodePixelHeightを使うだけで、メモリ使用量を大きく抑えられる場合があります。

7-2. DecodePixelWidth・DecodePixelHeightでリサイズ読み込みする

DecodePixelWidthDecodePixelHeightは、画像を読み込む段階でサイズを指定するためのプロパティです。

C#
var bitmap = new BitmapImage();

using (var stream = File.OpenRead(filePath))
{
bitmap.BeginInit();
bitmap.CacheOption = BitmapCacheOption.OnLoad;
bitmap.DecodePixelWidth = 300;
bitmap.StreamSource = stream;
bitmap.EndInit();
bitmap.Freeze();
}

この例では、横幅300px程度で画像をデコードします。元画像が大きくても、表示に必要なサイズに近い形で読み込めます。

縦幅を基準にしたい場合は、DecodePixelHeightを使います。

C#
bitmap.DecodePixelHeight = 200;

通常は、DecodePixelWidthDecodePixelHeightのどちらか一方だけを指定します。

C#
bitmap.DecodePixelWidth = 300;
// bitmap.DecodePixelHeight = 200; // 通常は同時指定しない

両方を指定すると、画像の縦横比やデコード結果が意図と異なる場合があります。サムネイル表示で縦横比を維持したい場合は、一方だけ指定するのが無難です。

サムネイル用の共通メソッドは次のように作れます。

C#
public static BitmapImage LoadResizedBitmap(string filePath, int width)
{
var bitmap = new BitmapImage();

using (var stream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read))
{
bitmap.BeginInit();
bitmap.CacheOption = BitmapCacheOption.OnLoad;
bitmap.CreateOptions = BitmapCreateOptions.IgnoreImageCache;
bitmap.DecodePixelWidth = width;
bitmap.StreamSource = stream;
bitmap.EndInit();
bitmap.Freeze();
}

return bitmap;
}

一覧表示と詳細表示で読み込みサイズを分けると、パフォーマンスと画質のバランスを取りやすくなります。

7-3. Freezeで別スレッド利用や描画負荷を抑える

BitmapImageはWPFのFreezableに関連するオブジェクトです。Freezeを呼び出すことで、オブジェクトを変更不可にできます。

C#
bitmap.Freeze();

Freezeするメリットは主に次のとおりです。

  • 変更不可になるため安全に扱える

  • 別スレッドで作成した画像をUIスレッドへ渡しやすくなる

  • WPF内部で最適化される可能性がある

  • 意図しないプロパティ変更を防げる

画像を読み込み後に変更しない場合は、Freezeを呼び出しておくのが実用的です。

C#
public BitmapImage LoadFrozenBitmap(string filePath)
{
var bitmap = new BitmapImage();

using (var stream = File.OpenRead(filePath))
{
bitmap.BeginInit();
bitmap.CacheOption = BitmapCacheOption.OnLoad;
bitmap.StreamSource = stream;
bitmap.EndInit();
bitmap.Freeze();
}

return bitmap;
}

バックグラウンドスレッドで画像を読み込み、UIスレッドで表示する場合にも役立ちます。

C#
var bitmap = await Task.Run(() => LoadFrozenBitmap(filePath));
PreviewImage.Source = bitmap;

FreezeしていないWPFオブジェクトを別スレッドから扱うと、スレッド所有権に関する例外が発生することがあります。非同期で画像を読み込む場合は、OnLoadFreezeを組み合わせるのが安全です。

7-4. 不要になったBitmapImageを解放する考え方

BitmapImageには、Disposeメソッドがありません。そのため、不要になった画像を明示的に破棄するというより、参照を外してガベージコレクションの対象にする考え方になります。

例えば、Imageコントロールに設定した画像を解放したい場合は、Sourcenullにします。

C#
PreviewImage.Source = null;

ViewModelで保持している場合は、プロパティをnullにします。

C#
PreviewImage = null;

画像一覧などでコレクションにBitmapImageを保持している場合は、不要な項目を削除します。

C#
Images.Clear();

ただし、次のような状態では画像が解放されません。

  • Image.Sourceに設定されたまま

  • ViewModelのプロパティに参照が残っている

  • コレクションに画像オブジェクトが残っている

  • byte配列も同時に保持している

  • イベントやキャッシュから参照されている

メモリ使用量が気になる場合は、画像オブジェクトだけでなく、画像を参照しているUIやViewModel、コレクションも確認する必要があります。

大量画像を扱う場合は、必要なときに読み込み、不要になったら参照を外す設計が重要です。

7-5. 大量画像・サムネイル一覧での注意点

大量の画像を一覧表示する場合、BitmapImageの使い方によってアプリの動作が大きく変わります。

避けたい実装は、すべての元画像をフルサイズで読み込んで一覧に保持することです。

C#
foreach (var path in imagePaths)
{
Images.Add(new BitmapImage(new Uri(path)));
}

このような実装は簡単ですが、画像数が増えるとメモリ使用量が急増し、画面表示も重くなります。

改善するには、サムネイル用のサイズで読み込みます。

C#
foreach (var path in imagePaths)
{
Images.Add(LoadThumbnail(path, 200));
}

さらに、UI側では仮想化を有効にすることも重要です。ListBoxListViewで大量の画像を表示する場合は、仮想化が効くレイアウトを使います。

XML
<ListBox ItemsSource="{Binding Images}"
VirtualizingPanel.IsVirtualizing="True"
VirtualizingPanel.VirtualizationMode="Recycling">
<ListBox.ItemTemplate>
<DataTemplate>
<Image Source="{Binding}"
Width="120"
Height="120"
Stretch="UniformToFill"/>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>

ただし、WrapPanelなどを使うと仮想化が効きにくくなることがあります。大量画像を扱う場合は、見た目だけでなく仮想化の有無も確認しましょう。

大量画像のポイントは次のとおりです。

読み込み時に縮小する
表示に必要な分だけ読み込む
不要な参照を残さない
UI仮想化を使う
非同期読み込みを検討する
Freezeしてスレッド間の扱いを安全にする

8. BitmapImageでよくあるエラーと解決方法

8-1. 画像が表示されない

画像が表示されない場合、まず確認すべきなのはパスです。

C#
MessageBox.Show(File.Exists(filePath).ToString());

外部ファイルを指定しているなら、File.Existstrueになるか確認します。

C#
if (!File.Exists(filePath))
{
MessageBox.Show("ファイルが存在しません。");
return;
}

プロジェクト内の画像を表示している場合は、ビルドアクションを確認します。

Resourceとして使う       → ビルド アクション: Resource
外部ファイルとして使う → ビルド アクション: Content

XAMLの相対パスが間違っているケースもよくあります。

XML
<Image Source="Images/sample.png"/>

画像の実際の配置がAssets/sample.pngなら、当然表示されません。

XML
<Image Source="Assets/sample.png"/>

また、Imageコントロールのサイズが指定されていない、親要素のレイアウトで見えない位置にある、VisibilityCollapsedになっている、といったUI側の問題もあります。

XML
<Image Source="{Binding PreviewImage}"
Width="300"
Height="200"
Visibility="Visible"/>

画像が表示されないときは、BitmapImageの問題と決めつけず、パス、ビルド設定、レイアウト、バインドの順に確認しましょう。

8-2. Streamが閉じられていて読み込めない

StreamからBitmapImageを作成したあとに、次のようなエラーが出ることがあります。

Cannot access a closed Stream.

原因は、Streamが閉じられた後にBitmapImageが画像データを読み込もうとしているためです。

問題のある例です。

C#
BitmapImage bitmap;

using (var stream = File.OpenRead(filePath))
{
bitmap = new BitmapImage();
bitmap.BeginInit();
bitmap.StreamSource = stream;
bitmap.EndInit();
}

PreviewImage.Source = bitmap;

このコードでは、usingを抜けた時点でStreamが閉じます。遅延読み込みが発生すると、閉じたStreamへアクセスしようとして失敗します。

解決策は、CacheOption.OnLoadを指定することです。

C#
BitmapImage bitmap;

using (var stream = File.OpenRead(filePath))
{
bitmap = new BitmapImage();
bitmap.BeginInit();
bitmap.CacheOption = BitmapCacheOption.OnLoad;
bitmap.StreamSource = stream;
bitmap.EndInit();
bitmap.Freeze();
}

PreviewImage.Source = bitmap;

Streamを閉じても画像を表示し続けたい場合は、OnLoadを必ず指定しましょう。

8-3. ファイルが別プロセスで使用中になる

画像を表示したあとに削除や上書きをしようとして、ファイル使用中エラーが発生する場合があります。

原因になりやすいコードは次のようなものです。

C#
PreviewImage.Source = new BitmapImage(new Uri(filePath));

この方法では、画像ファイルへの参照が残り、ファイルがロックされることがあります。

解決策は、StreamとOnLoadを使って読み込むことです。

C#
public BitmapImage LoadNoLock(string filePath)
{
var bitmap = new BitmapImage();

using (var stream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read))
{
bitmap.BeginInit();
bitmap.CacheOption = BitmapCacheOption.OnLoad;
bitmap.CreateOptions = BitmapCreateOptions.IgnoreImageCache;
bitmap.StreamSource = stream;
bitmap.EndInit();
bitmap.Freeze();
}

return bitmap;
}

画像を差し替える場合は、必要に応じて表示中の画像参照を外します。

C#
PreviewImage.Source = null;
PreviewImage.Source = LoadNoLock(filePath);

同じファイル名で画像を更新する処理では、IgnoreImageCacheも併用すると古い画像キャッシュの影響を避けやすくなります。

8-4. パス指定が間違っている

BitmapImageで最も多いトラブルのひとつが、パス指定の間違いです。

絶対パスなら次のように指定します。

C#
var bitmap = new BitmapImage(
new Uri(@"C:\Images\sample.png", UriKind.Absolute)
);

相対パスなら次のように指定します。

C#
var bitmap = new BitmapImage(
new Uri("Images/sample.png", UriKind.Relative)
);

pack URIなら次のように指定します。

C#
var bitmap = new BitmapImage(
new Uri("pack://application:,,,/Images/sample.png", UriKind.Absolute)
);

間違いやすいのは、プロジェクト内の画像を外部ファイルの相対パスとして扱ってしまうケースです。

リソースとして埋め込んだ画像は、実行フォルダにファイルとして存在するとは限りません。File.Exists("Images/sample.png")falseでも、WPFリソースとしては表示できる場合があります。

一方で、出力フォルダにコピーして外部ファイルとして扱う画像は、実行ファイルから見た相対パスで存在する必要があります。

迷った場合は、画像の扱いを次のどちらかに統一するとよいです。

固定画像     → Resourceとして埋め込む
可変画像 → 外部ファイルとして絶対パスで読み込む

8-5. UIスレッド以外でBitmapImageを扱うとエラーになる

WPFのUI要素は、基本的にUIスレッドで操作する必要があります。バックグラウンドスレッドで画像を読み込み、そのままUIに設定しようとすると、スレッド関連のエラーが発生することがあります。

安全な方法は、バックグラウンドスレッドでBitmapImageを作成し、FreezeしてからUIスレッドに渡すことです。

C#
private async Task LoadImageAsync(string filePath)
{
var bitmap = await Task.Run(() =>
{
var image = new BitmapImage();

using (var stream = File.OpenRead(filePath))
{
image.BeginInit();
image.CacheOption = BitmapCacheOption.OnLoad;
image.StreamSource = stream;
image.EndInit();
image.Freeze();
}

return image;
});

PreviewImage.Source = bitmap;
}

このコードでは、画像の読み込み処理をTask.Run内で実行しています。Freezeしてから返しているため、UIスレッドでImage.Sourceに設定しやすくなります。

重要なのは、UIコントロールであるPreviewImage自体はUIスレッドで更新することです。

C#
PreviewImage.Source = bitmap;

バックグラウンドスレッドで直接PreviewImage.Sourceを変更するのは避けましょう。

8-6. Web画像や非同期読み込みで失敗する

Web画像を読み込む場合、単純にURLをUriSourceへ指定する方法があります。

C#
var bitmap = new BitmapImage(
new Uri("https://example.com/image.png", UriKind.Absolute)
);

PreviewImage.Source = bitmap;

ただし、Web画像では次のような理由で失敗することがあります。

URLが間違っている
画像ファイルではなくHTMLが返っている
認証が必要
通信に失敗している
HTTPS証明書の問題がある
リダイレクトやアクセス制限がある

より制御したい場合は、HttpClientで画像データを取得し、MemoryStreamからBitmapImageを作成します。

C#
using System.Net.Http;

private static readonly HttpClient httpClient = new HttpClient();

public async Task<BitmapImage> LoadImageFromWebAsync(string url)
{
byte[] bytes = await httpClient.GetByteArrayAsync(url);

var bitmap = new BitmapImage();

using (var stream = new MemoryStream(bytes))
{
bitmap.BeginInit();
bitmap.CacheOption = BitmapCacheOption.OnLoad;
bitmap.StreamSource = stream;
bitmap.EndInit();
bitmap.Freeze();
}

return bitmap;
}

使い方は次のとおりです。

C#
PreviewImage.Source = await LoadImageFromWebAsync("https://example.com/image.png");

Web画像は通信エラーが起こり得るため、実際のアプリでは例外処理を入れておきます。

C#
try
{
PreviewImage.Source = await LoadImageFromWebAsync(url);
}
catch (Exception ex)
{
MessageBox.Show($"画像の読み込みに失敗しました: {ex.Message}");
}

非同期読み込みでは、画像取得、デコード、UI更新の責務を分けると、エラー処理やローディング表示を実装しやすくなります。

9. BitmapImageの応用|変換・保存・加工

9-1. BitmapImageをBitmapSourceとして扱う方法

BitmapImageBitmapSourceを継承しているため、BitmapSource型として扱えます。

C#
BitmapImage bitmapImage = LoadBitmapNoLock(filePath);
BitmapSource bitmapSource = bitmapImage;

WPFの画像処理では、BitmapSourceを引数に取るクラスが多くあります。例えば、画像を保存するエンコーダーや、トリミング用のCroppedBitmapなどです。

C#
BitmapSource source = bitmapImage;

Image.Sourceの型もImageSourceなので、継承関係としては次のように考えると理解しやすいです。

ImageSource
└ BitmapSource
└ BitmapImage

そのため、ViewModelでは具体的なBitmapImageではなく、ImageSourceBitmapSourceとしてプロパティを定義することもできます。

C#
public ImageSource PreviewImage { get; set; }

保存や加工を行うメソッドでは、BitmapSourceを受け取るようにしておくと、BitmapImage以外の画像にも対応できます。

C#
public void SaveAsPng(BitmapSource source, string filePath)
{
var encoder = new PngBitmapEncoder();
encoder.Frames.Add(BitmapFrame.Create(source));

using (var stream = new FileStream(filePath, FileMode.Create))
{
encoder.Save(stream);
}
}

9-2. System.Drawing.BitmapとBitmapImageを変換する方法

System.Drawing.BitmapとWPFのBitmapImageは別の画像クラスです。相互変換したい場合は、Streamを経由する方法がわかりやすいです。

System.Drawing.BitmapからBitmapImageへ変換する例です。

C#
using System.Drawing;
using System.Drawing.Imaging;
using System.IO;
using System.Windows.Media.Imaging;

public BitmapImage ConvertDrawingBitmapToBitmapImage(Bitmap drawingBitmap)
{
using (var memory = new MemoryStream())
{
drawingBitmap.Save(memory, ImageFormat.Png);
memory.Position = 0;

var bitmapImage = new BitmapImage();
bitmapImage.BeginInit();
bitmapImage.CacheOption = BitmapCacheOption.OnLoad;
bitmapImage.StreamSource = memory;
bitmapImage.EndInit();
bitmapImage.Freeze();

return bitmapImage;
}
}

逆に、BitmapImageSystem.Drawing.Bitmapに変換したい場合は、エンコードしてStreamに保存し、それをBitmapで読み込みます。

C#
public Bitmap ConvertBitmapImageToDrawingBitmap(BitmapImage bitmapImage)
{
using (var memory = new MemoryStream())
{
var encoder = new PngBitmapEncoder();
encoder.Frames.Add(BitmapFrame.Create(bitmapImage));
encoder.Save(memory);

using (var temp = new MemoryStream(memory.ToArray()))
{
return new Bitmap(temp);
}
}
}

注意点として、System.Drawing.Commonは用途や実行環境によって制約があります。WPFアプリ内で完結する画像表示・保存・簡単な加工であれば、できるだけWPFのBitmapSource系クラスを使うほうが自然です。

9-3. BitmapImageをPNGやJPEGとして保存する方法

BitmapImageをPNGとして保存するには、PngBitmapEncoderを使います。

C#
public void SaveBitmapImageAsPng(BitmapImage bitmapImage, string filePath)
{
var encoder = new PngBitmapEncoder();
encoder.Frames.Add(BitmapFrame.Create(bitmapImage));

using (var stream = new FileStream(filePath, FileMode.Create, FileAccess.Write))
{
encoder.Save(stream);
}
}

JPEGとして保存する場合は、JpegBitmapEncoderを使います。

C#
public void SaveBitmapImageAsJpeg(BitmapImage bitmapImage, string filePath, int qualityLevel = 90)
{
var encoder = new JpegBitmapEncoder
{
QualityLevel = qualityLevel
};

encoder.Frames.Add(BitmapFrame.Create(bitmapImage));

using (var stream = new FileStream(filePath, FileMode.Create, FileAccess.Write))
{
encoder.Save(stream);
}
}

qualityLevelはJPEGの品質を指定します。

C#
SaveBitmapImageAsJpeg(bitmap, @"C:\Images\output.jpg", 85);

PNGは可逆圧縮で、アイコンや透過画像に向いています。JPEGは写真に向いていますが、保存時に劣化します。

保存処理を汎用化する場合は、引数をBitmapSourceにすると便利です。

C#
public void SaveBitmapSourceAsPng(BitmapSource source, string filePath)
{
var encoder = new PngBitmapEncoder();
encoder.Frames.Add(BitmapFrame.Create(source));

using (var stream = new FileStream(filePath, FileMode.Create))
{
encoder.Save(stream);
}
}

BitmapImageだけでなく、トリミング後や変換後のBitmapSourceも保存できるようになります。

9-4. BitmapImageを回転・リサイズ・トリミングする方法

BitmapImage自体を直接編集するというより、WPFではBitmapSourceをもとに変換画像を作成します。

回転する場合は、TransformedBitmapを使います。

C#
public BitmapSource RotateBitmap(BitmapSource source, double angle)
{
var transform = new RotateTransform(angle);
var rotated = new TransformedBitmap(source, transform);
rotated.Freeze();

return rotated;
}

使い方は次のとおりです。

C#
var rotated = RotateBitmap(bitmapImage, 90);
PreviewImage.Source = rotated;

トリミングする場合は、CroppedBitmapを使います。

C#
public BitmapSource CropBitmap(BitmapSource source, int x, int y, int width, int height)
{
var rect = new Int32Rect(x, y, width, height);
var cropped = new CroppedBitmap(source, rect);
cropped.Freeze();

return cropped;
}

使い方は次のとおりです。

C#
var cropped = CropBitmap(bitmapImage, 100, 100, 300, 200);
PreviewImage.Source = cropped;

表示上のリサイズであれば、ImageコントロールのWidthHeightStretchで対応できます。

XML
<Image Source="{Binding PreviewImage}"
Width="300"
Height="200"
Stretch="Uniform"/>

読み込み時点でリサイズしたい場合は、DecodePixelWidthDecodePixelHeightを使います。

C#
bitmap.DecodePixelWidth = 300;

保存用に実際のピクセルサイズを変えたい場合は、TransformedBitmapRenderTargetBitmapなどを使った処理を検討します。単なる表示サイズ変更なのか、画像データそのものを変更したいのかで実装方法が変わります。

9-5. BitmapImageではなくBitmapSourceを使うべきケース

BitmapImageは画像ファイルやStreamから画像を読み込むときに便利ですが、すべての画像処理をBitmapImageで行う必要はありません。

次のようなケースでは、BitmapSourceとして扱うほうが適しています。

画像を保存する
画像をトリミングする
画像を回転する
画像を変換する
加工後の画像をImage.Sourceに表示する
BitmapImage以外の画像ソースも扱いたい

例えば、トリミング結果はCroppedBitmapです。

C#
BitmapSource cropped = new CroppedBitmap(bitmapImage, new Int32Rect(0, 0, 100, 100));

回転結果はTransformedBitmapです。

C#
BitmapSource rotated = new TransformedBitmap(bitmapImage, new RotateTransform(90));

これらはBitmapImageではありませんが、BitmapSourceとして扱えます。そして、Image.Sourceにも設定できます。

C#
PreviewImage.Source = rotated;

そのため、メソッドの引数やViewModelのプロパティは、具体的なBitmapImageではなく、より抽象的な型にしておくと柔軟です。

C#
public ImageSource PreviewImage { get; set; }

または、画像処理メソッドでは次のようにします。

C#
public void SaveImage(BitmapSource source, string filePath)
{
var encoder = new PngBitmapEncoder();
encoder.Frames.Add(BitmapFrame.Create(source));

using (var stream = new FileStream(filePath, FileMode.Create))
{
encoder.Save(stream);
}
}

読み込みにはBitmapImage、加工や保存にはBitmapSourceという役割分担で考えると、WPFの画像処理を整理しやすくなります。

まとめ

C#のWPFで画像を表示するなら、BitmapImageは基本となる重要なクラスです。ImageコントロールのSourceに設定するだけで画像を表示でき、ローカルファイル、リソース、Stream、byte配列、Base64文字列など、さまざまな入力形式に対応できます。

基本的な画像表示だけなら、次のように簡単に書けます。

C#
PreviewImage.Source = new BitmapImage(
new Uri(@"C:\Images\sample.jpg", UriKind.Absolute)
);

しかし、実用的なWPFアプリでは、ファイル占有やメモリ使用量を考慮した読み込み方が重要です。

特にファイルロックを避けたい場合は、次のようにCacheOption.OnLoadusingを組み合わせます。

C#
var bitmap = new BitmapImage();

using (var stream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read))
{
bitmap.BeginInit();
bitmap.CacheOption = BitmapCacheOption.OnLoad;
bitmap.CreateOptions = BitmapCreateOptions.IgnoreImageCache;
bitmap.StreamSource = stream;
bitmap.EndInit();
bitmap.Freeze();
}

この形で読み込めば、Streamを閉じても画像を表示でき、元ファイルの削除・上書き・差し替えもしやすくなります。

また、大きな画像や大量のサムネイルを扱う場合は、DecodePixelWidthDecodePixelHeightを使って読み込み時点でサイズを抑えることが大切です。

C#
bitmap.DecodePixelWidth = 300;

非同期読み込みや別スレッドでの処理では、読み込み後にFreezeしておくと安全に扱いやすくなります。

C#
bitmap.Freeze();

BitmapImageは画像の読み込みに便利な一方で、加工・保存・変換ではBitmapSourceとして扱う場面も多くあります。読み込みはBitmapImage、表示はImage.Source、加工や保存はBitmapSourceという役割で整理すると、WPFの画像処理を実装しやすくなります。

C#でBitmapImageを使うときは、単に表示できるコードを書くのではなく、次のポイントを意識することが重要です。

BeginInit・EndInitの間で設定する
Stream読み込みではCacheOption.OnLoadを指定する
ファイル占有を避けるならusingでStreamを閉じる
大きな画像はDecodePixelWidthで縮小読み込みする
変更しない画像はFreezeする
画像差し替えではIgnoreImageCacheを検討する
MVVMではImageSourceやBitmapSourceでバインドする

これらを押さえておけば、WPFでの画像表示、Stream読み込み、ファイル占有エラー対策、サムネイル表示、画像保存・加工まで、BitmapImageを安定して扱えるようになります。