なべひろBlog

プログラミングをメインに仕事に関するアレコレを発信しています。

PrismのUserControlを動的に配置する

はじめに

かなり前にWinFormsでカスタムコントロールを動的に生成し任意の場所に配置するアプリを作りました。

目的は部屋にある機械の状態監視です。

設置された機械と同じレイアウトの画面なら作業者はどの機械かの判断が簡単にできます。

機械は増えたり減ったりレイアウトが変わったりします。

なので画面デザインは固定されたコントロールを配置と言う訳にはいきません。

そろそろシステム全体を見直す時期が来たのでWPFで同じ事ができないか調査しました。

これが茨の道で、動的にコントロールを配置する記事は数が少なく自分が目的としている機能を実現できそうな情報はありませんでした。

それでも数少ない情報をピックアップしなんとか完成しました。

そんな苦難の中、C#に関しては私が尊敬する方が私のTwitterを見てサンプルを作成してくれました。

実はその少し前に完成はしていたのですが、初めてトライしたItemsControlの使い方が正しいのか判断できなかったので答え合わせができ助かりました。

私が作ったサンプルは

尚、このアプリは常に最大化された状態で使う事を前提としています。

なのでウィンドウサイズが小さくなると表示されているユーザコントロールは隠れてしまいます。

ユーザコントロール

ユーザコントロールはソリューションエクスプローラで「追加」-「新しい項目」で「Prism」-「WPF」-「Prism UserControl(WPF)」を選択します。

ユーザコントロールには自分の場所を設定するTopとLeftを持ち、この数値を変更する事により表示される座標を変えます。

初期の表示位置を決めるためユーザコントロールのコンストラクタに座標の引数を持たせユーザコントロールのインスタンスを生成するViewModelで座標を決めます。

ViewModel

コンストラクタでユーザコントロールのインスタンスを生成し、ControlListに追加します。

このControlListがItemsControlのItemsSourceにBindingされ表示される仕組みです。

// ユーザコントロールの一覧
public ObservableCollection ControlList { get; } = new();
public MainWindowViewModel()
{
    var c1 = new ControlViewModel(50.0, 50.0);
    _ = c1.AddTo(Disposable);
    ControlList.Add(c1);
    var c2 = new ControlViewModel(50.0, 150.0);
    _ = c2.AddTo(Disposable);
    ControlList.Add(c2);
}
XAML

上記ViewModelでどうすべきかは把握できていた(WinFormsでも似たような手法でやっていた)のですが、これをどうすれば表示できるのかが最大の難関でした。

実際作ってみたら案外簡単でしたが、この簡単な記述を見つけ出すのが大変でした。

<Canvas>
    <ItemsControl ItemsSource="{Binding ControlList}">
        <ItemsControl.ItemTemplate>
            <DataTemplate DataType="{x:Type vm:ControlViewModel}">
                <v:Control />
            </DataTemplate>
        </ItemsControl.ItemTemplate>
        <ItemsControl.ItemsPanel>
            <ItemsPanelTemplate>
                <Canvas/>
            </ItemsPanelTemplate>
        </ItemsControl.ItemsPanel>
        <ItemsControl.ItemContainerStyle>
            <Style TargetType="ContentPresenter">
                <Setter Property="Canvas.Left" Value="{Binding Left.Value, UpdateSourceTrigger=PropertyChanged}"/>
                <Setter Property="Canvas.Top" Value="{Binding Top.Value, UpdateSourceTrigger=PropertyChanged}"/>
            </Style>
        </ItemsControl.ItemContainerStyle>
    </ItemsControl>
</Canvas>

記述自体はこでだけなので色々なコントロールを配置するアプリに比べたら簡単です。

上記xamlを分解して解説(になってない)すると。

<ItemsControl ItemsSource="{Binding ControlList}">
ItemsControlのItemSourceはViewModelのコンストラクタで生成しリストに追加したControlListとなります。

<ItemsControl.ItemTemplate>
    <DataTemplate DataType="{x:Type vm:ControlViewModel}">
        <v:Control />
    </DataTemplate>
</ItemsControl.ItemTemplate>

ItemsControl.ItemTemplateはヘルプを見ると「各項目を表示するために使用される DataTemplate を取得または設定します。」との事です。

初見の私が説明する事はありませんw

<ItemsControl.ItemsPanel>
    <ItemsPanelTemplate>
        <Canvas/>
    </ItemsPanelTemplate>
</ItemsControl.ItemsPanel>

ItemsControl.ItemsPanelはヘルプを見ると「項目のレイアウトを制御するパネルを定義するテンプレートを取得または設定します。」と説明されています。

レイアウトを制御するのはCanvasだよってな事でしょうか...

<ItemsControl.ItemContainerStyle>
    <Style TargetType="ContentPresenter">
        <Setter Property="Canvas.Left" Value="{Binding Left.Value, UpdateSourceTrigger=PropertyChanged}"/>
        <Setter Property="Canvas.Top" Value="{Binding Top.Value, UpdateSourceTrigger=PropertyChanged}"/>
    </Style>
</ItemsControl.ItemContainerStyle>

ItemsControl.ItemContainerStyleはヘルプを見ると「項目ごとに生成されるコンテナー要素に適用される Style を取得または設定します。」と説明されています。

私はユーザコントロールにTopとLeftの変数を用意していましたが、それがBindingされCanvasの座標にユーザコントロールが表示されます。

ItemsControlを使えばコントロールの表示を柔軟にできる事がわかりました。

問題はこの機能を使いこなせるかですね。

Prism関連記事

MVVMなWPFアプリケーションでバーコードリーダーの情報をキャッチする。

PrismのIDialogServiceを試してみる

MVVMなWPFアプリケーションでドラッグ&ドロップをしてみる

Prismを使ったWPFアプリケーションで多重起動を抑制する

Prism使ったWPFでLivetCask.Messagingを使ってみる

Prismで複数のページを持ったアプリを作ってみる