概述
wpf的界面功能很强大,尤其是其绑定机制天然的支持mvvm模式,这样使得界面和业务逻辑可以完全分离,大大提高了自定义控件的灵活性和通用性。在做视频剪辑工具的时候是需要尺子控件的,在wpf中很容易实现一个自定义的尺子控件。但是在实际使用中会遇到一个问题,即尺子越长,渲染速度越慢,当其总刻度到达几百万时拖动会直接造成界面卡顿。所以需要给标尺控件加入局部刷新技术,即只渲染当前可见部分的刻度,减少看不见部分不必要的渲染。
原理说明
1、尺子实现
(1)自定义控件
定义一个尺子类,继承: FrameworkElement。
(2)重写OnRender
在OnRender中使用DrawingContext对象的DrawText方法绘制文字、DrawLine方法绘制刻度。
实现效果如下(单位是时间):
2、局部刷新
(1)获取可视区域
要实现局部刷新,需要获取当前控件可视区域。可视区域即是ScrollViewer的区域(如果标尺不在ScrollViewer滚动区域中,则其界面完全不需要变化,也就不存在局部刷新的问题)。
(2)计算可视区域刻度
根据滚动条的位置,以及其大小,计算当前区域需要显示的所有刻度。这里需要确保刻度的一致性,即消除浮点计算过程中的误差。最后通过DrawingContext的DrawText和DrawLine将局部刻度绘制即可。
程序设计
1、接口设计
(1)类
定义一个尺子类继承于FrameworkElement。
public class Ruler : FrameworkElement
(2)属性
定义一些必要的属性,提供给xaml使用。
/// <summary>
/// 尺子长度
///比如单位为分钟,则1位一分钟。
/// </summary>
public double Length{set;get;}
/// <summary>
/// 指针,指示某一刻度
/// </summary>
public double Chip{set;get;}
/// <summary>
/// 指针是否可见
/// </summary>
public bool ChipVisible{set;get;}
/// <summary>
/// 像素每单位
///比如设备的dpi为96,那这个属性设为96,以厘米为单位,那就是真实比例的尺子。
/// </summary>
public double PixelsPerUnit
/// <summary>
/// 缩放倍数
/// </summary>
public double Zoom{set;get;}
2、关键实现
(1)、获取可视区域
在OnRender方法中:
protected override void OnRender(DrawingContext drawingContext){
//查询是否存在滚动条
var sv = GetAncestor<ScrollViewer>(this);
if (sv != ParentScrollViewer && ParentScrollViewer != null)
{
ParentScrollViewer.ScrollChanged -= ScrollChangedEventHandler;
ParentScrollViewer = null;
}
else
{
ParentScrollViewer = sv;
if(ParentScrollViewer!=null)
ParentScrollViewer.ScrollChanged += ScrollChangedEventHandler;
}
if (ParentScrollViewer != null)
this.Width = DipHelper.CmToDip(Length) * this.Zoom;
//取得绘制区域
double start = LeftOffset;
if (ParentScrollViewer != null)
start += ParentScrollViewer.HorizontalOffset - ParentScrollViewer.Width * 0.5;
double end = start + (ParentScrollViewer == null ? this.ActualWidth : ParentScrollViewer.Width * 2d);
if (start < LeftOffset)
start = LeftOffset;
if (end > this.ActualWidth)
end = this.ActualWidth;
//计算可视区域刻度以及绘制代码......
}
(2)计算刻度
①刻度实体
class Mark
{
public double Pos { set; get; }
public double Num { set; get; }
}
②生成刻度
/// <summary>
/// 刻度参数
/// </summary>
class MarkParameter
{
public Scale FullScale { set; get; }//全尺寸范围
public Scale VisualScale { set; get; }//可视区域范围
public double Unit { set; get; }//单位长度
public double MaxBlank { set; get; }//最小空白
public double SubUnitRatio { set; get; }//单位转换比
public double NumberVisualWidth { set; get; }//显示数字的最小间距
public double NumberStart { set; get; }//起始的数字
public double NumberStep { set; get; } //数字增加的步长
}
/// <summary>
/// 生成刻度
/// </summary>
/// <param name="mp">刻度参数</param>
/// <returns></returns>
List<Mark> GenerateMarks(MarkParameter mp)
{
var unit = mp.Unit;
var step = mp.NumberStep;
while (unit > mp.MaxBlank)
{
unit /= mp.SubUnitRatio;
step /= mp.SubUnitRatio;
}
double a = mp.VisualScale.Start - mp.FullScale.Start;
int c = (int)(a / unit);
double s = c * unit + mp.FullScale.Start;
double e = mp.VisualScale.End;
return GenerateMarks(s, e, unit, mp.MaxBlank, mp.SubUnitRatio, mp.NumberVisualWidth, mp.NumberStart + c * step, step, c);
}
/// <summary>
/// 生成刻度
/// </summary>
/// <param name="start">起始位置</param>
/// <param name="end">结束位置</param>
/// <param name="unit">像素每单位</param>
/// <param name="maxBlank">最小空白</param>
/// <param name="subUnitRatio">单位转换比</param>
/// <param name="numberVisualWidth"></param>
/// <param name="startNum">起始的数字</param>
/// <param name="step">数字增加的补充</param>
/// <param name="index"></param>
/// <returns>刻度集合</returns>
List<Mark> GenerateMarks(double start, double end, double unit, double maxBlank, double subUnitRatio, double numberVisualWidth, double startNum, double step, int index)
{
List<Mark> marks = new List<Mark>();
Mark m;
double n, w;
int c;
while (unit > maxBlank)
{
unit /= subUnitRatio;
step /= subUnitRatio;
}
n = startNum;
if (index == 0)
w = numberVisualWidth;
else
//计算开始显示数字的位置
{
var x = numberVisualWidth / unit;
if (x > (int)x)
{
x += 1;
}
var y = (int)x;
if ((index) % y == 0)
w = numberVisualWidth;
else
{
w = (index) % y * unit;
}
}
c = (int)((end - start) / unit) + 1;
for (int j = 0; j < c; j++)
{
m = new Mark();
marks.Add(m);
m.Pos = j * unit + start;//采用乘是为了统一精度。
if (w >= numberVisualWidth)
{
m.Num = n;
w = unit;
}
else
{
m.Num = double.NaN;
w += +unit;
}
n += step;
}
return marks;
}
3、使用例子
直接在xaml中使用即可:
<Window x:Class="WpfRuler.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:WpfRuler"
xmlns:localCommon="clr-namespace:View.Common"
mc:Ignorable="d"
Title="MainWindow" Height="360" Width="640">
<Grid>
<StackPanel VerticalAlignment="Bottom" Orientation="Horizontal" Height="200" >
<ScrollBar VerticalAlignment="Top" Height="40" Width="100" ToolTip="放大、缩小右边标尺" x:Name="ScB_Zoom" Minimum="1" Maximum="200" Value="55" Orientation="Horizontal" />
<ScrollViewer x:Name="Scrl_Tracks" Width="540" Height="200" VerticalScrollBarVisibility="Disabled" HorizontalScrollBarVisibility="Visible" >
<ScrollViewer.Content>
<Border Background="Black" Height="40" VerticalAlignment="Top" >
<localCommon:Ruler ToolTip="单位(分:秒)"
PixelsPerUnit="96"
Marks="Down" HorizontalAlignment="Left" Height="40"
Length="0.5"
Chip="10"
ChipVisible="True"
Zoom="{Binding ElementName=ScB_Zoom,Path=Value}" >
</localCommon:Ruler>
</Border>
</ScrollViewer.Content>
<ScrollViewer.Resources>
</ScrollViewer.Resources>
</ScrollViewer>
</StackPanel>
</Grid>
</Window>
界面效果如下: