🌵

脱Crystal Reports

に公開

1. はじめに

通常、Windows Forms アプリケーションで帳票を作るときには「Crystal Reports」や「帳票専用ライブラリ」を使うことが多いですが、.NET 8.0 からは Windows Forms 上でも Razor(.razor)コンポーネントを直接 HTML に変換できる API (HtmlRenderer) が使えるようになりました。

これを利用すると、

  1. 帳票レイアウトを HTML/CSS でテンプレート化
  2. C# のコードからそのテンプレートをレンダリング(HTML 生成)
  3. 生成した HTML PDF 化

という流れで、Crystal Reports を使わずに、自由度の高い HTML/CSS ベースの帳票を作成できます。

2. 必要な環境

  • .NET8.0

3. プロジェクトの作成と設定

3-1. Windows Forms プロジェクトの作成

Visual Studioで通常通り、Windows Formsのプロジェクトを作成してください。
その時には必ず、フレームワークを.NET8.0に指定しましょう。
プロジェクト名は「RazorReportDemo」とします。

3-2. .csproj ファイルの編集

作成した.csprojは以下の編集してください。

//Microsoft.NET.Sdk.Razorを追加
- <Project Sdk="Microsoft.NET.Sdk">
+ <Project Sdk="Microsoft.NET.Sdk;Microsoft.NET.Sdk.Razor">

  <PropertyGroup>
    <OutputType>WinExe</OutputType>
    <TargetFramework>net8.0-windows</TargetFramework>
    <UseWindowsForms>true</UseWindowsForms>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
  </PropertyGroup>

</Project>
  • Microsoft.NET.Sdk.Razor を追加すると、.razor ファイルを自動的にビルド対象にしてくれます。

3-3. 必要なNuGet パッケージ

  • Microsoft.AspNetCore.Components.Web (Version="8.0.0")
    HtmlRenderer(Razor→HTML 生成)を提供
  • Microsoft.Extensions.DependencyInjection (Version="8.0.0")
    DI(依存性注入)コンテナを提供
  • Microsoft.Extensions.Logging.Console (Version="8.0.0")
    ログをコンソール表示できるようにする
  • PuppeteerSharp (Version="20.1.3")
    HTML→PDF 変換

4. モデルクラスの準備

「帳票で表示したいデータ」をまとめるためのモデル(データ構造)を定義します。

ReportModels.cs
using System;
using System.Collections.Generic;

namespace RazorReportDemo
{
    /// <summary>
    /// 帳票全体をまとめたルートモデル
    /// </summary>
    public class ReportViewModel
    {
        public HeaderInfo Header { get; set; }
        public List<DetailItem> Details { get; set; }
        public FooterInfo Footer { get; set; }
    }

    /// <summary>
    /// ヘッダー(会社名、帳票名、日付など)
    /// </summary>
    public class HeaderInfo
    {
        public string CompanyName { get; set; }
        public DateTime ReportDate { get; set; }
        public string Title { get; set; }
    }

    /// <summary>
    /// 明細行(1レコード分)
    /// </summary>
    public class DetailItem
    {
        public int No { get; set; }
        public string ProductName { get; set; }
        public int Quantity { get; set; }
        public decimal UnitPrice { get; set; }

        // 金額は数量 × 単価で計算プロパティとして用意
        public decimal Amount => Quantity * UnitPrice;
    }

    /// <summary>
    /// フッター(合計金額、備考など)
    /// </summary>
    public class FooterInfo
    {
        public decimal TotalAmount { get; set; }
        public string Remarks { get; set; }
    }
}

5. Razor コンポーネントの作成

次に、HTML/CSS で帳票のレイアウトを定義する .razor ファイルを作ります。

ReportComponent.razor
@namespace RazorReportDemo
@using Microsoft.Extensions.Logging
@using Microsoft.AspNetCore.Components

@typeparam TModel

@inject ILogger<ReportComponent<TModel>> Logger

@code {
    // 依存先(親コード)から渡されるモデル
    [Parameter]
    public RazorReportDemo.ReportViewModel Model { get; set; }
}

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <style>
        body { font-family: "Yu Gothic", sans-serif; }
        table { border-collapse: collapse; width: 100%; }
        th, td { border: 1px solid #333; padding: 4px; text-align: left; }
        .header, .footer { margin-bottom: 20px; }
    </style>
</head>
<body>
    <div class="header">
        <h1>@Model.Header.CompanyName</h1>
        <h2>@Model.Header.Title</h2>
        <p>日付:@Model.Header.ReportDate.ToString("yyyy/MM/dd")</p>
    </div>

    <table>
        <thead>
            <tr>
                <th>No</th>
                <th>品名</th>
                <th>数量</th>
                <th>単価</th>
                <th>金額</th>
            </tr>
        </thead>
        <tbody>
            @foreach (var item in Model.Details)
            {
                <tr>
                    <td>@item.No</td>
                    <td>@item.ProductName</td>
                    <td>@item.Quantity</td>
                    <td>@item.UnitPrice.ToString("N0")</td>
                    <td>@item.Amount.ToString("N0")</td>
                </tr>
            }
        </tbody>
    </table>

    <div class="footer">
        <p>合計金額:@Model.Footer.TotalAmount.ToString("N0")</p>
        <p>備考:@Model.Footer.Remarks</p>
    </div>
</body>
</html>

  1. @namespace RazorReportDemo
    • 生成されるコンポーネントクラスが RazorReportDemo 名前空間に属します。
    • これにより、後述する Form1.cs 側から ReportComponent<ReportViewModel> として参照できるようになります。
  2. @typeparam TModel[Parameter] public ReportViewModel Model { get; set; }
    • @typeparam TModel:このコンポーネントが汎用的な「モデル型」を受け取れるようにします。
    • [Parameter] public ReportViewModel Model:呼び出し側で渡す具体的なモデル(ReportViewModel)を受け取るプロパティ。
  3. @inject ILogger<ReportComponent<TModel>> Logger
    • ロガーを注入するコード。コンポーネントのレンダリング中にログを出したい場合に利用します。
    • DI(依存性注入)を使って、あらかじめ用意しておいたロギング機能を取り出せます。

6. Windows Forms(Form1.cs)側の実装

続いて、Form1.cs のコードを編集し、Razor コンポーネントをレンダリングして PDF 化するロジックを記述します。

Form1.cs
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using System.Windows.Forms;
using Microsoft.AspNetCore.Components;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using PuppeteerSharp;

namespace RazorReportDemo
{
    public partial class Form1 : Form
    {
        // DI(依存性注入)用のサービスプロバイダー
        private readonly IServiceProvider _services;
        // ロガーを生成するためのファクトリー
        private readonly ILoggerFactory _loggerFactory;
        // PuppeteerSharp が一度だけダウンロードした Chrome を扱うためのフェッチャー
        private readonly BrowserFetcher _fetcher;

        public Form1()
        {
            InitializeComponent();

            //DI とロギングの設定
            var services = new ServiceCollection();
            //Console(標準出力)にログを出力するよう登録
            services.AddLogging(builder => builder.AddConsole());
            //サービスプロバイダーを構築
            _services = services.BuildServiceProvider();
            //ILoggerFactoryをDI から取得
            _loggerFactory = _services.GetRequiredService<ILoggerFactory>();

            //PuppeteerSharpのChromeダウンロードはここで一度だけ
            var fetcher = new BrowserFetcher();
            fetcher.DownloadAsync().GetAwaiter().GetResult();
            _fetcher = fetcher;
        }

        private async void button1_Click(object sender, EventArgs e)
        {
            //モデル生成
            var model = CreateSampleModel();

            //RazorコンポーネントをHTMLにレンダリング
            string html = await RenderReportHtmlAsync(model);

            //HTMLをPDFに変換して保存ダイアログを表示
            await ConvertHtmlToPdfAsync(html);
        }

        /// <summary>
        /// サンプル用のモデルを作成するメソッド
        /// </summary>
        private ReportViewModel CreateSampleModel()
        {
            var header = new HeaderInfo
            {
                CompanyName = "ABC商事",
                Title = "月次報告",
                ReportDate = DateTime.Today
            };

            var details = new List<DetailItem>();
            for (int i = 1; i <= 5; i++)
            {
                details.Add(new DetailItem
                {
                    No = i,
                    ProductName = $"製品{i}",
                    Quantity = i * 2,
                    UnitPrice = 1000 + i * 100
                });
            }

            var footer = new FooterInfo
            {
                TotalAmount = 0,
                Remarks = "特になし"
            };
            foreach (var item in details)
            {
                footer.TotalAmount += item.Amount;
            }

            return new ReportViewModel
            {
                Header = header,
                Details = details,
                Footer = footer
            };
        }

        /// <summary>
        /// RazorコンポーネントをHTML文字列にレンダリングするメソッド
        /// </summary>
        private async Task<string> RenderReportHtmlAsync(ReportViewModel model)
        {
            //HtmlRendererを生成(IDisposableなので await using)
            await using var renderer = new HtmlRenderer(_services, _loggerFactory);

            // レンダリングに渡すパラメーターを辞書形式で準備
            var parameters = ParameterView.FromDictionary(new Dictionary<string, object?>
            {
                ["Model"] = model
            });

            //Dispatcher(Blazorの擬似UIスレッド)上で実行する必要がある
            string html = await renderer.Dispatcher.InvokeAsync(async () =>
            {
                // Razorコンポーネントをレンダリング → HtmlRootComponent を取得
                var rootComponent = await renderer.RenderComponentAsync<ReportComponent<ReportViewModel>>(parameters);
                // HtmlRootComponentからHTML文字列を取得
                return rootComponent.ToHtmlString();
            });

            return html;
        }

        /// <summary>
        /// HTML文字列をPDF化し、ユーザーに保存先を選ばせるメソッド
        /// </summary>
        private async Task ConvertHtmlToPdfAsync(string html)
        {
            // HeadlessChromeを起動(先ほどフェッチした Chrome を使う)
            await using var browser = await Puppeteer.LaunchAsync(new LaunchOptions
            {
                Headless = true
            });

            // 新しいページを開き、HTMLを設定
            var page = await browser.NewPageAsync();
            await page.SetContentAsync(html);

            // PDFバイト配列を取得(デフォルト設定で A4 ページ相当の PDF が生成される)
            var pdfBytes = await page.PdfDataAsync();

            // 保存先をユーザーに選ばせるダイアログを表示
            using var sfd = new SaveFileDialog
            {
                Filter = "PDF ファイル (*.pdf)|*.pdf",
                FileName = $"report_{DateTime.Now:yyyyMMdd}.pdf"
            };

            if (sfd.ShowDialog() == DialogResult.OK)
            {
                // 選択されたパスにバイト配列として書き出す
                System.IO.File.WriteAllBytes(sfd.FileName, pdfBytes);
                MessageBox.Show($"PDF を出力しました:{sfd.FileName}", "完了", MessageBoxButtons.OK, MessageBoxIcon.Information);
            }
        }
    }
}

9. まとめ

  • .NET 8.0 + Windows Forms 環境でも、わずかなコード追加で Razor コンポーネントを利用できるようになりました。
  • Razor → HTML を行う部分は HtmlRendererMicrosoft.AspNetCore.Components.Web 名前空間)を使うだけなので、ASP.NET のサーバーは不要です。
  • 生成した HTML を PuppeteerSharp で PDF 化することで、Crystal Reports のような外部ランタイムを使わずに電子帳票を作成できます。
  • HTML/CSS ベースなので、帳票レイアウトの自由度が高く、CSS を使った細かなスタイル調整も容易です。

Discussion