Visual Basic で簡単レポート作成（２）複数ページのレポート

はじめに
---
この記事は、WPFの`FixedDocument`を使ったレポート作成について書いた別記事『[Visual Basic で簡単レポート作成](https://qiita.com/cyberCatvb/items/3db7aeea414424f6776b)』の続きです。（前回記事も是非見てください）
前回は単一ページの例でしたが、今回はページが複数になるレポートについて書いてみました。

行数が変化するレポート
---
例えば、行数が変化する請求書を考えてみよう。
1行が1つの商品取引を表す表形式の請求書だ。商売が繁盛していれば、必ずしも明細が1ページに収まるとは限らない。例えば、1ページに30行までしか表示できない場合、31行目以降、溢れた行数に応じてページを追加する必要がある。それを考慮したレポートの見た目は、こんな感じだ。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/294209/01d1c060-ed93-7911-d749-6448667e2976.png)
以下で、このような表形式のレポートを作ってみる。

データの準備
---
まずは、データを格納する`Shukka`クラスを作る。プロパティは敢えて日本語にした。

```VB:Shukka.vb
Public Class Shukka
    ' 単純化の為、ここではデータ構造は深く考えない
    Public Property 出荷日 As Date
    Public Property 品名 As String
    Public Property 数量 As Double
    Public Property 単価 As Double
    Public Property 金額 As Double
End Class
```
表示データは、起動ごとに行数や表示内容がランダムに変化するようにしている。
出荷日は今日の200日前から開始して1行1日ずつ日にちを加え、品名は単純に連番を付与、数量と単価は起動ごとに数字がランダムに変化する。

```VB:MainWindowViewModel.vb
Private Function 出荷データ作成() As List(Of Shukka)
    Dim shukkas As New List(Of Shukka)
    Dim rand As New Random
    Dim count = rand.Next(10, 100) ' 行数をランダムに変化させる
    For i = 1 To count
        shukkas.Add(New Shukka With {
                .出荷日 = Now.AddDays(i - 200),
                .品名 = "商品" & i,
                .数量 = rand.Next(10, 100) * 100,
                .単価 = rand.Next(10, 100) * 10,
                .金額 = Math.Ceiling(.数量 * .単価)
            }
        )
    Next
    Return shukkas
End Function
```

DataGridで簡単な一覧表を作る
---
WPFで一覧表を作成するには`DataGrid`が最も適している。しかし、`DataGrid`のデフォルトではタイトル行が立体的になっていて帳票に相応しい見た目では無い。だが、それも設定次第でそれらしくなる。タイトル行の`Background`を白く塗ることで以下のような単純な表に変わってくれる。

:::note info
**DataGrid** の親は **Grid** から **Canvas** に変えておく。
:::
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/294209/6c91dfd3-e1bf-b447-44ba-73567460dcdd.png)
その他にも細かく設定が必要だが、各種設定値を自分で書き換えて、見え方がどうなるか試してみると良い。勘所がわかれば、後はXAMLをコピーして再利用できる。
以下は事務的で飾り気のない、表形式の帳票を作表している。

```XML:AttachUserControl.xaml
<UserControl x:Class="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>
```

これをデザインビューで見ると以下のように見える。
値が未設定なのでタイトル行と、ヘッダーのページ数とページ総数を区切る「OF」しか見えない。
![pagination2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/294209/2489e369-df53-e60d-1bb7-e196536e6b07.png)

Yieldでページを吐き出す
---
ページ作りでは、このXAMLに何を注ぎ込むかを考える。
ページに必要な値は何か。それは、**表示するデータそのもの**、**ページ番号**、**ページ総数**、の３つである。それを表現したのが以下の`AttachViewModel`クラスである。

```VB:AttachViewModel.vb
Public Class AttachViewModel
    Public Property Shukkas As List(Of Shukka)
    Public Property CurrentPage As Integer
    Public Property TotalPage As Integer
End Class
```
この`AttachViewModel`クラスの3つのメンバーが、XAMLの3つの`Binding`に注ぎ込まれる。

```XML:AttachUserControl.xaml
-- 引用① --
<TextBlock Text="{Binding CurrentPage}" .../>
<TextBlock Text=" OF " .../>
<TextBlock Text="{Binding TotalPage}" .../>
-- 引用② --
<DataGrid ItemsSource="{Binding Shukkas}" .../>
```
`DataGrid`内では、さらに各列と`Shukka`クラスの各メンバとを`Binding`して、はめ込んでいる。

以下は、このクラスを使ってページを作っていくメソッドである。
まずは、一旦表示する全てのデータを`shukkas`変数に代入する。次に1ページに何行表示するかを決める。ここでは30行にした。`shukkas`変数の行数を求めれば、何ページ必要になるかも求められる。
そして`For`ループ内の`Yield`で1ページずつページを吐き出す。

```VB:MainWindowViewModel.vb
Iterator Function GenerateUserControls() As IEnumerable(Of UserControl)
    Dim shukkas = 出荷データ作成()  ' 全データを取得
    Dim rowSize = 30             ' 1ページを30行に設定
    Dim rowCount = shukkas.Count ' 全データの行数を取得
    Dim totalPage = Math.Ceiling(rowCount / rowSize)
    Dim startRow = 0
    For currentPage = 1 To totalPage
        If startRow + rowSize > rowCount - 1 Then rowSize = rowCount - startRow
        ' 1ページずつ値を設定して、ページを吐き出す
        Yield New AttachUserControl With {
            .DataContext = New AttachViewModel With {
                .CurrentPage = currentPage,
                .TotalPage = totalPage,
                .Shukkas = shukkas.GetRange(startRow, rowSize)
            }
        }
        startRow += rowSize
    Next
End Function
```
`Yield`で吐き出す`AttachUserControl.xaml`の`DataContext`プロパティに、値を設定した`AttachViewModel`クラスを代入している。この代入により`Binding`にデータが注ぎ込まれるのだ。

以下は、上記の`GenerateUserControls`メソッドを`For Each`で回している。`GenerateUserControls`メソッド内で`Yield`されるたびに制御が外側の`For Each`に移り`report`変数が生成される。そしてこの`report`変数を`FixedDocument`に格納していく。一つ格納が終わると再び`GenerateUserControls`メソッド内に制御が戻る。そしてまた`Yield`されるたびに外に制御が移る。これの繰り返し。

```VB:MainWindowViewModel.vb
Public Property ReportViewer As FixedDocument

Public Sub New()
    Dim doc = New FixedDocument
    For Each report In GenerateUserControls()
        Dim page = New FixedPage With {.Width = 793.7, .Height = 1122.52}
        Dim pageContent = New PageContent
        page.Children.Add(report)
        pageContent.Child = page
        doc.Pages.Add(pageContent)
    Next
    ReportViewer = doc
End Sub
```
これは、`Return`とは違った挙動をする`Yield`の特徴を活かしたものである。
`Yield`とは「産む」という意味で、こうやって見ると`GenerateUserControls`メソッドが、`Yield`を使ってレポートを産み出している、正にレポートの生産工場のようにイメージできるだろう。

色々やってみる
---
`MainWindow.xaml`は前回記事『[Visual Basic で簡単レポート作成](https://qiita.com/cyberCatvb/items/3db7aeea414424f6776b)』と変わらない。

```XML:MainWindow.xaml
<Window x:Class="MainWindow"
        -- 省略 --
        Title="MainWindow" Height="600" Width="600">
    <Window.DataContext>
        <local:MainWindowViewModel/>
    </Window.DataContext>
    <Grid>
        <DocumentViewer Document="{Binding ReportViewer}"/>
    </Grid>
</Window>
```
例えば、表紙は表紙で別にあって、今回のような表形式のアタッチがそれに続く構成の場合、表紙用のユーザーコントロールとアタッチ用のユーザーコントロールを別々に用意して、`GenerateUserControls`メソッドを以下のように書き換えれば良い。
```VB:MainWindowViewModel.vb
' 単純化して書いてます
Iterator Function GenerateUserControls() As IEnumerable(Of UserControl)

    ' 表紙用のユーザーコントロール。別途、値設定用の CoverViewModel を用意する必要がある
    Yield New CoverUserControl With {.DataContext = CoverViewModel}

    ' アタッチ用のユーザーコントロール。ページ数に応じて、1ページずつ生成
    For p = 1 To pageCount
        Yield New AttachUserControl With {.DataContext = AttachViewModel} 
    Next

End Function
```
メソッド内に`Yield`をいくつ書いても良い。上の例では表紙とアタッチの生成のために二つの`Yield`を書いた。
戻り値は`IEnumerable(Of UserControl)`なので、`UserControl`クラスの派生型であれば、何でも`Yield`できる。

さらに、このロジック自体をループさせれば、複数の請求書を連続してプレビューすることも可能だろう。
例えば、表紙には取引内容を10行まで納められるが、10行を超えるときだけアタッチを作成させたり、複数のデータグループの各々についてアタッチの要否を判定してデータグループごとに動的にページ数を変化させたり、もできる。

簡単・便利な**WPF & XAML**によるレポート作成。複雑な構造のレポートも手軽に作れて、本当に便利だぜっ:laughing: