- UI组件
- 桌面应用
【免费下载链接】HandyControl
Contains some simple and commonly used WPF controls
本篇技术指南围绕 HandyControl 原生控件(native_controls)中ListBox 列表框的完整样式体系展开,系统讲解ListBoxBaseStyle、ListBoxCustom以及四种布局变体(WrapPanelHorizontalListBox、WrapPanelVerticalListBox、StackPanelHorizontalListBox、StackPanelVerticalListBox)的继承关系、源码实现与实战用法。读完本文,你将掌握如何以BasedOn方式在 HandyControl 主题之上定制 ListBox、如何通过自定义ItemTemplate实现个性化数据展示,以及如何一键切换水平/垂直的 StackPanel 与 WrapPanel 布局,并理解选中态、空列表占位与选中集合双向绑定等底层机制。
一、ListBox 样式体系一览:一条继承链贯穿五种样式
HandyControl 为 ListBox 提供的样式并非彼此独立,而是构成了一条清晰的BasedOn继承链。官方文档(doc/source/handycontrol/native_controls/listBox/index.md)定义的继承关系如下:
ListBoxBaseStyle └── ListBoxCustom(保留基础属性,数据展示交由用户自定义) ├── WrapPanelHorizontalListBox(WrapPanel + 水平) ├── WrapPanelVerticalListBox(WrapPanel + 垂直) ├── StackPanelHorizontalListBox(StackPanel + 水平) └── StackPanelVerticalListBox(StackPanel + 垂直)这五种样式均定义在 Themes/Styles/ListBox.xaml 中,而同目录下还存在一个隐式默认样式(不带x:Key,作用于所有 ListBox):
<!--默认样式--> <Style BasedOn="{StaticResource ListBoxBaseStyle}" TargetType="ListBox"/>也就是说,只要你引入 HandyControl 主题资源,所有未显式指定Style的 ListBox 会自动应用ListBoxBaseStyle的外观。五种具名样式则专门用于需要差异化定制的场景。
二、ListBoxBaseStyle:所有样式的基座
2.1 设计定位
ListBoxBaseStyle是 ListBox 的默认基础样式。官方文档明确指出:不推荐直接使用,它应该始终被其它样式以BasedOn的方式使用。这样做的目的是让派生样式只需覆盖少数属性即可获得完整的默认外观,避免重复定义模板。
在自定义样式时,标准写法如下:
<Style BasedOn="{StaticResource ListBoxBaseStyle}" TargetType="ListBox"/>BasedOn会完整继承基样式中的所有 Setter 与模板,你可以在此基础上继续追加自己的 Setter,例如覆盖背景色、圆角、间距等。
2.2 源码剖析:基样式的组成
ListBoxBaseStyle的真正实现位于 Themes/Styles/Base/ListBoxBaseStyle.xaml。它实际上由两部分组成:
(1)ListBoxItemBaseStyle(列表项基础样式)
<Style x:Key="ListBoxItemBaseStyle" BasedOn="{StaticResource BaseStyle}" TargetType="ListBoxItem"> <Setter Property="Padding" Value="10,0"/> <Setter Property="MinHeight" Value="{StaticResource DefaultControlHeight}"/> <Setter Property="Background" Value="{DynamicResource RegionBrush}"/> <Setter Property="BorderBrush" Value="Transparent"/> <Setter Property="BorderThickness" Value="0"/> <Setter Property="Margin" Value="0,0,0,2" /> <!-- 模板:带圆角的 Border + ContentPresenter --> </Style>列表项模板的关键交互触发器(Style.Triggers)定义了完整的选中态反馈:
| 状态 | 触发条件 | 效果 |
|---|---|---|
| 悬停 | IsMouseOver="True" | 背景切换为SecondaryRegionBrush |
| 选中 | IsSelected="True" | 背景切换为主题主色PrimaryBrush,前景切换为TextIconBrush |
| 失焦选中 | IsSelected=True且Selector.IsSelectionActive=false且hc:VisualElement.StaysHighlighted=false | 背景切换为DarkDefaultBrush,避免窗口失焦时选中项消失 |
| 禁用 | IsEnabled=false | 整体不透明度降至 0.4 |
| 边缘内容 | hc:EdgeElement.ShowEdgeContent=true | 模板切换为「16×16 左侧内容 + 正文」的横向布局 |
其中圆角由附加属性hc:BorderElement.CornerRadius提供,默认取DefaultCornerRadius资源。
(2)ListBoxBaseStyle(列表框基础样式)
<Setter Property="Background" Value="{DynamicResource RegionBrush}"/> <Setter Property="BorderBrush" Value="{DynamicResource BorderBrush}"/> <Setter Property="BorderThickness" Value="1"/> <Setter Property="hc:ScrollViewer.HorizontalScrollBarVisibility" Value="Disabled"/> <Setter Property="hc:ScrollViewer.VerticalScrollBarVisibility" Value="Auto"/> <Setter Property="hc:ScrollViewer.CanContentScroll" Value="true"/> <Setter Property="hc:ScrollViewer.PanningMode" Value="Both"/> <Setter Property="hc:BorderElement.CornerRadius" Value="{StaticResource DefaultCornerRadius}"/> <Setter Property="ItemContainerStyle" Value="{StaticResource ListBoxItemBaseStyle}"/>值得注意的细节:
- 滚动行为通过 HandyControl 附加属性控制:默认水平滚动条禁用(
Disabled)、垂直滚动条自动(Auto)、允许内容滚动(CanContentScroll=true,保证虚拟化)、支持双向触控平移(PanningMode=Both); - 空列表占位机制:模板中使用
hc:ToggleBlock绑定HasItems——有数据时展示ScrollViewer + ItemsPresenter,无数据时展示hc:Empty占位控件,避免空白区域带来的突兀感; - 圆角、边框、背景全部采用
DynamicResource引用主题资源,因此随皮肤(SkinDark / SkinDefault / SkinViolet 等)切换自动变色。
此外,ListBox.xaml 还基于ListBoxBaseStyle提供了紧凑变体ListBox.Small(配套ListBoxItemBaseStyle.Small,Padding=6,0、MinHeight=24),适合工具栏、下拉类紧凑场景。
三、ListBoxCustom:个性化定制数据的入口
3.1 设计定位与用法
ListBoxCustom继承自ListBoxBaseStyle,保留了 ListBox 的基本属性样式(边框、背景、滚动、选中反馈等),而数据显示样式完全交由当前用户自定义,从而实现个性化定制。官方文档给出的完整用例:
<ListBox Margin="10" ItemsSource="{Binding Datas}" Style="{DynamicResource ListBoxCustom}"> <ListBox.ItemTemplate> <DataTemplate> <Border BorderThickness="1" BorderBrush="Black" Margin="0,5"> <DockPanel LastChildFill="True"> <Path DockPanel.Dock="Left" Fill="YellowGreen" Width="20" Margin="10,0,10,0" HorizontalAlignment="Center" Data="{DynamicResource BubbleTailGeometry}"></Path> <TextBlock Padding="10" Text="{Binding Name}"></TextBlock> </DockPanel> </Border> </DataTemplate> </ListBox.ItemTemplate> </ListBox>这段代码展示了典型用法:ItemsSource绑定数据集合,Style通过DynamicResource引用ListBoxCustom,再通过ItemTemplate自定义每个条目的视觉呈现(此例为一个带左边小图标、右侧文本的气泡式条目)。注意官方示例统一使用DynamicResource引用样式,便于在运行时切换主题或动态替换样式资源。
3.2 源码剖析:ListBoxItemCustom 做了什么
ListBoxCustom之所以能实现"完全自定义显示",关键在于它同时替换了容器项样式(源码见 ListBox.xaml):
<Style x:Key="ListBoxItemCustom" TargetType="ListBoxItem"> <Setter Property="SnapsToDevicePixels" Value="True"/> <Setter Property="FocusVisualStyle" Value="{x:Null}"/> <Setter Property="Padding" Value="0"/> <Setter Property="Background" Value="Transparent"/> <Setter Property="BorderBrush" Value="Transparent"/> <Setter Property="BorderThickness" Value="0"/> <Setter Property="Template"> <Setter.Value> <ControlTemplate TargetType="ListBoxItem"> <ContentPresenter HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}" SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" VerticalAlignment="{TemplateBinding VerticalContentAlignment}"/> </ControlTemplate> </Setter.Value> </Setter> </Style> <Style x:Key="ListBoxCustom" BasedOn="{StaticResource ListBoxBaseStyle}" TargetType="ListBox"> <Setter Property="ItemContainerStyle" Value="{StaticResource ListBoxItemCustom}"/> <Setter Property="HorizontalContentAlignment" Value="Stretch"/> </Style>对比默认的ListBoxItemBaseStyle可以看到:
- 剥离了所有默认装饰:背景、边框全部置为
Transparent,Padding=0,移除焦点虚线框(FocusVisualStyle={x:Null}); - 模板瘦身为纯
ContentPresenter:不再包裹默认 Border,因此每个条目的背景、边框、间距完全由你的ItemTemplate决定; HorizontalContentAlignment=Stretch:让ItemTemplate的根元素横向拉伸填满整行,保证点击区域与条目宽度一致;- 由于透明背景,悬停/选中的默认变色被"让位"给你的自定义模板;若仍需选中反馈,可在你的
DataTemplate内自行绑定触发器实现。
四、四种布局样式:WrapPanel 与 StackPanel 的横竖组合
在ListBoxCustom之上,HandyControl 提供了四种预置布局样式,区别仅在ItemsPanel(条目承载面板)的容器类型与排列方向:
| 样式 | 继承自 | 容器 | 排列方向 |
|---|---|---|---|
WrapPanelHorizontalListBox | ListBoxCustom | WrapPanel | 水平(自动换行) |
WrapPanelVerticalListBox | ListBoxCustom | WrapPanel | 垂直(自动换列) |
StackPanelHorizontalListBox | ListBoxCustom | StackPanel | 水平(单行) |
StackPanelVerticalListBox | ListBoxCustom | StackPanel | 垂直(单列) |
4.1 WrapPanelHorizontalListBox(水平换行)
布局容器为 WrapPanel,水平方向显示:条目从左到右排列,空间不足时自动换行。
<ListBox Margin="10" ItemsSource="{Binding Datas}" Style="{DynamicResource WrapPanelHorizontalListBox}"> <ListBox.ItemTemplate> <DataTemplate> <Border BorderThickness="1" BorderBrush="Black" Margin="5,0"> <DockPanel LastChildFill="True"> <Path DockPanel.Dock="Left" Fill="YellowGreen" Width="20" Margin="10,0,10,0" HorizontalAlignment="Center" Data="{DynamicResource BubbleTailGeometry}"></Path> <TextBlock Padding="10" Text="{Binding Name}"></TextBlock> </DockPanel> </Border> </DataTemplate> </ListBox.ItemTemplate> </ListBox>4.2 WrapPanelVerticalListBox(垂直换列)
布局容器为 WrapPanel,垂直方向显示:条目从上到下排列,高度不足时自动换列。
<ListBox Margin="10" ItemsSource="{Binding Datas}" Style="{DynamicResource WrapPanelVerticalListBox}"> <ListBox.ItemTemplate> <DataTemplate> <Border BorderThickness="1" BorderBrush="Black" Margin="0,5"> <DockPanel LastChildFill="True"> <Path DockPanel.Dock="Left" Fill="YellowGreen" Width="20" Margin="10,0,10,0" HorizontalAlignment="Center" Data="{DynamicResource BubbleTailGeometry}"></Path> <TextBlock Padding="10" Text="{Binding Name}"></TextBlock> </DockPanel> </Border> </DataTemplate> </ListBox.ItemTemplate> </ListBox>4.3 StackPanelHorizontalListBox(水平单行)
布局容器为 StackPanel,水平方向显示:所有条目在单行内依次排列,不换行。
<ListBox Margin="10" ItemsSource="{Binding Datas}" Style="{DynamicResource StackPanelHorizontalListBox}"> <ListBox.ItemTemplate> <DataTemplate> <Border BorderThickness="1" BorderBrush="Black" Margin="5,0"> <DockPanel LastChildFill="True"> <Path DockPanel.Dock="Left" Fill="YellowGreen" Width="20" Margin="10,0,10,0" HorizontalAlignment="Center" Data="{DynamicResource BubbleTailGeometry}"></Path> <TextBlock Padding="10" Text="{Binding Name}"></TextBlock> </DockPanel> </Border> </DataTemplate> </ListBox.ItemTemplate> </ListBox>4.4 StackPanelVerticalListBox(垂直单列)
布局容器为 StackPanel,垂直方向显示:所有条目在单列内依次排列,即最接近原生 ListBox 的纵向列表形态。
<ListBox Margin="10" ItemsSource="{Binding Datas}" Style="{DynamicResource StackPanelVerticalListBox}"> <ListBox.ItemTemplate> <DataTemplate> <Border BorderThickness="1" BorderBrush="Black" Margin="0,1"> <DockPanel LastChildFill="True"> <Path DockPanel.Dock="Left" Fill="YellowGreen" Width="20" Margin="10,0,10,0" HorizontalAlignment="Center" Data="{DynamicResource BubbleTailGeometry}"></Path> <TextBlock Padding="10" Text="{Binding Name}"></TextBlock> </DockPanel> </Border> </DataTemplate> </ListBox.ItemTemplate> </ListBox>4.5 源码剖析:四种布局的实现方式
四种样式的源码实现极为简洁(见 ListBox.xaml),全部通过ItemsPanel属性注入预置的ItemsPanelTemplate:
<Style x:Key="WrapPanelHorizontalListBox" BasedOn="{StaticResource ListBoxCustom}" TargetType="ListBox"> <Setter Property="ItemsPanel" Value="{StaticResource WrapHorizontalItemsPanelTemplate}"/> </Style> <Style x:Key="WrapPanelVerticalListBox" BasedOn="{StaticResource ListBoxCustom}" TargetType="ListBox"> <Setter Property="ItemsPanel" Value="{StaticResource WrapVerticalItemsPanelTemplate}"/> </Style> <Style x:Key="StackPanelHorizontalListBox" BasedOn="{StaticResource ListBoxCustom}" TargetType="ListBox"> <Setter Property="ItemsPanel" Value="{StaticResource StackHorizontalItemsPanelTemplate}"/> </Style> <Style x:Key="StackPanelVerticalListBox" BasedOn="{StaticResource ListBoxCustom}" TargetType="ListBox"> <Setter Property="ItemsPanel" Value="{StaticResource StackVerticalItemsPanelTemplate}"/> </Style>这些ItemsPanelTemplate定义于 Themes/Styles/Base/ItemsPanelTemplate.xaml,是 HandyControl 布局体系的公共积木:
<ItemsPanelTemplate x:Key="StackHorizontalItemsPanelTemplate"> <StackPanel FocusVisualStyle="{x:Null}" Orientation="Horizontal"/> </ItemsPanelTemplate> <ItemsPanelTemplate x:Key="StackVerticalItemsPanelTemplate"> <StackPanel FocusVisualStyle="{x:Null}"/> </ItemsPanelTemplate> <ItemsPanelTemplate x:Key="WrapHorizontalItemsPanelTemplate"> <WrapPanel FocusVisualStyle="{x:Null}" HorizontalAlignment="Center" VerticalAlignment="Center"/> </ItemsPanelTemplate> <ItemsPanelTemplate x:Key="WrapVerticalItemsPanelTemplate"> <WrapPanel FocusVisualStyle="{x:Null}" Orientation="Vertical" HorizontalAlignment="Center" VerticalAlignment="Center"/> </ItemsPanelTemplate>几个关键点:
StackVertical的 StackPanel 未设置Orientation(默认即为垂直),而StackHorizontal显式设为Horizontal;- 两个 WrapPanel 模板均设置了居中对齐,因此条目不足一行/一列时会在容器内居中呈现;
- 同文件还提供了
VirtualizingStackHorizontal/VerticalItemsPanelTemplate(虚拟化栈布局)与DockItemsPanelTemplate等公共模板,方便你在自定义控件时复用; - 这类模板资源也复用于其他控件——例如 ElementGroupBaseStyle.xaml 中的元素分组即引用了相同的 Stack 面板模板,可见该资源文件的公共性。
五、ListBoxAttach.SelectedItems:选中集合的双向绑定
除了样式之外,HandyControl 还针对 ListBox 提供了一项实用的附加能力:将SelectedItems(选中项集合)暴露为可绑定的附加属性,便于在 MVVM 中直接拿到多选结果。实现位于 Controls/Attach/ListBoxAttach.cs:
public static readonly DependencyProperty SelectedItemsProperty = DependencyProperty.RegisterAttached( "SelectedItems", typeof(IList), typeof(ListBoxAttach), new FrameworkPropertyMetadata(default(IList), FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, OnSelectedItemsChanged));从注册参数可以看出:
- 类型为
IList,支持任何列表/集合类型的 ViewModel 属性; - 默认双向绑定(
BindsTwoWayByDefault),用户勾选时自动回写 ViewModel; - 内部通过
OnListBoxSelectionChanged在SelectionChanged事件中将listBox.SelectedItems复制到绑定属性;反向设置时则清空并重建SelectedItems,并用InternalActionProperty布尔标志防止双向同步造成的递归。
XAML 中的典型用法(命名空间hc引用 HandyControl,如xmlns:hc="https://handyorg.github.io/handycontrol"):
<ListBox ItemsSource="{Binding Datas}" SelectionMode="Multiple" Style="{DynamicResource ListBoxCustom}" hc:ListBoxAttach.SelectedItems="{Binding SelectedItems}"/>这样在 ViewModel 中只需一个IList SelectedItems属性即可双向感知用户的选中变化,无需手动挂接SelectionChanged事件。
六、跨平台扩展:Avalonia 版本的同名样式
HandyControl 在 Avalonia 分支中提供了与 WPF 版本同构的 ListBox 主题,见 src/Avalonia/HandyControl_Avalonia/Themes/Styles/ListBox.axaml。该文件以ControlTheme形式定义了:
ListBoxItemBaseStyle:同样的RegionBrush背景、Padding=10,0、MinHeight=DefaultControlHeight,并通过:pointerover、:selected、:disabled选择器实现悬停/选中/禁用反馈;ListBoxBaseStyle:同样包裹Border + ScrollViewer + ItemsPresenter,圆角同样来自hc:BorderElement.CornerRadius;- 隐式主题
{x:Type ListBox}、紧凑变体ListBox.Small以及ListBoxItemCustom+ListBoxCustom的自定义模式,与 WPF 版本一一对应。
如果你的项目同时维护 WPF 与 Avalonia 双端,这套样式命名与层级可以直接对照迁移,大幅降低双端 UI 的维护成本。
七、在 Demo 工程中查看实际效果
HandyControl 的官方 Demo 提供了 ListBox 的真实运行样例,见 src/Shared/HandyControlDemo_Shared/UserControl/Styles/ListBoxDemo.xaml。样例在hc:TransitioningContentControl内放置了一个WrapPanel,并排展示了两个宽度 200 的 ListBox:
- 第一个直接使用默认样式(隐式应用
ListBoxBaseStyle),ItemTemplate仅显示Name文本; - 第二个使用
Style="{StaticResource ListBox.Small}"紧凑变体,ItemTemplate同样绑定Name。
配合其代码后置ListBoxDemo.xaml.cs中的DataList数据集合,你可以直接运行 Demo 观察默认样式与紧凑样式的尺寸、间距与选中态差异,并以此为起点尝试ListBoxCustom及四种布局样式。
小结
HandyControl 的 ListBox 样式体系遵循清晰的BasedOn继承设计:ListBoxBaseStyle作为不可直接使用的基座,提供默认外观、滚动行为与选中态反馈;ListBoxCustom在此基础上剥离容器装饰,把数据显示完全交给用户的ItemTemplate;四种布局变体则通过公共ItemsPanelTemplate一键切换 WrapPanel/StackPanel 的水平与垂直方向。配合ListBoxAttach.SelectedItems的双向绑定选中集合能力,以及 Avalonia 分支的同构实现,你可以快速构建从简单列表到复杂网格、标签云、气泡列表等多种形态的列表框界面。所有样式与模板均可在 Themes/Styles/ListBox.xaml 与 Themes/Styles/Base/ListBoxBaseStyle.xaml 中查阅并二次定制。
- UI组件
- 桌面应用
【免费下载链接】HandyControl
Contains some simple and commonly used WPF controls
相关推荐
FAST 组件库 Listbox 类全解析:从 FoundationListboxElement 到 fast-listbox 的继承体系与实战指南
FAST 组件库 Listbox 类全解析:从 FoundationListboxElement 到 fast listbox 的继承体系与实战指南 @micr
前端UI组件Win11Debloat:免费给 Windows 10/11 瘦身,5 分钟该做什么?
Win11Debloat:免费给 Windows 10/11 瘦身,5 分钟该做什么? 新装的 Windows 11,是不是装完就开始被用不上的预装应用、后台遥
桌面应用CLI三步实现界面风格统一:HandyControl样式继承全攻略
三步实现界面风格统一:HandyControl样式继承全攻略 你是否还在为WPF界面开发中重复编写相似样式而烦恼?是否希望快速定制出符合产品风格的控件库?本文将
UI组件桌面应用
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考