前陣子遇到一個對齊上的麻煩,這個麻煩的點在於需要在渲染前取得所有 TextBlock 中最長的那一個當作所有 TextBlock 的寬度,類似 DataGrid 中 SizeToCell 那種效果。問題來了,渲染後的 ActualWidth 才有意義,如果要依賴 ActualWidth 的變更好像有點太麻煩了;所幸可以利用 FormattedText 事前計算,讓我們來看看這怎麼做。
原始無法對齊的狀況
第一個範例是展示造成困擾的狀況,完整程式碼可以參考 範例001,簡單摘錄畫面 xaml 如下:
<Grid Margin="12">
<Grid.RowDefinitions >
<RowDefinition Height="*"/>
<RowDefinition Height="72"/>
</Grid.RowDefinitions>
<ItemsControl ItemsSource="{Binding People}">
<ItemsControl.ItemTemplate >
<DataTemplate >
<Grid>
<Grid.ColumnDefinitions >
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<TextBlock Text="{Binding Name}" Grid.Column="0" FontFamily="新細明體" FontSize="12"
HorizontalAlignment="Stretch" Background="SkyBlue"/>
<TextBlock Text="{Binding City}" Grid.Column="1" FontFamily="新細明體" FontSize="12"
HorizontalAlignment="Stretch" Background="YellowGreen"/>
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<StackPanel Orientation="Horizontal" Grid.Row="1">
<Button Content="Add" Command="{Binding AddCommand}" Width="64" Height="32"/>
</StackPanel>
</Grid>
畫面上的 ItemTemplate 中的 Grid 有兩個 Column,寬度都設定為 Auto。執行後會是這個樣子:
很明顯歪七扭八,而我期待的是這個樣子:
FormattedText 用法簡介
FormattedText 在這邊的用法非常簡單,只要呼叫建構式產生一個執行個體就能取得寬度,這個建構式參數唯一比較麻煩的是 Typeface。
(1) 建立 Typeface,範例裡面的各參數是固定的,你可以依據需求使用變數或繫結到控制項的 Dependency Property。
new Typeface(new FontFamily("新細明體"), FontStyles.Normal, FontWeights.Normal, FontStretches.Normal);
(2) 使用上述的 Typeface [引數名稱 typeface] 、要渲染的字串 [引數名稱 text] 以及其他必要引數建立 FormattedText
FormattedText formattedText = new FormattedText(text, CultureInfo.CurrentCulture, FlowDirection.LeftToRight, typeface, 12.0, Brushes.Black, 1.0);
(3) 透過 Formatted.Width 屬性就可以取得渲染後的寬度。
改善後的範例
第二個範例是利用 FormattedText 解決的情境,完整程式碼可以參考 範例002:
public class MainViewModel : NotifyPropertyBase
{
private ObservableCollection<Person> _people;
public ObservableCollection<Person> People
{
get { return _people; }
set { SetProperty(ref _people, value); }
}
public MainViewModel()
{
var people = new ObservableCollection<Person>()
{
new Person {Name = "Amuro", City = "New York"},
new Person {Name = "Char Aznable", City = "New York"},
new Person {Name = "Bright Noa", City = "Tokyo"},
new Person {Name = "Kamille Bidan", City = "Las Vegas"},
new Person {Name = "Emma", City = "Singapore"},
new Person {Name = "Fa Yuiry", City = "Singapore"},
new Person {Name = "Banagher", City = "Taipei"},
new Person {Name = "Marida Cruz", City = "Las Vegas"},
new Person {Name = "Suletta Mercury", City = "Sydney"},
new Person {Name = "Mikazuki Augus", City = "Kaohsiung"},
};
#region compute width before binding
ComputeWidthOfText(people);
#endregion
People = people;
People.CollectionChanged += People_CollectionChanged;
}
public ICommand AddCommand
{
get => new RelayCommand(x =>
{
People.Add(new Person { Name = "Shrek family", City = "Far Far Away Kingdom" });
});
}
#region new method for collection changed
private void People_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
if (e.NewItems == null) return;
var typeface = GetTypeface();
foreach (var item in e.NewItems.Cast<Person>())
{
ComputeEachTextWidth(typeface, item);
}
}
#endregion
#region add new properties
private double _widthOfName;
public double WidthOfName
{
get { return _widthOfName; }
set { SetProperty(ref _widthOfName, value); }
}
private double _widthOfCity;
public double WidthOfCity
{
get { return _widthOfCity; }
set { SetProperty(ref _widthOfCity, value); }
}
#endregion
#region compute width before rendering
private void ComputeWidthOfText(ObservableCollection<Person> people)
{
var typeface = GetTypeface();
foreach (var item in people)
{
ComputeEachTextWidth(typeface, item);
}
}
private void ComputeEachTextWidth(Typeface typeface, Person item)
{
var formattedName = GetFormattedText(item.Name, typeface);
if (formattedName.Width > WidthOfName)
{
WidthOfName = formattedName.Width;
}
var formattedCity = GetFormattedText(item.City, typeface);
if (formattedCity.Width > WidthOfCity)
{
WidthOfCity = formattedCity.Width;
}
}
private Typeface GetTypeface()
{
return new Typeface(new FontFamily("新細明體"), FontStyles.Normal, FontWeights.Normal, FontStretches.Normal);
}
private FormattedText GetFormattedText(string text, Typeface typeface)
{
FormattedText formattedText = new FormattedText(text, CultureInfo.CurrentCulture, FlowDirection.LeftToRight, typeface, 12.0, Brushes.Black, 1.0);
return formattedText;
}
#endregion
}
View Model 裡面會多出一些計算用的方法以及要繫結到 Column Width 的屬性 ( WidthOfName 與 WidthOfCity );同時在 People Collection 的時候,需要計算新進的字串寬度是否會比原來的還寬。
Xaml 畫面就要補上 ColumnDefinition 資料繫結:
<Grid Margin="12">
<Grid.RowDefinitions >
<RowDefinition Height="*"/>
<RowDefinition Height="72"/>
</Grid.RowDefinitions>
<ItemsControl ItemsSource="{Binding People}">
<ItemsControl.ItemTemplate >
<DataTemplate >
<Grid>
<Grid.ColumnDefinitions >
<ColumnDefinition Width="{Binding RelativeSource={RelativeSource AncestorType=ItemsControl}, Path=DataContext.WidthOfName}"/>
<ColumnDefinition Width="{Binding RelativeSource={RelativeSource AncestorType=ItemsControl}, Path=DataContext.WidthOfCity}"/>
</Grid.ColumnDefinitions>
<TextBlock Text="{Binding Name}" Grid.Column="0" FontFamily="新細明體" FontSize="12"
HorizontalAlignment="Stretch" Background="SkyBlue"/>
<TextBlock Text="{Binding City}" Grid.Column="1" FontFamily="新細明體" FontSize="12"
HorizontalAlignment="Stretch" Background="YellowGreen"/>
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<StackPanel Orientation="Horizontal" Grid.Row="1">
<Button Content="Add" Command="{Binding AddCommand}" Width="64" Height="32"/>
</StackPanel>
</Grid>
順便附帶按下 Add Button 後的畫面: