WPF 视频传输控制
WPF Video Transport Control
我对自定义控件比较陌生(在代码中从头开始编写控件 - 而不仅仅是对现有控件进行样式设置)。我正在尝试复制 YouTube 视频控件,你知道那个...
首先我想开发"timeline"(透明的灰色条,显示视频的当前位置并允许用户拖动以更改位置)。随着预览面板和所有其他内容稍后推出...
我目前已部分呈现控件,并且悬停动画和缩放效果很好...
但是,我正在努力编写正确的代码以允许我拖动 "thumb"。当我尝试左键单击代表我的拇指的 Ellipse
时,包含 Canvas
的离开事件会根据 WPF 文档触发,所以没有抱怨,我只是不; t知道如何实现我想要的,事实上,如果我已经做的是正确的方法。
代码:
[ToolboxItem(true)]
[DisplayName("VideoTimeline")]
[Description("Controls which allows the user navigate video media. In addition is can display a " +
"waveform repesenting the audio channels for the loaded video media.")]
//[TemplatePart(Name = "PART_ThumbCanvas", Type = typeof(Canvas))]
[TemplatePart(Name = "PART_TimelineCanvas", Type = typeof(Canvas))]
[TemplatePart(Name = "PART_WaveformCanvas", Type = typeof(Canvas))]
[TemplatePart(Name = "PART_PreviewCanvas", Type = typeof(Canvas))]
[TemplatePart(Name = "PART_Thumb", Type = typeof(Ellipse))] // Is this the right thing to be doing?
public class VideoTimeline : Control
{
private Canvas thumbCanvas;
private Canvas timelineCanvas;
private Canvas waveformCanvas;
private Canvas previewCanvas;
private Rectangle timelineOuterBox = new Rectangle();
private Rectangle timelineProgressBox = new Rectangle();
private Rectangle timelineSelectionBox = new Rectangle();
private Ellipse timelineThumb = new Ellipse();
private Path previewWindow = new Path();
private Point mouseDownPosition;
private Point currentMousePosition;
private const int TIMELINE_ANIMATION_DURATION = 400;
private const string HIGHLIGHT_FILL = "#878787";
private double __timelineWidth;
#region Initialization.
static VideoTimeline()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(VideoTimeline),
new FrameworkPropertyMetadata(typeof(VideoTimeline)));
}
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
//thumbCanvas = GetTemplateChild("PART_ThumbCanvas") as Canvas;
//thumbCanvas.Background = new SolidColorBrush(Colors.Transparent);
//thumbCanvas.Children.Add(timelineThumb);
timelineThumb = EnforceInstance<Ellipse>("PART_Thumb");
timelineThumb.MouseLeftButtonDown -= TimelineThumb_MouseLeftButtonDown;
timelineThumb.MouseLeftButtonDown += TimelineThumb_MouseLeftButtonDown;
timelineCanvas = GetTemplateChild("PART_TimelineCanvas") as Canvas;
timelineCanvas.Background = new SolidColorBrush(Colors.Transparent);
timelineCanvas.Children.Add(timelineOuterBox);
timelineCanvas.Children.Add(timelineSelectionBox);
timelineCanvas.Children.Add(timelineProgressBox);
timelineCanvas.Children.Add(timelineThumb);
previewCanvas = GetTemplateChild("PART_PreviewCanvas") as Canvas;
previewCanvas.Background = new SolidColorBrush(Colors.Transparent);
previewCanvas.Children.Add(previewWindow);
}
private T EnforceInstance<T>(string partName) where T : FrameworkElement, new()
{
return GetTemplateChild(partName) as T ?? new T();
}
protected override void OnTemplateChanged(ControlTemplate oldTemplate, ControlTemplate newTemplate)
{
base.OnTemplateChanged(oldTemplate, newTemplate);
if (timelineCanvas != null)
timelineCanvas.Children.Clear();
SetDefaultMeasurements();
}
#endregion // Initialization.
#region Event Overrides.
protected override void OnRenderSizeChanged(SizeChangedInfo sizeInfo)
{
base.OnRenderSizeChanged(sizeInfo);
//UpdateWaveformCacheScaling();
SetDefaultMeasurements();
UpdateAllRegions();
}
protected override void OnMouseLeftButtonDown(MouseButtonEventArgs e)
{
base.OnMouseLeftButtonDown(e);
Canvas c = e.OriginalSource as Canvas;
if (c == null)
c = Utils.FindParent<Canvas>(e.OriginalSource as FrameworkElement);
if (c != null)
{
CaptureMouse();
mouseDownPosition = e.GetPosition(c);
if (c.Name == "PART_TimelineCanvas")
{
Trace.WriteLine("OnMouseLeftDown over TimeLine");
}
}
}
protected override void OnMouseLeftButtonUp(MouseButtonEventArgs e)
{
base.OnMouseLeftButtonUp(e);
ReleaseMouseCapture();
}
protected override void OnMouseMove(MouseEventArgs e)
{
base.OnMouseMove(e);
currentMousePosition = e.GetPosition(thumbCanvas);
if (Mouse.Captured == null)
{
Canvas c = e.OriginalSource as Canvas;
if (c == null)
c = Utils.FindParent<Canvas>(e.OriginalSource as FrameworkElement);
}
}
#endregion // Event Overrides.
#region Drawing Methods and Events.
private void UpdateAllRegions()
{
UpdateTimelineCanvas();
}
private void UpdateTimelineCanvas()
{
if (timelineCanvas == null)
return;
SetDefaultMeasurements();
// Bounding timeline box.
timelineOuterBox.Fill = new SolidColorBrush(
(Color)ColorConverter.ConvertFromString("#878787")) { Opacity = 0.25 };
timelineOuterBox.StrokeThickness = 0.0;
timelineOuterBox.Width = __timelineWidth;
timelineOuterBox.Height = TimelineThickness;
timelineOuterBox.Margin = new Thickness(TimelineExpansionFactor * TimelineThickness,
(timelineCanvas.RenderSize.Height - TimelineThickness) / 2, 0, 0);
timelineOuterBox.SnapsToDevicePixels = true;
// Selection timeline box.
timelineSelectionBox.Fill = TimelineSelectionBrush;
timelineSelectionBox.Width = 0.0;
timelineSelectionBox.Height = TimelineThickness;
timelineSelectionBox.Margin = new Thickness(TimelineExpansionFactor * TimelineThickness,
(timelineCanvas.RenderSize.Height - TimelineThickness) / 2, 0, 0);
timelineSelectionBox.SnapsToDevicePixels = true;
// Progress timeline box.
timelineProgressBox.Fill = TimelineProgressBrush;
timelineProgressBox.StrokeThickness = 0.0;
timelineProgressBox.Width = 0.0;
timelineProgressBox.Height = TimelineThickness;
timelineProgressBox.Margin = new Thickness(TimelineExpansionFactor * TimelineThickness,
(timelineCanvas.RenderSize.Height - TimelineThickness) / 2, 0, 0);
timelineProgressBox.SnapsToDevicePixels = true;
// Animation and selection.
timelineCanvas.MouseEnter -= TimelineCanvas_MouseEnter;
timelineCanvas.MouseEnter += TimelineCanvas_MouseEnter;
timelineCanvas.MouseLeave -= TimelineCanvas_MouseLeave;
timelineCanvas.MouseLeave += TimelineCanvas_MouseLeave;
timelineCanvas.MouseMove -= TimelineCanvas_MouseMove;
timelineCanvas.MouseMove += TimelineCanvas_MouseMove;
timelineCanvas.MouseDown -= TimelineCanvas_MouseDown;
timelineCanvas.MouseDown += TimelineCanvas_MouseDown;
// The draggable thumb.
timelineThumb.Fill = TimelineThumbBrush;
//timelineThumb.Stroke = new SolidColorBrush(Colors.Black);
//timelineThumb.StrokeThickness = 0.5;
timelineThumb.VerticalAlignment = VerticalAlignment.Center;
timelineThumb.Height = timelineThumb.Width = 0.0;
timelineThumb.Margin = new Thickness(TimelineExpansionFactor * TimelineThickness,
timelineCanvas.RenderSize.Height / 2, 0, 0);
timelineThumb.SnapsToDevicePixels = true;
timelineThumb.MouseLeftButtonDown -= TimelineThumb_MouseLeftButtonDown;
timelineThumb.MouseLeftButtonDown += TimelineThumb_MouseLeftButtonDown;
timelineThumb.MouseLeftButtonUp -= TimelineThumb_MouseLeftButtonUp;
timelineThumb.MouseLeftButtonUp += TimelineThumb_MouseLeftButtonUp;
// Preview window.
}
private void TimelineCanvas_MouseDown(object sender, MouseButtonEventArgs e)
{
Trace.WriteLine("POON");
}
private void SetDefaultMeasurements()
{
if (timelineCanvas != null)
__timelineWidth = timelineCanvas.RenderSize.Width - 2 * 2 * TimelineThickness;
}
private void TimelineCanvas_MouseEnter(object sender, MouseEventArgs e)
{
timelineThumb.ResetAnimation(Ellipse.WidthProperty, Ellipse.HeightProperty);
timelineProgressBox.ResetAnimation(Rectangle.HeightProperty, Rectangle.MarginProperty);
timelineSelectionBox.ResetAnimation(Rectangle.HeightProperty, Rectangle.MarginProperty);
timelineOuterBox.ResetAnimation(Rectangle.HeightProperty, Rectangle.MarginProperty);
CircleEase easing = new CircleEase();
easing.EasingMode = EasingMode.EaseOut;
// Thumb animation.
Thickness margin = new Thickness(0,
(timelineCanvas.RenderSize.Height - 2 * TimelineExpansionFactor * TimelineThickness) / 2, 0, 0);
EllpiseDiameterAnimation(timelineThumb, TimelineThickness * TimelineExpansionFactor * 2, margin, easing);
// Timeline animation.
margin = new Thickness(TimelineExpansionFactor * TimelineThickness,
(timelineCanvas.RenderSize.Height - (TimelineThickness * TimelineExpansionFactor)) / 2, 0, 0);
TimelineHeightAnimation(timelineProgressBox, TimelineThickness * TimelineExpansionFactor, margin, easing);
TimelineHeightAnimation(timelineSelectionBox, TimelineThickness * TimelineExpansionFactor, margin, easing);
TimelineHeightAnimation(timelineOuterBox, TimelineThickness * TimelineExpansionFactor, margin, easing);
double selectionWidth = (currentMousePosition.X / RenderSize.Width) * timelineOuterBox.Width;
timelineSelectionBox.Width = selectionWidth;
Trace.WriteLine("MouseENTER Canvas");
}
private void TimelineCanvas_MouseLeave(object sender, MouseEventArgs e)
{
timelineThumb.ResetAnimation(Ellipse.WidthProperty, Ellipse.HeightProperty);
timelineProgressBox.ResetAnimation(Rectangle.HeightProperty, Rectangle.MarginProperty);
timelineSelectionBox.ResetAnimation(Rectangle.HeightProperty, Rectangle.MarginProperty);
timelineOuterBox.ResetAnimation(Rectangle.HeightProperty, Rectangle.MarginProperty);
CircleEase easing = new CircleEase();
easing.EasingMode = EasingMode.EaseOut;
// Thumb animation.
Thickness margin = new Thickness(TimelineExpansionFactor * TimelineThickness, timelineCanvas.RenderSize.Height / 2, 0, 0);
EllpiseDiameterAnimation(timelineThumb, 0.0, margin, easing);
// Timeline animation.
margin = new Thickness(TimelineExpansionFactor * TimelineThickness,
(timelineCanvas.RenderSize.Height - TimelineThickness) / 2, 0, 0);
TimelineHeightAnimation(timelineProgressBox, TimelineThickness, margin, easing);
TimelineHeightAnimation(timelineSelectionBox, TimelineThickness, margin, easing);
TimelineHeightAnimation(timelineOuterBox, TimelineThickness, margin, easing);
if (!isDraggingThumb)
timelineSelectionBox.Width = 0.0;
Trace.WriteLine("MouseLeave Canvas");
}
private void TimelineCanvas_MouseMove(object sender, MouseEventArgs e)
{
Point relativePosition = e.GetPosition(timelineOuterBox);
double selectionWidth = (relativePosition.X / timelineOuterBox.Width) * timelineOuterBox.Width;
timelineSelectionBox.Width = selectionWidth.Clamp(0.0, timelineOuterBox.Width);
if (isDraggingThumb)
{
timelineProgressBox.Width = timelineSelectionBox.Width;
Thickness thumbMargin = new Thickness(TimelineExpansionFactor * TimelineThickness,
(timelineCanvas.RenderSize.Height - (TimelineThickness * TimelineExpansionFactor)) / 2, 0, 0);
timelineThumb.Margin = thumbMargin;
}
}
private bool isDraggingThumb = false;
private void TimelineThumb_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
CaptureMouse();
isDraggingThumb = true;
Trace.WriteLine("Dragging Thumb");
}
private void TimelineThumb_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{
ReleaseMouseCapture();
isDraggingThumb = false;
Trace.WriteLine("STOPPED Dragging Thumb");
}
#endregion // Drawing Methods and Events.
#region Animation Methods.
private void EllpiseDiameterAnimation(Ellipse ellipse, double diameter, Thickness margin, IEasingFunction easing)
{
AnimationTimeline widthAnimation = ShapeWidthAnimation(ellipse, diameter, easing);
AnimationTimeline heightAnimation = ShapeHeightAnimation(ellipse, diameter, easing);
AnimationTimeline marginAnimation = ShapeMarginAnimation(ellipse, margin, easing);
Storyboard storyboard = new Storyboard();
storyboard.Children.Add(widthAnimation);
storyboard.Children.Add(heightAnimation);
storyboard.Children.Add(marginAnimation);
storyboard.Begin(this);
}
private void TimelineHeightAnimation(Rectangle rectangle, double height, Thickness margin, IEasingFunction easing)
{
AnimationTimeline heightAnimation = ShapeHeightAnimation(rectangle, height, easing);
AnimationTimeline marginAnimation = ShapeMarginAnimation(rectangle, margin, easing);
Storyboard storyboard = new Storyboard();
storyboard.Children.Add(marginAnimation);
storyboard.Children.Add(heightAnimation);
storyboard.Begin(this);
}
private AnimationTimeline ShapeMarginAnimation(Shape shape, Thickness margin, IEasingFunction easing)
{
ThicknessAnimation marginAnimation = new ThicknessAnimation(
margin, TimeSpan.FromMilliseconds((TIMELINE_ANIMATION_DURATION)));
if (easing != null)
marginAnimation.EasingFunction = easing;
Storyboard.SetTarget(marginAnimation, shape);
Storyboard.SetTargetProperty(marginAnimation, new PropertyPath(Rectangle.MarginProperty));
return marginAnimation;
}
private AnimationTimeline ShapeWidthAnimation(Shape shape, double width, IEasingFunction easing)
{
DoubleAnimation widthAnimation = new DoubleAnimation(
width, TimeSpan.FromMilliseconds(TIMELINE_ANIMATION_DURATION));
if (easing != null)
widthAnimation.EasingFunction = easing;
Storyboard.SetTarget(widthAnimation, shape);
Storyboard.SetTargetProperty(widthAnimation, new PropertyPath(Shape.WidthProperty));
return widthAnimation;
}
private AnimationTimeline ShapeHeightAnimation(Shape shape, double height, IEasingFunction easing)
{
DoubleAnimation heightAnimation = new DoubleAnimation(
height, TimeSpan.FromMilliseconds(TIMELINE_ANIMATION_DURATION));
if (easing != null)
heightAnimation.EasingFunction = easing;
Storyboard.SetTarget(heightAnimation, shape);
Storyboard.SetTargetProperty(heightAnimation, new PropertyPath(Shape.HeightProperty));
return heightAnimation;
}
#endregion // Animation Methods.
// Lots of DependencyProperties here...
}
XAML风格
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:MediaControlBuilder">
<Style TargetType="{x:Type local:VideoTimeline}">
<Setter Property="TimelineProgressBrush" Value="DarkOrange"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type local:VideoTimeline}">
<Border Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}">
<Grid>
<Grid.RowDefinitions>
<!--<RowDefinition Height="*"/>-->
<!--<RowDefinition Height="15"/>-->
<RowDefinition Height="*"/>
<RowDefinition Height="20"/>
<!--<RowDefinition Height="*"/>-->
</Grid.RowDefinitions>
<Canvas Name="PART_PreviewCanvas"
Grid.Row="0"
ClipToBounds="True"/>
<Canvas Name="PART_ThumbCanvas"
Grid.Row="1"
ClipToBounds="True"/>
<Canvas Name="PART_TimelineCanvas"
Grid.Row="1"
ClipToBounds="True"/>
<Canvas Name="PART_WaveformCanvas"
Grid.Row="1"
ClipToBounds="True"/>
</Grid>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>
我的问题是:
- 我绘制可拖动拇指的方法是否正确?
- 我怎样才能真正更改代码以使我的 "thumb" 拖动正常工作?
感谢您的宝贵时间。
Ps。具有工作代码的 GitHub 项目是 here,因此您可以重现我遇到的问题。如果有人想帮我开发这个控件,那就太棒了!
Pps。我知道我可以重写滑块以获得 "timeline" 的功能,但这只是更全面控制的第一部分,因此需要从头开始编写。
在创建新的自定义控件时,您不应该 "write control from scratch in code"。
更好的方法是基于现有控件的新实现。在您的情况下,您想创建一个自定义滑块控件,以便您的自定义控件可以继承 Slider
利用现有功能,例如拇指拖动逻辑和开始、结束、值属性。
当扩展现有控件时,您从原始控件的默认模板开始,它可以通过 VS 获得。滑块将具有您应该特别感兴趣的元素:
<Track x:Name="PART_Track" Grid.Column="1">
<Track.DecreaseRepeatButton>
<RepeatButton Command="{x:Static Slider.DecreaseLarge}" Style="{StaticResource RepeatButtonTransparent}"/>
</Track.DecreaseRepeatButton>
<Track.IncreaseRepeatButton>
<RepeatButton Command="{x:Static Slider.IncreaseLarge}" Style="{StaticResource RepeatButtonTransparent}"/>
</Track.IncreaseRepeatButton>
<Track.Thumb>
<Thumb x:Name="Thumb" Focusable="False" Height="11" OverridesDefaultStyle="True" Template="{StaticResource SliderThumbVerticalDefault}" VerticalAlignment="Top" Width="18"/>
</Track.Thumb>
</Track>
通过在模板中使用所需的元素,您的基本控件将处理所有基本的滑块功能。从这里开始,您可以更改基本控件功能,按照您想要的方式设置滑块部件的样式并添加任何新功能。
如果您不想公开不适用于您的时间轴控件的 Slider
属性,例如 Minimum
,只需在您的模板中使用 Slider
控件。
我认为您的问题集中在时间轴滑块上。无需创建自己的。只需使用 Slider control。您可以重新设置样式以使填充变为红色,其余部分变为半透明。然后可以将 Value 绑定到 MediaElement 控件的 Position,将 Slider 的 Maximum 绑定到 Duration。
<Slider Value="{Binding Position.Milliseconds, ElementName=MediaPlayer}"
Maximum="{Binding Duration.TimeSpan.Milliseconds, , ElementName=MediaPlayer}"
Style="{StaticResource YouTubeSliderStyle}" />
当值发生变化时,您可以更新 MediaElement 的位置。您只想在用户更改值时执行此操作(而不是在由于 Position 更新而更改时)。为此,您可以监听 mousedown/up 和 keydown/up 事件。在这些事件期间,您可以(取消)订阅 ValueChanged 事件并更新位置。
private void UpdatePosition(long time)
{
MediaPlayer.Position = TimeSpan.FromMilliseconds(time);
}
更新: show/hide 拇指的方法。
您可以通过两种方式显示或隐藏拇指。第一个是创建新的 Slider 控件和 show/hide 鼠标悬停时的滑块。
class YouTubeSlider : Slider
{
private Thumb _thumb;
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
_thumb = (Thumb)GetTemplateChild("Thumb");
_thumb.Opacity = 0;
}
protected override void OnMouseEnter(MouseEventArgs e)
{
base.OnMouseEnter(e);
_thumb.Opacity = 1;
}
protected override void OnMouseLeave(MouseEventArgs e)
{
base.OnMouseLeave(e);
_thumb.Opacity = 0;
}
}
二是在控件的样式内处理。 (为简洁起见删除了一些部分)
<ControlTemplate x:Key="SliderHorizontal" TargetType="{x:Type Slider}">
<Border x:Name="border" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" Background="{TemplateBinding Background}" SnapsToDevicePixels="True">
<!-- Elements -->
<Track.Thumb>
<Thumb x:Name="Thumb" Opacity="0" Focusable="False" Height="18" OverridesDefaultStyle="True" Template="{StaticResource SliderThumbHorizontalDefault}" VerticalAlignment="Center" Width="11"/>
</Track.Thumb>
<!-- closing tags -->
</Border>
<ControlTemplate.Triggers>
<!-- missing triggers -->
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Opacity" TargetName="Thumb" Value="1"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
我不确定,但我认为这可以解决您的问题:
private void TimelineCanvas_MouseMove(object sender, MouseEventArgs e)
{
Point relativePosition = e.GetPosition(timelineOuterBox);
double selectionWidth = (relativePosition.X / timelineOuterBox.Width) * timelineOuterBox.Width;
timelineSelectionBox.Width = selectionWidth.Clamp(0.0, timelineOuterBox.Width);
if (isDraggingThumb)
{
timelineProgressBox.Width = timelineSelectionBox.Width;
//Thickness thumbMargin = new Thickness(TimelineThickness * TimelineExpansionFactor,
// (timelineCanvas.RenderSize.Height - (TimelineThickness * TimelineExpansionFactor)) / 2, 0, 0);
//timelineThumb.Margin = thumbMargin;
Canvas.SetLeft(timelineThumb, timelineProgressBox.Width);
}
}
private bool isDraggingThumb = false;
private void TimelineThumb_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
e.Handled = true;
//CaptureMouse();
isDraggingThumb = true;
Trace.WriteLine("Dragging Thumb");
}
private void TimelineThumb_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{
e.Handled = true;
//ReleaseMouseCapture();
isDraggingThumb = false;
Trace.WriteLine("STOPPED Dragging Thumb");
}
您可以通过处理事件参数来停止冒泡,离开事件不会被触发。
要更改拇指的位置,您必须设置 Canvas 的左附加 属性。
此外,您还必须重置 isdraggingThumb :
/// <summary>
/// Invoked when an unhandled MouseLeftButtonUp routed event reaches an element in
/// its route that is derived from this class. Implement this method to add class
/// handling for this event.
/// </summary>
/// <param name="e">The MouseButtonEventArgs that contains the event data. The event
/// data reports that the left mouse button was released.</param>
protected override void OnMouseLeftButtonUp(MouseButtonEventArgs e)
{
isDraggingThumb = false;
我对自定义控件比较陌生(在代码中从头开始编写控件 - 而不仅仅是对现有控件进行样式设置)。我正在尝试复制 YouTube 视频控件,你知道那个...
首先我想开发"timeline"(透明的灰色条,显示视频的当前位置并允许用户拖动以更改位置)。随着预览面板和所有其他内容稍后推出...
我目前已部分呈现控件,并且悬停动画和缩放效果很好...
但是,我正在努力编写正确的代码以允许我拖动 "thumb"。当我尝试左键单击代表我的拇指的 Ellipse
时,包含 Canvas
的离开事件会根据 WPF 文档触发,所以没有抱怨,我只是不; t知道如何实现我想要的,事实上,如果我已经做的是正确的方法。
代码:
[ToolboxItem(true)]
[DisplayName("VideoTimeline")]
[Description("Controls which allows the user navigate video media. In addition is can display a " +
"waveform repesenting the audio channels for the loaded video media.")]
//[TemplatePart(Name = "PART_ThumbCanvas", Type = typeof(Canvas))]
[TemplatePart(Name = "PART_TimelineCanvas", Type = typeof(Canvas))]
[TemplatePart(Name = "PART_WaveformCanvas", Type = typeof(Canvas))]
[TemplatePart(Name = "PART_PreviewCanvas", Type = typeof(Canvas))]
[TemplatePart(Name = "PART_Thumb", Type = typeof(Ellipse))] // Is this the right thing to be doing?
public class VideoTimeline : Control
{
private Canvas thumbCanvas;
private Canvas timelineCanvas;
private Canvas waveformCanvas;
private Canvas previewCanvas;
private Rectangle timelineOuterBox = new Rectangle();
private Rectangle timelineProgressBox = new Rectangle();
private Rectangle timelineSelectionBox = new Rectangle();
private Ellipse timelineThumb = new Ellipse();
private Path previewWindow = new Path();
private Point mouseDownPosition;
private Point currentMousePosition;
private const int TIMELINE_ANIMATION_DURATION = 400;
private const string HIGHLIGHT_FILL = "#878787";
private double __timelineWidth;
#region Initialization.
static VideoTimeline()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(VideoTimeline),
new FrameworkPropertyMetadata(typeof(VideoTimeline)));
}
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
//thumbCanvas = GetTemplateChild("PART_ThumbCanvas") as Canvas;
//thumbCanvas.Background = new SolidColorBrush(Colors.Transparent);
//thumbCanvas.Children.Add(timelineThumb);
timelineThumb = EnforceInstance<Ellipse>("PART_Thumb");
timelineThumb.MouseLeftButtonDown -= TimelineThumb_MouseLeftButtonDown;
timelineThumb.MouseLeftButtonDown += TimelineThumb_MouseLeftButtonDown;
timelineCanvas = GetTemplateChild("PART_TimelineCanvas") as Canvas;
timelineCanvas.Background = new SolidColorBrush(Colors.Transparent);
timelineCanvas.Children.Add(timelineOuterBox);
timelineCanvas.Children.Add(timelineSelectionBox);
timelineCanvas.Children.Add(timelineProgressBox);
timelineCanvas.Children.Add(timelineThumb);
previewCanvas = GetTemplateChild("PART_PreviewCanvas") as Canvas;
previewCanvas.Background = new SolidColorBrush(Colors.Transparent);
previewCanvas.Children.Add(previewWindow);
}
private T EnforceInstance<T>(string partName) where T : FrameworkElement, new()
{
return GetTemplateChild(partName) as T ?? new T();
}
protected override void OnTemplateChanged(ControlTemplate oldTemplate, ControlTemplate newTemplate)
{
base.OnTemplateChanged(oldTemplate, newTemplate);
if (timelineCanvas != null)
timelineCanvas.Children.Clear();
SetDefaultMeasurements();
}
#endregion // Initialization.
#region Event Overrides.
protected override void OnRenderSizeChanged(SizeChangedInfo sizeInfo)
{
base.OnRenderSizeChanged(sizeInfo);
//UpdateWaveformCacheScaling();
SetDefaultMeasurements();
UpdateAllRegions();
}
protected override void OnMouseLeftButtonDown(MouseButtonEventArgs e)
{
base.OnMouseLeftButtonDown(e);
Canvas c = e.OriginalSource as Canvas;
if (c == null)
c = Utils.FindParent<Canvas>(e.OriginalSource as FrameworkElement);
if (c != null)
{
CaptureMouse();
mouseDownPosition = e.GetPosition(c);
if (c.Name == "PART_TimelineCanvas")
{
Trace.WriteLine("OnMouseLeftDown over TimeLine");
}
}
}
protected override void OnMouseLeftButtonUp(MouseButtonEventArgs e)
{
base.OnMouseLeftButtonUp(e);
ReleaseMouseCapture();
}
protected override void OnMouseMove(MouseEventArgs e)
{
base.OnMouseMove(e);
currentMousePosition = e.GetPosition(thumbCanvas);
if (Mouse.Captured == null)
{
Canvas c = e.OriginalSource as Canvas;
if (c == null)
c = Utils.FindParent<Canvas>(e.OriginalSource as FrameworkElement);
}
}
#endregion // Event Overrides.
#region Drawing Methods and Events.
private void UpdateAllRegions()
{
UpdateTimelineCanvas();
}
private void UpdateTimelineCanvas()
{
if (timelineCanvas == null)
return;
SetDefaultMeasurements();
// Bounding timeline box.
timelineOuterBox.Fill = new SolidColorBrush(
(Color)ColorConverter.ConvertFromString("#878787")) { Opacity = 0.25 };
timelineOuterBox.StrokeThickness = 0.0;
timelineOuterBox.Width = __timelineWidth;
timelineOuterBox.Height = TimelineThickness;
timelineOuterBox.Margin = new Thickness(TimelineExpansionFactor * TimelineThickness,
(timelineCanvas.RenderSize.Height - TimelineThickness) / 2, 0, 0);
timelineOuterBox.SnapsToDevicePixels = true;
// Selection timeline box.
timelineSelectionBox.Fill = TimelineSelectionBrush;
timelineSelectionBox.Width = 0.0;
timelineSelectionBox.Height = TimelineThickness;
timelineSelectionBox.Margin = new Thickness(TimelineExpansionFactor * TimelineThickness,
(timelineCanvas.RenderSize.Height - TimelineThickness) / 2, 0, 0);
timelineSelectionBox.SnapsToDevicePixels = true;
// Progress timeline box.
timelineProgressBox.Fill = TimelineProgressBrush;
timelineProgressBox.StrokeThickness = 0.0;
timelineProgressBox.Width = 0.0;
timelineProgressBox.Height = TimelineThickness;
timelineProgressBox.Margin = new Thickness(TimelineExpansionFactor * TimelineThickness,
(timelineCanvas.RenderSize.Height - TimelineThickness) / 2, 0, 0);
timelineProgressBox.SnapsToDevicePixels = true;
// Animation and selection.
timelineCanvas.MouseEnter -= TimelineCanvas_MouseEnter;
timelineCanvas.MouseEnter += TimelineCanvas_MouseEnter;
timelineCanvas.MouseLeave -= TimelineCanvas_MouseLeave;
timelineCanvas.MouseLeave += TimelineCanvas_MouseLeave;
timelineCanvas.MouseMove -= TimelineCanvas_MouseMove;
timelineCanvas.MouseMove += TimelineCanvas_MouseMove;
timelineCanvas.MouseDown -= TimelineCanvas_MouseDown;
timelineCanvas.MouseDown += TimelineCanvas_MouseDown;
// The draggable thumb.
timelineThumb.Fill = TimelineThumbBrush;
//timelineThumb.Stroke = new SolidColorBrush(Colors.Black);
//timelineThumb.StrokeThickness = 0.5;
timelineThumb.VerticalAlignment = VerticalAlignment.Center;
timelineThumb.Height = timelineThumb.Width = 0.0;
timelineThumb.Margin = new Thickness(TimelineExpansionFactor * TimelineThickness,
timelineCanvas.RenderSize.Height / 2, 0, 0);
timelineThumb.SnapsToDevicePixels = true;
timelineThumb.MouseLeftButtonDown -= TimelineThumb_MouseLeftButtonDown;
timelineThumb.MouseLeftButtonDown += TimelineThumb_MouseLeftButtonDown;
timelineThumb.MouseLeftButtonUp -= TimelineThumb_MouseLeftButtonUp;
timelineThumb.MouseLeftButtonUp += TimelineThumb_MouseLeftButtonUp;
// Preview window.
}
private void TimelineCanvas_MouseDown(object sender, MouseButtonEventArgs e)
{
Trace.WriteLine("POON");
}
private void SetDefaultMeasurements()
{
if (timelineCanvas != null)
__timelineWidth = timelineCanvas.RenderSize.Width - 2 * 2 * TimelineThickness;
}
private void TimelineCanvas_MouseEnter(object sender, MouseEventArgs e)
{
timelineThumb.ResetAnimation(Ellipse.WidthProperty, Ellipse.HeightProperty);
timelineProgressBox.ResetAnimation(Rectangle.HeightProperty, Rectangle.MarginProperty);
timelineSelectionBox.ResetAnimation(Rectangle.HeightProperty, Rectangle.MarginProperty);
timelineOuterBox.ResetAnimation(Rectangle.HeightProperty, Rectangle.MarginProperty);
CircleEase easing = new CircleEase();
easing.EasingMode = EasingMode.EaseOut;
// Thumb animation.
Thickness margin = new Thickness(0,
(timelineCanvas.RenderSize.Height - 2 * TimelineExpansionFactor * TimelineThickness) / 2, 0, 0);
EllpiseDiameterAnimation(timelineThumb, TimelineThickness * TimelineExpansionFactor * 2, margin, easing);
// Timeline animation.
margin = new Thickness(TimelineExpansionFactor * TimelineThickness,
(timelineCanvas.RenderSize.Height - (TimelineThickness * TimelineExpansionFactor)) / 2, 0, 0);
TimelineHeightAnimation(timelineProgressBox, TimelineThickness * TimelineExpansionFactor, margin, easing);
TimelineHeightAnimation(timelineSelectionBox, TimelineThickness * TimelineExpansionFactor, margin, easing);
TimelineHeightAnimation(timelineOuterBox, TimelineThickness * TimelineExpansionFactor, margin, easing);
double selectionWidth = (currentMousePosition.X / RenderSize.Width) * timelineOuterBox.Width;
timelineSelectionBox.Width = selectionWidth;
Trace.WriteLine("MouseENTER Canvas");
}
private void TimelineCanvas_MouseLeave(object sender, MouseEventArgs e)
{
timelineThumb.ResetAnimation(Ellipse.WidthProperty, Ellipse.HeightProperty);
timelineProgressBox.ResetAnimation(Rectangle.HeightProperty, Rectangle.MarginProperty);
timelineSelectionBox.ResetAnimation(Rectangle.HeightProperty, Rectangle.MarginProperty);
timelineOuterBox.ResetAnimation(Rectangle.HeightProperty, Rectangle.MarginProperty);
CircleEase easing = new CircleEase();
easing.EasingMode = EasingMode.EaseOut;
// Thumb animation.
Thickness margin = new Thickness(TimelineExpansionFactor * TimelineThickness, timelineCanvas.RenderSize.Height / 2, 0, 0);
EllpiseDiameterAnimation(timelineThumb, 0.0, margin, easing);
// Timeline animation.
margin = new Thickness(TimelineExpansionFactor * TimelineThickness,
(timelineCanvas.RenderSize.Height - TimelineThickness) / 2, 0, 0);
TimelineHeightAnimation(timelineProgressBox, TimelineThickness, margin, easing);
TimelineHeightAnimation(timelineSelectionBox, TimelineThickness, margin, easing);
TimelineHeightAnimation(timelineOuterBox, TimelineThickness, margin, easing);
if (!isDraggingThumb)
timelineSelectionBox.Width = 0.0;
Trace.WriteLine("MouseLeave Canvas");
}
private void TimelineCanvas_MouseMove(object sender, MouseEventArgs e)
{
Point relativePosition = e.GetPosition(timelineOuterBox);
double selectionWidth = (relativePosition.X / timelineOuterBox.Width) * timelineOuterBox.Width;
timelineSelectionBox.Width = selectionWidth.Clamp(0.0, timelineOuterBox.Width);
if (isDraggingThumb)
{
timelineProgressBox.Width = timelineSelectionBox.Width;
Thickness thumbMargin = new Thickness(TimelineExpansionFactor * TimelineThickness,
(timelineCanvas.RenderSize.Height - (TimelineThickness * TimelineExpansionFactor)) / 2, 0, 0);
timelineThumb.Margin = thumbMargin;
}
}
private bool isDraggingThumb = false;
private void TimelineThumb_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
CaptureMouse();
isDraggingThumb = true;
Trace.WriteLine("Dragging Thumb");
}
private void TimelineThumb_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{
ReleaseMouseCapture();
isDraggingThumb = false;
Trace.WriteLine("STOPPED Dragging Thumb");
}
#endregion // Drawing Methods and Events.
#region Animation Methods.
private void EllpiseDiameterAnimation(Ellipse ellipse, double diameter, Thickness margin, IEasingFunction easing)
{
AnimationTimeline widthAnimation = ShapeWidthAnimation(ellipse, diameter, easing);
AnimationTimeline heightAnimation = ShapeHeightAnimation(ellipse, diameter, easing);
AnimationTimeline marginAnimation = ShapeMarginAnimation(ellipse, margin, easing);
Storyboard storyboard = new Storyboard();
storyboard.Children.Add(widthAnimation);
storyboard.Children.Add(heightAnimation);
storyboard.Children.Add(marginAnimation);
storyboard.Begin(this);
}
private void TimelineHeightAnimation(Rectangle rectangle, double height, Thickness margin, IEasingFunction easing)
{
AnimationTimeline heightAnimation = ShapeHeightAnimation(rectangle, height, easing);
AnimationTimeline marginAnimation = ShapeMarginAnimation(rectangle, margin, easing);
Storyboard storyboard = new Storyboard();
storyboard.Children.Add(marginAnimation);
storyboard.Children.Add(heightAnimation);
storyboard.Begin(this);
}
private AnimationTimeline ShapeMarginAnimation(Shape shape, Thickness margin, IEasingFunction easing)
{
ThicknessAnimation marginAnimation = new ThicknessAnimation(
margin, TimeSpan.FromMilliseconds((TIMELINE_ANIMATION_DURATION)));
if (easing != null)
marginAnimation.EasingFunction = easing;
Storyboard.SetTarget(marginAnimation, shape);
Storyboard.SetTargetProperty(marginAnimation, new PropertyPath(Rectangle.MarginProperty));
return marginAnimation;
}
private AnimationTimeline ShapeWidthAnimation(Shape shape, double width, IEasingFunction easing)
{
DoubleAnimation widthAnimation = new DoubleAnimation(
width, TimeSpan.FromMilliseconds(TIMELINE_ANIMATION_DURATION));
if (easing != null)
widthAnimation.EasingFunction = easing;
Storyboard.SetTarget(widthAnimation, shape);
Storyboard.SetTargetProperty(widthAnimation, new PropertyPath(Shape.WidthProperty));
return widthAnimation;
}
private AnimationTimeline ShapeHeightAnimation(Shape shape, double height, IEasingFunction easing)
{
DoubleAnimation heightAnimation = new DoubleAnimation(
height, TimeSpan.FromMilliseconds(TIMELINE_ANIMATION_DURATION));
if (easing != null)
heightAnimation.EasingFunction = easing;
Storyboard.SetTarget(heightAnimation, shape);
Storyboard.SetTargetProperty(heightAnimation, new PropertyPath(Shape.HeightProperty));
return heightAnimation;
}
#endregion // Animation Methods.
// Lots of DependencyProperties here...
}
XAML风格
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:MediaControlBuilder">
<Style TargetType="{x:Type local:VideoTimeline}">
<Setter Property="TimelineProgressBrush" Value="DarkOrange"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type local:VideoTimeline}">
<Border Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}">
<Grid>
<Grid.RowDefinitions>
<!--<RowDefinition Height="*"/>-->
<!--<RowDefinition Height="15"/>-->
<RowDefinition Height="*"/>
<RowDefinition Height="20"/>
<!--<RowDefinition Height="*"/>-->
</Grid.RowDefinitions>
<Canvas Name="PART_PreviewCanvas"
Grid.Row="0"
ClipToBounds="True"/>
<Canvas Name="PART_ThumbCanvas"
Grid.Row="1"
ClipToBounds="True"/>
<Canvas Name="PART_TimelineCanvas"
Grid.Row="1"
ClipToBounds="True"/>
<Canvas Name="PART_WaveformCanvas"
Grid.Row="1"
ClipToBounds="True"/>
</Grid>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>
我的问题是:
- 我绘制可拖动拇指的方法是否正确?
- 我怎样才能真正更改代码以使我的 "thumb" 拖动正常工作?
感谢您的宝贵时间。
Ps。具有工作代码的 GitHub 项目是 here,因此您可以重现我遇到的问题。如果有人想帮我开发这个控件,那就太棒了!
Pps。我知道我可以重写滑块以获得 "timeline" 的功能,但这只是更全面控制的第一部分,因此需要从头开始编写。
在创建新的自定义控件时,您不应该 "write control from scratch in code"。
更好的方法是基于现有控件的新实现。在您的情况下,您想创建一个自定义滑块控件,以便您的自定义控件可以继承 Slider
利用现有功能,例如拇指拖动逻辑和开始、结束、值属性。
当扩展现有控件时,您从原始控件的默认模板开始,它可以通过 VS 获得。滑块将具有您应该特别感兴趣的元素:
<Track x:Name="PART_Track" Grid.Column="1">
<Track.DecreaseRepeatButton>
<RepeatButton Command="{x:Static Slider.DecreaseLarge}" Style="{StaticResource RepeatButtonTransparent}"/>
</Track.DecreaseRepeatButton>
<Track.IncreaseRepeatButton>
<RepeatButton Command="{x:Static Slider.IncreaseLarge}" Style="{StaticResource RepeatButtonTransparent}"/>
</Track.IncreaseRepeatButton>
<Track.Thumb>
<Thumb x:Name="Thumb" Focusable="False" Height="11" OverridesDefaultStyle="True" Template="{StaticResource SliderThumbVerticalDefault}" VerticalAlignment="Top" Width="18"/>
</Track.Thumb>
</Track>
通过在模板中使用所需的元素,您的基本控件将处理所有基本的滑块功能。从这里开始,您可以更改基本控件功能,按照您想要的方式设置滑块部件的样式并添加任何新功能。
如果您不想公开不适用于您的时间轴控件的 Slider
属性,例如 Minimum
,只需在您的模板中使用 Slider
控件。
我认为您的问题集中在时间轴滑块上。无需创建自己的。只需使用 Slider control。您可以重新设置样式以使填充变为红色,其余部分变为半透明。然后可以将 Value 绑定到 MediaElement 控件的 Position,将 Slider 的 Maximum 绑定到 Duration。
<Slider Value="{Binding Position.Milliseconds, ElementName=MediaPlayer}"
Maximum="{Binding Duration.TimeSpan.Milliseconds, , ElementName=MediaPlayer}"
Style="{StaticResource YouTubeSliderStyle}" />
当值发生变化时,您可以更新 MediaElement 的位置。您只想在用户更改值时执行此操作(而不是在由于 Position 更新而更改时)。为此,您可以监听 mousedown/up 和 keydown/up 事件。在这些事件期间,您可以(取消)订阅 ValueChanged 事件并更新位置。
private void UpdatePosition(long time)
{
MediaPlayer.Position = TimeSpan.FromMilliseconds(time);
}
更新: show/hide 拇指的方法。 您可以通过两种方式显示或隐藏拇指。第一个是创建新的 Slider 控件和 show/hide 鼠标悬停时的滑块。
class YouTubeSlider : Slider
{
private Thumb _thumb;
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
_thumb = (Thumb)GetTemplateChild("Thumb");
_thumb.Opacity = 0;
}
protected override void OnMouseEnter(MouseEventArgs e)
{
base.OnMouseEnter(e);
_thumb.Opacity = 1;
}
protected override void OnMouseLeave(MouseEventArgs e)
{
base.OnMouseLeave(e);
_thumb.Opacity = 0;
}
}
二是在控件的样式内处理。 (为简洁起见删除了一些部分)
<ControlTemplate x:Key="SliderHorizontal" TargetType="{x:Type Slider}">
<Border x:Name="border" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" Background="{TemplateBinding Background}" SnapsToDevicePixels="True">
<!-- Elements -->
<Track.Thumb>
<Thumb x:Name="Thumb" Opacity="0" Focusable="False" Height="18" OverridesDefaultStyle="True" Template="{StaticResource SliderThumbHorizontalDefault}" VerticalAlignment="Center" Width="11"/>
</Track.Thumb>
<!-- closing tags -->
</Border>
<ControlTemplate.Triggers>
<!-- missing triggers -->
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Opacity" TargetName="Thumb" Value="1"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
我不确定,但我认为这可以解决您的问题:
private void TimelineCanvas_MouseMove(object sender, MouseEventArgs e)
{
Point relativePosition = e.GetPosition(timelineOuterBox);
double selectionWidth = (relativePosition.X / timelineOuterBox.Width) * timelineOuterBox.Width;
timelineSelectionBox.Width = selectionWidth.Clamp(0.0, timelineOuterBox.Width);
if (isDraggingThumb)
{
timelineProgressBox.Width = timelineSelectionBox.Width;
//Thickness thumbMargin = new Thickness(TimelineThickness * TimelineExpansionFactor,
// (timelineCanvas.RenderSize.Height - (TimelineThickness * TimelineExpansionFactor)) / 2, 0, 0);
//timelineThumb.Margin = thumbMargin;
Canvas.SetLeft(timelineThumb, timelineProgressBox.Width);
}
}
private bool isDraggingThumb = false;
private void TimelineThumb_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
e.Handled = true;
//CaptureMouse();
isDraggingThumb = true;
Trace.WriteLine("Dragging Thumb");
}
private void TimelineThumb_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{
e.Handled = true;
//ReleaseMouseCapture();
isDraggingThumb = false;
Trace.WriteLine("STOPPED Dragging Thumb");
}
您可以通过处理事件参数来停止冒泡,离开事件不会被触发。
要更改拇指的位置,您必须设置 Canvas 的左附加 属性。
此外,您还必须重置 isdraggingThumb :
/// <summary>
/// Invoked when an unhandled MouseLeftButtonUp routed event reaches an element in
/// its route that is derived from this class. Implement this method to add class
/// handling for this event.
/// </summary>
/// <param name="e">The MouseButtonEventArgs that contains the event data. The event
/// data reports that the left mouse button was released.</param>
protected override void OnMouseLeftButtonUp(MouseButtonEventArgs e)
{
isDraggingThumb = false;