WPFプログラミング備忘録

WPF Behaviorを使ってコントロールの機能を強化しよう

WPF

はじめに

この記事ではBehaviorを使ってコントロールの機能を強化できることを確認します。

サンプルとしてTextBoxコントロールに実装する機能を以下のとおりとします。

  • フォーカスが当たったときは背景色を黄色にする
  • フォーカスが外れたときは背景色を灰色にする
  • Enterキーを押すと次のコントロールへフォーカスを移動する
  • Escキーを押すと前のコントロールへフォーカスを移動する

開発環境は以下のとおりです。

オペレーティングシステムWindows10 x64
Visual StudioMicrosoft Visual Studio Community 2019 Version 16.11.2
.NET Framework4.7.2

Microsoft.Xaml.Behaviorsのインストール

以下の手順でMicrosoft.Xaml.Behaviorsをプロジェクトにインストールします。

  1. メニューから「ツール」->「NuGet パッケージ マネージャー」->「ソリューションの NuGet パッケージの管理」を選択する
  2. 「参照」を選択しMicrosoft.Xaml.Behaviorsで検索する
  3. 左側の一覧からMicrosoft.Xaml.Behaviors.Wpfを選択する
  4. 右側のプロジェクトの一覧からインストール先のプロジェクトを選択する
  5. 「インストール」ボタンを押す

参照設定にMicrosoft.Xaml.Behaviorsが追加されます。

Behaviorを拡張したクラスの作成

クラスを作成します。「プロジェクトを右クリック」->「追加」->「クラス」でクラスを追加します。

今回はChangeTextBoxColorという名前にします。

ChangeTextBoxColor.csファイルに以下のusing宣言を追加します。

using System.Windows;
using System.Windows.Input;
using System.Windows.Controls;
using System.Windows.Media;
using Microsoft.Xaml.Behaviors;

ChangeTextBoxColorクラスをpublicクラスとした後、基底クラスにBehavior<TextBox>を指定します。

OnAttachedメソッドとOnDetachingメソッドをオーバーライドします。

overrideと入力してスペースキーを押すとメソッドの一覧が表示されるので上記2つのメソッドを追加してください。

フォーカスが当たったときと外れたときの背景色のために2つのSolidColorBrush型のpublicプロパティを追加します。

全ての実装を終えたChangeTextBoxColorクラスは以下のとおりです。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Controls;
using System.Windows.Media;
using Microsoft.Xaml.Behaviors;
using System.Windows;
using System.Windows.Input;

namespace WpfBehaviorSample
{
    public class ChangeTextBoxColor : Behavior<TextBox>
    {
        public SolidColorBrush BackBrushOnGotFocus { get; set; }
        public SolidColorBrush BackBrushOnLostFocus { get; set; }

        protected override void OnAttached()
        {
            base.OnAttached();

            AssociatedObject.GotFocus += AssociatedObject_GotFocus;

            AssociatedObject.LostFocus += AssociatedObject_LostFocus;

            AssociatedObject.KeyDown += AssociatedObject_KeyDown;
        }

        private void AssociatedObject_LostFocus(object sender, RoutedEventArgs e)
        {
            AssociatedObject.Background = BackBrushOnLostFocus;
        }

        private void AssociatedObject_GotFocus(object sender, RoutedEventArgs e)
        {
            AssociatedObject.Background = BackBrushOnGotFocus;
        }

        private void AssociatedObject_KeyDown(object sender, KeyEventArgs e)
        {
            if((Keyboard.Modifiers == ModifierKeys.None) && (e.Key == Key.Enter))
            {
                (AssociatedObject as UIElement)?.MoveFocus(new TraversalRequest(FocusNavigationDirection.Next));
                
                e.Handled = true;
            } 
            else if((Keyboard.Modifiers == ModifierKeys.None) && (e.Key == Key.Escape))
            {
                (AssociatedObject as UIElement)?.MoveFocus(new TraversalRequest(FocusNavigationDirection.Previous));
                
                e.Handled = true;
            }
        }

        protected override void OnDetaching()
        {
            base.OnDetaching();

            AssociatedObject.GotFocus -= AssociatedObject_GotFocus;

            AssociatedObject.LostFocus -= AssociatedObject_LostFocus;

            AssociatedObject.KeyDown -= AssociatedObject_KeyDown;
        }
    }
}

ここで、AssociatedObjectプロパティはBehaviorに関連づけられているオブジェクトを返します。今回の場合はTextBoxです。

Behaviorで拡張したテキストボックスコントロールの配置

MainWindow.xamlを開きます。Behaviorsを使用可能にするためにWindow要素に以下の属性を追記してください。

xmlns:bhv="http://schemas.microsoft.com/xaml/behaviors"

Gridを削除してStackPanelを追加し3つのTextBoxコントロールを配置します。

全ての実装を終えたMainWindow.xamlは以下のとおりです。

<Window x:Class="WpfBehaviorSample.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:WpfBehaviorSample"
        xmlns:bhv="http://schemas.microsoft.com/xaml/behaviors"
        mc:Ignorable="d"
        Title="MainWindow" Height="250" Width="400">
 
    <StackPanel VerticalAlignment="Center" FocusManager.FocusedElement="{Binding ElementName=tb01}">
 
        <TextBox x:Name="tb01" Width="240" Height="30" Margin="10" Background="LightGray">
            <bhv:Interaction.Behaviors>
                <local:ChangeTextBoxColor BackBrushOnGotFocus="LightYellow"
                                          BackBrushOnLostFocus="LightGray"/>
            </bhv:Interaction.Behaviors>
        </TextBox>

        <TextBox Width="240" Height="30" Margin="10" Background="LightGray">
            <bhv:Interaction.Behaviors>
                <local:ChangeTextBoxColor BackBrushOnGotFocus="LightYellow"
                                          BackBrushOnLostFocus="LightGray"/>
            </bhv:Interaction.Behaviors>
        </TextBox>

        <TextBox Width="240" Height="30" Margin="10" Background="LightGray">
            <bhv:Interaction.Behaviors>
                <local:ChangeTextBoxColor BackBrushOnGotFocus="LightYellow"
                                          BackBrushOnLostFocus="LightGray"/>
            </bhv:Interaction.Behaviors>
        </TextBox>
    
    </StackPanel>

</Window>

プログラム起動時に一番上のTextBoxコントロールにフォーカスを当てるためFocusManagerを使用しています。

ビルドして実行すると以下のウィンドウが表示されます。

一番上のTextBoxコントロールにフォーカスが当たっているので背景色が黄色になっています。

上から二番目のTextBoxコントロールにフォーカスを移動すると下図のとおり背景色が黄色になります。

一番下のTextBoxコントロールも同様です。

Enterキーを押すと次のコントロールへフォーカスが移動し、Escキーを押すと前のコントロールへフォーカスが移動することもわかります。

しかし、このプログラムではウィンドウ上にComboBoxなどTextBox以外のコントロールを置きたい場合にフォーカス移動に対応できません。フォーカス移動専用のBehaviorを作成した方が良いでしょう。

そこで、 ChangeTextBoxColorクラスのときと同様に、新たにMoveFocusControlというクラスを追加します。TextBox以外のコントロールにも対応するため基底クラスにはBehavior<UIElement>を指定します。フォーカス移動に関するコードはこのクラスに実装します。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Controls;
using Microsoft.Xaml.Behaviors;
using System.Windows;
using System.Windows.Input;

namespace WpfBehaviorSample
{
    public class MoveFocusControl : Behavior<UIElement>
    {
        private void AssociatedObject_KeyDown(object sender, KeyEventArgs e)
        {
            var control = e.OriginalSource as UIElement;

            if ((Keyboard.Modifiers == ModifierKeys.None) && (e.Key == Key.Enter))
            {
                control.MoveFocus(new TraversalRequest(FocusNavigationDirection.Next));

                e.Handled = true;
            }
            else if ((Keyboard.Modifiers == ModifierKeys.None) && (e.Key == Key.Escape))
            {
                control.MoveFocus(new TraversalRequest(FocusNavigationDirection.Previous));

                e.Handled = true;
            }
        }

        protected override void OnAttached()
        {
            base.OnAttached();

            AssociatedObject.KeyDown += AssociatedObject_KeyDown;
        }

        protected override void OnDetaching()
        {
            base.OnDetaching();

            AssociatedObject.KeyDown -= AssociatedObject_KeyDown;
        }
    }
}

ChangeTextBoxColorクラスからはフォーカス移動に関するコードを削除します。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Controls;
using System.Windows.Media;
using Microsoft.Xaml.Behaviors;
using System.Windows;

namespace WpfBehaviorSample
{
    public class ChangeTextBoxColor : Behavior<TextBox>
    {
        public SolidColorBrush BackBrushOnGotFocus { get; set; }
        public SolidColorBrush BackBrushOnLostFocus { get; set; }

        protected override void OnAttached()
        {
            base.OnAttached();

            AssociatedObject.GotFocus += AssociatedObject_GotFocus;

            AssociatedObject.LostFocus += AssociatedObject_LostFocus;
        }

        private void AssociatedObject_LostFocus(object sender, RoutedEventArgs e)
        {
            AssociatedObject.Background = BackBrushOnLostFocus;
        }

        private void AssociatedObject_GotFocus(object sender, RoutedEventArgs e)
        {
            AssociatedObject.Background = BackBrushOnGotFocus;
        }

        protected override void OnDetaching()
        {
            base.OnDetaching();

            AssociatedObject.GotFocus -= AssociatedObject_GotFocus;

            AssociatedObject.LostFocus -= AssociatedObject_LostFocus;
        }
    }
}

画面にはComboBoxとListBoxを追加しておきます。

Window要素の直下にMoveFocusControlを配置します。こうすることでMoveFocusControlクラス内のAssociatedObjectプロパティはMainWindowオブジェクトを返すようになります。

<Window x:Class="WpfBehaviorSample.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:WpfBehaviorSample"
        xmlns:bhv="http://schemas.microsoft.com/xaml/behaviors"
        mc:Ignorable="d"
        Title="MainWindow" Height="350" Width="400">

    <bhv:Interaction.Behaviors>
        <local:MoveFocusControl />
    </bhv:Interaction.Behaviors>
    
    <StackPanel VerticalAlignment="Center" FocusManager.FocusedElement="{Binding ElementName=tb01}">
 
        <TextBox x:Name="tb01" Width="240" Height="30" Margin="10" Background="LightGray">
            <bhv:Interaction.Behaviors>
                <local:ChangeTextBoxColor BackBrushOnGotFocus="LightYellow"
                                          BackBrushOnLostFocus="LightGray"/>
            </bhv:Interaction.Behaviors>
        </TextBox>

        <TextBox Width="240" Height="30" Margin="10" Background="LightGray">
            <bhv:Interaction.Behaviors>
                <local:ChangeTextBoxColor BackBrushOnGotFocus="LightYellow"
                                          BackBrushOnLostFocus="LightGray"/>
            </bhv:Interaction.Behaviors>
        </TextBox>

        <TextBox Width="240" Height="30" Margin="10" Background="LightGray">
            <bhv:Interaction.Behaviors>
                <local:ChangeTextBoxColor BackBrushOnGotFocus="LightYellow"
                                          BackBrushOnLostFocus="LightGray"/>
            </bhv:Interaction.Behaviors>
        </TextBox>

        <ComboBox Width="240" Height="30" Margin="10">
            <ComboBoxItem Content="Orange" />
            <ComboBoxItem Content="Apple" />
            <ComboBoxItem Content="Grape" />
        </ComboBox>

        <ListBox Width="240" Margin="10">
            <ListBoxItem Content="Toyota" />
            <ListBoxItem Content="Honda" />
            <ListBoxItem Content="Nissan" />
        </ListBox>
    
    </StackPanel>

</Window>

プログラムをビルドして実行すると以下のウィンドウが表示されます。

最初のプログラムと同様にフォーカスを持ったTextBoxは背景色が黄色になります。

すべてのコントロール間で、Enterキーを押すと次のコントロールへフォーカスが移動し、Escキーを押すと前のコントロールへフォーカスが移動することがわかります。

今回はBehaviorを使ってコントロールの機能を強化しました。Behaviorは上手く使えば煩わしいコードビハインドを減らすことができます。

以上です。

コメント