WPF 多选下拉+搜索过滤_wpf下拉选项增加搜索

目录

示例截图

调用方法

控件代码

样式文件


示例截图

调用方法

<Window x:Class="MultiSelectDemo.MainWindow"

xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"

xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"

xmlns:local="clr-namespace:MultiSelectDemo"

xmlns:ctrl="clr-namespace:MultiSelectDemo.Controls"

Title="搜索多选下拉框示例" Height="600" Width="700"

WindowStartupLocation="CenterScreen"

Background="#F5F7FA">

<Window.DataContext>

<local:MainViewModel/>

</Window.DataContext>

<Grid Margin="40">

<Grid.RowDefinitions>

<RowDefinition Height="Auto"/>

<RowDefinition Height="*"/>

</Grid.RowDefinitions>

<!-- 标题 -->

<TextBlock Grid.Row="0"

Text="WPF 搜索多选下拉框控件"

FontSize="24"

FontWeight="Bold"

Foreground="#303133"

Margin="0,0,0,30"/>

<!-- 内容区域 -->

<StackPanel Grid.Row="1">

<!-- 使用控件 -->

<StackPanel Margin="0,0,0,20">

<TextBlock Text="选择城市:" FontSize="14" Foreground="#606266" Margin="0,0,0,8"/>

<!-- 使用封装的控件 -->

<ctrl:MultiSelectComboBox

ItemsSource="{Binding Cities}"

SelectedItems="{Binding SelectedCities, Mode=TwoWay}"

DisplayMemberPath="Label"

ValueMemberPath="Value"

Placeholder="请选择城市..."

MinWidth="300"/>

</StackPanel>

<!-- 显示已选结果 -->

<Border Background="#E8F4FD"

BorderBrush="#409EFF"

BorderThickness="1"

CornerRadius="4"

Padding="16"

Margin="0,0,0,20">

<StackPanel>

<TextBlock Text="已选结果:" FontSize="14" FontWeight="Bold" Foreground="#409EFF" Margin="0,0,0,8"/>

<TextBlock Text="{Binding ResultText}"

Foreground="#606266"

FontSize="13"

TextWrapping="Wrap"/>

</StackPanel>

</Border>

<!-- 说明文字 -->

<Border Background="#FFF7E6"

BorderBrush="#E6A23C"

BorderThickness="1"

CornerRadius="4"

Padding="16">

<StackPanel>

<TextBlock Text="控件特性:" FontSize="14" FontWeight="Bold" Foreground="#E6A23C" Margin="0,0,0,8"/>

<TextBlock Foreground="#606266" FontSize="13" TextWrapping="Wrap">

<Run Text="&#x2022; ItemsSource 绑定数据源"/>

<LineBreak/>

<Run Text="&#x2022; SelectedItems 双向绑定获取已选项"/>

<LineBreak/>

<Run Text="&#x2022; DisplayMemberPath/ValueMemberPath 自定义属性"/>

<LineBreak/>

<Run Text="&#x2022; Placeholder 设置占位提示"/>

<LineBreak/>

<Run Text="&#x2022; 搜索过滤 + 全选/清空"/>

</TextBlock>

</StackPanel>

</Border>

</StackPanel>

</Grid>

</Window>

控件代码

using System;

using System.Collections;

using System.Collections.ObjectModel;

using System.ComponentModel;

using System.Linq;

using System.Windows;

using System.Windows.Controls;

using System.Windows.Controls.Primitives;

using System.Windows.Data;

using System.Windows.Media;

using System.Windows.Media.Animation;

namespace MultiSelectDemo.Controls;

public class MultiSelectComboBox : Control

{

static MultiSelectComboBox()

{

DefaultStyleKeyProperty.OverrideMetadata(typeof(MultiSelectComboBox),

new FrameworkPropertyMetadata(typeof(MultiSelectComboBox)));

}

// 依赖属性

public static readonly DependencyProperty ItemsSourceProperty =

DependencyProperty.Register("ItemsSource", typeof(IEnumerable), typeof(MultiSelectComboBox),

new PropertyMetadata(null, OnItemsSourceChanged));

public static readonly DependencyProperty SelectedItemsProperty =

DependencyProperty.Register("SelectedItems", typeof(IList), typeof(MultiSelectComboBox),

new PropertyMetadata(null, OnSelectedItemsChanged));

public static readonly DependencyProperty DisplayMemberPathProperty =

DependencyProperty.Register("DisplayMemberPath", typeof(string), typeof(MultiSelectComboBox),

new PropertyMetadata("Label"));

public static readonly DependencyProperty ValueMemberPathProperty =

DependencyProperty.Register("ValueMemberPath", typeof(string), typeof(MultiSelectComboBox),

new PropertyMetadata("Value"));

public static readonly DependencyProperty PlaceholderProperty =

DependencyProperty.Register("Placeholder", typeof(string), typeof(MultiSelectComboBox),

new PropertyMetadata("请选择..."));

public static readonly DependencyProperty SearchTextProperty =

DependencyProperty.Register("SearchText", typeof(string), typeof(MultiSelectComboBox),

new PropertyMetadata("", OnSearchTextChanged));

public static readonly DependencyProperty IsDropDownOpenProperty =

DependencyProperty.Register("IsDropDownOpen", typeof(bool), typeof(MultiSelectComboBox),

new PropertyMetadata(false));

// 使用 FrameworkPropertyMetadata 并添加 BindsTwoWayByDefault

public static readonly DependencyProperty SelectedTextProperty =

DependencyProperty.Register("SelectedText", typeof(string), typeof(MultiSelectComboBox),

new FrameworkPropertyMetadata("", FrameworkPropertyMetadataOptions.BindsTwoWayByDefault));

// 属性包装器 - 必须有 setter 才能在 XAML 中设置

public IEnumerable ItemsSource

{

get => (IEnumerable)GetValue(ItemsSourceProperty);

set => SetValue(ItemsSourceProperty, value);

}

public IList SelectedItems

{

get => (IList)GetValue(SelectedItemsProperty);

set => SetValue(SelectedItemsProperty, value);

}

public string DisplayMemberPath

{

get => (string)GetValue(DisplayMemberPathProperty);

set => SetValue(DisplayMemberPathProperty, value);

}

public string ValueMemberPath

{

get => (string)GetValue(ValueMemberPathProperty);

set => SetValue(ValueMemberPathProperty, value);

}

public string Placeholder

{

get => (string)GetValue(PlaceholderProperty);

set => SetValue(PlaceholderProperty, value);

}

public bool IsDropDownOpen

{

get => (bool)GetValue(IsDropDownOpenProperty);

set => SetValue(IsDropDownOpenProperty, value);

}

public string SelectedText

{

get => (string)GetValue(SelectedTextProperty);

set => SetValue(SelectedTextProperty, value);

}

public string SearchText

{

get => (string)GetValue(SearchTextProperty);

set => SetValue(SearchTextProperty, value);

}

// 内部集合 - 公开供模板绑定

public ObservableCollection<SelectItemWrapper> InternalItems { get; } = new();

public ObservableCollection<SelectItemWrapper> FilteredItems { get; } = new();

private bool _isSyncing = false;

private static void OnItemsSourceChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)

{

var ctrl = (MultiSelectComboBox)d;

ctrl.RebuildInternalItems();

}

private static void OnSelectedItemsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)

{

var ctrl = (MultiSelectComboBox)d;

ctrl.SyncFromExternal();

}

private static void OnSearchTextChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)

{

var ctrl = (MultiSelectComboBox)d;

ctrl.ApplyFilter();

}

public override void OnApplyTemplate()

{

base.OnApplyTemplate();

if (GetTemplateChild("PART_Popup") is Popup popup)

{

popup.Opened += OnPopupOpened;

popup.Closed += OnPopupClosed;

}

if (GetTemplateChild("PART_SelectAllBtn") is Button selectAllBtn)

selectAllBtn.Click += (s, e) => SelectAllFiltered();

if (GetTemplateChild("PART_ClearBtn") is Button clearBtn)

clearBtn.Click += (s, e) => ClearAll();

if (GetTemplateChild("PART_SearchBox") is TextBox searchBox)

searchBox.TextChanged += (s, e) => SearchText = searchBox.Text;

}

private void OnPopupOpened(object? sender, EventArgs e)

{

if (GetTemplateChild("PART_SearchBox") is TextBox box)

{

box.Text = SearchText;

box.Focus();

}

if (GetTemplateChild("PART_ArrowRotate") is RotateTransform rotate)

{

var anim = new DoubleAnimation(0, 180, TimeSpan.FromMilliseconds(200));

rotate.BeginAnimation(RotateTransform.AngleProperty, anim);

}

}

private void OnPopupClosed(object? sender, EventArgs e)

{

if (GetTemplateChild("PART_ArrowRotate") is RotateTransform rotate)

{

var anim = new DoubleAnimation(180, 0, TimeSpan.FromMilliseconds(200));

rotate.BeginAnimation(RotateTransform.AngleProperty, anim);

}

}

private void RebuildInternalItems()

{

InternalItems.Clear();

if (ItemsSource == null)

{

UpdateSelectedText();

ApplyFilter();

return;

}

foreach (var item in ItemsSource)

{

var wrapper = new SelectItemWrapper

{

Source = item,

DisplayText = GetDisplayText(item)

};

wrapper.IsSelectedChanged += OnWrapperSelectionChanged;

InternalItems.Add(wrapper);

}

SyncFromExternal();

ApplyFilter();

}

private void OnWrapperSelectionChanged(SelectItemWrapper wrapper)

{

if (_isSyncing) return;

UpdateSelectedText();

SyncToExternal();

}

private void SyncFromExternal()

{

if (SelectedItems == null || InternalItems.Count == 0) return;

_isSyncing = true;

try

{

foreach (var wrapper in InternalItems)

{

wrapper.IsSelected = SelectedItems.Contains(wrapper.Source);

}

}

finally

{

_isSyncing = false;

}

UpdateSelectedText();

}

private void SyncToExternal()

{

if (SelectedItems == null) return;

SelectedItems.Clear();

foreach (var wrapper in InternalItems.Where(w => w.IsSelected))

SelectedItems.Add(wrapper.Source);

}

private void ApplyFilter()

{

FilteredItems.Clear();

var keyword = SearchText?.Trim().ToLower() ?? "";

foreach (var item in InternalItems)

{

if (string.IsNullOrEmpty(keyword) ||

item.DisplayText.ToLower().Contains(keyword))

{

FilteredItems.Add(item);

}

}

}

private void UpdateSelectedText()

{

var selected = InternalItems.Where(w => w.IsSelected).Select(w => w.DisplayText).ToList();

var text = selected.Count == 0 ? Placeholder : string.Join(", ", selected);

SetValue(SelectedTextProperty, text);

}

private string GetDisplayText(object item)

{

if (item == null) return "";

var path = DisplayMemberPath;

if (string.IsNullOrEmpty(path)) return item.ToString() ?? "";

var prop = item.GetType().GetProperty(path);

return prop?.GetValue(item)?.ToString() ?? item.ToString() ?? "";

}

public void SelectAllFiltered()

{

foreach (var item in FilteredItems)

item.IsSelected = true;

}

public void ClearAll()

{

SetValue(SearchTextProperty, "");

foreach (var item in InternalItems)

item.IsSelected = false;

}

}

// 包装类 - 独立文件更清晰

public class SelectItemWrapper : INotifyPropertyChanged

{

private bool _isSelected;

public object? Source { get; set; }

public string DisplayText { get; set; } = "";

public bool IsSelected

{

get => _isSelected;

set

{

if (_isSelected != value)

{

_isSelected = value;

OnPropertyChanged(nameof(IsSelected));

IsSelectedChanged?.Invoke(this);

}

}

}

public event Action<SelectItemWrapper>? IsSelectedChanged;

public event PropertyChangedEventHandler? PropertyChanged;

protected void OnPropertyChanged(string name)

=> PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));

}

样式文件

<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"

xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"

xmlns:ctrl="clr-namespace:MultiSelectDemo.Controls">

<Style TargetType="{x:Type ctrl:MultiSelectComboBox}">

<Setter Property="Height" Value="36"/>

<Setter Property="MinWidth" Value="200"/>

<Setter Property="Background" Value="White"/>

<Setter Property="BorderBrush" Value="#DCDFE6"/>

<Setter Property="BorderThickness" Value="1"/>

<Setter Property="FontSize" Value="14"/>

<Setter Property="Foreground" Value="#606266"/>

<Setter Property="Template">

<Setter.Value>

<ControlTemplate TargetType="{x:Type ctrl:MultiSelectComboBox}">

<Grid>

<!-- 输入框按钮 -->

<ToggleButton x:Name="PART_Toggle"

Background="{TemplateBinding Background}"

BorderBrush="{TemplateBinding BorderBrush}"

BorderThickness="{TemplateBinding BorderThickness}"

Padding="12,8"

HorizontalContentAlignment="Left"

IsChecked="{Binding IsDropDownOpen, Mode=TwoWay, RelativeSource={RelativeSource TemplatedParent}}"

Cursor="Hand">

<Grid>

<Grid.ColumnDefinitions>

<ColumnDefinition Width="*"/>

<ColumnDefinition Width="Auto"/>

</Grid.ColumnDefinitions>

<!-- 显示已选文本 -->

<TextBlock Grid.Column="0"

Text="{Binding SelectedText, Mode=OneWay, RelativeSource={RelativeSource TemplatedParent}}"

TextTrimming="CharacterEllipsis"

VerticalAlignment="Center"

Foreground="{TemplateBinding Foreground}"

FontSize="{TemplateBinding FontSize}"/>

<!-- 下拉箭头 -->

<Path Grid.Column="1"

Data="M0,0 L5,5 L10,0"

Stroke="#909399"

StrokeThickness="1.5"

Stretch="Uniform"

Width="10" Height="10"

Margin="8,0,0,0"

RenderTransformOrigin="0.5,0.5">

<Path.RenderTransform>

<RotateTransform x:Name="PART_ArrowRotate" Angle="0"/>

</Path.RenderTransform>

</Path>

</Grid>

<ToggleButton.Style>

<Style TargetType="ToggleButton">

<Setter Property="Template">

<Setter.Value>

<ControlTemplate TargetType="ToggleButton">

<Border x:Name="Border"

Background="{TemplateBinding Background}"

BorderBrush="{TemplateBinding BorderBrush}"

BorderThickness="{TemplateBinding BorderThickness}"

Padding="{TemplateBinding Padding}"

CornerRadius="4">

<ContentPresenter HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"

VerticalAlignment="Center"/>

</Border>

<ControlTemplate.Triggers>

<Trigger Property="IsChecked" Value="True">

<Setter TargetName="Border" Property="BorderBrush" Value="#409EFF"/>

</Trigger>

<Trigger Property="IsMouseOver" Value="True">

<Setter TargetName="Border" Property="BorderBrush" Value="#C0C4CC"/>

</Trigger>

</ControlTemplate.Triggers>

</ControlTemplate>

</Setter.Value>

</Setter>

</Style>

</ToggleButton.Style>

</ToggleButton>

<!-- 下拉弹出框 -->

<Popup x:Name="PART_Popup"

AllowsTransparency="True"

Placement="Bottom"

PlacementTarget="{Binding ElementName=PART_Toggle}"

StaysOpen="False"

IsOpen="{Binding IsDropDownOpen, Mode=TwoWay, RelativeSource={RelativeSource TemplatedParent}}">

<Border Background="White"

BorderBrush="#E4E7ED"

BorderThickness="1"

CornerRadius="6"

Margin="0,4,0,0"

MinWidth="{TemplateBinding MinWidth}">

<Border.Effect>

<DropShadowEffect BlurRadius="12" ShadowDepth="2" Opacity="0.2"/>

</Border.Effect>

<Grid>

<Grid.RowDefinitions>

<RowDefinition Height="Auto"/>

<RowDefinition Height="Auto"/>

<RowDefinition Height="*"/>

</Grid.RowDefinitions>

<!-- 搜索框 -->

<Border Grid.Row="0"

BorderBrush="#E4E7ED"

BorderThickness="0,0,0,1"

Padding="12,10">

<Grid>

<Grid.ColumnDefinitions>

<ColumnDefinition Width="Auto"/>

<ColumnDefinition Width="*"/>

</Grid.ColumnDefinitions>

<TextBlock Grid.Column="0" Text="🔍" FontSize="14" Foreground="#C0C4CC" Margin="0,0,8,0"/>

<TextBox Grid.Column="1"

x:Name="PART_SearchBox"

BorderThickness="0"

FontSize="14"

Background="Transparent"

Foreground="#606266"

VerticalAlignment="Center"

Padding="0"/>

</Grid>

</Border>

<!-- 操作按钮 -->

<Border Grid.Row="1"

BorderBrush="#E4E7ED"

BorderThickness="0,0,0,1"

Padding="12,8">

<StackPanel Orientation="Horizontal">

<Button x:Name="PART_SelectAllBtn"

Content="全选"

FontSize="12"

Padding="10,4"

Margin="0,0,8,0"

Cursor="Hand"

Foreground="#409EFF">

<Button.Template>

<ControlTemplate TargetType="Button">

<Border Background="Transparent"

BorderBrush="#DCDFE6"

BorderThickness="1"

CornerRadius="3"

Padding="{TemplateBinding Padding}"

x:Name="BtnBorder">

<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>

</Border>

<ControlTemplate.Triggers>

<Trigger Property="IsMouseOver" Value="True">

<Setter TargetName="BtnBorder" Property="Background" Value="#ECF5FF"/>

</Trigger>

</ControlTemplate.Triggers>

</ControlTemplate>

</Button.Template>

</Button>

<Button x:Name="PART_ClearBtn"

Content="清空"

FontSize="12"

Padding="10,4"

Cursor="Hand"

Foreground="#909399">

<Button.Template>

<ControlTemplate TargetType="Button">

<Border Background="Transparent"

BorderBrush="#DCDFE6"

BorderThickness="1"

CornerRadius="3"

Padding="{TemplateBinding Padding}"

x:Name="BtnBorder">

<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>

</Border>

<ControlTemplate.Triggers>

<Trigger Property="IsMouseOver" Value="True">

<Setter TargetName="BtnBorder" Property="Background" Value="#F5F7FA"/>

</Trigger>

</ControlTemplate.Triggers>

</ControlTemplate>

</Button.Template>

</Button>

</StackPanel>

</Border>

<!-- 选项列表 - 使用 CheckBox -->

<ScrollViewer Grid.Row="2"

MaxHeight="280"

VerticalScrollBarVisibility="Auto"

Padding="0,4">

<ItemsControl ItemsSource="{Binding FilteredItems, RelativeSource={RelativeSource TemplatedParent}}">

<ItemsControl.ItemTemplate>

<DataTemplate DataType="ctrl:SelectItemWrapper">

<!-- 使用 CheckBox 并自定义样式 -->

<CheckBox IsChecked="{Binding IsSelected, Mode=TwoWay}"

Padding="12,10"

Cursor="Hand"

Content="{Binding DisplayText}">

<CheckBox.Style>

<Style TargetType="CheckBox">

<Setter Property="Template">

<Setter.Value>

<ControlTemplate TargetType="CheckBox">

<Border Background="Transparent"

Padding="{TemplateBinding Padding}"

x:Name="ItemBorder">

<Grid>

<Grid.ColumnDefinitions>

<ColumnDefinition Width="Auto"/>

<ColumnDefinition Width="*"/>

</Grid.ColumnDefinitions>

<!-- 勾选框 -->

<Border Grid.Column="0"

Width="18" Height="18"

BorderBrush="#DCDFE6"

BorderThickness="1"

CornerRadius="3"

Background="White"

Margin="0,0,10,0"

x:Name="CheckBorder">

<!-- 勾选图标 -->

<Path x:Name="CheckPath"

Data="M3,8 L8,13 L14,4"

Stroke="White"

StrokeThickness="2.5"

Stretch="Uniform"

Visibility="Collapsed"

Margin="3"/>

</Border>

<!-- 文字 -->

<ContentPresenter Grid.Column="1"

VerticalAlignment="Center"

TextBlock.FontSize="14"

TextBlock.Foreground="#606266"/>

</Grid>

</Border>

<ControlTemplate.Triggers>

<Trigger Property="IsChecked" Value="True">

<Setter TargetName="CheckBorder" Property="Background" Value="#409EFF"/>

<Setter TargetName="CheckBorder" Property="BorderBrush" Value="#409EFF"/>

<Setter TargetName="CheckPath" Property="Visibility" Value="Visible"/>

</Trigger>

<Trigger Property="IsMouseOver" Value="True">

<Setter TargetName="ItemBorder" Property="Background" Value="#F5F7FA"/>

</Trigger>

</ControlTemplate.Triggers>

</ControlTemplate>

</Setter.Value>

</Setter>

</Style>

</CheckBox.Style>

</CheckBox>

</DataTemplate>

</ItemsControl.ItemTemplate>

</ItemsControl>

</ScrollViewer>

</Grid>

</Border>

</Popup>

</Grid>

</ControlTemplate>

</Setter.Value>

</Setter>

</Style>

</ResourceDictionary>

相关推荐
FuckPatience3 小时前
WPF 列表控件自动拉伸子元素的宽度
wpf
LCG元3 小时前
【Go后端开发】从 0 到生产级:高性能分布式网关全实现 + 接口限流熔断降级实战
分布式·golang·wpf
枫叶林FYL18 小时前
项目九:异步高性能爬虫与数据采集中枢 —— 基于 Crawl<sub>4</sub>AI 与 Playwright 的现代化数据采集平台 项目总览
爬虫·python·深度学习·wpf
她说彩礼65万1 天前
WPF 多值转换器
wpf
无心水1 天前
【分布式利器:金融级】金融级分布式架构开源框架全景解读
人工智能·分布式·金融·架构·开源·wpf·金融级框架
她说彩礼65万1 天前
WPF 转换器
wpf
WPF工业上位机2 天前
匠心研智造,同心赴新程-WPF硬件通讯之串口&Socket
wpf
爱炸薯条的小朋友2 天前
C#由窗体原子表溢出造成的软件闪退,根本原因补充
开发语言·c#·wpf
晚风一隅3 天前
阿里云盘古存储系统:EB级分布式存储的架构革命与技术突破
wpf