jk's notes
  • CH05 内容控件 (Content Control)

CH05 内容控件 (Content Control)

内容控件用于展示用户需要看到的内容, 是一般不会被修改的内容.

作者描述了控件的分类, 可以分为内容控件, 布局控件, 用户交互控件. 并对其进行了说明. 然后作者将 ListView 和 TreeView 定位为内容控件.

每一个控件都含有大量的事件与属性, 这里主要介绍常用的.

然后作者简要说明了一下属性, 方法, 与事件, 为没基础的读者进行说明. 例如:

<Button Name="btnClear" Content="Button" Click="btnClear_Click" />

WPF 包含大量对象, 但本书不会介绍所有, 有些控件对象并非直接使用. 例如下面, 通过 ToolTip 属性来设置 ToolTip 对象:

<TextBox ToolTip="自定义 ZIP 编码" />

如果要使用独立的 ToolTip 对象, 代码应该是:

<TextBox>
	<TextBox.ToolTip>
  	<ToolTip>
    	自定义 ZIP 编码
    </ToolTip>
  </TextBox.ToolTip>
</TextBox>

如深入浅出里面介绍的, 简介利于阅读的 XAML 代码更合适. 比较推崇第一种语法.

image-20250618150745825

这里的 ZIP 是邮政编码的含义

控件概览

下面表格列举的控件的作用:

控件作用
Border为单个子控件绘制一个边框.
BulletDecorator在列表前显示标号.
DocumentViewer用于显示 DixedDocument 的内容, 例如 XPS 文件.
FlowDocumentPageViewer使用 viewing mode 在页面中展示 FlowDocument.
FlowDocumentReader使用 Page, Scroll, TwoPage mode 显示 FlowDocument.
FlowDocumentScrollViewer使用 Scroll modal 显示 FlowDocument.
GroupBox给单个子元素添加一个标题与边框.
Image像是一张图片.
Label显示一段用户不能修改的文本.
ListView显示一组数据, 在一组布局中.
MediaElement播放音频或视频.
Popup展示一个浮动区域, 例如 ContentMenu 或 ToolTip.
ProgressBar展示进度信息.
Separator在菜单中展示一条水平线.
TextBlock展示一段只读的文本, 相比与 Label, 它可以带有简单格式.
ToolTip鼠标悬浮时弹出提示.
TreeView使用树结构显示组织架构.

下面讨论这些控件. 将其分为三类: 图形, 文本, 空间控件.

图形控件

图形控件就是用来显示图形, 包括线, 形状, 图片, 以及多媒体输出等.

通常在设计时这些控件就已设置, 不需要在运行时进行修改, 也不需要用用户交互.

Image

该控件用于展示图片, 使用也很简单

  • 首先打开项目, 右键添加现有项.
  • 选择图片, 然后添加.
  • 然后设置 Image 控件的 Source 属性即可.

Image 有两个比较重要的属性: Stretch 和 StretchDirection.

Stretch 控制是否缩放, 取值有:

  • None 不进行缩放, 原始尺寸输出.
  • Fill 拉伸图片, 可能会变形.
  • Uniform 图片宽高同时被拉伸, 并尽可能大的填满控件.
  • UniformToFill 图片宽高被拉伸. 但宽高尺寸与控件不同, 可能会显示不完整.

StretchDirection 控制缩放方向, 取值有:

  • UpOnly 只允许放大
  • DownOnly 只允许缩小
  • Both 可同时缩小与放大

使用语法示例:

<Image 
    Width="Auto"
    Height="Auto"
    HorizontalAlignment="Stretch"
    VerticalAlignment="Stretch"
    Source="/1690436791750269.png" 
    Stretch="Uniform" StretchDirection="Both" 
    />

MediaElement

用于播放媒体音乐与视频. 设计时, 其最重要的属性是 Source, 用于指定需要播放的文件.

默认情况下, MediaElement 加载文件后就会立即播放. 如果要手动控制, 设置 LoadedBehavior 属性为 Manual. 然后再给控件设置 x:Name, 然后通过调用方法来控制播放暂停等操作.

可以使用的方法有: Play(), Pause().

播放进度可以通过读写 Position 属性来实现, 例如: mm.Position = new TimeSpan(0).

MediaElement 没有足够的属性来通过 XAML 来控制播放, 需要使用后台代码来实现.

但 XAML 中有一个 SoundPlayerAction 的命令, 可以用来播放音频文件. 可以通过触发器来调用该命令. 例如, 点击 btnSoundPlayerAction 时触发:

<Window.Triggers>
	<EventTrigger RoutedEvent="ButtonBase.Click"
                SourceName="btnSoundPlayerAction">
  	<SoundPlayerAction Source="xxx.wav"/>
  </EventTrigger>
</Window.Triggers>

文本控件

文本控件主要用于显示文本.

FlowDocument 和 FixedDocument 除了可以显示文本, 还可以显示其他内容. 但主要还是为了排版文本. 这里作者将其归类为文本控件.

DocumentViewer

FlowDocument 可以根据运行时容器的调整实时的重新排列内容.

相反 DocumentViewer 是固定的, 不会重拍内容, 就像 PDF 一样.

下面的代码创建了 FixedDocument 对象, 并包含两页的内容. 为了节省空间, 省略了不重要的代码.

jk: 代码似乎没跑起来, 不明确的错误. 代码结构是 DocumentViewer 中首先设置了背景为线性渐变. 然后 DocumentViewer 中包含一个 FixedDocument, 在 FixedDocument 中又包含了两个 PageContent, 即为两页内容. 在 PageContent 中又包含了 FixedPage.

FlowDocument

该控件用于展示文档, 其内可以包含:

  • Paragraphs 段落.
  • Tables 表格.
  • Lists 列表.
  • Floaters 浮动模块.
  • Figures 图片.
  • 用户交互控件, 例如按钮, 文本框等.
  • 3D 对象.

更多细节在 Ch21 会介绍. 下面介绍 FlowDocument 的不同样式. 如果将其放在 Window 中, 就好像使用了 FlowDocumentReader 一样.

FlowDocumentPageViewer (略)

FlowDocumentPageViewer 以页模式展示 FlowDocument. 该模式下仅显示一页的内容. 控件下方可以实现切换.

页面大小由 PageHeight 和 PageWidth 决定.

FlowDocumentReader (略)

FlowDocumentReader 使用三种模式来查看页面, 分别是 Page, Scroll, 和 TwoPage.

FlowDocumentScrollViewer (略)

Label

用于显示不可变文本, 使用 Content 来设置内容. 可以设置前景背景色, 字体字号等.

Label 显示的文本不可更改也不允许获得用户焦点, 所以无法进行复制等操作, 如果需要不可改, 但要可以复制选中, 可以使用 TextBox, 然后设置其只读 (IsReadOnly) 即可.

Pop-Up

Popup 会在窗体显示一个浮动的区域. 类似于上下文菜单或 ToolTip, 不同的是它不会自动显示与隐藏, 需要通过编码来控制.

一般添加鼠标进入 (MouseEnter) 与鼠标离开 (MouseLeave) 事件, 在事件中设置 IsOpen 属性 true 或 false 来控制显示与隐藏.

下面是控制显示位置的属性

属性含义
PlacementTarget弹出窗基于该控件进行定位, 如果省略, 则基于容器定位. 其 XAML 语法类似于 PlacementTarget="{Binding ElementName=xxx}".
Placement定位方式. 例如停靠对齐等.
PlacementRectangle如果有该属性, 会在定位目标中确认一个矩形, 基于该矩形进行定位.
HorizontalOffset水平偏移量
VerticalOffset垂直偏移量

弹出位置由 Placement 属性来控制, 其取值可以是:

值定位
Absolute基于平面左上角.
Relative基于 PlacementTarget 左上角.
Bottom
Center
Right
Left
Top
AbsolutePoint基于屏幕左上角, 由 *Offset 来定位.
Mouse基于鼠标位置定位.
MousePoint基于鼠标位置, 由 *Offset 来定位.
Custom由 CustomPopupPlacementCallback 决定.

jk: Absolute 没有实验出来

默认情况下, 除非设置 IsOpen=false, 否则弹出窗会一直显示. 可以将 StaysOpen 设置为 false, 它便会在失焦等操作后自动隐藏.

最后是动画:

  • AllowsTransparency 是否启用动画
  • PopupAnimation 动画方式. None 不使用动画, Scroll 从一个角滚动显示, Slide 从上至下卷帘门显示, Fade 淡入淡出的显示.

TextBlock

用于显示只读的文本内容. 相比于 Label, TextBlock 更为灵活:

  • 可以设置文本样式, 例如粗体, 斜体, 或下划线等.
  • 添加折行
  • 修改间距
  • 折叠文本
  • 截断文本, 并显示省略号
  • 添加装饰, 如中划线, 下划线, 上划线等
  • 使用上下标等

一些常用属性有:

属性含义
LineHeight表示两线之间的距离
LineStackingStrategy可以是 BlockLineHeight(行高由 LineHeight 决定) 或 MaxHeight (每行的高度设置为适合其内容)
TextTrimming表示如何处理不合适的单词. None 单词被截断; WordEllipsis 在单词边界处截断, 显示省略号; CharacterEllipsis 在句子边界处截断, 显示省略.
TextWrapping确定文本框的末尾是否换行. 可取值: NoWrap, Wrap, WrapWithOverflow. 过长时, WrapWithOverflow 允许单词超出 TextBlock 外, 然后截断.

TextBlock 内可以包含内联标签:

标签含义
Bold粗体.
Hyperlink超链接. 可点击.
InlineUIContainer包含其他控件.
Italic斜体.
LineBreak换行.
Run包含一段文本. 有自己独立的字体属性. 逻辑上类似于 HTML 中的 span.
Span一组内联元素的组. 类似于 Run. 感觉逻辑上与 html 中的 div 类似.
Underline下划线.

有一种写 HTML 的感觉.

<TextBlock>
    TextBlock 比 Label <Italic>强大</Italic>的多.
</TextBlock>

TextBlock 控件中也可以包含其他控件, 与文本流混排.

ToolTip

在另一个控件上弹出一个层显示信息. 一般的用法是给控件设置 ToolTip 属性.

也可以构建 ToolTip 控件来实现.

空间控件

Border

用于显示边框或背景.

一个常见的用法是, 当做分组来使用, 给页面中的一组按钮等控件分组.

jk: 逻辑上与 GroupBox 有点像.

该组件是单子组件的, 但可以包含 Grid 等组件作为子组件使用. 其常见属性:

属性含义
Background设置背景 Brush.
BorderBrush用于绘制边界的 Brush.
BorderThickness用于设置四周的边框粗细.
CornerRadius用于设置四周圆角半径. 0 则表示矩形.
<Border HorizontalAlignment="Center" VerticalAlignment="Center" 
    Width="150" Height="100" CornerRadius="20" 
    BorderBrush="#FFFF8000" BorderThickness="5">
    <Border.BitmapEffect>
        <DropShadowBitmapEffect/>
    </Border.BitmapEffect>
    <Border.Background>
        <LinearGradientBrush EndPoint="0.5,1" StartPoint="0.5,0">
            <GradientStop Color="#FF00FF00" Offset="0"/>
            <GradientStop Color="#FF004000" Offset="1"/>
        </LinearGradientBrush>
    </Border.Background>
    <TextBlock Width="100" Height="60" FontWeight="Bold" 
            TextWrapping="Wrap" 
            TextAlignment="Center" 
            Text="This TextBlock is contained inside the Border."/>
</Border>

会得到:

image-20250620102826680

BulletDecorator

该控件在列表项前显示引导点. 它是单子节点的组件.

其重要的属性是 Bullet, 理论上可以包含任何内容.

<StackPanel>
    <BulletDecorator Height="25">
        <BulletDecorator.Bullet>
            <Image Source="/1690436791750269.png" Height="20"></Image>
        </BulletDecorator.Bullet>
        <Label>一项</Label>
    </BulletDecorator>
    <BulletDecorator Height="25">
        <BulletDecorator.Bullet>
            <Image Source="/1690436791750269.png" Height="20"></Image>
        </BulletDecorator.Bullet>
        <Label>二项</Label>
    </BulletDecorator>
</StackPanel>

image-20250620111811411

有种像 HTML 中的 UL-LI 标签. 但它只是定义了一项, 需要多个组合使用.

GroupBox

常用属性有:

  • Header 头部显示的文字
  • BorderBrush 边框颜色
  • Background 描述背景颜色
  • Foreground 头部文字颜色

可以设置边框粗细 (BorderThickness) 为 0 来隐藏边框. 由于某些原因, 边框粗细大于 2 时会模糊.

该组件也是单子元素组件.

ListView

ListView 用于在一种或多种布局中展示一组数据. 可以自定义布局, 通常会使用类似 Grid 的布局. 例如:

image-20250620144657822

ListBox 的优势是显示来自数据源的数据, 例如数据库等.

jk: 应该是数据绑定的功能比较强大. 可以按照多个维度显示多组数据.

数据绑定在 ch18 介绍, 这里仅介绍一些基本内容, 来熟悉一下该控件.

要理解该案例, 需要在后台代码与 XAML 中来回切换. 下面的示例定义了一个 BookInfo 类. 有四个属性 Author, Title, Year, 和 Price.

public class BookInfo {
  public string Author { get; set; }
  public string Title { get; set; }
  public string Year { get; set; }
  public string Price { get; set; }
}

jk: 作者的代码里还有构造函数, 并采用了完整的属性, 字段定义.

然后使用 ObservableCollection<T> 来创建 BookInfo 的集合, 并初始化. 将该集合赋值给 ListView 的 DataContext 属性.

然后在 ListView 中需要指定什么数据要被现实出来 (就是绑定数据源的什么属性).

<ListView Name="lvwPeople" Background="{x:Null}" ItemSource="{Binding}">
	<ListView.View>
  	<GridView>
    	<GridViewColumn Header="Author" Width="100" DisplayMemberBinding="{Binding Path=Author}" />
      <GridViewColumn Header="Title" Width="100" DisplayMemberBinding="{Binding Path=Title}" />
    	<GridViewColumn Header="Year" Width="100" DisplayMemberBinding="{Binding Path=Year}" />
      <GridViewColumn Header="Price" Width="100" DisplayMemberBinding="{Binding Path=Price}" />
    </GridView>
  </ListView.View>
</ListView>

代码中, ListView 的 ItemSource 属性设置为 ItemSource="{Binding}" 在是在说所有的属性都是利用绑定来获取的. 本例中就是说, 控件需要使用, 附加在 DataContent 属性上的, 由 ObservableCollection 实例化的集合.

ListView 的 View 属性, 代码中的 ListView.View 定义了如何显示. GridView 则是以表格的形式进行显示, GridViewColumn 定义了每一列的显示.

GridViewColumn 的属性 Header 定义了表格列的表头内容, Width 定义了初始化的列宽. 而 DisplayMemberBinding 则定义了该怎么去找需要显示的数据.

jk: 典型的入门经典的风格, 作者解释的非常详细. 这里略.

然后作者说 ListView 非常强大, 结合触发器与 XAML 可以定义多种视图, 多种交互效果. 不幸的是, 强大代表着复杂, 细节不可能在这里说清楚. 细节可以参考微软MSDN文档.

最后运行的结果类似于

image-20250620153420589

ProgressBar

用于展示进度条, 如果应用中存在耗时操作, 建议使用它, 来给用户提供执行进度的提示.

其主要属性有: Minimunm, Maximum, 以及 Value.

但是需要注意的是, 如果在 UI 线程执行耗时操作, UI 不会被立即刷新, 也就是说耗时操作会阻塞进度条的变化.

一种办法是使用多线程, 然后作者简要解释了一下线程. 不幸的是, 控件只允许在本线程中被修改. 所以在其他线程中无法直接修改进度条控件的属性. 大多数的处理办法是在线程代码中通过 Dispatcher 对象来调用用户线程的代码.

image-20250620160118885

需要使用:

this.Dispatcher.Invoke(() => {
  prb.Value = i;
});

这个确实很麻烦, 但是还有一个更简单的办法. 使用 BackgroundWorker. 它看起来好像在后台线程与前台线程中进行必要的切换. 它会在后台线程处理任务, 然后在前台线程处理 UI 的设置.

基本用法:

  • 示例化 BackgroundWorker
  • 设置属性 WorkerReportsProgress 为 true. 该属性表示是否可以报告进度更新.
  • 注册事件: DoWork 事件, 启动时触发; ProgressChanged 事件, 进度发生变化时触发; RunWorkerCompleted 事件, 结束时触发.
  • 在 DoWork 事件处理函数中进行耗时处理, 并根据需要, 调用 <backgroundWorker>.ReportProgress(...) 方法, 来引发 ProgressChanged 事件.
  • 在 ProgressChanged 事件中操作 UI 控件, ReportProgress() 方法传入的参数可以在事件参数 e.ProgressPercentage 中获得到.
  • 在程序需要触发 BackgroundWorker 的时候调用其 RunWorkAsync() 方法.

值得研究一下其参数. https://learn.microsoft.com/zh-cn/dotnet/api/system.componentmodel.backgroundworker?view=net-9.0

然后作者详细解释了代码的执行.

Separator

Separator 在菜单项与工具栏的按钮之间绘制一条横线或一条竖线.

菜单使用 Menu 控件, 工具栏是 ToolBar 控件.

这里可以基于 VS 的数据源的向导来学习菜单控件的用法, 然后同时观察生成的 XAML 来学习其使用.

image-20250620162516352

TreeView

容器控件是 TreeView, 成员项控件是 TreeViewItem. 其中 TreeViewItem 展开与否使用 IsExpanded 属性.

基本用法举例:

<TreeView >
    <TreeViewItem Header="树节点1" IsExpanded="True">
        <TreeViewItem Header="1-1"></TreeViewItem>
        <TreeViewItem Header="1-2"></TreeViewItem>
        <TreeViewItem Header="1-3"></TreeViewItem>
    </TreeViewItem>
    <TreeViewItem Header="树节点2">
        <TreeViewItem Header="2-1"></TreeViewItem>
        <TreeViewItem Header="2-2"></TreeViewItem>
        <TreeViewItem Header="2-3"></TreeViewItem>
    </TreeViewItem>
    <TreeViewItem Header="树节点3"></TreeViewItem>
    <TreeViewItem Header="树节点4"></TreeViewItem>
    <TreeViewItem Header="树节点5"></TreeViewItem>
</TreeView>

ch18 会介绍到高级用法 (数据绑定)

小结

控件可以参考: https://learn.microsoft.com/zh-cn/dotnet/desktop/wpf/controls/control-library?redirectedfrom=MSDN

Last Updated:
Contributors: jk