网上有很多关于TextBlock的基础教程,但对于实际使用却远远不够。加上相关资料较少,于是我写了本帖作为补充。
基础
TextBlock常用的添加文本方式是直接设置Text属性。但需要显示不同字体或颜色时单一性的Text就远远不够了。这时就要需要用到Run这个类。每个Run对象可以设置独立的字体、字体大小、颜色等等属性。
XAML代码
<TextBlock x:Name="tb" FontSize="14" TextWrapping="Wrap">
<Run Foreground="Blue" FontWeight="Bold">第一 行</Run>
<LineBreak/>
<Run Text="第二 行\n" FontSize="16"/>
</TextBlock>
注意:
1、第一个Run和第二个之间的区别。上述代码以空格为例:第一个Run遵循的是xml写法,第二个则是代码风格。
2、换行除了用换行符,也可以用LineBreak。
3、TextBlock中的FontSize会令其中的所有Run的FontSize都是14(默认12),可以单独修改(如第二个Run)。
4、不建议同时使用TextBlock的Text属性和Run。
CS代码
相对于xaml方式,在cs代码中设置文本是一种动态设置方式。
Run run = new Run("第一行");
run.Foreground = Brushes.Blue;
run.FontWeight = FontWeights.Bold;
tb.Inlines.Add(run); // 添加到末尾
为方便在多线程执行任务时打印进度信息,可以封装成一个函数(函数Add将Run追加到结尾处。提醒:字符串需要有转义字符 \n 才能换行):
/// <summary>
/// 追加文本到TextBlock(可用于多线程)
/// </summary>
/// <param name="text">文本内容</param>
/// <param name="color">字体颜色</param>
/// <param name="size">字体大小</param>
/// <param name="isBold">是否粗体</param>
private void addText(string text, SolidColorBrush color, int size = 14, bool isBold = false) {
Dispatcher.Invoke(() => {
tb.Inlines.Add(new Run(text) {
Foreground = color,
FontSize = size,
FontWeight = isBold ? FontWeights.Bold : FontWeights.Normal
});
});
}
高级
TextBlock除了可以显示文字,还可以显示表情或图标。说是表情,实则是一种“特殊”字体——矢量图SVG。具代表性的是iconfont(网上有很多关于WPF如果使用iconfont的帖子,这里就不赘述了)。效果:
图中的“new” 是矢量图。下面两种方式都能得到图片中的效果。
XAML代码
<TextBlock x:Name="tb" FontSize="14" TextWrapping="Wrap">
<Run Foreground="Blue" FontWeight="Bold">第一行</Run>
<LineBreak/>
<Run Text="第二行" FontSize="16"/>
<Path x:Name="newIcon" Width="32" Height="16" VerticalAlignment="Bottom" Data="{StaticResource NewIcon}" Fill="Red" Stretch="Fill"/>
</TextBlock>
CS代码
string svg = "M505.6512 39.0144c-261.2224 3.4816-470.1184 218.112-466.6368";
System.Windows.Shapes.Path path = new System.Windows.Shapes.Path {
Width = 32,
Height = 16,
VerticalAlignment = VerticalAlignment.Bottom,
Data = Geometry.Parse(svg),
Fill = Brushes.Red,
Stretch = Stretch.Fill // 重要。默认None(不显示)
};
tb.Inlines.Add(path);
通过以上对TextBlock的了解,我们就可以实现一些具有实用性的功能 ^_^
创意玩法
有如下使用情景:当用户打开程序后,后台与服务器连接并检查是否有新版本发布。当存在新版本时我们期望有所提示能令用户察觉。例如在菜单项MenuItem的文本后面显示一个图标,效果如下(有新版本时显示“new”图标,否则不显示):
这里主要分为两部分:MenuItem.Icon(左边的绿色图标)和MenuItem.Header(右边的文字+红色图标)。代码:
<Menu Background="Transparent">
<MenuItem Header="帮助">
<MenuItem FontSize="14">
<MenuItem.Icon>
<Path Data="{StaticResource UpdateIcon}" Fill="Green" Stretch="Fill"/>
</MenuItem.Icon>
<MenuItem.Header>
<TextBlock>
<Run Text="检查更新 "/>
<Path x:Name="newIcon" Width="32" Height="16" Stretch="Fill" VerticalAlignment="Bottom" Data="{StaticResource NewIcon}"/>
</TextBlock>
</MenuItem.Header>
</MenuItem>
<Separator/>
</MenuItem>
</Menu>
于是利用前面讲到的内容就能轻松实现在菜单项MenuItem的文字后加图标的效果了。
剩下的动态显示图标的操作就简单了,只需要在cs代码中设置Path的Fill属性的颜色就可以了:
new System.Threading.Thread(() => {
// TODO:你的检查更新的业务代码
Dispatcher.Invoke(() => {
// 有两种方式设置Fill。根据实际情况选择吧
// 1、直接设置颜色
newIcon.Fill = Brushes.Red;
// 2、调用资源文件中的颜色
newIcon.SetResourceReference(Shape.FillProperty, "ColorBrush");
});
}).Start();
注意:在上面的xaml代码中并没对newIcon设置Fill,所以初始状态下“new”图标是不显示的。如果需要在cs代码中令显示的图标隐藏起来不显示(例如:热更新完成),将Fill设置为Brushes.Transparent(透明)即可。
进阶
上面的创意玩法用于单一选项时还可以。但如果有多个Path(或组件)需要标记时,为了修改而命名多个控件显然不是一个好的操作。为了能够在诸如TreeView之类的容器中实现控制多个控件的效果,这里对创意玩法的代码做一些调整。
下面以“年级”TreeView为例说明:TreeView中有不同学部,学部之下有不同年级(如下)。假设现在要将高二的Path由隐藏转为可视。
P.S.. 实际应用中可以使用模版的方式实现,这里为了方便讲解就不用了。
<TreeView x:Name="tree">
<TreeViewItem Header="小学" IsExpanded="True" IsSelected="False" FontSize="16">
</TreeViewItem>
<TreeViewItem Header="初中" IsExpanded="True" IsSelected="False" FontSize="16">
</TreeViewItem>
<TreeViewItem Header="高中" IsExpanded="True" IsSelected="False" FontSize="16">
<TreeViewItem FontSize="14">
<TreeViewItem.Header>
<TextBlock>
<Run Text="高一"/>
<!-- 	是制表符 -->
<Run Text="		"/>
<Path Data="M535.9 165.2l96.5 195.4c3.9 7.9 11.4 13.3 20.1 14.6l215.7 31.3c21.9 3.2 30.6 30 14.8 45.5L726.8 604.1c-6.3 6.1-9.1 14.9-7.7 23.6L756 842.5c3.7 21.8-19.1 38.4-38.7 28.1L524.4 769.2c-7.8-4.1-17-4.1-24.8 0L306.7 870.6c-19.6 10.3-42.4-6.3-38.7-28.1l36.8-214.8c1.5-8.6-1.4-17.5-7.7-23.6L141.1 452c-15.8-15.4-7.1-42.3 14.8-45.5l215.7-31.3c8.7-1.3 16.2-6.7 20.1-14.6l96.5-195.4c9.7-19.9 37.9-19.9 47.7 0z" Margin="0,-3" Visibility="Hidden" Height="16" Width="16" Fill="Orange" Stretch="Fill"/>
</TextBlock>
</TreeViewItem.Header>
</TreeViewItem>
<TreeViewItem FontSize="14">
<TreeViewItem.Header>
<TextBlock>
<Run Text="高二"/>
<Run Text="		"/>
<Path Data="M535.9 165.2l96.5 195.4c3.9 7.9 11.4 13.3 20.1 14.6l215.7 31.3c21.9 3.2 30.6 30 14.8 45.5L726.8 604.1c-6.3 6.1-9.1 14.9-7.7 23.6L756 842.5c3.7 21.8-19.1 38.4-38.7 28.1L524.4 769.2c-7.8-4.1-17-4.1-24.8 0L306.7 870.6c-19.6 10.3-42.4-6.3-38.7-28.1l36.8-214.8c1.5-8.6-1.4-17.5-7.7-23.6L141.1 452c-15.8-15.4-7.1-42.3 14.8-45.5l215.7-31.3c8.7-1.3 16.2-6.7 20.1-14.6l96.5-195.4c9.7-19.9 37.9-19.9 47.7 0z" Margin="0,-3" Visibility="Hidden" Height="16" Width="16" Fill="Orange" Stretch="Fill"/>
</TextBlock>
</TreeViewItem.Header>
</TreeViewItem>
<TreeViewItem FontSize="14">
<TreeViewItem.Header>
<TextBlock>
<Run Text="高三"/>
<Run Text="		"/>
<Path Data="M535.9 165.2l96.5 195.4c3.9 7.9 11.4 13.3 20.1 14.6l215.7 31.3c21.9 3.2 30.6 30 14.8 45.5L726.8 604.1c-6.3 6.1-9.1 14.9-7.7 23.6L756 842.5c3.7 21.8-19.1 38.4-38.7 28.1L524.4 769.2c-7.8-4.1-17-4.1-24.8 0L306.7 870.6c-19.6 10.3-42.4-6.3-38.7-28.1l36.8-214.8c1.5-8.6-1.4-17.5-7.7-23.6L141.1 452c-15.8-15.4-7.1-42.3 14.8-45.5l215.7-31.3c8.7-1.3 16.2-6.7 20.1-14.6l96.5-195.4c9.7-19.9 37.9-19.9 47.7 0z" Margin="0,-3" Visibility="Hidden" Height="16" Width="16" Fill="Orange" Stretch="Fill"/>
</TextBlock>
</TreeViewItem.Header>
</TreeViewItem>
</TreeViewItem>
</TreeView>
后台查找目标代码:
foreach (TreeViewItem tvi in tree.Items) { // 获取一级控件,即:小学、初中、高中
foreach (TreeViewItem child in tvi.Items) { // 获取二级控件,例:高一、高二、高三
header = ((Run)((TextBlock)child.Header).Inlines.ElementAt(0)).Text;
// 只有标题为“高二”的组件可显示,其它隐藏
((Path)((InlineUIContainer)((TextBlock)child.Header).Inlines.ElementAt(4)).Child).Visibility = header.Equals("高二") ? Visibility.Visible : Visibility.Hidden;
}
}
解析:首先通过 (TextBlock)child.Header 得到自定义的Header控件。然后通过属性 Inlines 得到TextBlock的所有内容。(这里开始有点绕)Inlines得到的是InlineCollection类对象(相当于数组),它装载的都是Inline类对象。平时在TextBlock中使用的LineBreak、Run、Span等等都是派生自Inline类。所以属性Inlines包含了在xaml中定义的文本和自定义控件。
注意:这里有个坑,例子中TextBlock里面定义了3个元素,但实际有5个元素;程序会在元素与元素间填充一个内容为空格的Run对象(解决方法:多个元素写在同一行)。
还有一点,按照TextBlock对内容的要求,正常情况下是不能直接添加控件的,必须是Inline类对象。这里就需要用到InlineUIContainer 类了,它的作用相当于桥梁,使普通控件能够添加到TextBlock里,使其表现力更加丰富。例子中当添加Path到TextBlock中时,实际上是帮我们做了如下的简化(两者效果一样):
// 标准版
<TextBlock>
<Run Text="高一"/>
<Run Text="		"/>
<InlineUIContainer>
<Path Data="M535.9 165.2l96.5 195.4c3.9 7.9 11.4 13.3 20.1 14.6l215.7 31.3c21.9 3.2 30.6 30 14.8 45.5L726.8 604.1c-6.3 6.1-9.1 14.9-7.7 23.6L756 842.5c3.7 21.8-19.1 38.4-38.7 28.1L524.4 769.2c-7.8-4.1-17-4.1-24.8 0L306.7 870.6c-19.6 10.3-42.4-6.3-38.7-28.1l36.8-214.8c1.5-8.6-1.4-17.5-7.7-23.6L141.1 452c-15.8-15.4-7.1-42.3 14.8-45.5l215.7-31.3c8.7-1.3 16.2-6.7 20.1-14.6l96.5-195.4c9.7-19.9 37.9-19.9 47.7 0z" Margin="0,-3" Visibility="Hidden" Height="16" Width="16" Fill="Orange" Stretch="Fill"/>
</InlineUIContainer>
</TextBlock>
// 简化版
<TextBlock>
<Run Text="高一"/>
<Run Text="		"/>
<!--UI控件自动封装到InlineUIContainer中-->
<Path Data="M535.9 165.2l96.5 195.4c3.9 7.9 11.4 13.3 20.1 14.6l215.7 31.3c21.9 3.2 30.6 30 14.8 45.5L726.8 604.1c-6.3 6.1-9.1 14.9-7.7 23.6L756 842.5c3.7 21.8-19.1 38.4-38.7 28.1L524.4 769.2c-7.8-4.1-17-4.1-24.8 0L306.7 870.6c-19.6 10.3-42.4-6.3-38.7-28.1l36.8-214.8c1.5-8.6-1.4-17.5-7.7-23.6L141.1 452c-15.8-15.4-7.1-42.3 14.8-45.5l215.7-31.3c8.7-1.3 16.2-6.7 20.1-14.6l96.5-195.4c9.7-19.9 37.9-19.9 47.7 0z" Margin="0,-3" Visibility="Hidden" Height="16" Width="16" Fill="Orange" Stretch="Fill"/>
</TextBlock>
嵌入一个面板也是可以的(官方例子)。
正是由于TextBlock对外部控件的处理方式,所以cs代码中索引4得到是InlineUIContainer对象而不是Path对象。然后通过属性 Child 得到Path对象。
吐槽一句,个人认为WPF还是很优秀的(至少在我用过的UI中)。虽有官方中文文档支持,可惜细节的描述不够,网上资讯少之又少。今天写的这些还是自己看文档 + 不断调试摸索出来的。不然谁知道它可以简写,取组件还得通过InlineUIContainer对象啊。