WPF TreeView大数据量多层级搜索定位
来源:互联网 发布:服装收银软件 编辑:程序博客网 时间:2024/06/06 15:54
最近在做公司内部IM,使用的是网易云信SDK,有需要的同学可以去了解一下。
今天主要说一说公司组织架构这一块,需求是在搜索框输入员工姓名或者首字母,搜索框实时自动匹配到存在的员工,选中某一员工后在组织结构层级树中定位到该员工,就类似于PC版QQ的搜索框。
综上,我们涉及到的控件主要有两个:1.搜索框 2.TreeView
了解WPF的同学肯定立马会想到这个搜索框应该用AutoCompleteBox来做了,没错,我们先通过NuGet引入WPFToolkit,然后在对应的xaml页面引入命名空间:
xmlns:tookit=”clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls.Input.Toolkit”
下来就可以使用了,
<tookit:AutoCompleteBox x:Name="searchControl" MinimumPopulateDelay="100" ValueMemberPath="Search" FilterMode="Custom" DropDownClosing="SearchControl_DropDownClosing" Style="{DynamicResource AutoCompleteBoxStyle1}"> <tookit:AutoCompleteBox.ItemTemplate> <DataTemplate> <TextBlock Text="{Binding Search}"/> </DataTemplate> </tookit:AutoCompleteBox.ItemTemplate> </tookit:AutoCompleteBox>
我这边赋值了3个属性1个事件:
MinimumPopulateDelay=”100”//用户停止输入后多久触发自动匹配,单位毫秒
ValueMemberPath=”Search”//后台对应的关键词属性,即根据实体中的“Search”字段来匹配
FilterMode=”Custom”//自定义过滤模式,需后台代码支持
DropDownClosing=”SearchControl_DropDownClosing”//在这个事件里处理关键词匹配
遗憾的是tookit:AutoCompleteBox没有水印功能,我们只好自己实现一下,眼尖的同学肯定已经看到AutoCompleteBoxStyle1这个样式了:
<Style x:Key="AutoCompleteBoxStyle1" TargetType="{x:Type tookit:AutoCompleteBox}"> <Setter Property="IsTabStop" Value="True"/> <Setter Property="BorderThickness" Value="0"/> <Setter Property="BorderBrush"> <Setter.Value> <LinearGradientBrush EndPoint="0.5,1" StartPoint="0.5,0"> <GradientStop Color="#FFA3AEB9" Offset="0"/> <GradientStop Color="#FF8399A9" Offset="0.375"/> <GradientStop Color="#FF718597" Offset="0.375"/> <GradientStop Color="#FF617584" Offset="1"/> </LinearGradientBrush> </Setter.Value> </Setter> <Setter Property="Background" Value="Transparent"/> <Setter Property="Foreground" Value="Black"/> <Setter Property="MinWidth" Value="45"/> <Setter Property="Template"> <Setter.Value> <ControlTemplate TargetType="{x:Type tookit:AutoCompleteBox}"> <Border CornerRadius="12 12 0 0" Background="#65D1DF"> <Grid Opacity="{TemplateBinding Opacity}" > <Grid> <StackPanel Orientation="Horizontal" Visibility="{Binding ElementName=Text,Path=Text.Length,Converter={StaticResource AutoCompeleteBoxWaterMarkConverter}}" > <Image Source="../Resources/Images/icon_fangdajing.png" HorizontalAlignment="Left" Width="12" Margin="8 0 0 0"/> <TextBlock Text="搜索用户" Foreground="#ffffff" VerticalAlignment="Center" Padding="4 0 0 0" ></TextBlock> </StackPanel> <TextBox x:Name="Text" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" Background="{TemplateBinding Background}" Foreground="{TemplateBinding Foreground}" IsTabStop="True" Margin="0" Padding="{TemplateBinding Padding}"/> </Grid> <Popup x:Name="Popup"> <Grid Background="{TemplateBinding Background}" > <Border x:Name="PopupBorder" BorderThickness="0" Background="#11000000" HorizontalAlignment="Stretch" Opacity="1"> <Border.RenderTransform> <TranslateTransform X="1" Y="1"/> </Border.RenderTransform> <Border BorderBrush="#65D1DF" BorderThickness="1" CornerRadius="0" HorizontalAlignment="Stretch" Opacity="1" Padding="0" Background="#ffffff"> <Border.RenderTransform> <TransformGroup> <TranslateTransform X="-1" Y="-1"/> </TransformGroup> </Border.RenderTransform> <ListBox x:Name="Selector" BorderThickness="0" Background="{TemplateBinding Background}" Foreground="{TemplateBinding Foreground}" ScrollViewer.HorizontalScrollBarVisibility="Auto" ItemTemplate="{TemplateBinding ItemTemplate}" ItemContainerStyle="{TemplateBinding ItemContainerStyle}" ScrollViewer.VerticalScrollBarVisibility="Auto"/> </Border> </Border> </Grid> </Popup> </Grid> </Border> </ControlTemplate> </Setter.Value> </Setter> </Style>
我是加了一个StackPanel,其中有我们的“水印”:一个image+一个TextBlock,通过AutoCompeleteBoxWaterMarkConverter来控制“水印”显示与否:
public class AutoCompeleteBoxWaterMark:IValueConverter{ public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) { if (value==null) { return Visibility.Visible; } int length = (int)value; if (length > 0) { return Visibility.Collapsed; } return Visibility.Visible; } public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) { throw new NotImplementedException(); }}
注意:要引用自定义的converter,同样需要引入对应的命名空间。
下面开始组装数据,由于后台给到的数据是包含层级的,所以我们可以直接将数据绑定到TreeView的数据源,但是对于tookit:AutoCompleteBox的数据我们就需要处理一下了,可以用递归将原来有层级的数据放到一个list里,将该list绑定到tookit:AutoCompleteBox数据源上,tookit:AutoCompleteBox就可以通过这个源数据来匹配用户输入了,这里我就省去组装代码直接上结果了:
this.userTreeControl.ItemsSource = groups; //treeview数据源
this.searchControl.ItemsSource = users; //tookit:AutoCompleteBox数据源
下面开始定义tookit:AutoCompleteBox的Filter,先在构造函数里注册一下:
this.searchControl.ItemFilter += SearchControl_ItemFilter;
然后实现SearchControl_ItemFilter,该方法中两个helper由于代码太长这里就不贴了,网上都可以找到,同时我也上传到我的资源中心了:
private bool SearchControl_ItemFilter(string search, object item) { string text = CommonHelper.GetPropertyValue(item, "Search").ToString().ToLower(); string tmp = text.Split(':').FirstOrDefault(); text += ChineseCharHelper.GetFirstLetter(tmp).ToLower(); return text.Contains(search.ToLower()); }
以上,tookit:AutoCompleteBox算是完成了。
下面开始说TreeView,在tookit:AutoCompleteBox的DropDownClosing()事件中通过this.userTreeControl.SelectItem(result) 这行代码来触发在TreeView中定位,SelectItem()方法代码如下,这段代码网上比较多,百度TreeViewHelper即可:
/// <summary> /// Searches a TreeView for the provided object and selects it if found /// </summary> /// <param name="treeView">The TreeView containing the item</param> /// <param name="item">The item to search and select</param> public static void SelectItem(this TreeView treeView, object item) { ExpandAndSelectItem(treeView, item); } /// <summary> /// Finds the provided object in an ItemsControl's children and selects it /// </summary> /// <param name="parentContainer">The parent container whose children will be searched for the selected item</param> /// <param name="itemToSelect">The item to select</param> /// <returns>True if the item is found and selected, false otherwise</returns> private static bool ExpandAndSelectItem(ItemsControl parentContainer, object itemToSelect) { //check all items at the current level foreach (Object item in parentContainer.Items) { TreeViewItem currentContainer = parentContainer.ItemContainerGenerator.ContainerFromItem(item) as TreeViewItem; //if the data item matches the item we want to select, set the corresponding //TreeViewItem IsSelected to true if (item == itemToSelect && currentContainer != null) { currentContainer.IsSelected = true; currentContainer.BringIntoView(); currentContainer.Focus(); //the item was found return true; } } //if we get to this point, the selected item was not found at the current level, so we must check the children foreach (Object item in parentContainer.Items) { TreeViewItem currentContainer = parentContainer.ItemContainerGenerator.ContainerFromItem(item) as TreeViewItem; //if children exist if (currentContainer != null && currentContainer.Items.Count > 0) { //keep track of if the TreeViewItem was expanded or not bool wasExpanded = currentContainer.IsExpanded; //expand the current TreeViewItem so we can check its child TreeViewItems currentContainer.IsExpanded = true; //if the TreeViewItem child containers have not been generated, we must listen to //the StatusChanged event until they are if (currentContainer.ItemContainerGenerator.Status != GeneratorStatus.ContainersGenerated) { //store the event handler in a variable so we can remove it (in the handler itself) EventHandler eh = null; eh = new EventHandler(delegate { if (currentContainer.ItemContainerGenerator.Status == GeneratorStatus.ContainersGenerated) { if (ExpandAndSelectItem(currentContainer, itemToSelect) == false) { //The assumption is that code executing in this EventHandler is the result of the parent not //being expanded since the containers were not generated. //since the itemToSelect was not found in the children, collapse the parent since it was previously collapsed currentContainer.IsExpanded = false; } //remove the StatusChanged event handler since we just handled it (we only needed it once) currentContainer.ItemContainerGenerator.StatusChanged -= eh; } }); currentContainer.ItemContainerGenerator.StatusChanged += eh; } else //otherwise the containers have been generated, so look for item to select in the children { if (ExpandAndSelectItem(currentContainer, itemToSelect) == false) { //restore the current TreeViewItem's expanded state currentContainer.IsExpanded = wasExpanded; } else //otherwise the node was found and selected, so return true { return true; } } } } //no item was found return false; }
到这里,功能部分已经完成了,然后当我选中某个员工,发现得有15秒TreeView才能自动定位到该员工,但是当我第2次搜索的时候又会变得非常快了,这是什么原因呢,看ExpandAndSelectItem()方法我们就会知道,因为我们只是给TreeView绑定了数据源,但是这个TreeView还未被打开过,等于说它的子树还没生成,于是ExpandAndSelectItem()帮我们把每一层级都打开,这明显是个耗时的过程,当我们第2次再搜索的时候这棵树已经被相当于被打开过了,所以会变的很快。
另外,这里慢跟数据量和层级多少也是有关系的,如果像QQ那样只有一层,又或者只有百十来条数据,那自然不会很慢。我这边大概4000条数据,4~5层层级。
15秒显然是不能忍受的,那么这个问题怎么解决呢?这里提供一个讨巧的方法,既然没被打开过,那我初始化的时候打开一遍不就行了!如果你介意打开后影响美观,那再关上就是了。
//打开public static void ExpandAllSubtree(this TreeView treeView){ foreach (var t in treeView.Items) { DependencyObject o = treeView.ItemContainerGenerator.ContainerFromItem(t); ((TreeViewItem)o).ExpandSubtree(); }}//关闭 public static void CollapseAll(this TreeView treeView){ CollapseTreeViewItems(treeView);}private static void CollapseTreeViewItems(ItemsControl parentContainer){ foreach (var item in parentContainer.Items) { DependencyObject o = parentContainer.ItemContainerGenerator.ContainerFromItem(item); if (o != null) { ((TreeViewItem)o).IsExpanded = false; if (((TreeViewItem)o).HasItems) { CollapseTreeViewItems(((TreeViewItem)o)); } } }}
那么对TreeView初始化的时候就变成这样:
this.userTreeControl.ItemsSource = groups;
this.userTreeControl.ExpandAllSubtree();
this.userTreeControl.CollapseAll();
其实,TreeViewHelper里也有个打开整棵树的方法ExpandAll(),但是通过这个方法同样需要15秒,看一下代码就可以知道,这个方法基本和SelectItem()是一样的,都要针对尚未打开的子树进行StatusChanged事件的注册与注销,耗时即在此。所以我选择用ExpandSubtree()来打开整个树,这个方法是自带的。
不管怎样,总算解决了……
参考链接
- WPF TreeView大数据量多层级搜索定位
- treeview 遍历搜索 定位目标
- WPF Treeview搜索高亮显示
- wpf treeview
- WPF: 使用DrawVisual提高大数据量时的绘图性能
- WPF: 使用DrawVisual提高大数据量时的绘图性能
- SQL Server百万级大数据量删除
- wpf中绑定treeview
- WPF初探--TreeView
- WPF Treeview的问题
- WPF TreeView Win8 样式
- WPF TreeView HierarchicalDataTemplate
- WPF TreeView Binding
- WPF TreeView控件使用
- wpf之TreeView绑定
- WPF TreeView Command
- WPF ,WinForm的TreeView
- WPF TreeView tools
- copy_to_user
- 确切的说spring框架是做什么的?(翻译自stackoverflow的一个回答)
- Github-Android Studio上首次与非首次提交项目
- tomcat启动多个应用logback初始化失败报Web app root system property already set to different value: 'webapp.root'
- 北宋名臣王安石后世传人在松滋
- WPF TreeView大数据量多层级搜索定位
- 迷宫问题 dfs bfs
- 字符数组、字符串、整型数之间的转化
- memset()、memcpy()、memcmp()的使用方法
- shell知识点
- solr中Cache综述
- springMvc 配置 controller 配置 spring-dispatcher-servlet.xml
- 关于 redis3.2.9 在 centos7 的 配置 及 Jedis 与 虚拟机 centos7 redis 连接的问题
- HashMap使用对象作为key