如何在TextBlock渲染前取得字串的渲染寬度

  • 40
  • 0

前陣子遇到一個對齊上的麻煩,這個麻煩的點在於需要在渲染前取得所有 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 後的畫面: