はじめに
この記事は、WPFのFixedDocumentのDocumentViewerを使ったレポート作成について書いた別記事『WPFで簡単レポート作成』の続きです。(前回記事も是非見てください)
前回は単一ページの例でしたが、今回はページが複数になるレポートの例を書いてみました。
行数が変化するレポート
例えば、行数が変化する請求書を考えてみよう。
1行が1つの商品取引を表す表形式の請求書だ。商売が繁盛していれば、必ずしも明細が1ページに収まるとは限らない。例えば、1ページに30行までしか表示できない場合、31行目以降、溢れた行数に応じてページを追加する必要がある。それを考慮したレポートの見た目は、こんな感じだ。

以下で、画像のような表形式のレポートを作ってみる。
データの準備
まずは、データを格納するShukkaクラスを作る。プロパティは敢えて日本語にした。
internal class Shukka
{
    public DateTime 出荷日 { get; set; }
    public string? 品名 { get; set; }
    public decimal 数量 { get; set; }
    public decimal 単価 { get; set; }
    public decimal 金額
    {
        get { return Math.Ceiling(数量 * 単価); }
    }
}
表示データは、起動ごとに行数や表示内容がランダムに変化するようにしている。
出荷日は今日の200日前から開始して1行1日ずつ日にちを加え、品名は単純に連番を付与、数量と単価は起動ごとに数字がランダムに変化する。
private List<Shukka> 商品出荷データ作成()
{
    var shukkas = new List<Shukka>();
    var rand = new Random();
    var count = rand.Next(10, 100); // 行数をランダムに変化させる
    for (int i = 1; i < count; i++)
    {
        shukkas.Add(new Shukka()
        {
            出荷日 = DateTime.Now.AddDays(i - 200), // 200日前から開始
            品名 = "商品" + i,               // 連番を付与
            数量= rand.Next(10, 100) * 100, // 数字をランダムに変化させる
            単価 = rand.Next(10, 100) * 10, // 数字をランダムに変化させる
        });
    }
    return shukkas;
}
DataGridで簡単な一覧表を作る
WPFで一覧表を作成するにはDataGridが最も適している。しかし、DataGridのデフォルトではタイトル行が立体的になっていて帳票に相応しい見た目では無い。だが、それも設定次第でそれらしくなる。タイトル行のBackgroundを白く塗ることで以下のような単純な表に変わってくれる。
【必須】DataGridの親はGridからCanvasに変えること

その他にも細かく設定が必要だが、各種設定値を自分で書き換えて、見え方がどうなるか試してみると良い。勘所がわかれば、後はXAMLをコピーして再利用できるので、さほど苦労を感じない。
以下は事務的で飾り気のない、表形式の帳票を作表している。
<UserControl x:Class="MultiPageReport.UserControls.AttachUserControl"
             -- 省略 --
             Height = "1122.52" Width = "793.7">
    <UserControl.Resources>
        <Style x:Key="Header1" TargetType="DataGridColumnHeader">
            <Setter Property="Background" Value="White"/>
            <Setter Property="BorderBrush" Value="Black"/>
            <Setter Property="BorderThickness" Value="0,0,1,1"/>
            <Setter Property="HorizontalContentAlignment" Value="Center"/>
            <Setter Property="Height" Value="0.76cm"/>
        </Style>
        <Style x:Key="Text1" TargetType="TextBlock">
            <Setter Property="VerticalAlignment" Value="Center"/>
            <Setter Property="HorizontalAlignment" Value="Left"/>
        </Style>
        <Style x:Key="Text2" TargetType="TextBlock">
            <Setter Property="VerticalAlignment" Value="Center"/>
            <Setter Property="HorizontalAlignment" Value="Right"/>
        </Style>
    </UserControl.Resources>
    <Canvas Background="White">
        <StackPanel Orientation="Horizontal" Margin="17cm,1cm">
            <TextBlock Text="{Binding CurrentPage}" FontFamily="MS Mincho"/>
            <TextBlock Text=" OF " FontFamily="MS Mincho"/>
            <TextBlock Text="{Binding TotalPage}" FontFamily="MS Mincho"/>
        </StackPanel>
        <DataGrid ItemsSource="{Binding Shukkas}" RowHeight="30" AutoGenerateColumns="False"
                  RowHeaderWidth="0" BorderBrush="Black" BorderThickness="1,1,0,0" CanUserAddRows="False"
                  VerticalContentAlignment="Center" HorizontalContentAlignment="Center" FontFamily="MS Mincho"
                  IsHitTestVisible="False" Margin="2cm,2cm" Width="17cm" ColumnHeaderStyle="{StaticResource Header1}">
            <DataGrid.Columns>
                <DataGridTextColumn Binding="{Binding 出荷日,StringFormat=yyyy/MM/dd}"
                                    Header="出荷日" Width="*" 
                                    ElementStyle="{StaticResource Text1}"/>
                <DataGridTextColumn Binding="{Binding 品名}" Header="品名" Width="*" 
                                    ElementStyle="{StaticResource Text1}"/>
                <DataGridTextColumn Binding="{Binding 数量,StringFormat={}{0:N0}}" 
                                    Header="数量" Width="*" 
                                    ElementStyle="{StaticResource Text2}"/>
                <DataGridTextColumn Binding="{Binding 単価,StringFormat=C,ConverterCulture=ja-JP}" 
                                    Header="単価" Width="*" 
                                    ElementStyle="{StaticResource Text2}"/>
                <DataGridTextColumn Binding="{Binding 金額,StringFormat=C,ConverterCulture=ja-JP}" 
                                    Header="金額" Width="*" 
                                    ElementStyle="{StaticResource Text2}"/>
            </DataGrid.Columns>
        </DataGrid>
    </Canvas>
</UserControl>
これをデザインビューで見ると以下のように映る。

ページヘッダー用のTextBlockを3つ置いて、ページ番号と総ページ数も表示するが、値が未設定なので、タイトル行だけしかみえない。
yield returnでページを吐き出す
ページ作りでは、前節のXAMLに何を注ぎ込むかを考える。
ページに必要な値は何か。それは、表示するデータそのもの、ページ番号、ページ総数、の3つである。それを表現したのが以下のAttachViewModelクラスである。
internal class AttachViewModel
{
    public List<Shukka> Shukkas { get; set; }
    public int CurrentPage { get; set; }
    public int TotalPage { get; set; }
}
以下は、このクラスを使ってページを作っていくメソッドである。
まずは、一旦表示する全てのデータをshukkas変数に代入する。次に1ページに何行表示するかを決める。ここでは30行にした。shukkasの行数を求めれば、何ページ必要になるかも求められる。
そしてforループ内のyield returnで1ページずつページを吐き出す。
private IEnumerable<UserControl> GenerateUserControls()
{
    var shukkas = 商品出荷データ作成(); // 全データを取得
    int rowSize = 30;               // 1ページ30行に設定
    int rowCount = shukkas.Count;   // 全データの行数を取得 
    int totalPage = (int)Math.Ceiling((decimal)rowCount / (decimal)rowSize); // 総ページ数を求める
    int startRow = 0;
    for (int currentPage = 1; currentPage <= totalPage; currentPage++)
    {
        if (startRow + rowSize > rowCount - 1) { rowSize = rowCount - startRow; }
        yield return new AttachUserControl() // データを設定しつつ、ページを吐き出す
        {
            DataContext = new AttachViewModel()
            {
                CurrentPage = currentPage,
                TotalPage = totalPage,
                Shukkas = shukkas.GetRange(startRow, rowSize), // 表示するデータ行を決める
            }
        };
        startRow += rowSize;
    }
}
以下は、上記のGenerateUserControlsメソッドをforeachで回している。GenerateUserControlsメソッド内でyield returnされるたびに制御が外側のforeachに移りreport変数が生成される。そしてこのreport変数をFixedDocumentに格納していく。一つ格納が終わると再びGenerateUserControlsメソッド内に制御が戻る。そしてまたyield  returnされるたびに外に制御が移る。これの繰り返し。
public FixedDocument ReportViewer { get; }
public MainWindowViewModel()
{
    var doc = new FixedDocument();
    // GenerateUserControlsメソッド内で yield return されるたびに report が生成され、foreachに制御が戻る
    foreach (UserControl report in GenerateUserControls())
    {
        var page = new FixedPage() { Height = 1122.52, Width = 793.7 };
        var pageContent = new PageContent();
        page.Children.Add(report);
        pageContent.Child = page;
        doc.Pages.Add(pageContent);
    }
    ReportViewer = doc;
}
こうやって見るとGenerateUserControlsメソッドが、レポートの生産工場のようにイメージできるだろう。
色々やってみる
MainWindow.xamlは前回記事『WPFで簡単レポート作成』と変わらない。
<!-- x:Class の名前空間は前回と変わってます -->
<Window x:Class="MultiPageReport.MainWindow"
        -- 省略 --
        Title="MainWindow" Height="600" Width="600">
    <Window.DataContext>
        <local:MainWindowViewModel/>
    </Window.DataContext>
    <Grid>
        <DocumentViewer Document="{Binding ReportViewer}"/>
    </Grid>
</Window>
表紙があってアタッチがそれに続く構造の請求書の場合、表紙用のユーザーコントロールとアタッチ用のユーザーコントロールを別々に用意して、GenerateUserControlsメソッドを以下のように書き換えれば良い。
private IEnumerable<UserControl> GenerateUserControls()
{
    //
    // 表紙用のユーザーコントロール。別途、値設定用の CoverViewModel を用意する必要がある
    //
    yield return new CoverUserControl() { DataContext = CoverViewModel, };
    //
    // アタッチ用のユーザーコントロール。ページ数に応じて、1ページずつ生成
    //
    for (int currentPage = 1; currentPage <= totalPage; currentPage++)
    {
        yield return new AttachUserControl() { DataContext = AttachViewModel, }; 
    }
}
戻り値はIEnumerable<UserControl>なので、UserControlクラスから派生していれば、何でもyield returnできる。
さらに、このロジック自体をループさせれば、可変ページアタッチを伴った複数の請求書を連続してプレビューすることも可能だろう。色々試せて、腕が鳴ると言うところだ。
簡単・便利なWPF & XAMLによるレポート作成。複雑な構造のレポートも手軽に作れて、本当に楽しいぜっ
