InputMan for WPF コントロールの背景色を変更する。

昨日「サードパーティ製 WPF 数値コントロールのカスタマイズ」というエントリを書いたわけですが、実は昨日書いてなかった話が一つあって、InputMan for WPF のコントロールが編集中だと背景色が変更できないという問題が起きてました。ホントは以下の図のとおりに実行したいのですが、これが出来ない(汗


私が携わっている Forms のシステムの方では InputMan の各コントロールに対してサブクラスを設け、ReadOnly なら Focus 時は LemonChiffon になるよう設定してますが、これを InputMan for WPF で実現するなら以下のようなスタイルになると思います。しかし IsReadOnly = true の場合はコントロールに反映されても、IsFocused = true の場合、Background に XAML で設定したブラシは反映されません。

<Style TargetType="im:GcTextBox">
	<Style.Triggers>
		<Trigger Property="IsFocused" Value="true" >
			<Setter Property="Background" Value="LemonChiffon" />
		 </Trigger>
		<Trigger Property="IsReadOnly" Value="true" >
			<Setter Property="Background" Value="AliceBlue" />
		</Trigger>
	</Style.Triggers>
</Style>

<Style TargetType="im:GcNumber">
	<Style.Triggers>
		<Trigger Property="IsFocused" Value="true" >
			<Setter Property="Background" Value="LemonChiffon" />
		</Trigger>
		<Trigger Property="IsReadOnly" Value="true" >
			<Setter Property="Background" Value="AliceBlue" />
		</Trigger>
	</Style.Triggers>
</Style>

<Style TargetType="im:GcDateTime">
	<Style.Triggers>
		<Trigger Property="IsFocused" Value="true" >
			<Setter Property="Background" Value="LemonChiffon" />
		</Trigger>
		<Trigger Property="IsReadOnly" Value="true" >
			<Setter Property="Background" Value="AliceBlue" />
			<Setter Property="DropDownButtonVisibility" Value="NotShown" />
		</Trigger>
	</Style.Triggers>
</Style>


で、色々調べてみたところ、まず IsFocused プロパティですが、コントロールに Focus が当たっても True になりません。GotFocus イベントで拾うと標準の WPF コントロールなら IsFocused プロパティが True になるのですが、InputMan のコントロールは False のままです。この挙動、どうもバグっぽいような気がします。

Private Sub TextBox1_GotFocus(sender As Object, e As RoutedEventArgs) Handles TextBox1.GotFocus
	If TextBox1.IsFocused Then
		MessageBox.Show("フォーカスがあるよー") ' これは実行されるが・・・
	End If
End Sub

Private Sub GcTextBox1_GotFocus(sender As Object, e As RoutedEventArgs) Handles GcTextBox1.GotFocus
	If GcTextBox1.IsFocused Then
		MessageBox.Show("フォーカスがあるよー") ' こちらは実行されない・・・
	End If
End Sub

対処方法(2011/04/22 追記)

有難いことに GrapeCity さんから直接回答を頂きました。

InputMan for WPFでは、入力をフィールドという単位で構成している仕様上、コントロール内部に別のエディターが存在し、そのエディタがフォーカスを受けとります。そのため、コントロールのIsFocusedプロパティが意図した値を返さない場合がございます。恐れ入りますが、現時点ではこの動作は仕様となっております。


InputMan for WPFでキーボードフォーカスを持つコントロールの背景色を指定する場合には、以下のサンプルのように、IsFocused プロパティではなく IsKeyboardFocusWithin プロパティを使用ください。
このプロパティはキーボード フォーカスが要素内またはビジュアルツリーの子要素内に設定されている場合にtrueを返すため、InputMan for WPFの内部のエディタがキーボードフォーカスを受け取っていた場合でも判定することが可能です。


なるほど。以下のように IsKeyboardFocusWithin を使うと、こちらの意図したとおりの挙動になります。

<Style TargetType="im:GcTextBox">
	<Style.Triggers>
		<Trigger Property="IsKeyboardFocusWithin" Value="true" >
			<Setter Property="Background" Value="LemonChiffon" />
		 </Trigger>
		<Trigger Property="IsReadOnly" Value="true" >
			<Setter Property="Background" Value="AliceBlue" />
		</Trigger>
	</Style.Triggers>
</Style>

<Style TargetType="im:GcNumber">
	<Style.Triggers>
		<Trigger Property="IsKeyboardFocusWithin" Value="true" >
			<Setter Property="Background" Value="LemonChiffon" />
		</Trigger>
		<Trigger Property="IsReadOnly" Value="true" >
			<Setter Property="Background" Value="AliceBlue" />
		</Trigger>
	</Style.Triggers>
</Style>

<Style TargetType="im:GcDateTime">
	<Style.Triggers>
		<Trigger Property="IsKeyboardFocusWithin" Value="true" >
			<Setter Property="Background" Value="LemonChiffon" />
		</Trigger>
		<Trigger Property="IsReadOnly" Value="true" >
			<Setter Property="Background" Value="AliceBlue" />
			<Setter Property="DropDownButtonVisibility" Value="NotShown" />
		</Trigger>
	</Style.Triggers>
</Style>


この話、標準の ComboBox も同様で、IsEditable プロパティを True にした ComboBox の場合、IsFocused だと Background の設定がビューに反映されません。

<Style TargetType="ComboBox" >
	<Setter Property="IsEditable" Value="true" />
	<Style.Triggers>
		<Trigger Property="IsFocused" Value="true">
			<Setter Property="Background" Value="LemonChiffon" />
			<Setter Property="InputMethod.IsInputMethodEnabled" Value="False" />
		</Trigger>
	</Style.Triggers>
</Style>

ただし、以下のように IsKeyboardFocusWithin プロパティを使ったら変更が反映されました。

<Style TargetType="ComboBox" >
	<Setter Property="IsEditable" Value="true" />
	<Style.Triggers>
		<Trigger Property="IsKeyboardFocusWithin" Value="true">
			<Setter Property="Background" Value="LemonChiffon" />
			<Setter Property="InputMethod.IsInputMethodEnabled" Value="False" />
		</Trigger>
	</Style.Triggers>
</Style>

この辺り、まだまだ情報が少ないのが難点ですが、当ブログでは今後もこういう細かい TIPS をどんどん紹介していきたいと思います。


回避方法

(注:以下は当初の記事なので、一応参考のため残しておきますが、対応は上記 IsKeyboardFocusWithin を使ってください)
で、散々調べた結果、回避方法を見つけました。
まず、コントロールの要素にアクセスするクラスを用意します。使っているクラスは以前の記事で用意したものですが、さらに改良を加えてます。このクラスの拡張メソッド「FindChild」 はコントロールの要素を検索して返します。

using System;
using System.Windows;
using System.Windows.Data;
using System.Windows.Media;

/// <summary>
/// UIElement 子要素検索ヘルパークラス
/// </summary>
public static class UIChildFinder {

	/// <summary>
	/// 子要素検索の拡張メソッドです。
	/// </summary>
	public static DependencyObject FindChild
			(this DependencyObject reference, Type childType, string childName) {

		DependencyObject foundChild = null;
		if (reference != null) {

			int childrenCount = VisualTreeHelper.GetChildrenCount(reference);
			for (int i = 0; i < childrenCount; i++) {

				var child = VisualTreeHelper.GetChild(reference, i);

				// 子要素が検索する方でなければ
				if (child.GetType() != childType) {
					// 再帰的に階層をたどります。
					foundChild = FindChild(child, childType, childName);
					if (foundChild != null) break;

				} else if (!string.IsNullOrEmpty(childName)) {
					var frameworkElement = child as FrameworkElement;

					// 子要素の名前が検索条件に設定されてるなら
					if (frameworkElement != null && frameworkElement.Name == childName) {
						// 子要素がリクエストした名前にヒットした
						foundChild = child;
						break;
					}
				} else {
					// 子要素が見つかった
					foundChild = child;
					break;
				}
			}
		}
		return foundChild;
	}

	/// <summary>
	/// 子要素検索の拡張メソッドです。
	/// </summary>
	public static DependencyObject FindChild
			(this DependencyObject reference, string childTypeName, string childName) {

		DependencyObject foundChild = null;
		if (reference != null) {

			int childrenCount = VisualTreeHelper.GetChildrenCount(reference);
			for (int i = 0; i < childrenCount; i++) {

				var child = VisualTreeHelper.GetChild(reference, i);

				// 子要素が検索する方でなければ
				if (child.GetType().Name != childTypeName) {
					// 再帰的に階層をたどります。
					foundChild = FindChild(child, childType, childName);
					if (foundChild != null) break;

				} else if (!string.IsNullOrEmpty(childName)) {
					var frameworkElement = child as FrameworkElement;

					// 子要素の名前が検索条件に設定されてるなら
					if (frameworkElement != null && frameworkElement.Name == childName) {
						// 子要素がリクエストした名前にヒットした
						foundChild = child;
						break;
					}
				} else {
					// 子要素が見つかった
					foundChild = child;
					break;
				}
			}
		}
		return foundChild;
	}
}


私の環境で調べたところ、 InputMan の Control を編集中は Background プロパティを設定しても View に反映されません。なぜかは判りませんが コントロールの要素で BD という名前を持つ Border を変更しなければなりません。そこで以下のように、GotFocus イベントを使ってコントロールインスタンスにアクセスし、以下のように子要素「BD」にアクセスして、BackGround を変更します。(なぜかこっちのコードは VB だが気にしないように・・・)

Option Explicit On
Option Strict On

Imports System.Windows
Imports GrapeCity.Windows.InputMan
Imports ClassLibrary1


Public Class MainWindow

	Private Sub GcTextBox1_GotFocus(sender As Object, e As RoutedEventArgs) Handles GcTextBox1.GotFocus
		Dim border = GcTextBox1.FindChild(GetType(Border), "Bd")
		CType(border, Border).Background = New SolidColorBrush(Colors.LemonChiffon)
	End Sub

	Private Sub GcTextBox1_LostFocus(sender As Object, e As RoutedEventArgs) Handles GcTextBox1.LostFocus
		Dim border = GcTextBox1.FindChild(GetType(Border), "Bd")
		CType(border, Border).Background = GcTextBox1.Background
	End Sub

	Private Sub GcDateTime_GotFocus(sender As Object, e As RoutedEventArgs) Handles GcDateTime1.GotFocus
		Dim border = GcDateTime1.FindChild(GetType(Border), "Bd")
		CType(border, Border).Background = New SolidColorBrush(Colors.LemonChiffon)
	End Sub

	Private Sub GcDateTime_LostFocus(sender As Object, e As RoutedEventArgs) Handles GcDateTime1.LostFocus
		Dim border = GcDateTime1.FindChild(GetType(Border), "Bd")
		CType(border, Border).Background = GcDateTime1.Background
	End Sub

	Private Sub GcNumber_GotFocus(sender As Object, e As RoutedEventArgs) Handles GcNumber1.GotFocus
		Dim border = GcNumber1.FindChild(GetType(Border), "Bd")
		CType(border, Border).Background = New SolidColorBrush(Colors.LemonChiffon)
	End Sub

	Private Sub GcNumber_LostFocus(sender As Object, e As RoutedEventArgs) Handles GcNumber1.LostFocus
		Dim border = GcNumber1.FindChild(GetType(Border), "Bd")
		CType(border, Border).Background = GcNumber1.Background
	End Sub

End Class


実行すると、こうなります。


ちと手間食いましたが、やっと仕様を実現できました。実コードでは添付ビヘイビアにして対応するつもりです。でも疲れた・・・