はじめに
この記事は、WPFのFixedDocument
を使ったレポート作成について書いた別記事『Visual Basic で簡単レポート作成』の続きです。(前回記事も是非見てください)
前回は単一ページの例でしたが、今回はページが複数になるレポートについて書いてみました。
行数が変化するレポート
例えば、行数が変化する請求書を考えてみよう。
1行が1つの商品取引を表す表形式の請求書だ。商売が繁盛していれば、必ずしも明細が1ページに収まるとは限らない。例えば、1ページに30行までしか表示できない場合、31行目以降、溢れた行数に応じてページを追加する必要がある。それを考慮したレポートの見た目は、こんな感じだ。
以下で、このような表形式のレポートを作ってみる。
データの準備
まずは、データを格納するShukka
クラスを作る。プロパティは敢えて日本語にした。
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日ずつ日にちを加え、品名は単純に連番を付与、数量と単価は起動ごとに数字がランダムに変化する。
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
を白く塗ることで以下のような単純な表に変わってくれる。
DataGrid の親は Grid から Canvas に変えておく。
その他にも細かく設定が必要だが、各種設定値を自分で書き換えて、見え方がどうなるか試してみると良い。勘所がわかれば、後は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」しか見えない。
Yieldでページを吐き出す
ページ作りでは、このXAMLに何を注ぎ込むかを考える。
ページに必要な値は何か。それは、表示するデータそのもの、ページ番号、ページ総数、の3つである。それを表現したのが以下のAttachViewModel
クラスである。
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
に注ぎ込まれる。
-- 引用① --
<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ページずつページを吐き出す。
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
されるたびに外に制御が移る。これの繰り返し。
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 で簡単レポート作成』と変わらない。
<Window x:Class="MainWindow"
-- 省略 --
Title="MainWindow" Height="600" Width="600">
<Window.DataContext>
<local:MainWindowViewModel/>
</Window.DataContext>
<Grid>
<DocumentViewer Document="{Binding ReportViewer}"/>
</Grid>
</Window>
例えば、表紙は表紙で別にあって、今回のような表形式のアタッチがそれに続く構成の場合、表紙用のユーザーコントロールとアタッチ用のユーザーコントロールを別々に用意して、GenerateUserControls
メソッドを以下のように書き換えれば良い。
' 単純化して書いてます
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によるレポート作成。複雑な構造のレポートも手軽に作れて、本当に便利だぜっ