WPF MultiRangeSlider 控件
MultiRangeSlider 控件,
引言
有时需要指定不相交的范围。我最近的一个实际应用是为一个控件,该控件根据不同的缩放级别设置不同的地图显示。您可以使用带有两列(“从”和“到”)的 **DataGrid** 来解决这个问题。但在这种情况下,您必须跟踪值的变化,以确保范围不相交。此外,您还必须在用户输入错误数据时发出信号,例如通过将网格单元格染成红色,或者通过静默丢弃更改。这会产生复杂的逻辑并使用户感到困惑。对于开发者和用户来说,通过多范围控件指定不相交的范围要简单得多,因为该控件实际上不允许设置错误的值。
不幸的是,标准 Visual Studio 控件中没有这样的元素。有很多关于指定单个范围(带有两个滑块)的控件的文章。常见的方法是将一个滑块叠加在另一个(相同的)滑块上并跟踪值。
我将这种方法总结为将其扩展到无限数量的滑块,并添加用户交互,让用户有机会在运行时添加或删除新范围。
主要思想
主要思想是每个内部滑块都与两个相邻的滑块相连接,并与它们协同工作。
内部滑块包含四个属性
LeftValue
– 与滑块关联的范围的左值(等于普通 Slider 的Value
属性,即滑块在滑块轴上的位置)。RightValue
– 与滑块关联的范围的右值。MinimumValue
–LeftValue
的最小边界,等于前一个滑块的RightValue
。MaximumValue
–RightValue
的最大边界,等于下一个滑块的LeftValue
。
当我将滑块向右移动时,我将修改前一个滑块的 RightValue
(因此前一个范围增大),前一个滑块的 MaximumValue
(因此我可以将前一个滑块的前进),我自己的 LeftValue
,以及下一个滑块的 MinimumValue
。
从图中可以看出,要描述 N 个范围,需要 N+1 个滑块(滑块滑块),因为最后一个滑块滑块定义了最后一个范围的右值(倒数第二个滑块的 RightValue
)。
当然,对于第一个和最后一个滑块,您必须考虑它们的左边界和右边界分别是滑块的 Minimum
和 Maximum
。
MinimumValue
和 MaximumValue
属性是为了防止滑块相互重叠,但有一个陷阱。
第一个想法是检查滑块值(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
控件的方法:绑定和非绑定。
绑定方式
您可以将 WitMultiRangeSlider
的 ItemsSource
属性绑定到表示范围的对象的集合。此外,您还需要在对象中指定范围的左值(**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();
}
}
}
该类包含以下字段
From
– 范围的左边界To
– 范围的右边界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**
绑定到 WitMultiRangeSlider
的 MultiRangeSliderBarClicked
事件来处理对 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
和交互的两种方式。