Q058. XamDataGrid でセルに収まらない値をツールチップで表示するには?

A. いささか凝った方法ですが、MultiDataTriggerIMultiValueConverter を使って設定します。


発端と問題

WindowsForms の DataGridView の場合、セルからはみ出したデータにマウス持ってくとツールチップが表示されるのですが、残念ながら WPF の標準 DataGrid も XamDataGrid もこの UI はサポートされてません。
当初どうしていいか全く判らず、苦肉の策で

<Setter Property="ToolTip" Value="{Binding RelativeSource={x:Static RelativeSource.Self}, Path=Value}"/>
<Style.Triggers>
	<DataTrigger Binding="{Binding RelativeSource={x:Static RelativeSource.Self}, Path=Value}" Value="">
		<Setter Property="ToolTip" Value="{x:Null}" />
	</DataTrigger>
</Style.Triggers>


なんてことをしてたのですが、Infaragistics さんに問い合わせてみたら DataTrigger とコンバーターを使う方法を教えて頂きました。

Option Explicit On
Option Strict On

Imports System.Globalization
Imports System.Windows.Data
Imports Infragistics.Windows.DataPresenter

Class ValueToLengthConverter
	Implements IValueConverter

	Public Function Convert(ByVal value As Object, ByVal targetType As System.Type, ByVal parameter As Object, ByVal culture As CultureInfo) As Object Implements IValueConverter.Convert
		Dim myString As String
		If value Is Nothing Then
			Return False
		Else
			 myString = value.ToString()
			If (myString.Length > 10) Then
				Return True
			Else
				Return False
			End If
		End If
	End Function
 
	Public Function ConvertBack(ByVal value As Object, ByVal targetType As System.Type, ByVal parameter As Object, ByVal culture As CultureInfo) As Object Implements IValueConverter.ConvertBack
		Return Nothing
	End Function
End Class
<DataTrigger Binding="{Binding RelativeSource={x:Static RelativeSource.Self}, Path=Value, Converter={StaticResource converter}}" Value="True">
   <Setter Property="ToolTip" Value="{Binding RelativeSource={x:Static RelativeSource.Self}, Path=Value}"  />
</DataTrigger>


なるほど、Converter ってこうやって使うのか!と目から鱗だったのですが、残念ながら長さが固定されてしまうのでこのままだとうまくありません。シーンに応じてコンバーターのパラメータに長さを渡してやるといいですよとアドバイスを頂きましたが、Binding.ConverterParameter って悲しいかなリテラルしか渡せないためバインドデータが渡せません。(;_;)


MultiDataTrigger と IMultiValueConverter の組み合わせで解決

どうにかしてセル幅とさらにフォント情報も渡せないかと検索しまくってたら、MultiDataTrigger と IMultiValueConverter の組み合わせが見つかりました。
で、応用したパターンがこれ。MultiValueConverter にActualWidth と Value はもちろんフォント情報もごっそり渡し、コンバーター内で FormattedText クラスのインスタンスを生成して文字列の幅とセル幅を比較するという方法を採用してみました。

Option Explicit On
Option Strict On

Imports System.Globalization
Imports System.Windows.Data
Imports Infragistics.Windows.DataPresenter

Public Class ValueToLengthConverter
	Implements IMultiValueConverter

	Private Enum Path
		ActualWidth
		FontFamily
		FontSize
		FontStretch
		FontStyle
		FontWeight
		Value
	End Enum

	Public Function Convert(values() As Object, targetType As System.Type,
				parameter As Object, culture As CultureInfo) As Object Implements IMultiValueConverter.Convert
		Dim myFontWidth = GetFontLength(values)
		Dim myWidth = GetWidth(values)
		If (myWidth <> 0) AndAlso (myFontWidth > myWidth) Then
			Return True
		Else
			Return False
		End If

	End Function

	Public Function ConvertBack(value As Object, targetTypes() As System.Type,
			parameter As Object, culture As CultureInfo) As Object() Implements IMultiValueConverter.ConvertBack
		Return Nothing
	End Function

	Private Function GetFontLength(values() As Object) As Double
		Dim ret As Double = 0.0F
		If values IsNot Nothing AndAlso values(Path.FontFamily) IsNot Nothing Then
			Dim value = GetValue(values)
			Dim fontFamily = CType(values(Path.FontFamily), FontFamily)
			Dim fontSize = CType(values(Path.FontSize), Double)
			Dim fontStrech = CType(values(Path.FontStretch), FontStretch)
			Dim fontStyle = CType(values(Path.FontStyle), FontStyle)
			Dim fontWeight = CType(values(Path.FontWeight), FontWeight)

			Dim ft As FormattedText = New FormattedText(value,
					CultureInfo.CurrentCulture,
					FlowDirection.LeftToRight,
					New Typeface(fontFamily, fontStyle, fontWeight, fontStrech),
					fontSize,
					Brushes.Black)
			ret = ft.Width + fontSize ' fontSize は補正値
		End If
		Return ret
	End Function

	Private Function GetValue(values() As Object) As String
		Dim ret As String = String.Empty
		If values IsNot Nothing AndAlso values(Path.Value) IsNot Nothing Then
			ret = values(Path.Value).ToString()
		End If
		Return ret
	End Function

	Private Function GetWidth(values() As Object) As Double
		Dim ret As Double = 0.0F
		If values IsNot Nothing AndAlso values(Path.ActualWidth) IsNot Nothing Then
			ret = CDbl(values(Path.ActualWidth))
		End If
		Return ret
	End Function

End Class


C# 版のコードです。

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

public class ValueToLengthConverter : IMultiValueConverter {

	private enum Path : int {
		ActualWidth = 0,
		FontFamily,
		FontSize,
		FontStretch,
		FontStyle,
		FontWeight,
		Value
	}

	public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture) {
		bool ret = false;
		var myFontWidth = GetFontLength(values);
		var myWidth = GetWidth(values);
		if ((myWidth != 0) && (myFontWidth > myWidth)) {
			ret = true;
		}
		return ret;
	}

	public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture) {
		return null;
	}

	private double GetFontLength(Object[] values) {
		double ret = 0d;
		if ((values != null) && (values[(int)Path.FontFamily] != null)) {
			var value = GetValue(values);
			var fontFamily = (FontFamily)values[(int)Path.FontFamily];
			var fontSize = (double)values[(int)Path.FontSize];
			var fontStrech = (FontStretch)values[(int)Path.FontStretch];
			var fontStyle = (FontStyle)values[(int)Path.FontStyle];
			var fontWeight = (FontWeight)values[(int)Path.FontWeight];

			var ft = new FormattedText(value,
					CultureInfo.CurrentCulture,
					FlowDirection.LeftToRight,
					new Typeface(fontFamily, fontStyle, fontWeight, fontStrech),
					fontSize,
					Brushes.Black);
			ret = ft.Width + fontSize / 2 ; // fontSize / 2 は補正値
		}
		return ret;
	}

	private string GetValue(Object[] values) {
		string ret  = string.Empty;
		if ((values != null) && (values[(int)Path.Value] != null)) {
			ret = values[(int)Path.Value].ToString();
		}
		return ret;
	}

	private double GetWidth(Object[] values) {
		double ret = 0d;
		if ((values != null) && (values[(int)Path.ActualWidth] != null)) {
			ret = (double)values[(int)Path.ActualWidth];
		}
		return ret;
	}
}
<Window x:Class="MainWindow"
		xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
		xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
		xmlns:igDP="http://infragistics.com/DataPresenter"
		xmlns:local="clr-namespace:WpfApplication1"
		Title="MainWindow" Height="350" Width="525" >
	<Window.Resources>
		<local:ValueToLengthConverter x:Key="converter" />
		<Style TargetType="{x:Type igDP:CellValuePresenter}">
			<Style.Triggers>
				<MultiDataTrigger>
					<MultiDataTrigger.Conditions>
						<Condition Value="True">
							<Condition.Binding>
								<MultiBinding Converter="{StaticResource converter}">
									<Binding Path="ActualWidth" RelativeSource="{RelativeSource self}" />
									<Binding Path="FontFamily" RelativeSource="{RelativeSource self}" />
									<Binding Path="FontSize" RelativeSource="{RelativeSource self}" />
									<Binding Path="FontStretch" RelativeSource="{RelativeSource self}" />
									<Binding Path="FontStyle" RelativeSource="{RelativeSource self}" />
									<Binding Path="FontWeight" RelativeSource="{RelativeSource self}" />
									<Binding Path="Value" RelativeSource="{RelativeSource self}" />
								</MultiBinding>
							</Condition.Binding>
						</Condition>
					</MultiDataTrigger.Conditions>
					<Setter Property="ToolTip" 
								Value="{Binding RelativeSource={x:Static RelativeSource.Self}, Path=Value}"  />
				</MultiDataTrigger>
			</Style.Triggers>
		</Style>
	</Window.Resources>
	<Grid>
		<Grid.RowDefinitions>
			<RowDefinition Height="50" />
			<RowDefinition />
		</Grid.RowDefinitions>
		<igDP:XamDataGrid Grid.Row="1" BindToSampleData="True" />
	</Grid>
</Window>


するとこうなります。



この Tips ホントに需要あるのか怪しいですが、Forms で出来たことが WPF だと出来なくなってるよー!というお叱りに対処するには欠かせない(?)テクニックですよね(汗


WPF FAQ の目次に戻る