65.9K
CodeProject 正在变化。 阅读更多。
Home

WPF MultiRangeSlider 控件

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.92/5 (10投票s)

2013年8月5日

CPOL

5分钟阅读

viewsIcon

57479

downloadIcon

3215

MultiRangeSlider 控件, 用于指定不相交的范围。

引言

有时需要指定不相交的范围。我最近的一个实际应用是为一个控件,该控件根据不同的缩放级别设置不同的地图显示。您可以使用带有两列(“从”和“到”)的 **DataGrid** 来解决这个问题。但在这种情况下,您必须跟踪值的变化,以确保范围不相交。此外,您还必须在用户输入错误数据时发出信号,例如通过将网格单元格染成红色,或者通过静默丢弃更改。这会产生复杂的逻辑并使用户感到困惑。对于开发者和用户来说,通过多范围控件指定不相交的范围要简单得多,因为该控件实际上不允许设置错误的值。

不幸的是,标准 Visual Studio 控件中没有这样的元素。有很多关于指定单个范围(带有两个滑块)的控件的文章。常见的方法是将一个滑块叠加在另一个(相同的)滑块上并跟踪值。

我将这种方法总结为将其扩展到无限数量的滑块,并添加用户交互,让用户有机会在运行时添加或删除新范围。

主要思想

主要思想是每个内部滑块都与两个相邻的滑块相连接,并与它们协同工作。

内部滑块包含四个属性

  1. LeftValue – 与滑块关联的范围的左值(等于普通 Slider 的 Value 属性,即滑块在滑块轴上的位置)。
  2. RightValue – 与滑块关联的范围的右值。
  3. MinimumValueLeftValue 的最小边界,等于前一个滑块的 RightValue
  4. MaximumValueRightValue 的最大边界,等于下一个滑块的 LeftValue

当我将滑块向右移动时,我将修改前一个滑块的 RightValue(因此前一个范围增大),前一个滑块的 MaximumValue(因此我可以将前一个滑块的前进),我自己的 LeftValue,以及下一个滑块的 MinimumValue

从图中可以看出,要描述 N 个范围,需要 N+1 个滑块(滑块滑块),因为最后一个滑块滑块定义了最后一个范围的右值(倒数第二个滑块的 RightValue)。

当然,对于第一个和最后一个滑块,您必须考虑它们的左边界和右边界分别是滑块的 MinimumMaximum

MinimumValueMaximumValue 属性是为了防止滑块相互重叠,但有一个陷阱。

第一个想法是检查滑块值(LeftValue 或普通滑块的 Value)是否在边界内。

if (value > MinimumValue && value < MaximumValue)
       return true;
return false;

但是滑块的值是滑块滑块的中心

我解决这个问题的办法是为不同的滑块设置不同的比例,这样滑块的值就落在了滑块的边界内。

滑块的比例将取决于滑块的视觉大小,您需要在调整控件大小时更改比例,但这最简单的方法。

总而言之,我的控件中生成内部滑块的过程有三个步骤

1. 根据指定的设置创建滑块,并绑定到指定的范围。

private void CreateSliders()
{
   foreach (var item in ItemsSource)
        Items.Add(CreateSlider(item));
            
   InitSliders();
            
}

2. 初始化滑块,创建到相邻滑块值的绑定,并创建最后一个滑块(该滑块未绑定到任何范围对象)。

private void InitSliders()
{
   Items.First().IsFirst = true;  
   for(int i = 0; i < Items.Count; i++)
   {
        InitSliderMinimum(i > 0? Items[i-1] : null, Items[i]);
        InitSliderMaximum(Items[i], i < Items.Count - 1 ? Items[i + 1] : null);
   } 
   Items.Add(CreateLastSliderFromItem(Items.Last()));
 
   ArrangeSliders();
}
 
private void InitSliderMaximum(WitMultiRangeSliderItem slider, 
    WitMultiRangeSliderItem nextSlider)
{
   slider.SetBinding(WitMultiRangeSliderItem.MaximumValueProperty, nextSlider == null ? 
        GetBinding(slider, x => x.RightValue) : 
        GetBinding(nextSlider, x => x.LeftValue));
}
 
private void InitSliderMinimum(WitMultiRangeSliderItem previousSlider, 
    WitMultiRangeSliderItem slider)
{
   if (previousSlider == null) 
        slider.MinimumValue = Minimum;
   else 
        slider.SetBinding(WitMultiRangeSliderItem.MinimumValueProperty, 
            GetBinding(previousSlider, x => x.LeftValue));
}
 
private WitMultiRangeSliderItem CreateLastSliderFromItem(WitMultiRangeSliderItem lastItem)
{
   var slider = new WitMultiRangeSliderItem
   {
        Item = null,
        IsLast = true,
        MaximumValue = Maximum
   };
 
   slider.SetBinding(WitMultiRangeSliderItem.LeftValueProperty, 
        GetBinding(lastItem, x => x.RightValue));
   slider.SetBinding(WitMultiRangeSliderItem.MinimumValueProperty,
        GetBinding(lastItem, x => x.LeftValue));
 
   return slider;
 
} 

3. 重新缩放滑块轴

private void ArrangeSliders()
{
   var nValues = Items.Count - 1;
 
   for (int i = 0; i < nValues; i++)
   {
        Items[i].Maximum = Maximum + ThumbValue * (nValues - i);
        Items[i].Minimum = Minimum - ThumbValue * i;
   }
 
   Items.Last().Minimum = Minimum - ThumbValue * nValues;
}
 
private double ThumbValue
{
   get { return ActualWidth > 0? m_thumbWidth * (Maximum - Minimum)/ActualWidth : 0; }
}

用法

我的解决方案包含两个类:WitMultiRangeSliderItem 和 **WitMultiRangeSlider**。第一个类 – **WitMultiRangeSliderItem**,表示普通滑块并继承 Slider 类。第二个类 – **WitMultiRangeSlider**,是管理 WitMultiRangeSliderItem 集合的容器。

使用这些控件非常简单,并且有两种使用 WitMultiRangeSlider 控件的方法:绑定和非绑定。

绑定方式

您可以将 WitMultiRangeSliderItemsSource 属性绑定到表示范围的对象的集合。此外,您还需要在对象中指定范围的左值(**WitMultiRangeSlider** 的 LeftValue 属性)、右值(**WitMultiRangeSlider** 的 RightValue 属性)以及范围的 **Minimum** / **Maximum** 值。

<InWit:WitMultiRangeSlider ItemsSource="{Binding RangeItems}" 
   SelectedItem="{Binding SelectedRange, Mode=TwoWay}" 
   LeftValueBinding="{Binding From, Mode=TwoWay}" 
   RightValueBinding="{Binding To, Mode=TwoWay}" 
   Minimum="0.0" Maximum="22.0"/>  

您可以为选定项(来自 ItemsSource 集合的对象)指定绑定。您还可以设置 TickFrequency 和 **IsSnapToTickEnabled**,这些值将被传输到内部滑块(**WitMultiRangeSliderItem**)。

WitMultiRangeSlider 包含 MultiRangeSliderBarClicked 事件。它传递用户点击的位置。因此,当用户点击滑块栏时,您可以实现自动添加新滑块的行为,并将点击位置作为新滑块的“从”值。

非绑定方式

当您将 ItemsSource 绑定到对象集合时,控件会自动创建 WitMultiRangeSliderItem 元素并设置它们的绑定。您可以通过 Items 属性手动将 WitMultiRangeSliderItem 元素添加到 WitMultiRangeSlider 控件中。

<InWit:WitMultiRangeSlider Minimum="0.0"Maximum="2200.0">
    <InWit:WitMultiRangeSlider.Items>
        <InWit:WitMultiRangeSliderItem 
           LeftValue="{Binding UnboundRange1.From, Mode=TwoWay}"
           RightValue="{Binding UnboundRange1.To, Mode=TwoWay}" />
        <InWit:WitMultiRangeSliderItem 
           LeftValue="{Binding UnboundRange2.From, Mode=TwoWay}"
           RightValue="{Binding UnboundRange2.To, Mode=TwoWay}"/>
        <InWit:WitMultiRangeSliderItem 
           LeftValue="{Binding UnboundRange3.From, Mode=TwoWay}"
           RightValue="{Binding UnboundRange3.To, Mode=TwoWay}"/>
        <InWit:WitMultiRangeSliderItem LeftValue="{Binding UnboundRange4.From, Mode=TwoWay}"
           RightValue="{Binding UnboundRange4.To, Mode=TwoWay}"/>
    </InWit:WitMultiRangeSlider.Items>
</InWit:WitMultiRangeSlider> 

甚至可以不进行任何绑定

<InWit:WitMultiRangeSlider Minimum="0.0" Maximum="2200.0">
    <InWit:WitMultiRangeSlider.Items>
        <InWit:WitMultiRangeSliderItem LeftValue="500" RightValue="700"/>
        <InWit:WitMultiRangeSliderItem LeftValue="700" RightValue="1200"/>
        <InWit:WitMultiRangeSliderItem LeftValue="1200" RightValue="1600"/>
    </InWit:WitMultiRangeSlider.Items>
</InWit:WitMultiRangeSlider>

在这种情况下,您必须使用 WitMultiRangeSliderItem(来自 Slider)的 ValueChanged 事件来跟踪范围的变化。

注意,您只能使用 ItemsSource 或 Items,但不能同时使用。

简单的实现示例

任务是创建一个控件,用于管理一组不相交的范围,并能够添加新范围和修改用户数据。

您有一个类来表示带有用户数据的范围

public class RangeItem : INotifyPropertyChanged
{
   public event PropertyChangedEventHandler PropertyChanged = delegate { };
   
   private int m_from;
   private int m_to;
   private string m_name; 
 
   public int From
   {
        get { return m_from; }
        set
        {
            m_from = value;
            this.FirePropertyChanged();
        }
   }

   public int To
   {
        get { return m_to; }
        set
        {
            m_to = value;
            this.FirePropertyChanged();
        }
   }
 
   public string Name
   {
        get { return m_name; }
        set
        {
            m_name = value;
            this.FirePropertyChanged();
        }
    } 
} 

该类包含以下字段

  1. From – 范围的左边界
  2. To – 范围的右边界
  3. Name – 用户数据

您应该为范围集合创建一个视图模型,并提供一个添加新范围的命令:

public class RangesViewModel : INotifyPropertyChanged
{
 
   public event PropertyChangedEventHandler PropertyChanged = delegate { };
 
   private readonly ObservableContentCollection<RangeItem> m_rangeItems;
   private RangeItem m_selectedRange;
 
   private readonly Command m_insertRangeCmd;
 
   public RangesViewModel()
   {
        m_rangeItems = new ObservableContentCollection<RangeItem>
                            {
                                new RangeItem {From = 0, To = 13, Name = "BoundRange0"},
                                new RangeItem {From = 13, To = 17, Name = "BoundRange1"},
                            };
 
        m_insertRangeCmd = new DelegateCommand(x => InsertRange((int)(double)x));
   } 
 
   private void InsertRange(int level)
   {
        if (level > m_rangeItems.Last().To)
            InsertRightRange(level);
        else if (level < m_rangeItems.First().From)
            InsertLeftRange(level);
        else
        {
            var previousRange = m_rangeItems.First(x => x.To >= level);
 
            var newRange = new RangeItem
                               {
                                   From = level, 
                                   To = previousRange.To, 
                                   Name = string.Format("BoundRange{0}", m_rangeItems.Count)
                               };
 
           m_rangeItems.Insert(m_rangeItems.IndexOf(previousRange) + 1, newRange);
 
            previousRange.To = level;
        }
 
   }
 
   private void InsertRightRange(int level)
   {
        var rightRange = new RangeItem
                             {
                                 From = m_rangeItems.Last().To, 
                                 To = level, 
                                 Name = string.Format("BoundRange{0}", m_rangeItems.Count)
                             };
 
        m_rangeItems.Add(rightRange);
   }
 
   private void InsertLeftRange(int level)
   {
        var leftRange = new RangeItem
                            {
                                From = level, 
                                To = m_rangeItems.First().From, 
                                Name = string.Format("BoundRange{0}", m_rangeItems.Count)
                            };
 
        m_rangeItems.Insert(0, leftRange);
   }
 
    
   public ObservableContentCollection<RangeItem> RangeItems
   {
        get { return m_rangeItems; }
   }
 
   public RangeItem SelectedRange
   {
        get { return m_selectedRange; }
        set
        {
            m_selectedRange = value;
            this.FirePropertyChanged();
        }
   }
 
    
   public Command InsertRangeCmd
   {
        get { return m_insertRangeCmd; }
   }
} 

由于 WitMultiRangeSlider 的 **MultiRangeSliderBarClicked 事件会将用户点击的滑块值作为参数传递,因此您应该创建一个新的范围,将传递的参数作为新范围的 From 值,并将下一个滑块的 From 值作为新范围的 To 值。因此,您应该在点击点拆分现有范围。**

现在,您可以将 RangesViewModel 中的 RangeItems 绑定到 **WitMultiRangeSlider** 的 ItemsSource 来管理范围,并将 **InsertRangeCmd** 绑定到 WitMultiRangeSliderMultiRangeSliderBarClicked 事件来处理对 WitMultiRangeSlider 的双击。您还可以将 RangesViewModel 中的 RangeItems 绑定到普通 DataGrid 的 **ItemsSource** 来修改用户数据(Name 属性):

 <InWit:WitMultiRangeSlider ItemsSource="{Binding RangeItems}" 
        SelectedItem="{Binding SelectedRange, Mode=TwoWay}" 
        LeftValueBinding="{Binding From, Mode=TwoWay}" 
    	RightValueBinding="{Binding To, Mode=TwoWay}" 
    	Minimum="0.0" Maximum="22.0">
   <i:Interaction.Triggers>
        <i:EventTrigger EventName="MultiRangeSliderBarClicked">
            <U:InvokeCommandActionWithParam Command="{Binding InsertRangeCmd}" 
             	CommandParameter="{Binding RelativeSource={RelativeSource Self}, 
                	Path=InvokeParameter, Converter={StaticResource EventArgsToDouble}}"/>
        </i:EventTrigger>
   </i:Interaction.Triggers>
</InWit:WitMultiRangeSlider>
<DataGrid ItemsSource="{Binding RangeItems}" 
	SelectedItem="{Binding SelectedRange, Mode=TwoWay}" 
	CanUserAddRows="False" CanUserDeleteRows="False" 
	CanUserReorderColumns="False" 
	AutoGenerateColumns="False">
   <DataGrid.Columns>
        <DataGridTextColumn Width="*" Header="Name" Binding="{Binding Name, Mode=TwoWay}" 
		SortMemberPath="Name"/>
        <DataGridTextColumn Width="Auto" MinWidth="40" Header="From" Binding="{Binding From}" 
		IsReadOnly="True" SortMemberPath="From"/>
        <DataGridTextColumn Width="Auto" MinWidth="40" Header="To" Binding="{Binding To}" 
		IsReadOnly="True" SortMemberPath="To"/>
   </DataGrid.Columns>
</DataGrid>

在附加的示例中,您将看到使用 WitMultiRangeSlider 和交互的两种方式。

© . All rights reserved.