WPF

コンテキストメニュー制御いろいろ(C# / WPF / MVVM)

メインウィンドウWPF でコンテキストメニューを表示すること自体は簡単ですが、制御しようと思うと幅広い考え方をする必要があり分かりづらいので、まとめて整理しました。

テストプログラムは GitHub に上げてあります。MVVM ライブラリは Livet を使用しています。Visual Studio 2022 + .NET 6 にて動作確認しています。


シンプルなコンテキストメニュー

素の状態のサンプルとして、ラベルにコンテキストメニューを付けています。ContextMenu タグを付けるだけの簡単なお仕事です。

<Label Content="シンプルなコンテキストメニュー:右クリックでオープンする(左クリックではオープンしない)" >
<Label.ContextMenu>
    <ContextMenu>
        <MenuItem Header="シンプルラベルメニュー 1" Command="{Binding MenuItemTestClickedCommand}" CommandParameter="シンプルラベルメニュー 1" />     
(省略...)
        </MenuItem>
    </ContextMenu>
</Label.ContextMenu>
</Label>

ラベル右クリックラベルを右クリックするとコンテキストメニューが表示されます。

テストプログラムでは、コンテキストメニューを捕捉したことが分かるようにするため、コンテキストメニューをクリックするとステータスバーにメッセージを表示します。

左クリックでもオープンするコンテキストメニュー

通常、コンテキストメニューは右クリックで表示されますが、EventTrigger でイベントを捕捉することにより、ボタン左クリックでもコンテキストメニューを表示することができます。

<Button Name="ButtonLeftSample" Content="左クリックでもオープンするコンテキストメニュー(コントロールのイベントを捕捉してオープンする)" Margin="0,10,0,0" >
<behaviors:Interaction.Triggers>
    <behaviors:EventTrigger EventName="Click">
        <behaviors:ChangePropertyAction TargetObject="{Binding ContextMenu, ElementName=ButtonLeftSample}" PropertyName="IsOpen" Value="True"/>
        <behaviors:ChangePropertyAction TargetObject="{Binding ContextMenu, ElementName=ButtonLeftSample}" PropertyName="PlacementTarget" Value="{Binding ElementName=ButtonLeftSample, Mode=OneWay}"/>
    </behaviors:EventTrigger>
</behaviors:Interaction.Triggers>
<Button.ContextMenu>
    <ContextMenu >
        <MenuItem Header="左クリックメニュー 1" Command="{Binding MenuItemTestClickedCommand}" CommandParameter="左クリックメニュー 1" />
        <MenuItem Header="左クリックメニュー 2" Command="{Binding MenuItemTestClickedCommand}" CommandParameter="左クリックメニュー 2" />
    </ContextMenu>
</Button.ContextMenu>
</Button>

ラベルで同様のことをやろうと思った場合、ラベルには Click イベントが無いので、代わりに MouseLeftButtonUp イベントを捕捉するとそれっぽくなるのではないでしょうか(ラベル以外でマウスボタンを押して、ドラッグでラベルまで移動してきて、そこでマウスボタンを離した場合もコンテキストメニューが表示されてしまいますが……)。

テキストボックスデフォルトのコンテキストメニューを非表示にする

テキストボックスには、デフォルトでコンテキストメニュー(コピー、貼り付け等)が付いてきます。

このコンテキストメニューを表示したくない場合は、コンテキストメニューを null にします。

<TextBox Width="100" Text="非表示" VerticalAlignment="Center" ContextMenu="{x:Null}" />

リストボックスのアイテムのみ右クリックでオープンするコンテキストメニュー

ListBoxリストボックスにコンテキストメニューを付けると、リストボックス内のどこをクリックしてもコンテキストメニューが表示されます。

リストボックスのアイテムがある部分を右クリックした場合のみコンテキストメニューを表示したい場合は、ItemContainerStyle で ListBoxItem に対してコンテキストメニューを付けます。

この時、DataContext を指定しないとコマンドを捕捉できません。これは、コンテキストメニューの VisualTree が変な感じになっているからのようです。WPF の嫌なところです……。

<ListBox ItemsSource="{Binding NarrowListBoxItems}" Width="290" >
    <ListBox.ItemContainerStyle>
        <Style TargetType="ListBoxItem" >
            <Setter Property="Tag" Value="{Binding RelativeSource={RelativeSource AncestorType=Window}, Path=DataContext}"/>
            <Setter Property="ContextMenu">
                <Setter.Value>
                    <ContextMenu DataContext="{Binding Path=PlacementTarget.Tag, RelativeSource={RelativeSource Self}}">
                        <MenuItem Header="アイテムのみ ListBox メニュー 1" Command="{Binding MenuItemTestClickedCommand}" CommandParameter="アイテムのみ ListBox メニュー 1" />
                        <MenuItem Header="アイテムのみ ListBox メニュー 2" Command="{Binding MenuItemTestClickedCommand}" CommandParameter="アイテムのみ ListBox メニュー 2" />
                    </ContextMenu>
                </Setter.Value>
            </Setter>
        </Style>
    </ListBox.ItemContainerStyle>
</ListBox>
DataGrid についても同様のやり方で、アイテムがある部分のみでのコンテキストメニュー表示ができます。

DataGrid の場合、TargetType を DataGridRow にします。

動的に状態を制御するコンテキストメニュー

コンテキストメニューのヘッダー(ラベル)等を動的に制御したい場合は、ContextMenuOpening イベントを捕捉すると簡単です。ContextMenuOpening イベントハンドラー内で、ヘッダーや IsEnabled などをいじることで、コンテキストメニューの状態を動的に制御できます。

Visibility をいじることで、ある程度の項目の増減もできます。

例えば、ファイルを右クリックでファイル系メニュー、フォルダーを右クリックでフォルダー系メニューを表示したい場合、両方のメニューを記述しておいて、不要なメニューの Visibility を Collapsed にすることで対応できます。

<Label Content="動的に状態を制御するコンテキストメニュー" Margin="0,10,0,0" >
<behaviors:Interaction.Triggers>
    <behaviors:EventTrigger EventName="ContextMenuOpening">
        <l:LivetCallMethodAction MethodName="PrepareDynamicStateContextMenu" MethodTarget="{Binding}" />
    </behaviors:EventTrigger>
</behaviors:Interaction.Triggers>
<Label.ContextMenu>
    <ContextMenu>
        <MenuItem Header="{Binding MenuItemHeaderDynamicState}" HeaderStringFormat="{}{0} 1" Command="{Binding MenuItemTestClickedCommand}" CommandParameter="動的状態制御メニュー 1" />
        <MenuItem Header="{Binding MenuItemHeaderDynamicState}" HeaderStringFormat="{}{0} 2" Command="{Binding MenuItemTestClickedCommand}" CommandParameter="動的状態制御メニュー 2" IsEnabled="{Binding IsDynamicStateMenuItemEnabled}" />
        <MenuItem Header="{Binding MenuItemHeaderDynamicState}" HeaderStringFormat="{}{0} 3" Command="{Binding MenuItemTestClickedCommand}" CommandParameter="動的状態制御メニュー 3" Visibility="{Binding DynamicStateMenuItemVisibility}"/>
    </ContextMenu>
</Label.ContextMenu>
</Label>

動的にアイテム数を制御するコンテキストメニュー

Visibility の制御ではまかなえないほどフリーダムにコンテキストメニューのアイテムを変更したい場合は、ItemsSource をバインドし、ContextMenuOpening イベントで ItemsSource を変更します。

<Label Content="動的にアイテム数を制御するコンテキストメニュー" >
<behaviors:Interaction.Triggers>
    <behaviors:EventTrigger EventName="ContextMenuOpening">
        <l:LivetCallMethodAction MethodName="PrepareDynamicItemsContextMenu" MethodTarget="{Binding}" />
    </behaviors:EventTrigger>
</behaviors:Interaction.Triggers>
<Label.ContextMenu>
    <ContextMenu ItemsSource="{Binding DynamicMenuItems}" />
</Label.ContextMenu>
</Label>

コンテキストメニューでサブフォルダーを列挙して表示するようなケースでは、こちらの方法が良いかと思います。

XAML を動的に読み込むサンプルプログラム 【C#/WPF/MVVM】

Runプログラム実行時に XAML を読み込んで UI コントロールを生成する方法のまとめ。

「タブを増やす」ボタンをクリックするたびにタブが増えていく。

サンプルプログラムは GitHub にて:
Visual Studio 2022 / .NET 6 で動作確認。

大まかな手順

STEP 1:XAML 作成

UserControl動的に生成したいコントロール(DynamicTabItem.xaml)をユーザーコントロールとして作成する。

以下の 3 つの作業を行う。

[A]
UmekomiDynamicTabItem.xaml ファイルのプロパティーでビルドアクションを「埋め込みリソース」にする。

[B]
csDynamicTabItem.xaml に紐付いている DynamicTabItem.xaml.cs を削除する。

[C]
xClassDynamicTabItem.xaml の先頭に記述されている x:Class の記述を削除する。

STEP 2:動的読み込み

リソースから DynamicTabItem.xaml を読み込み、XamlReader でコントロール化する。ButtonAddTabItemClicked() 参照。

注意点

動的に読み込む XAML 内の名前空間は、アセンブリを指定する必要がある。指定が無いと XamlReader が例外を吐く。

例えば、ビューモデルの名前空間は
xmlns:vm="clr-namespace:TestXamlReader.ViewModels"
ではなく
xmlns:vm="clr-namespace:TestXamlReader.ViewModels;assembly=TestXamlReader"
とする。

(補足)
別のプロジェクトで、assembly を指定するとビルド時に MC3074 エラー「タグ 'XXX' は、XML 名前空間 'clr-namespace:YYY;assembly=ZZZ' にありません。」が発生したことがあった。
対処方法が分からず、xaml 上では assembly を指定せず、実行時に文字列置換で assembly 指定を追加することで凌いだことがある。

サードパーティーの名前空間を使いたい時も同様で、サンプルプログラムでは GongSolutions.WPF.DragDrop の添付プロパティーを使っているが、
xmlns:dd="urn:gong-wpf-dragdrop"
ではなく
xmlns:dd="clr-namespace:GongSolutions.Wpf.DragDrop;assembly=GongSolutions.Wpf.DragDrop"
とする。


ActualWidth のバインド&取得と、ビヘイビアの紐解き 【C#/WPF/MVVM】

Windowビヘイビアについて少し考えてみた。

サンプルプログラムは GitHub に置いてある。Livet 使用。

ActualWidth はバインドできない

C# WPF MVVM での開発において、FrameworkElement.ActualWidth は XAML からのバインドができない(VM で ActualWidth を取得できない)。

例えば Window.Title なら「Title="{Binding Hoge}"」のようにバインドできるが、「ActualWidth="{Binding Hoge}"」はエラーとなる。Title も ActualWidth も依存プロパティー(依存関係プロパティー)だが、ActualWidth は読み取り専用のため、「Setter が無い」旨のエラーとなる。

書き込みできないだけなら、「ActualWidth="{Binding ActualWidth, Mode=OneWayToSource}"」のような片方向バインドはさせてくれ、と思うのだが、それもできない。標準コントロールの全プロパティーはバインド可能にしておいて欲しいのだが、そんな日は来るのだろうか。

Livet で楽ちん

バインドできないものをバインドさせるには、通常はビヘイビアを自作する。

が、ActualWidth 程度であれば、ビヘイビアを自作するまでもなく Livet の XXXXSetStateToSourceAction を使うのが簡単。サンプルプログラムでは、同類の ActualHeight について、Livet の WindowSetStateToSourceAction を使用してバインドすることにより、VM で ActualHeight が取得できるようになっている。
<behaviors:Interaction.Triggers>
    <behaviors:EventTrigger EventName="SizeChanged">
        <l:WindowSetStateToSourceAction Source="{Binding ActualHeight, Mode=TwoWay}" Property="ActualHeight" />
    </behaviors:EventTrigger>
</behaviors:Interaction.Triggers>

今回はビヘイビアについて少し考えてみるのが目的なので、ActualWidth についてビヘイビアを自作する。

ビヘイビアで ActualWidth をバインド可能にする

ビヘイビアの作り方については「ビヘイビア WPF」あたりでググればたくさん出てくるが、コードの内容についての解説があまり見当たらないので、少し考えてみた(コード全体については冒頭のサンプルプログラム参照)。

XAML からバインド可能にするには依存プロパティーを作る必要があり、それが
public static readonly DependencyProperty ActualWidthProperty =
        DependencyProperty.Register(nameof(ActualWidth), typeof(Double), typeof(WindowBindingSupportBehavior),
        new FrameworkPropertyMetadata(0.0, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault));
の部分。そして、依存プロパティーとリンクする普通のプロパティーが
public Double ActualWidth
{
    get => (Double)GetValue(ActualWidthProperty);
    set => SetValue(ActualWidthProperty, value);
}
の部分。

依存プロパティーの作成と登録を行うのが DependencyProperty.Register() で、その第一引数 nameof(ActualWidth) がプロパティー名。依存プロパティー名 = プロパティー名 + "Property" となっている必要がある。なっていないと、実行時にエラーになる。ビルド時にエラーにしてほしいところだ。

多くのサイトで「DependencyProperty.Register("ActualWidth"...」のようにプロパティー名を即値で書いているが、「nameof(ActualWidth)」と書く方が多少安全になるかと思う。

DependencyProperty.Register() の第二引数で変数の型、第三引数でビヘイビアクラスを指定する。第四引数の FrameworkPropertyMetadata 内でプロパティーのデフォルト値を指定する。

依存プロパティー登録により ActualWidth のバインドが可能となったので、次は、ActualWidth の値を更新して有用な値にするところ。
ウィンドウのサイズが変更された際に ActualWidth を変化させれば、常に ActualWidth を最新の値に設定できる。

OnAttached() で SizeChanged イベントハンドラー(ControlSizeChanged)を登録し、そのイベントハンドラー内で ActualWidth を更新する。

以上で、ActualWidth をバインドして使えるようになる。

サンプルプログラムを起動してウィンドウの横幅を変化させると、ActualWidth の値も変化する。

ビヘイビアで IsActive をバインド可能にする

Window.IsActive もバインドできないが、これもバインド可能にしてみる。

IsActive は本来読み取り専用だが、ここでは書き込みも可能にして、true にされたらウィンドウをアクティブ化する、ということをしてみる。

基本的なやり方は ActualWidth の時と同じだが、ViewModel から書き込まれた時に処理を行うために、DependencyProperty.Register() で、書き込まれた時のイベントハンドラー SourceIsActiveChanged を指定している。

private static void SourceIsActiveChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args)
{
    if ((obj is not WindowBindingSupportBehavior thisObject) || thisObject.AssociatedObject == null)
    {
        return;
    }

    if ((Boolean)args.NewValue)
    {
        thisObject.AssociatedObject.Activate();
    }
}

イベントハンドラーの引数 obj にビヘイビアクラスが DependencyObject 型として格納されているので、WindowBindingSupportBehavior 型にキャストして thisObject とする。

thisObject.AssociatedObject がウィンドウ自体なので、これでウィンドウに対する操作がやりたい放題できる。Activate() を呼べばウィンドウがアクティブ化される。

サンプルプログラムでは、チェックボックスでウィンドウのアクティブ状態を表示する。他のウィンドウをアクティブにすると、チェックが外される。

「5 秒後にアクティブ」ボタンをクリックすると、5 秒後にサンプルプログラムのウィンドウをアクティブ化する。ボタンクリック後、他のウィンドウをアクティブにして 5 秒待つと、サンプルプログラムがアクティブになる。


WPF で動画に文字や図形を合成して再生する方法

C# 動画プログラミングの 1 つです。

WPF の MediaElement や MediaPlayer で動画を再生する方法を紹介したページはいくつか見たのですが、通常再生のみが紹介されており、加工しながらの再生について言及されているページが見当たらなかったので、ここにまとめてみます。

目次


やりたいこと

overlay2動画再生時に、再生している動画の映像に、テキスト等を重ねます。

例として、映像の左上に灰色の長方形を描き、フレームレートや動画再生位置(秒数)といった文字を重ねてみます。

サンプルコード

GitHub にアップしてありますので、そちらからダウンロードしてください。

動作環境については GitHub リポジトリ内のヘルプファイルを参照して下さい。

実装の基本方針

WPF には、(動画に限らず)表示の更新を行うタイミング(つまりフレームごと)で発生するイベントがあり、それが CompositionTarget.Rendering イベントです。

このイベント内で再生中の動画をキャプチャした上で、テキスト等を書き込んでいく、というのが基本的な考え方です。

プログラムの大まかな動作としては、ドラッグ&ドロップされた動画を再生(もちろん音声も)しつつ、上記の方法で映像にテキスト等を重ねています。

プログラムの解説

MainWindow.xaml.cs の解説です。

ファイルをドロップされた際に発生するイベント Window_Drop() において、動画の再生 Play() と、重ね合わせの準備 PrepareOverlay() を行っています。

Play() は単純に、ドロップされたファイルを MediaPlayer で再生しているだけです。

今回の 1 つのポイントは PrepareOverlay() で、ここでフレーム描画イベントハンドラー CompositionTarget.Rendering の設定を行っています。

ここで設定されたイベントハンドラーは WPF での描画が必要なタイミングで常に呼びだされるため、仮に動画が再生されていなくても定期的に呼びだされます。呼びだされる頻度は環境によって異なるとのことで、私の環境では通常 60 fps 程度で呼びだされていますが、100 fps 程度に上がることもあります。あくまでも「WPF 描画のフレームレート」であって、動画のフレームレートではないことに留意してください。

イベントハンドラーとして設定された CompositionTargetRendering() の後半部分で合成を行っています。

DrawingVisual の DrawingContext に対して、動画、図形(背景の灰色四角形)、テキスト(フレームレートや再生位置)を書き込み、最後にそれらを表示用のビットマップ(mBmp)に書き込みます。

ImagemBmp は PrepareOverlay() で Image コントロールのソースに指定されているため、mBmp の内容がユーザーの目に見える形で表示されることになります。

WPF で動画を扱うメリット

WPF で動画を扱うメリットは、なんといっても手軽なことです。

加工しない再生だけなら MediaElement コントロール 1 つで簡単に動画を再生できますし、加工しても今回のサンプルコードのようにわずかなコード量で実現できます。

標準の C# 開発環境のみで実現できるため、追加のサードパーティーライブラリを用意したり、それをユーザーにインストールしてもらう(または一緒に配布する)という手間もありません。

GPULoadGPU ハードウェア支援が使える場合は、自動的に活用してくれているようです。

また、詳しくは未検証ですが、インストールされているコーデックライブラリ(K-Lite 等)も WPF 側で自動的に利用してくれているようで、扱える動画の形式も無限に広がっていきます。

WPF で動画を扱うデメリット

WPF で動画を扱うデメリットは、(動画の)フレームを厳密に扱えないのではないかということです(扱える方法をご存じの方は教えてください)。

まず、根本的な課題として、動画のフレームレートが取得できません。動画の縦横サイズ等はプロパティーで取得できるのですが、フレームレートを取得するプロパティーはありません。

また、WPF で動きのあるものを処理する際はアニメーションという仕組み(Animatable クラスないしは IAnimatable インターフェース)に則っており、動画(MediaElement / MediaPlayer)も例外ではありませんが、動きの管理が時間単位で行われています。

何らかの方法で動画のフレームレートを取得できたとしても、厳密な意味でのフレーム送りはできず、1 フレームは○○ミリ秒だから○○ミリ秒進める、というような形でのフレーム送りになるのではないかと思います(この辺りは未検証ですが)。30 fps だと 1 フレーム当たり 33.3333.... ミリ秒ですが、小数点の丸めによって微妙に進まない(または 2 フレーム進んでしまう)瞬間が発生したり、そもそも可変フレームレートだとどうしようもないのではないかと思います。

スクリーンショットの動画

このページ冒頭のスクリーンショットに映っている動画は、ダブルレンさんの「お願い☆EternalSummer!」という曲です。素敵な曲なので、是非聴いてみて下さい!


歌いたい時はニコカラバージョンでどうぞ。








月別アーカイブ
記事検索
最新コメント
  • ライブドアブログ