WPF(C#)からExcelにBitmapImageを挿入する方法|Microsoft.Office.Interop.Excelで画像貼り付け

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側で貼り付ける方法です。実装が簡単で、採用例も多い手法です。

基本手順(最小実装)

  1. BitmapImageをクリップボードへセット
  2. Excelを起動してブック/シートを作成
  3. 貼り付けたいセル(例:A1)を指定して貼り付け
  4. 必要なら貼り付けた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 / DpiYDPI1インチあたりのピクセル数

換算は次の通りです。

  • ポイント = ピクセル × 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)→ApplicationCOMの参照カウントを安全に落とすため
FinalReleaseComObjectMarshal.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で枠フィットまで作り、必要があればクリップボード方式を補助的に使う、という構成が現場では安定しやすいです。

この記事を書いた人

実務の現場で詰まりがちなポイントを地図にするITブログ「IT trip」を運営。Windows/Office(Teams・Excel)からSQL、サーバ運用、ガジェットまで、再現性のある手順と“なぜそうなるか”を丁寧に解説します。読んだらすぐ試せること、そして迷った人の次の一歩が見えることを大切にしています。

コメント

コメントする

目次