在某些应用场景中,我们需要做可视化的范围选择。例如,在进行录像剪辑的时候,我们希望在播放时间轴上通过拖动两个可移动的控件来确定两控件之间的时间轴为我们希望进行录像剪辑的时间范围。WPF中并没有这样的预定义控件,所以如果需要有这样的应用场景,则需要自定义这样的控件。本文便是简述定制这样一个控件的基本的思路。
一 基本结构
先来看一下这样一个控件的基本结构,如上图所示,总体可分为4个部分,1是整个可选择的范围,2是选中的范围,3是左右两个选择器,可在选择范围轴上移动,4是选择信息显示按钮。
从控件构成来说,1、2都可以用Path来实现,3的上下两个部分也可以用Path实现,4则是一个TextBlock(之所以不选择Label,是希望能用到TextBlock的TextTrimming属性)。
二 代码结构
为了便于复用,我将此控件单独封装成了一个库(如有需要,也可以很方便的与其他自定义空间库合并),总体上代码的结构非常简单:一个RangeSelector类的cs代码文件RangeSelector.cs,用于控件的逻辑控制;一个控件默认模板的xaml代码文件RangeSelector.xaml;另外还有三个用于控件辅助控制的数据转换类(Converter)。
三 默认模板
RangeSelector.xaml定义了控件的默认外观,根据(一)里的基本结构,控件必须要包含以下几个命名部分:
PART_Range:为Path控件,用于展示总的选择范围。
PART_Canvas:为Canvas控件,用于承载其他绘制控件的容器,之所以选择Canvas,因为他可以通过SetLeft和SetTop方法方便的设置控件的绝对位置,为选择器的移动提供了方便。
PART_SelectedRange:为Path控件,选中的范围。
PART_RangeSelector1/PART_RangeSelector2:范围选择器,本文用两个Path组合的Grid来实现。
PART_LowerMessageTextBlock/PART_UpperMessageTextBlock:为TextBlock控件,用于显示选择的范围信息。
具体代码如下:
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:RangeSelectors"
xmlns:cvt="clr-namespace:RangeSelectors.Converter">
<cvt:DoubleToGridLengthConverter x:Key="doubleToGridLengthConverter"/>
<cvt:RangePathMarginConverter x:Key="rangePathMarginConverter"/>
<cvt:SelectorUpShapeConverter x:Key="selectorUpShapeConverter"/>
<cvt:SelectorDownShapeConverter x:Key="selectorDownShapeConverter"/>
<Style TargetType="{x:Type local:RangeSelector}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type local:RangeSelector}">
<Border Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}">
<Grid>
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition Height="auto"/>
<RowDefinition/>
</Grid.RowDefinitions>
<Path x:Name="PART_Range" Grid.Row="1" Panel.ZIndex="0"
Fill="{TemplateBinding RangeColor}"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Stretch="Fill">
<Path.Margin>
<MultiBinding Converter="{StaticResource rangePathMarginConverter}">
<Binding Path="SelectorWidth" RelativeSource="{RelativeSource Mode=FindAncestor, AncestorType={x:Type local:RangeSelector}}"/>
<Binding Path="SelectorHeight" RelativeSource="{RelativeSource Mode=FindAncestor, AncestorType={x:Type local:RangeSelector}}"/>
</MultiBinding>
</Path.Margin>
</Path>
<Canvas x:Name="PART_Canvas" Grid.Row="1" HorizontalAlignment="Stretch" VerticalAlignment="Top">
<Path x:Name="PART_SelectedRange" Grid.Row="1" Panel.ZIndex="1"
Fill="{TemplateBinding SelectedRangeColor}"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Stretch="Fill">
<Path.Margin>
<MultiBinding Converter="{StaticResource rangePathMarginConverter}">
<Binding Path="SelectorWidth" RelativeSource="{RelativeSource Mode=FindAncestor, AncestorType={x:Type local:RangeSelector}}"/>
<Binding Path="SelectorHeight" RelativeSource="{RelativeSource Mode=FindAncestor, AncestorType={x:Type local:RangeSelector}}"/>
</MultiBinding>
</Path.Margin>
</Path>
<Grid x:Name="PART_RangeSelector1" Panel.ZIndex="0"
Canvas.Left="0" Canvas.Top="0" Background="Transparent">
<Grid.RowDefinitions>