WPF 实现视频会议与会人员动态布局

  • 框架使用.NET40
  • Visual Studio 2019;
  • 接着上一篇是基于Grid实现的视频查看感觉有点浪费,所以修改为基于Panel又重新实现了。
  • Panel EndInit()后绘制鼠标经过的选中效果。
  • 当鼠标移动到候选封面区时,动画从上一次鼠标的位置到当前鼠标位置做移动动画。.
WPF 实现视频会议与会人员动态布局

1)新建 SixGirdView.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 SixGirdView : Panel
    {
        public static readonly DependencyProperty SelectBrushProperty =
            DependencyProperty.Register("SelectBrush", typeof(Brush), typeof(SixGirdView),
                new PropertyMetadata(Brushes.Red));

        public static readonly DependencyProperty BorderThicknessProperty =
            DependencyProperty.Register("BorderThickness", typeof(Thickness), typeof(SixGirdView),
                new PropertyMetadata(new Thickness(1)));

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

        private readonly int _columns = 3;

        private readonly int _rows = 3;

        private Border _border;

        private int _last;

        private Rect _lastRect;

        private Storyboard _storyboard;

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

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

        public override void EndInit()
        {
            base.EndInit();
            var children = InternalChildren;

            if (_border == null && children.Count >= 1)
            {
                _border = new Border
                {
                    BorderThickness = BorderThickness,
                    BorderBrush = SelectBrush
                };
                _border.RenderTransform = new TranslateTransform();
                children.Add(_border);
            }
        }

        protected override Size MeasureOverride(Size availableSize)
        {
            var children = InternalChildren;
            int numCol = 0, numRow = 0;
            var isRow = false;
            Point point = default;
            Size size = default;
            _dicRect.Clear();
            double _width = availableSize.Width / _columns, _height = availableSize.Height / _rows;
            for (int i = 0, count = children.Count; i < count; ++i)
            {
                if (i >= 6) continue;
                var uIElement = children[i];
                if (uIElement != null)
                {
                    uIElement.Measure(availableSize);
                    if (i == 0)
                    {
                        point = new Point(0, 0);
                        size = new Size(availableSize.Width / _columns * 2, availableSize.Height / _rows * 2);
                        numRow++;
                    }
                    else
                    {
                        var num = i - 1;
                        var x = 0d;
                        if (!isRow)
                        {
                            x = _width * 2;
                            numCol = numRow + 1;
                            if (numCol < _columns)
                            {
                                point = new Point(x, 0);
                            }
                            else
                            {
                                point = new Point(x, _height);
                                isRow = true;
                                numCol = 0;
                            }

                            numRow++;
                        }
                        else
                        {
                            x = _width * numCol;
                            numCol++;
                            x = x >= availableSize.Width ? 0 : x;

                            point = new Point(x, _height * 2);
                        }

                        size = new Size(_width, _height);
                    }

                    uIElement.Arrange(new Rect(point, size));
                    if (i >= 6 || i == 0) continue;
                    var rect = new Rect(point.X, point.Y, size.Width, size.Height);
                    _dicRect.Add(rect, i);
                }
            }


            _last = _last == 0 ? 1 : _last;
            if (_border != null)
            {
                _border.Measure(availableSize);
                point = new Point(0, 0);
                size = new Size(availableSize.Width / _columns, availableSize.Height / _columns);
                _border.Arrange(new Rect(point, size));
                var _translateTransform = (TranslateTransform) _border.RenderTransform;
                if (_last == 1)
                {
                    _translateTransform.X = availableSize.Width / _columns * 2;
                }
                else
                {
                    var uIElement = InternalChildren[_last];
                    if (uIElement != null)
                    {
                        var rect = _dicRect.FirstOrDefault(x => x.Value == _last).Key;
                        if (rect != null)
                        {
                            point = new Point(rect.X, rect.Y);
                            CreateStoryboard(point);
                        }
                    }
                }
            }


            return availableSize;
        }

        protected override void OnPreviewMouseMove(MouseEventArgs e)
        {
            base.OnPreviewMouseMove(e);
            var currentPoint = e.GetPosition(this);
            if (_lastRect.Contains(currentPoint))
                return;

            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));
            _lastRect = model;
        }

        private void CreateStoryboard(Point point)
        {
            var sineEase = new SineEase {EasingMode = EasingMode.EaseOut};
            if (_storyboard == null)
            {
                _storyboard = new Storyboard();
            }
            else
            {
                _storyboard.Stop();
                _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)新建 SixGirdViewExample.xaml 代码如下:

<UserControl x:Class="WPFDevelopers.Samples.ExampleViews.SixGirdViewExample"
             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:SixGirdView BorderThickness="1" SelectBrush="Red">
            <wpfdev:SixGirdView.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:SixGirdView.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:SixGirdView>
        <controls:CodeViewer.SourceCodes>
            <controls:SourceCodeModel 
                CodeSource="/WPFDevelopers.SamplesCode;component/ExampleViews/SixGirdViewExample.xaml" 
                CodeType="Xaml"/>
            <controls:SourceCodeModel 
                CodeSource="/WPFDevelopers.SamplesCode;component/ExampleViews/SixGirdViewExample.xaml.cs" 
                CodeType="CSharp"/>
        </controls:CodeViewer.SourceCodes>
    </controls:CodeViewer>
</UserControl>

3)新建 SixGirdViewExample.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 SixGirdViewExample : UserControl
    {
        public SixGirdViewExample()
        {
            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 实现视频会议与会人员动态布局