WPF(C#)でSQL Serverに保存した画像(byte[])をBitmapImageへ復元し、Microsoft.Office.Interop.Excelで生成したワークシートの左上など任意の位置に表示したい――帳票出力や検査報告書の自動作成でよく出る要件です。この記事では、クリップボード貼り付けとShapes.AddPicture(座標指定)の2手法を、セル範囲に収める調整方法や落とし穴まで含めてまとめます。
なぜ「Excelに画像を貼る」のが意外と難しいのか
Excel Interopで画像を挿入すると、画像は「セルの値」ではなく図形(Shape)として扱われます。つまり、A1セルに画像を貼ったとしても、画像はセルの一部ではなく、セルの上に重なるオブジェクトです。そのため、次の点を意識する必要があります。
- 「どのセルに置くか」ではなく、最終的にLeft / Top(座標)とWidth / Height(サイズ)をどう決めるかが重要
- 貼り付け後に行列幅を変更すると、画像がずれて見えることがある(
Placementの設定が必要) - InteropはCOMなので、オブジェクト解放を怠るとExcel.exeが残る
準備:Microsoft.Office.Interop.Excelを使うための参照設定
WPF(.NET Framework / .NET 6以降)いずれでもExcel Interopは利用できますが、参照の入れ方とビルド構成でつまずきやすいです。最低限、次を確認してください。
| チェック項目 | ポイント | よくある症状 |
|---|---|---|
| 参照(Interop) | Excel用のInteropと、MsoTriState等のためにOffice Coreも参照する | MsoTriStateが解決できない / 型が見つからない |
| 32bit / 64bit | インストールされているOfficeに合わせてx86/x64を選ぶ | 実行時にCOM例外が出る、起動できない |
| 実行環境 | Interopは「Excelがインストールされたクライアント」での実行が基本 | サーバーで不安定、Excelが表示できない |
名前空間のエイリアスを使うと可読性が上がります(記事内のサンプルもこの書き方です)。
using Excel = Microsoft.Office.Interop.Excel;
using Office = Microsoft.Office.Core;
前提:SQL Serverのbyte[]をBitmapImageに復元する(WPF)
SQL Serverにvarbinary(max)などで保存した画像をWPFで扱う場合、byte[]→BitmapImageの復元を正しく行うと後工程が安定します。特に重要なのが、ストリーム寿命の問題を避けるためのOnLoadと、スレッド間利用のためのFreezeです。
using System.IO;
using System.Windows.Media.Imaging;
public static BitmapImage BytesToBitmapImage(byte[] bytes)
{
if (bytes == null || bytes.Length == 0) return null;
using (var ms = new MemoryStream(bytes))
{
var image = new BitmapImage();
image.BeginInit();
image.CacheOption = BitmapCacheOption.OnLoad; // ストリーム破棄後も使える
image.StreamSource = ms;
image.EndInit();
image.Freeze(); // 別スレッドに渡す可能性があるなら必須
return image;
}
}
以降は、このBitmapImage(または元のbyte[])をExcelに挿入します。
Excelへ画像挿入の代表的な2方式(比較表)
| 方式 | メリット | デメリット / 注意 | 向いているケース |
|---|---|---|---|
| クリップボード経由(貼り付け) | 実装が短く、BitmapImageをそのまま扱える。Excel側の「貼り付け」と同じ感覚。 | ユーザーのクリップボードを一時的に上書きする。環境によって貼り付けが不安定なことがある。 | 社内ツール・単発帳票・手軽さ優先 |
| Shapes.AddPicture(座標指定) | Left/Top/Width/Heightで厳密に制御できる。クリップボードを汚さない。安定しやすい。 | 一時ファイルが必要(パス指定)。ピクセル→ポイント換算が必要。 | 位置・サイズが厳密な帳票、連続出力、クリップボードNG |
方式1:クリップボード経由でBitmapImageを貼り付ける
WPFのClipboard.SetImageで画像をクリップボードに入れ、Excel側で貼り付ける方法です。実装が簡単で、採用例も多い手法です。
基本手順(最小実装)
- BitmapImageをクリップボードへセット
- Excelを起動してブック/シートを作成
- 貼り付けたいセル(例:A1)を指定して貼り付け
- 必要なら貼り付けたShapeの位置・サイズを調整
using Excel = Microsoft.Office.Interop.Excel;
using System.Runtime.InteropServices;
using System.Windows; // Clipboard
using System.Windows.Media.Imaging;
public static void InsertByClipboard(BitmapSource bitmap, string excelPath)
{
if (bitmap == null) throw new ArgumentNullException(nameof(bitmap));
Excel.Application excelApp = null;
Excel.Workbook workbook = null;
Excel.Worksheet sheet = null;
try
{
// 1) クリップボードへ(WPFはSTAスレッド上で呼ぶ)
Clipboard.SetImage(bitmap);
// 2) Excel起動
excelApp = new Excel.Application();
excelApp.DisplayAlerts = false;
excelApp.ScreenUpdating = false;
workbook = excelApp.Workbooks.Add();
sheet = (Excel.Worksheet)workbook.ActiveSheet;
// 3) 貼り付け位置(A1)
Excel.Range dest = (Excel.Range)sheet.Cells[1, 1];
// 選択してPasteするのが最も確実(ただしUI操作)
dest.Select();
sheet.Paste(); // クリップボードの画像を貼り付け
// ※選択を避けたい場合は、環境によっては次も動きます
// sheet.Paste(dest);
// 4) 保存
workbook.SaveAs(excelPath);
}
finally
{
// 後片付け(COM解放は後述のチェックリスト参照)
if (workbook != null) workbook.Close(false);
if (excelApp != null) excelApp.Quit();
if (sheet != null) Marshal.FinalReleaseComObject(sheet);
if (workbook != null) Marshal.FinalReleaseComObject(workbook);
if (excelApp != null) Marshal.FinalReleaseComObject(excelApp);
}
}
クリップボードを汚したくない場合の実践的な対策
業務アプリだと「ユーザーがコピーしていた内容を消したくない」という要件がよくあります。完全に元通りにするのは難しいケースもありますが、最低限「元のDataObjectを退避→復元」をしておくと体験が良くなります。
using System.Windows;
public static void WithPreservedClipboard(Action action)
{
IDataObject backup = null;
try
{
backup = Clipboard.GetDataObject(); // 退避
action();
}
finally
{
if (backup != null)
{
// true: 可能な限り保持
Clipboard.SetDataObject(backup, true);
}
}
}
上記を使う場合は、先ほどの処理を次のように包みます。
WithPreservedClipboard(() =>
{
Clipboard.SetImage(bitmap);
// ExcelへPaste...
});
貼り付け後に「A1~C3の範囲」に収めたい(位置・サイズ調整)
クリップボード貼り付けは「貼ったら終わり」ではなく、実運用では画像のサイズ調整がほぼ必須です。貼り付けた直後、Excelでは画像はShapeとして追加されるので、最後に追加されたShapeを取得して調整します。
Excel.Shape GetLastShape(Excel.Worksheet sheet)
{
// 直近で追加されたShapeが最後に来ることが多い(確実性を上げるならShape名で管理)
return sheet.Shapes.Item(sheet.Shapes.Count);
}
void FitLastShapeToRange(Excel.Worksheet sheet, string rangeA1, string rangeB2, bool keepAspect)
{
var range = sheet.Range[rangeA1, rangeB2];
float left = (float)(double)range.Left;
float top = (float)(double)range.Top;
float boxW = (float)(double)range.Width;
float boxH = (float)(double)range.Height;
var shape = GetLastShape(sheet);
shape.Left = left;
shape.Top = top;
if (!keepAspect)
{
shape.Width = boxW;
shape.Height = boxH;
return;
}
// 縦横比を保って内接させる
float w = (float)shape.Width;
float h = (float)shape.Height;
float scale = Math.Min(boxW / w, boxH / h);
shape.Width = w * scale;
shape.Height = h * scale;
// センタリングしたい場合
shape.Left = left + (boxW - shape.Width) / 2;
shape.Top = top + (boxH - shape.Height) / 2;
}
ただし、クリップボード貼り付けは環境によって「Pasteが失敗する」「Excelを非表示で動かすと不安定」などの声もあります。帳票を大量生成する、運用端末が多い、といった場合は次の座標指定方式が安定しやすいです。
方式2:Shapes.AddPictureで座標を指定して挿入する(推奨)
Shapes.AddPictureは、Excelの図形として画像を挿入し、Left/Top/Width/Height(ポイント単位)を指定できます。クリップボードに依存しないため、安定性と制御性のバランスが良い方法です。
前準備:BitmapImageを一時ファイルに保存する(PNG推奨)
JPGは軽い反面、再圧縮で文字や線がにじむことがあります。帳票のロゴや検査写真なら、まずはPNGで保存して問題ないケースが多いです(透明背景が必要ならPNGが必須)。
using System.IO;
using System.Windows.Media.Imaging;
public static string SaveBitmapSourceToTempPng(BitmapSource bitmap)
{
string tempPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N") + ".png");
var encoder = new PngBitmapEncoder();
encoder.Frames.Add(BitmapFrame.Create(bitmap));
using (var fs = new FileStream(tempPath, FileMode.Create, FileAccess.Write))
{
encoder.Save(fs);
}
return tempPath;
}
Excelの座標単位(ポイント)とWPFのピクセルの違い
ExcelのShapes.AddPictureで指定する幅・高さはポイント(pt)です。一方、WPFのBitmapSource.PixelWidthはピクセル。DPIが96の画像なら「1px ≒ 0.75pt」ですが、DPIが異なる画像や拡大縮小を考えると、明示的な換算が安全です。
| 項目 | 単位 | 備考 |
|---|---|---|
| Excel Range.Left / Top | ポイント(pt) | シート左上を基準にした座標 |
| Excel Range.Width / Height | ポイント(pt) | 列幅・行高の合計で決まる |
| WPF BitmapSource.PixelWidth / PixelHeight | ピクセル(px) | 画像の実ピクセル数 |
| WPF BitmapSource.DpiX / DpiY | DPI | 1インチあたりのピクセル数 |
換算は次の通りです。
- ポイント = ピクセル × 72 ÷ DPI
public static float PxToPoint(double px, double dpi)
{
// Excelはポイント指定(1inch=72pt)
return (float)(px * 72.0 / dpi);
}
セルを基準に左上へ配置する(C3の左上など)
セルのLeft/Topを取れば「そのセルの左上」にぴったり置けます。貼り付け位置が「1~3行×1~3列付近」と決まっているなら、基準セル(例:A1)や範囲(例:A1:C3)を起点にするのが実装も読みやすいです。
using Excel = Microsoft.Office.Interop.Excel;
using Office = Microsoft.Office.Core;
using System.Runtime.InteropServices;
public static void InsertByAddPicture(BitmapSource bitmap, string excelPath)
{
if (bitmap == null) throw new ArgumentNullException(nameof(bitmap));
Excel.Application excelApp = null;
Excel.Workbook workbook = null;
Excel.Worksheet sheet = null;
string tempImagePath = null;
try
{
tempImagePath = SaveBitmapSourceToTempPng(bitmap);
excelApp = new Excel.Application();
excelApp.DisplayAlerts = false;
excelApp.ScreenUpdating = false;
workbook = excelApp.Workbooks.Add();
sheet = (Excel.Worksheet)workbook.ActiveSheet;
// 例:A1セル左上へ
Excel.Range cell = (Excel.Range)sheet.Cells[1, 1];
float left = (float)(double)cell.Left;
float top = (float)(double)cell.Top;
// 画像サイズ(px→pt換算)
float w = PxToPoint(bitmap.PixelWidth, bitmap.DpiX);
float h = PxToPoint(bitmap.PixelHeight, bitmap.DpiY);
sheet.Shapes.AddPicture(
tempImagePath,
Office.MsoTriState.msoFalse, // LinkToFile(リンクしない)
Office.MsoTriState.msoTrue, // SaveWithDocument(ブックに埋め込む)
left,
top,
w,
h
);
workbook.SaveAs(excelPath);
}
finally
{
if (workbook != null) workbook.Close(false);
if (excelApp != null) excelApp.Quit();
if (sheet != null) Marshal.FinalReleaseComObject(sheet);
if (workbook != null) Marshal.FinalReleaseComObject(workbook);
if (excelApp != null) Marshal.FinalReleaseComObject(excelApp);
if (!string.IsNullOrEmpty(tempImagePath) && File.Exists(tempImagePath))
File.Delete(tempImagePath);
}
}
A1~C3のセル範囲に「きっちり収める」実装
帳票の枠に写真を収めたい場合は、範囲(レンジ)のLeft/Top/Width/Heightを枠と見なし、画像を内接させるのが定番です。縦横比を維持する場合は、枠に対して短い側に合わせて縮小し、余白分をセンタリングします。
public static void AddPictureFitToRange(
Excel.Worksheet sheet,
string imagePath,
Excel.Range targetRange,
double imgPixelW,
double imgPixelH,
double imgDpiX,
double imgDpiY,
bool keepAspect = true,
bool center = true)
{
float boxLeft = (float)(double)targetRange.Left;
float boxTop = (float)(double)targetRange.Top;
float boxW = (float)(double)targetRange.Width;
float boxH = (float)(double)targetRange.Height;
float imgWpt = PxToPoint(imgPixelW, imgDpiX);
float imgHpt = PxToPoint(imgPixelH, imgDpiY);
float w = imgWpt;
float h = imgHpt;
if (keepAspect)
{
float scale = Math.Min(boxW / imgWpt, boxH / imgHpt);
w = imgWpt * scale;
h = imgHpt * scale;
}
else
{
w = boxW;
h = boxH;
}
float left = boxLeft;
float top = boxTop;
if (center)
{
left = boxLeft + (boxW - w) / 2;
top = boxTop + (boxH - h) / 2;
}
var shape = sheet.Shapes.AddPicture(
imagePath,
Office.MsoTriState.msoFalse,
Office.MsoTriState.msoTrue,
left,
top,
w,
h
);
// 行/列の変更に追従させたい場合(枠と一緒に動かす・サイズも変更する)
shape.Placement = Excel.XlPlacement.xlMoveAndSize;
}
呼び出し側は次のイメージです。
Excel.Range box = sheet.Range["A1", "C3"];
AddPictureFitToRange(
sheet,
tempImagePath,
box,
bitmap.PixelWidth,
bitmap.PixelHeight,
bitmap.DpiX,
bitmap.DpiY,
keepAspect: true,
center: true
);
実運用で差が出るポイント
Excelを埋め込み表示している場合でも、挿入処理は同じ
WPF側でExcelをウィンドウ内に埋め込んでいる(例:別ウィンドウハンドルをホストしている)場合でも、画像挿入自体はブック/シートに対する操作です。つまり、今回の2方式はいずれもそのまま適用できます。違いが出るのは「可視/非可視」「ユーザー操作との競合」です。
- ユーザーがExcel側を操作できる状態なら、クリップボード方式は競合しやすい
- 安定性優先ならAddPicture方式が無難
Clipboard.SetImageはSTAスレッドで実行する
WPFのUIスレッドはSTAですが、画像取得やExcel出力をバックグラウンドタスクで回しているとMTAになりがちです。MTAからクリップボードAPIを呼ぶと例外になったり、貼り付けが不安定になります。UIを固めたくない場合は、Excel処理専用のSTAスレッドを立てるのが安全です。
public static void RunOnStaThread(Action action)
{
Exception ex = null;
var t = new Thread(() =>
{
try { action(); }
catch (Exception e) { ex = e; }
});
t.SetApartmentState(ApartmentState.STA);
t.Start();
t.Join();
if (ex != null) throw ex;
}
COM解放を怠るとExcel.exeが残る(チェックリスト)
Interopで最も多い事故が「Excelが裏で残ってメモリを食い続ける」問題です。原因は、COM参照が残っていることがほとんどです。次のチェックリストを守るだけで発生率が大きく下がります。
| 項目 | やること | 理由 |
|---|---|---|
| 参照を短命にする | RangeやShapesなどを長く保持しない | 参照が残るとExcelプロセスが終了できない |
| 解放は逆順 | 子(Range/Shape)→親(Worksheet/Workbook)→Application | COMの参照カウントを安全に落とすため |
| FinalReleaseComObject | Marshal.FinalReleaseComObjectを使う | 参照カウントを確実に0にする |
| try/finally | 例外が出てもQuit/Closeが必ず走る構造にする | 帳票出力中断時にExcelが残るのを防ぐ |
| Visible運用 | 非表示で不安定なら一時的にVisibleにする | Paste系が可視状態を要求する環境がある |
よくあるトラブルと対処
貼り付け(Paste)が失敗する / 画像が入らない
- 原因:Excelが非表示、他アプリがクリップボードをロック、STAでない、貼り付け先が未アクティブ
- 対処:AddPicture方式に切り替える/Paste直前だけ
excelApp.Visible = true;/貼り付け前にSelect()する/リトライを入れる
クリップボードが開けない(CLIPBRD_E_CANT_OPENなど)
セキュリティソフトや常駐アプリがクリップボードを短時間ロックしていると、Clipboard.SetImageで例外になることがあります。業務端末で発生する場合は、短いリトライを入れると安定します。
public static void RetryClipboardSetImage(BitmapSource bitmap, int retry = 5, int waitMs = 50)
{
for (int i = 0; i < retry; i++)
{
try
{
Clipboard.SetImage(bitmap);
return;
}
catch
{
Thread.Sleep(waitMs);
}
}
// 最後に投げ直す(原因調査用)
Clipboard.SetImage(bitmap);
}
画像サイズが想定より大きい/小さい
- 原因:px→pt換算を省略している、DPIが96ではない画像が混ざっている
- 対処:本記事の換算式(px×72÷DPI)を使う。枠(Range.Width/Height)にフィットさせる実装にする
行列幅を変えると画像がずれる
- 原因:Shapeの
Placementが既定値(移動しない/サイズ変更しない)になっている - 対処:
shape.Placement = Excel.XlPlacement.xlMoveAndSize;を設定して枠に追従させる
大量出力で遅い / 一時ファイルが増える
- 対処:一時ファイル名を使い回さない(競合回避)。処理完了後に必ず削除。複数画像を入れる場合は、Excel起動は1回にしてブック内で繰り返す
Interop以外という選択肢(サーバー実行・Office未インストール対策)
Excel Interopは「ExcelがインストールされたWindowsクライアントで動かす」前提の技術です。Webサーバーやバッチサーバーで安定稼働させたい場合、Officeに依存しないライブラリを検討する価値があります。
- Open XML SDK:.xlsxを直接生成。自由度は高いが実装コストも高め
- EPPlus / ClosedXML:帳票生成が書きやすい。画像挿入APIも用意されている
ただし、既存資産がInterop前提で「Excelの見た目をそのまま使う」必要があるなら、今回の2方式(特にAddPicture)を押さえるのが近道です。
まとめ:おすすめの選び方
- とにかく簡単に、短いコードで貼りたい → クリップボード方式
- クリップボードを汚したくない/位置とサイズを厳密に制御したい/運用端末が多い → Shapes.AddPicture方式
「A1~C3に収める」「枠に追従させる」「DPIを考慮してサイズがずれない」あたりまで実装しておくと、帳票の品質が一段上がります。まずはAddPictureで枠フィットまで作り、必要があればクリップボード方式を補助的に使う、という構成が現場では安定しやすいです。

コメント