🏧

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

2024/12/16に公開

はじめに

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

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

ビルドして起動すると、以下の画像のような画面が立ち上がります。
alt text

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

AvalonDockのビュー:ドキュメントウィンドウとツールウィンドウ

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

それぞれどのような画面かはVisualStudio等を見てみるとわかりやすいかと思います。
alt text

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

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

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

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

AvalonDockの基本的な構造

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

alt text

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

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

コードではMainWindow.xamlの部分で書いています。

<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を配置することができるところでしょうか。これによりさまざまな分割の仕方でウィンドウを配置することができます。

開発Tips① : 動的なウィンドウの追加

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

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

例えば、新しくブラウザを追加したい場合は以下のようなメソッドを作くることになると思います。

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()があります。
このメソッドでは指定したタイプの要素を再帰的に検索し、リストとして返してくれます。

例① : 特定のドキュメントウィンドウの取得

例えば、サンプルアプリのブラウザウィンドウの一覧を取得する場合は以下のようになります。

//ブラウザ画面を取得
var browserDocumentList = MainWindow.LayoutPanel.Descendents().OfType<LayoutDocument>().Where(x => x.Content.GetType() == typeof(WebBrowserControl));

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

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

//存在している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の取得なども可能です

//フローティングウィンドウの取得
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を扱っているプロジェクトなどあまり見かけないですが良いライブラリだと思いますので今後も使っていきたいなと思います。

Discussion