如何实现 WPF 视频封面查看器

  • 框架使用.NET40
  • Visual Studio 2019;
  • 实现视频封面查看器NineGridView基于Grid实现列,使用列做主封面展示视频播放(也可以做rtsp视频流播放),还剩下个做候选封面区展示,当点击封面区某个封面时进行替换主封面区视频(流)播放。.
  • 当鼠标移动到候选封面区时,动画从上一次鼠标的位置到当前鼠标位置做移动动画。

如何实现 WPF 视频封面查看器1)新建 NineGridView.cs 代码如下:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;

namespace WPFDevelopers.Controls
{
    public class NineGridView : Grid
    {
        private int _rows = 3;

        private int _columns = 3;

        private Dictionary<Rect, int> _dicRect = new Dictionary<Rect, int>();

        private Border _border;

        private Storyboard _storyboard;

        private Rect _lastRect;

        private int _last;

        public static readonly DependencyProperty SelectBrushProperty =
          DependencyProperty.Register("SelectBrush", typeof(Brush), typeof(NineGridView),
              new PropertyMetadata(Brushes.Red));
        
        public static readonly DependencyProperty BorderThicknessProperty =
       DependencyProperty.Register("BorderThickness", typeof(Thickness), typeof(NineGridView),
           new PropertyMetadata(new Thickness(1)));

        public NineGridView()
        {
            Loaded += NineGridView_Loaded;
            SizeChanged += NineGridView_SizeChanged;
        }

        public Brush SelectBrush
        {
            get => (Brush)GetValue(SelectBrushProperty);
            set => SetValue(SelectBrushProperty, value);
        }
      
        public Thickness BorderThickness
        {
            get => (Thickness)GetValue(BorderThicknessProperty);
            set => SetValue(BorderThicknessProperty, value);
        }

        private void NineGridView_SizeChanged(object sender, SizeChangedEventArgs e)
        {
            if (_border == null || _last == 0) return;
            var frameworkElement = InternalChildren[_last] as FrameworkElement;
            if (frameworkElement == null) return;
            _border.Width = frameworkElement.ActualWidth;
            _border.Height = frameworkElement.ActualHeight;
            var point = frameworkElement.TranslatePoint(new Point(0, 0), this);
            CreateStoryboard(point);
        }
        private void NineGridView_Loaded(object sender, RoutedEventArgs e)
        {
            RowDefinitions.Clear();
            for (int i = 0; i < _rows; i++)
            {
                var row1 = new RowDefinition();
                RowDefinitions.Add(row1);
            }
            ColumnDefinitions.Clear();
            for (int i = 0; i < _columns; i++)
            {
                var col1 = new ColumnDefinition();
                ColumnDefinitions.Add(col1);
            }
            UIElementCollection children = InternalChildren;

            int numCol = 0, numRow = 0;
            for (int i = 0, count = children.Count; i < count; ++i)
            {
                if (i > 6) return;
                UIElement child = children[i];
                if (child != null)
                {
                    if (i == 0)
                    {
                        SetRowSpan(child, 2);
                        SetColumnSpan(child, 2);
                    }
                    else
                    {
                        var num = i - 1;
                        var col = GetColumnSpan(children[num]);
                        col = col == 1 ? GetColumn(children[num]) : col;
                        if (i + 1 <= _columns)
                        {
                            SetColumn(child, col);
                            SetRow(child, numRow);
                            numRow++;
                        }
                        else
                        {
                            var row = GetRowSpan(children[0]);
                            SetColumn(child, numCol);
                            SetRow(child, row);
                            numCol++;
                        }
                    }
                }
            }
            if(_border != null)
                Children.Remove(_border);
            _border = new Border
            {
                BorderThickness = BorderThickness,
                BorderBrush = SelectBrush,
                VerticalAlignment = VerticalAlignment.Top,
                HorizontalAlignment = HorizontalAlignment.Left
            };
            _border.Name = "PART_Adorner";
            _border.RenderTransform = new TranslateTransform();
            SetRowSpan(_border, _rows);
            SetColumnSpan(_border, _columns);
            _border.Width = ActualWidth / _columns - 2;
            _border.Height = ActualHeight / _rows - 2;
            var _translateTransform = (TranslateTransform)_border.RenderTransform;
            _translateTransform.X = _border.Width * 2 + 4;
            Children.Add(_border);
            _last = 1;
        }

        protected override void OnPreviewMouseMove(MouseEventArgs e)
        {
            base.OnPreviewMouseMove(e);
            var currentPoint = e.GetPosition(this);
            if (_lastRect.Contains(currentPoint))
                return;
            _dicRect.Clear();
            UIElementCollection children = InternalChildren;
            for (int i = 0, count = children.Count; i < count; ++i)
            {
                if (i >= 6 || i == 0) continue;
                var child = children[i] as FrameworkElement;
                if (child != null)
                {
                    var point = child.TranslatePoint(new Point(0, 0), this);
                    var rect = new Rect(point.X, point.Y, child.ActualWidth, child.ActualHeight);
                    _dicRect.Add(rect, i);
                }
            }


            var model = _dicRect.Keys.FirstOrDefault(x => x.Contains(currentPoint));
            if (model == default) return;
            _dicRect.TryGetValue(model, out _last);
            if (_border == null) return;

            CreateStoryboard(new Point(model.X, model.Y));
            _border.Width = model.Width;
            _border.Height = model.Height;
            _lastRect = model;
        }
        void CreateStoryboard(Point point)
        {
            var sineEase = new SineEase() { EasingMode = EasingMode.EaseOut };
            if (_storyboard == null)
                _storyboard = new Storyboard();
            else
                _storyboard.Children.Clear();
            var animationX = new DoubleAnimation
            {
                Duration = TimeSpan.FromMilliseconds(500),
                To = point.X,
                EasingFunction = sineEase
            };
            Storyboard.SetTargetProperty(animationX, new PropertyPath("(Border.RenderTransform).(TranslateTransform.X)"));
            _storyboard.Children.Add(animationX);
            var animationY = new DoubleAnimation
            {
                Duration = TimeSpan.FromMilliseconds(500),
                To = point.Y,
                EasingFunction = sineEase
            };
            Storyboard.SetTargetProperty(animationY, new PropertyPath("(Border.RenderTransform).(TranslateTransform.Y)"));
            _storyboard.Children.Add(animationY);
            _storyboard.Begin(_border);

        }
    }
}

2)新建 NineGridViewExample.xaml 代码如下:

<UserControl x:Class="WPFDevelopers.Samples.ExampleViews.NineGridViewExample"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             xmlns:wpfdev="https://github.com/WPFDevelopersOrg/WPFDevelopers"
              xmlns:controls="clr-namespace:WPFDevelopers.Samples.Controls"
             xmlns:local="clr-namespace:WPFDevelopers.Samples.ExampleViews"
             mc:Ignorable="d" 
             d:DesignHeight="450" d:DesignWidth="800">
    <controls:CodeViewer>
        <wpfdev:NineGridView BorderThickness="1" SelectBrush="Red">
            <wpfdev:NineGridView.Resources>
                <Style TargetType="TextBlock">
                    <Setter Property="Foreground" Value="White"/>
                    <Setter Property="VerticalAlignment" Value="Center"/>
                    <Setter Property="HorizontalAlignment" Value="Center"/>
                </Style>
                <Style TargetType="Border">
                    <Setter Property="Margin" Value="1"/>
                </Style>
            </wpfdev:NineGridView.Resources>
            <MediaElement x:Name="MyMediaElement" Loaded="MyMediaElement_Loaded"
                          MediaEnded="MyMediaElement_MediaEnded"/>
            <Border Background="#282C34">
                <wpfdev:SmallPanel>
                    <TextBlock Text="信号源1"/>
                    <Border Background="{DynamicResource PrimaryNormalSolidColorBrush}"
                            VerticalAlignment="Top"
                            HorizontalAlignment="Right"
                            Padding="10,4"
                            CornerRadius="3">
                        <TextBlock Text="HD"/>
                    </Border>
                </wpfdev:SmallPanel>
            </Border>
            <Border Background="#282C34">
                <TextBlock Text="无信号"/>
            </Border>
            <Border Background="#282C34">
                <TextBlock Text="无信号"/>
            </Border>
            <Border Background="#282C34">
                <TextBlock Text="无信号"/>
            </Border>
            <Border Background="#282C34">
                <TextBlock Text="无信号"/>
            </Border>
        </wpfdev:NineGridView>
        <controls:CodeViewer.SourceCodes>
            <controls:SourceCodeModel 
                CodeSource="/WPFDevelopers.SamplesCode;component/ExampleViews/NineGridViewExample.xaml" 
                CodeType="Xaml"/>
            <controls:SourceCodeModel 
                CodeSource="/WPFDevelopers.SamplesCode;component/ExampleViews/NineGridViewExample.xaml.cs" 
                CodeType="CSharp"/>
        </controls:CodeViewer.SourceCodes>
    </controls:CodeViewer>
</UserControl>

3)新建 NineGridViewExample.xaml.cs 代码如下:

using System;
using System.IO;
using System.Windows;
using System.Windows.Controls;

namespace WPFDevelopers.Samples.ExampleViews
{
    /// <summary>
    /// NineGridViewExample.xaml 的交互逻辑
    /// </summary>
    public partial class NineGridViewExample : UserControl
    {
        public NineGridViewExample()
        {
            InitializeComponent();
        }

        private void MyMediaElement_Loaded(object sender, RoutedEventArgs e)
        {
            var path = "E:\\DCLI6K5UIAEmH9R.mp4";
            if (File.Exists(path))
                MyMediaElement.Source = new Uri("path");
        }

        private void MyMediaElement_MediaEnded(object sender, RoutedEventArgs e)
        {
            MyMediaElement.Position = new TimeSpan(0);
        }
        
    }
}
如何实现 WPF 视频封面查看器