ドッキングウィンドウで情報過多なアプリを作る~AvalonDock~

# はじめに
プライベートで家計簿アプリを作ろうとしたときに、マルチービュー画面にしたく、ドッキングウィンドウライブラリのAvalonDockを利用しました。
せっかくなので解説をしてみようと思います。

# AvalonDockとは
[リポジトリはこちら](https://github.com/Dirkster99/AvalonDock)

WPF向けのライブラリでNuGetからインストールすることが可能です。
Githubには以下のように書かれています。

>AvalonDock is a WPF Document and Tool Window layout container that is used to arrange documents and tool windows in similar ways than many well known IDEs, such as, Eclipse, Visual Studio, PhotoShop and so forth.

Eclipse、Visual Studio、PhotoShop などのIDE同じようにドキュメントとツールウィンドウを配置するためのレイアウトコンテナです、という感じです。

実際に触った方がわかりやすいと思いますのでサンプルとして、はじめにで触れた家計簿アプリのコードを置いておきます。

https://github.com/suuuum/KAKEIBO

:::note info
以下の環境を想定しています
* VisualStudio2022
* .NET6
:::

ビルドして起動すると、以下の画像のような画面が立ち上がります。
![app_image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3799996/b4f91bb0-5200-3014-e9b4-f6d1e69e2b19.png)


こんな感じで、一つのアプリの上に複数のビューを配置することができます。
また、これらのビューは大きさや位置を自由に変更することが可能です。

:::note info
アプリケーションの簡単な使い方は以下です。
* webブラウザからクレジットカードの明細csvファイルの取得
* CsvImportビューからデータをインポート
* PaymentビューやTrendビューで支払い状況をグラフ化

明細csvファイルはサンプルとしてリポジトリにsample.csvを置いています。
:::

# AvalonDockのビュー：ドキュメントウィンドウとツールウィンドウ
AvalonDockではドキュメントウィンドウとツールウィンドウの二種類のウィンドウがあります。

それぞれどのような画面かはVisualStudio等を見てみるとわかりやすいかと思います。
![window_image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3799996/cb363d4b-24a9-eb3b-9656-b21c28d6d7da.png)


ドキュメントウィンドウは編集の対象になるような、メインとなるコンテンツを表示するウィンドウです。
私の家計簿アプリではすべてドキュメントウィンドウで実装をしています。

ツールウィンドウの方は、補助情報を表示するようなサブウィンドウに当たります。
VisualStudioではログ表示やエクスプローラーなどはツールウィンドウになっていますね。
また、ツールウィンドウの方は画面の左右にピン止めして非表示にすることも可能です。
画像だとツールボックスや診断ツールがピン止め非表示になっていますね。

ドキュメントウィンドウ、ツールウィンドウともにフローティングをさせることができます。

次の章でも触れますが、ドキュメントウィンドウは`LayoutDocument`,ツールウィンドウは`LayoutAnchorable`として定義されています。

# AvalonDockの基本的な構造
サンプルの家計簿アプリの画像のように、一つの画面に三つのウィンドウを乗っけるときの構造は次のようになっています。

![layout_image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3799996/371c324d-d858-ef07-1058-f58c7285d148.png)


ウィンドウの中身として、`LayoutDocument`のみを書いていますが、ツールウィンドウ表示のために`LayoutAnchorablePane`,`LayoutAnchorable`もLayoutPanel配下に置くことができます。

また、LayoutDocument(LayuotAnchorable)はLayoutDocumentPane(LayoutAnchorablePane)配下に複数置くことができ、複数タブ表示させることが可能です。

コードではMainWindow.xamlの部分で書いています。
```xml
<Window x:Class="KAKEIBO.Views.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:prism="http://prismlibrary.com/"
        xmlns:avalonDock="https://github.com/Dirkster99/AvalonDock"
        xmlns:local="clr-namespace:KAKEIBO.Views"
        prism:ViewModelLocator.AutoWireViewModel="True"
        Title="MainWindow" Height="900" Width="1600">
    <DockPanel>
        <Menu DockPanel.Dock="Top" Background="#001016" Foreground="White" >
            <MenuItem Header="CSV Import" Command="{Binding OpenCsvImportCommand}"/>
            <MenuItem Header="Payment Viewer" Command="{Binding OpenPaymentViewerCommand}"/>
            <MenuItem Header="Trend" Command="{Binding OpenTrendCommand}"/>
            <MenuItem Header="Web Browser" Command="{Binding OpenWebBrowserCommand}"/>
        </Menu>
        <Grid>
            <avalonDock:DockingManager x:Name="dockManager">
                <avalonDock:LayoutRoot>
                    <avalonDock:LayoutPanel Orientation="Horizontal" x:Name="MainPanel">
                        <avalonDock:LayoutDocumentPane>
                            <avalonDock:LayoutDocument Title="Browser" ContentId="Browser1">
                                <local:WebBrowserControl />
                            </avalonDock:LayoutDocument>
                        </avalonDock:LayoutDocumentPane>

                        <avalonDock:LayoutPanel Orientation="Vertical">
                            <avalonDock:LayoutDocumentPane>
                                <avalonDock:LayoutDocument Title="Payment Viewer" ContentId="PaymentViewerDocument">
                                    <local:PaymentViewerControl />
                                </avalonDock:LayoutDocument>
                                <avalonDock:LayoutDocument Title="CSV Import" ContentId="CsvImportDocument">
                                    <local:CsvImportControl />
                                </avalonDock:LayoutDocument>
                            </avalonDock:LayoutDocumentPane>
                            <avalonDock:LayoutDocumentPane >
                                <avalonDock:LayoutDocument Title="Trend Viewer" ContentId="TrendViewerDocument">
                                    <local:TrendControl />
                                </avalonDock:LayoutDocument>
                            </avalonDock:LayoutDocumentPane>
                        </avalonDock:LayoutPanel>
                    </avalonDock:LayoutPanel>
                </avalonDock:LayoutRoot>
            </avalonDock:DockingManager>
        </Grid>
    </DockPanel>
</Window>
```

ポイントは`LayoutPanel`の下に再度`LayoutPanel`を配置することができるところでしょうか。これによりさまざまな分割の仕方でウィンドウを配置することができます。

:::note info
ウィンドウをフローティングさせた場合は構造としてはどうなるかですが、`LayoutPane`からは外れてしまいます。`LayoutFloatingWindow`というウィンドウが作られ、その中に`LayoutDocument`が配置されるようです。

また、`LayoutFloatingWindow`自体は、DockingManagerやLayoutRootから辿ることができます。
:::

# 開発Tips① : 動的なウィンドウの追加
ボタンクリックなどを契機に動的にウィンドウを追加することも可能です。

今回作成したプログラムでは`WindowAgent`クラスに、画面の追加等の処理をまとめています。

例えば、新しくブラウザを追加したい場合は以下のようなメソッドを作くることになると思います。
```csharp
public class SampleWindowAgent
{
    //メインウィンドウ（およびLayoutPanle）を参照できるようにしておく
    public static MainWindow MainWindow{get;set;}

    public void AddWebBrowser()
    {
        //配置するDocumentを作成
        //Contentに配置したい画面のViewを入れる
        var newBrowser = new LayoutDocument{Title="Browser",ContantId="BrowserId",Content= new WebBrowsrControl()}
        
        //Documentを配置するDocumentPaneを作成し、Childrenに追加
        var newDocpane = new LayoutDocumentPane();
        newDocpane.Children.Add(newBrowser);
        //メインウィンドウのLayoutPanelに追加する
        MainWindow.LayoutPanel.Children.Add(newDocpane);
    } 
}
```

# 開発Tips②:ウィンドウの取得

様々な場面で便利なメソッドとして`Descendents()`があります。
このメソッドでは指定したタイプの要素を再帰的に検索し、リストとして返してくれます。

## 例① : 特定のドキュメントウィンドウの取得
例えば、サンプルアプリのブラウザウィンドウの一覧を取得する場合は以下のようになります。
```csharp
//ブラウザ画面を取得
var browserDocumentList = MainWindow.LayoutPanel.Descendents().OfType<LayoutDocument>().Where(x => x.Content.GetType() == typeof(WebBrowserControl));
```

## 例② : 特定のドキュメントウィンドウを持つLayoutDocumenPaneを取得

動的にウィンドウを追加する場合などで、追加先のLayoutDocumentPaneを取得したいとき等があります。

```csharp
//存在しているLayoutDocumentPaneのリストを取得
var existingDocumentPanes = MainWindow.LayoutPanel.Descendents().OfType<LayoutDocumentPane>();

//ブラウザ画面を持つDocumentPaneを取得
var haveBrowserDocumentPane = existingDocumentPanes.FirstOrDefault(x => x.Descendents().OfType<LayoutDocument>().Any(x => x.Content.GetType() == typeof(WindowBrowserControl)));
```

## 例➂ : フローティングウィンドウの取得
フローティングウィンドウの取得や、そこから特定のDocumentの取得なども可能です
```csharp
//フローティングウィンドウの取得
var floatingWindow = MainWindow.DockingManager.Layout.Descendents().OfType<LayoutFloatingWindow>().FirstOrDefault();

//ブラウザDocumentの取得
var layout = floatingWindow.Descendents().OfType<LayoutDocument>().FirstOrDefault(x => x.Content.GetType() == typeof(WindowBrowserControl));
```


# まとめ
簡単にAvalonDockの解説を行いました。

構造は中々難しいところはありますが、金融の分野ではドッキングウィンドウは比較的よく見かけますし、一つの画面でいろんな情報を見れるのは確かに便利だと思います。

WPFを扱っているプロジェクトなどあまり見かけないですが良いライブラリだと思いますので今後も使っていきたいなと思います。