Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

WPF Video Transport Control

Tags:

c#

wpf

controls

I am relatively new to custom controls (writing control from scratch in code - not merely styling existing controls). I am having a go at replicating the YouTube video control, you know the one...

enter image description here

To start with I want to develop the "timeline" (the transparent grey bar, which displays the current position of the video and allows the user to drag to change position). With the preview panel and all the rest coming later on...

I currently have the control partially rendered and the hover animations and scale working very well...

enter image description here

However, I am struggling to write the correct code to allow me to drag the "thumb". When I try and handle my left click on the Ellipse that is representing my thumb, the leave event of the containing Canvas fires, in accordance with the WPF documentation, so no complaints, I just don;t know how to achieve what I want and indeed if what I have done already is the correct approach.

The code:

[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...
}

The XAML style

<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>

My questions are:

  1. Is my approach for drawing the draggable thumb correct?
  2. How can I actually change the code to get the dragging of my "thumb" to work?

Thanks for your time.

Ps. the GitHub project with the working code is here so you can reproduce the problem I am having. If anyone wants to help me develop this control, that would be awesome!

Pps. I am aware I could override a slider to get my functionality for the "timeline", but this is just the first part of a much more comprehensive control and hence needs to be written from scratch.

like image 357
MoonKnight Avatar asked May 15 '17 21:05

MoonKnight


2 Answers

I'm not sure but I think that can resolve your problem :

    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");
    }

You can stop the bubbling by handling the event args, and the leave event won't be fired.

To change the position of the thumb, you have to set the Left attached property of the Canvas.

Additionnaly you will have to reset 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;
like image 196
Tarboeuf Avatar answered Oct 08 '22 01:10

Tarboeuf


When creating new custom controls you should not "write control from scratch in code". Better is to base your new implementation of an existing control. In your case you want to create a custom slider control so your custom control could inherit from Slider leveraging existing functionality like thumb dragging logic and Start, End, Value properties.

When extending an existing control you start with original control's default template, it can be obtained with VS. A slider will have an element that you should be particularly interested in:

<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>

By using the required elements in the template your base control will take care of all basic slider capabilities. Starting from this, you can alter the base control functionality, style the slider parts the way you want and add any new functionality.

If you don't want to expose Slider properties that are not applicable for your Timeline control, like Minimum, just use a Slider control in your template.

like image 29
Novitchi S Avatar answered Oct 08 '22 01:10

Novitchi S