WPFで簡単レポート作成

<おことわり>
---

* @FKbelm さんの書かれた『[XAML内でDataContextのプロパティにIntelliSenseが効くようにする](https://qiita.com/FKbelm/items/10f9276e217b5bf83cb0)』を適用して加筆修正しました。@FKbelm さん、ありがとうございます。
* MainWindow.xamlについて重要な書き換えをしました。詳しくは『[編集履歴](https://qiita.com/cyberCatvb/items/1bf86e75b8e5738d432f/revisions)』をご参照ください。

はじめに
---
この記事は、[Visual Basic Advent Calendar 2021](https://qiita.com/advent-calendar/2021/vb)に投稿した『[Visual Basic で簡単レポート作成](https://qiita.com/cyberCatvb/items/3db7aeea414424f6776b)』のC#版です。若干加筆修正しています。

また、以下の続編もありますので、あわせてご覧ください。
『[WPFで簡単レポート作成（２）複数ページのレポート](https://qiita.com/cyberCatvb/items/9d128e12dd6b7ea31eef)』
『[WPFで簡単レポート作成（３）ページ指定印刷](https://qiita.com/cyberCatvb/items/96ce501b11b3f96145e4)』

帳票は無くならない
---
請求書、依頼書、出荷明細書など、企業で使う帳票は、ペーパーレスが徐々に浸透しつつある今でも無くなる気配がない。ペーパーレスになったところで、見たいものを簡潔にまとめ上げるレイアウト情報としての帳票は、まだまだ存在し続けるだろう。
そんな帳票を作るのに、AccessのレポートやRDLCなどを使っている人も多いと思うが、

**WPF + C#** でやってみたら意外に楽しかった:laughing:
FixedDocumentを使う
---
WPFでレポート作成するのなら、FixedDocumentクラスがお勧めだ。
レポートの大枠が、以下のようにでき上がっている。
![大枠.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/294209/c94e3d5e-0689-f7ef-f022-1a0446cc0495.png)
この枠の中に、Xamlで帳票を組み立てていく。

Xamlで帳票を組み立てる
---
せっかくなので、最新のVisual Studio 2022を使い、.NET6で作ってみよう。
まずはWPFプロジェクトを作成する。
![CS_WPFプロジェクト作成.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/294209/d7ef1a60-d407-0186-d151-bd88aaf5a357.png)
次にUserControlを追加。
![CS_UserControl追加.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/294209/2982a0be-7387-2771-a187-50fa076f9f43.png)
追加したUserControlにXamlをゴリゴリ書いていく。Xamlデザイナーに慣れていない人は、デザインビューでの編集はしないことだ。思い通りにコントロールを配置できず、イライラが増して嫌になる。デザインビューは仕上がりを視覚的に確認するものと割り切った方がいい。
XamlビューでXamlのテキストを直接書き込んでいく。書くと言っても大部分はコピぺで済む。
まずは`Grid`を`Canvas`に変える。`Canvas`は、レポート内の文字や罫線が固定されるので帳票作成に向いている。`Canvas`内はセンチメートルでサイズを書ける。
以下、単純な帳票を書いてみた。

```xml:ShukkaUserControl.xaml
<UserControl x:Class="ShukkaReport.ShukkaUserControl"
    -- 省略 --
    DataContext="{StaticResource ShukkaVM}"
    Height="1122.51" Width="793.7">
    <UserControl.Resources>
        <Style x:Key="Line1" TargetType="Line">
            <Setter Property="StrokeThickness" Value="1"/>
            <Setter Property="Stroke" Value="Black"/>
        </Style>
    </UserControl.Resources>
    <Canvas Background="White">
        <!--横線-->
        <Line X1="6cm" X2="15cm" Y1="3.5cm" Y2="3.5cm" Style="{StaticResource Line1}"/>
        <Line X1="1cm" X2="20cm" Y1="5cm" Y2="5cm" Style="{StaticResource Line1}"/>
        <Line X1="1cm" X2="20cm" Y1="7cm" Y2="7cm" Style="{StaticResource Line1}"/>
        <Line X1="1cm" X2="20cm" Y1="15cm" Y2="15cm" Style="{StaticResource Line1}"/>
        <Line X1="1cm" X2="20cm" Y1="17cm" Y2="17cm" Style="{StaticResource Line1}"/>
        <Line X1="1cm" X2="20cm" Y1="19cm" Y2="19cm" Style="{StaticResource Line1}"/>
        <!--縦線-->
        <Line X1="1cm" X2="1cm" Y1="5cm" Y2="19cm" Style="{StaticResource Line1}"/>
        <Line X1="10.5cm" X2="10.5cm" Y1="7cm" Y2="19cm" Style="{StaticResource Line1}"/>
        <Line X1="20cm" X2="20cm" Y1="5cm" Y2="19cm" Style="{StaticResource Line1}"/>
        <!--文字列-->
        <TextBlock Text="出 荷 指 図 書" Margin="6.65cm,2cm" Width="8cm" Height="1.5cm" FontSize="46"/>
        <TextBlock Text="出荷指図№" Margin="1.25cm,5.25cm" Height="1.5cm" FontSize="36" FontWeight="Bold"/>
        <TextBlock Text="納品先" Margin="1.25cm,7.25cm" Height="1cm" FontSize="26"/>
        <TextBlock Text="住所" Margin="10.75cm,7.25cm" Height="1cm" FontSize="26"/>
        <TextBlock Text="出荷日" Margin="1.25cm,15.25cm" Height="1cm" FontSize="26"/>
        <TextBlock Text="納品日" Margin="1.25cm,17.25cm" Height="1cm" FontSize="26"/>
        <TextBlock Text="個数" Margin="10.75cm,15.25cm" Height="1cm" FontSize="26"/>
        <TextBlock Text="運送便" Margin="10.75cm,17.25cm" Height="1cm" FontSize="26"/>
        <!--設定値-->
        <TextBlock Text="{Binding 出荷指図NO}" Margin="7cm,5.25cm" Height="1.5cm" Width="10cm" FontSize="36"/>
        <TextBlock Text="{Binding 納品先}" Margin="1.25cm,9cm" Height="5cm" FontSize="20"/>
        <TextBlock Text="{Binding 住所}" Margin="10.75cm,9cm" Height="5cm" FontSize="20"/>
        <TextBlock Text="{Binding 出荷日}" Margin="4cm,15.5cm" Height="1cm" FontSize="20"/>
        <TextBlock Text="{Binding 納品日}" Margin="4cm,17.5cm" Height="1cm" FontSize="20"/>
        <TextBlock Text="{Binding 個数}" Margin="13cm,15.5cm" Height="1cm" FontSize="20"/>
        <TextBlock Text="{Binding 運送便}" Margin="14cm,17.5cm" Height="1cm" FontSize="20"/>
    </Canvas>
</UserControl>
```

:::note info
クラス名に名前空間が付加されるのは、VBと異なる。
:::
ページサイズはA4にしている。ここはピクセルでの設定となる。
![出荷指図書.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/294209/28e14156-e67f-f5cb-1fb3-316a7c1ff934.png)
このレベルならAccessのレポートで十分作れるのだが、Xamlなら更に複雑なことができるのは想像がつくだろう。
コントロールをマウスでドラッグして線が斜めになったとか、部分的に表示位置をずらしたくなったとか、Accessレポートなどではうんざりする修正作業も、Xamlならストレス無く、各コントロールの座標数字を書き換えるだけで済む。

ここで、3行目の、
>DataContext="{StaticResource ShukkaVM}

は、本来ならViewModelを作成してから書く。（:warning:これを書かない方法を後述しています）


ViewModelの作成
---
以下は追加したユーザーコントロールのViewModelである。コードビハインドは一切汚さない。ふざけたコードだが、わかりやすさ重視だ:grin:

```c#:ShukkaViewModel.cs
namespace ShukkaReport.ViewModels
{
    public class ShukkaViewModel
    {
        public string 出荷指図NO { get; }
        public string 納品先 { get; }
        public string 住所 { get; }
        public string 出荷日 { get; }
        public string 納品日 { get; }
        public string 個数 { get; }
        public string 運送便 { get; }
        public ShukkaViewModel()
        {
            this.出荷指図NO = "SK20211224-001";
            this.納品先 = "株式会社ほげ商事";
            this.住所 = "東京都中央区〇〇町〇丁目-〇-〇\nレポートビル１階";
            this.出荷日 = "12月24日";
            this.納品日 = "12月25日";
            this.個数 = "10個";
            this.運送便 = "佐山運輸";
        }
    }
}
```
以下はウインドのViewModelである。こちらもコードビハインドは一切汚さない。

```c#:MainWindowViewModel.cs
using System.Windows.Documents;

namespace ShukkaReport.ViewModels
{
    public class MainWindowViewModel
    {
        public FixedDocument ReportViewer { get; }
        public MainWindowViewModel()
        {
            var report = new ShukkaUserControl();
            var page = new FixedPage() { Width = 793.7, Height = 1122.51 };
            var pageContent = new PageContent();
            var doc = new FixedDocument();
            page.Children.Add(report);
            pageContent.Child = page;
            doc.Pages.Add(pageContent);
            ReportViewer = doc;
        }
    }
}
```
ここでFixedDocumentをこしらえて`ReportViewer`プロパティにセットしている。
今回は単一ページだが、複数ページも実装可能。これを見れば自力で作れると思う。
2ページ目以降は書式が変わってアタッチシート、なんてこともできるだろう。
FixedDocumentの構造は、

親）FixedDocument
子）PageContent
孫）FixedPage

で、孫のFixedPageの子供としてUserControlを`Children.Add`する。

App.xamlの編集（:warning:これを書かない方法を後述）
---
アプリで二つのViewModelを利用できるようにリソースとして登録する。

```xml:App.xaml
<Application x:Class="ShukkaReport.App"
    -- 省略 --
    xmlns:local="clr-namespace:ShukkaReport.ViewModels"
    StartupUri="MainWindow.xaml">
    <Application.Resources>
        <local:MainWindowViewModel x:Key="MainVM"/>
        <local:ShukkaViewModel x:Key="ShukkaVM"/>
    </Application.Resources>
</Application>
```

ここの`x:Key`は自分の好みで名付ける。ウインドウとユーザーコントロールのDataContextに、これらの名前を各々設定する。

:::note info
インテリセンスで各ViewModelが現れない場合は、xmlns:local の記載を各ViewModelのCSファイルの名前空間と一致させる。
:::
MainWindow.xamlの編集
---

```xml:MainWindow.xaml
<Window x:Class="ShukkaReport.MainWindow"
    -- 省略 --
    DataContext="{StaticResource MainVM}"
    Title="MainWindow" Height="600" Width="600">
    <Grid>
        <DocumentViewer Document="{Binding ReportViewer}"/>
    </Grid>
</Window>
```
:::note info
クラス名に名前空間が付加されるのは、VBと異なる。
:::
3行目で`DataContext`にApp.xamlで設定した MainWindowViewModel のリソースをセットしている。（:warning:これを書かない方法を次節に示しています）
また、`Grid`内で`DocumentViewer`の`Document`に`ReportViewer`をバインドしている。
:warning:App.xaml に書かない方法
---
@FKbelm さんの書かれた『[XAML内でDataContextのプロパティにIntelliSenseが効くようにする](https://qiita.com/FKbelm/items/10f9276e217b5bf83cb0)』の前半部分を適用すると、App.xaml には何も書かずに済みます。これは見習いたい。@FKbelm さん、ありがとうございます。 

```xml:MainWindow.xaml
<Window x:Class="MainWindow"
    -- 省略 -- >
    <Window.DataContext>
        <local:MainWindowViewModel/>
    </Window.DataContext>
```

```xml:ShukkaUserControl.xaml
<UserControl x:Class="ShukkaUserControl"
    -- 省略 -- >
    <UserControl.DataContext>
        <local:ShukkaViewModel/>
    </UserControl.DataContext>
```
そして、
>DataContext="{StaticResource MainVM}"

と

>DataContext="{StaticResource ShukkaVM}"

は消します。


実行結果
---
![実行結果.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/294209/c27730c0-6fc0-5a6f-9784-df25ed7503af.png)
印刷や拡大縮小などの機能が既に組み込まれていて、それらを実装する必要はない。あー何て楽なんだろう。UXの時代に古臭い見た目だが、そんなことは気にしない:laughing:
（ボタン・アイコンの変更は、『[WPFで簡単レポート作成（３）ページ指定印刷](https://qiita.com/cyberCatvb/items/96ce501b11b3f96145e4)』をご参照ください:point_left:）

**WPF**による**デスクトップ・アプリ**は、まだまだ事務処理系の現場で使い倒せるのだ:bangbang: