侠盗猪荣 發表於 2025-4-13 20:13:00

Avalonia跨平台实战(三),自定义控件之Camera控件

<h3 id="上文讲到avalonia中比较多的便利性今天我们来讲一下自定义控件">上文讲到Avalonia中比较多的便利性,今天我们来讲一下自定义控件</h3>
<hr>
<ul>
<li>
<h3 id="研究了个把礼拜avalonia发现生态并不是很完善">研究了个把礼拜Avalonia,发现生态并不是很完善</h3>
<h4 id="首先是国内net人数少且市场占有率也低导致avalonia相关的文档和教学视频也少">首先是国内.NET人数少,且市场占有率也低,导致Avalonia相关的文档和教学视频也少</h4>
<h4 id="其次是对于avalonia这个新事务来讲控件库不完善虽然官方提供了很多控件库也有其他一些控件库但是还是有很多控件没有例如富文本编辑器word报表流媒体控件好像官方需要付费才能使用">其次是对于Avalonia这个新事务来讲,控件库不完善,虽然官方提供了很多控件库,也有其他一些控件库,但是还是有很多控件没有,例如,富文本编辑器,word,报表,流媒体控件好像官方需要付费才能使用.....</h4>
<hr>
<h4 id="在这个情况下因为本身行业和之前项目的关系有用到流媒体控件需要调用摄像头来呈现影像但是找了一圈发现并没有满足的控件">在这个情况下,因为本身行业和之前项目的关系,有用到流媒体控件,需要调用摄像头来呈现影像,但是找了一圈发现并没有满足的控件....</h4>
<h4 id="那我们应该怎么办呢没办法只能手撸一个话不多说先上效果图左边是开启的视频窗口右侧为采集的帧画面">那我们应该怎么办呢,没办法,只能手撸一个,话不多说,先上效果图,左边是开启的视频窗口,右侧为采集的帧画面</h4>
<img src="https://img2024.cnblogs.com/blog/923811/202504/923811-20250413185306852-549502608.png" alt="image" loading="lazy">
<h4 id="那这个效果是怎么实现的呢我们需要了解几个点">那这个效果是怎么实现的呢,我们需要了解几个点</h4>
<ul>
<li>
<h5 id="影像是什么">影像是什么</h5>
<h6 id="首先我们需要知道相机或者说摄像头捕获的影像是什么是一帧一帧的画面你也可以理解为照片一帧即一张照片那知道了这个我们就清楚影像无非就是连续帧画面播放出来的效果也就是一帧一帧的画面切换形成了我们眼中看到的视频影像">首先我们需要知道相机或者说摄像头捕获的影像是什么,是一帧一帧的画面,你也可以理解为照片,一帧即一张照片。那知道了这个我们就清楚,影像无非就是连续帧画面播放出来的效果,也就是一帧一帧的画面切换,形成了我们眼中看到的视频影像</h6>
</li>
<li>
<h5 id="如何自定义控件">如何自定义控件</h5>
<h6 id="自定义控件分两种第一种就是用空模板从零开始创建一个控件第二个就是基于已有控件来定义自己想要的用户控件">自定义控件分两种,第一种就是用空模板从零开始创建一个控件,第二个就是基于已有控件来定义自己想要的用户控件</h6>
</li>
</ul>
<hr>
<h4 id="话不多说开干这里操作影像使用的库是opencvsharp4在你的项目中引入下面的包根据自己平台引入对应的runtime包这里我使用的是win平台测试">话不多说,开干,这里操作影像使用的库是OpenCvSharp4,在你的项目中引入下面的包,根据自己平台引入对应的runtime包,这里我使用的是win平台测试</h4>
<pre><code>&lt;PackageReference Include="OpenCvSharp4" Version="4.10.0.20241108" /&gt;
&lt;PackageReference Include="OpenCvSharp4.Extensions" Version="4.10.0.20241108" /&gt;
&lt;PackageReference Include="OpenCvSharp4.runtime.win" Version="4.10.0.20241108" /&gt;
</code></pre>
<h5 id="首先我们新建一个usercontrol放入一个image控件">首先我们新建一个UserControl,放入一个Image控件</h5>
<img src="https://img2024.cnblogs.com/blog/923811/202504/923811-20250413190841658-353616669.png" alt="image" loading="lazy">
<h5 id="接下来我们需要定义一些必要的属性和方法这些属性是对外暴露的且需要注册到用户控件中建议不了解的小伙伴去官网了解一下自定义控件基础avalonia自定义控件官方文档">接下来,我们需要定义一些必要的属性和方法,这些属性是对外暴露的,且需要注册到用户控件中,建议不了解的小伙伴去官网了解一下自定义控件基础。Avalonia自定义控件官方文档</h5>
<h5 id="完整代码如下">完整代码如下</h5>
<pre><code>public partial class Camera : UserControl
{
        private VideoCapture? _capture;//视频捕捉器
        private CancellationTokenSource? _cancellationTokenSource;//线程令牌
        private bool _isRunning;//视频状态

        public static readonly StyledProperty&lt;bool&gt; IsOpenCameraProperty =
    AvaloniaProperty.Register&lt;Camera, bool&gt;(
      nameof(IsOpenCamera), defaultValue: false);

        public static readonly StyledProperty&lt;WriteableBitmap&gt; CurrentBitmapProperty =
    AvaloniaProperty.Register&lt;Camera, WriteableBitmap&gt;(
      nameof(CurrentBitmap));

        public event EventHandler&lt;string&gt;? CameraErrorOccurred;
        public Camera()
        {
            InitializeComponent();
            this.GetObservable(IsOpenCameraProperty).Subscribe(OnIsOpenCameraChanged);
        }

        public bool IsOpenCamera
        {
            get =&gt; GetValue(IsOpenCameraProperty);
            set =&gt; SetValue(IsOpenCameraProperty, value);
        }

        public WriteableBitmap CurrentBitmap
        {
            get =&gt; GetValue(CurrentBitmapProperty);
            set =&gt; SetValue(CurrentBitmapProperty, value);
        }

        private void OnIsOpenCameraChanged(bool isOpen)
        {
            if (isOpen)
              StartCamera();
            else
              StopCamera();
        }

        /// &lt;summary&gt;
        /// 开启摄像头
        /// &lt;/summary&gt;
        private void StartCamera()
        {
            if (_isRunning) return;
            _capture = new VideoCapture(0);
            if (!_capture.IsOpened())
            {
              _capture.Dispose();
              _capture = null;

              CameraErrorOccurred?.Invoke(this, "未找到可用的摄像头或设备已被占用。");
              return;
            }
            _cancellationTokenSource = new CancellationTokenSource();
            _isRunning = true;

            Task.Run(() =&gt; CaptureLoop(_cancellationTokenSource.Token));
        }

        /// &lt;summary&gt;
        /// 关闭摄像头
        /// &lt;/summary&gt;
        private void StopCamera()
        {
                    if (!_isRunning) return;
                    _cancellationTokenSource?.Cancel();
                    _capture?.Release();
                    _capture?.Dispose();
                    _isRunning = false;
        }

        /// &lt;summary&gt;
        /// 捕获帧画面更新到Image控件上
        /// &lt;/summary&gt;
        /// &lt;param name="token"&gt;&lt;/param&gt;
        private void CaptureLoop(CancellationToken token)
        {
            using var mat = new Mat();
            while (!token.IsCancellationRequested &amp;&amp; _capture!.IsOpened())
            {
              _capture.Read(mat);
              if (mat.Empty())
                    continue;

              var bitmap = ConvertMatToBitmap(mat);
              Dispatcher.UIThread.InvokeAsync(() =&gt;
              {
                    CurrentBitmap = bitmap;
                    VideoImage.Source = bitmap;
              });

              Thread.Sleep(30); // 控制帧率
            }
        }

        /// &lt;summary&gt;
        /// 用户控件销毁时释放资源
        /// &lt;/summary&gt;
        /// &lt;param name="e"&gt;&lt;/param&gt;
        protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
        {
                base.OnDetachedFromVisualTree(e);
                StopCamera();
        }

        /// &lt;summary&gt;
        /// 将帧画面转换为Bitmap
        /// &lt;/summary&gt;
        /// &lt;param name="mat"&gt;&lt;/param&gt;
        /// &lt;returns&gt;&lt;/returns&gt;
        private static WriteableBitmap ConvertMatToBitmap(Mat mat)
        {
                using var ms = mat.ToMemoryStream();
                ms.Seek(0, SeekOrigin.Begin);
                return WriteableBitmap.Decode(ms);
        }
}
</code></pre>
<hr>
<h5 id="这里可以看到我们定义了isopencamera来控制是否开启摄像头currentbitmap为当前帧画面">这里可以看到,我们定义了<code>IsOpenCamera</code>来控制是否开启摄像头,<code>CurrentBitmap</code>为当前帧画面。</h5>
<h5 id="我们还需监听一下这个isopencamera的状态来控制视频的捕捉在构造函数中有这么一句代码">我们还需监听一下这个<code>IsOpenCamera</code>的状态来控制视频的捕捉,在构造函数中有这么一句代码</h5>
<pre><code>public Camera()
{
    InitializeComponent();
    this.GetObservable(IsOpenCameraProperty).Subscribe(OnIsOpenCameraChanged);
}
</code></pre>
<h5 id="在构造函数中我们需注入属性的监听来执行某些事件">在构造函数中我们需注入属性的监听来执行某些事件</h5>
<h5 id="在开启摄像头事件startcamera中我们使用了线程来循环执行视频捕捉事件通过捕捉每一帧的画面更新到image控件上实现视频的实时预览">在开启摄像头事件<code>StartCamera</code>中我们使用了线程来循环执行视频捕捉事件,通过捕捉每一帧的画面,更新到Image控件上,实现视频的实时预览。</h5>
<hr>
<h5 id="接下来我们在别的地方使用这个控件">接下来,我们在别的地方使用这个控件</h5>
<ul>
<li>
<h5 id="view代码">View代码</h5>
</li>
</ul>
<pre><code>&lt;UserControl xmlns="https://github.com/avaloniaui"
         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"
         mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
         xmlns:local="using:GeneralPurposeProgram.Controls"
         xmlns:vm="using:GeneralPurposeProgram.ViewModels.UserViewModels"
         x:DataType="vm:HomeViewModel"
         x:Class="GeneralPurposeProgram.Views.UserViews.HomeView"&gt;
&lt;Design.DataContext&gt;
    &lt;vm:HomeViewModel&gt;&lt;/vm:HomeViewModel&gt;
&lt;/Design.DataContext&gt;
&lt;Grid ColumnDefinitions="*,300"&gt;
    &lt;Grid Grid.Column="0" RowDefinitions="50,300,*"&gt;
      &lt;StackPanel Spacing="20" Grid.Row="0" Orientation="Horizontal"&gt;
            &lt;Button Content="开始摄像头" HotKey="F5" Command="{Binding StartCameraCommand}" Margin="0,0,0,10" Width="150" /&gt;
            &lt;Button Content="关闭摄像头" HotKey="F6" Command="{Binding StopCameraCommand}" Margin="0,0,0,10" Width="150" /&gt;
            &lt;Button Content="采集图像" HotKey="F10" Command="{Binding CaptureFrameCommand}" Margin="0,0,0,10" Width="150" /&gt;
      &lt;/StackPanel&gt;
      &lt;StackPanel Orientation="Horizontal" Grid.Row="1"&gt;
            &lt;local:Camera x:Name="CameraVideo"
                              IsOpenCamera="{Binding IsOpenCamera,Mode=TwoWay}"
                              CurrentBitmap="{Binding PreviewImage,Mode=TwoWay}" /&gt;
      &lt;/StackPanel&gt;
    &lt;/Grid&gt;

    &lt;Grid Grid.Column="1"&gt;
      &lt;ScrollViewer VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Auto"&gt;
            &lt;ListBox ItemsSource="{Binding Images}"&gt;
                &lt;ListBox.ItemTemplate&gt;
                  &lt;DataTemplate&gt;
                        &lt;Image Source="{Binding}" Height="260" Stretch="Uniform" /&gt;
                  &lt;/DataTemplate&gt;
                &lt;/ListBox.ItemTemplate&gt;
            &lt;/ListBox&gt;
      &lt;/ScrollViewer&gt;
    &lt;/Grid&gt;
&lt;/Grid&gt;
&lt;/UserControl&gt;
</code></pre>
<ul>
<li>
<h5 id="viewmodel代码">ViewModel代码</h5>
</li>
</ul>
<pre><code>public class HomeViewModel : ViewModelBase
{
        private WriteableBitmap? _previewImage;
        public WriteableBitmap? PreviewImage
        {
                get =&gt; _previewImage;
                set =&gt; this.RaiseAndSetIfChanged(ref _previewImage, value);
        }

        private ObservableCollection&lt;WriteableBitmap&gt; _images = [];

        public ObservableCollection&lt;WriteableBitmap&gt; Images
        {
                get =&gt; _images;
                set =&gt; this.RaiseAndSetIfChanged(ref _images, value);
        }

        public ReactiveCommand&lt;Unit, Unit&gt; StartCameraCommand { get; }
        public ReactiveCommand&lt;Unit, Unit&gt; StopCameraCommand { get; }
        public ReactiveCommand&lt;Unit, Unit&gt; CaptureFrameCommand { get; }

        private bool _isOpenVideo = false;

        public bool IsOpenVideo
        {
                get =&gt; _isOpenVideo;
                set =&gt; this.RaiseAndSetIfChanged(ref _isOpenVideo, value);
        }

        public HomeViewModel()
        {
                StartCameraCommand = ReactiveCommand.Create(StartCamera);
                StopCameraCommand = ReactiveCommand.Create(StopCamera);
                CaptureFrameCommand = ReactiveCommand.Create(CaptureFrame);
                Images = [];
        }

        private void StartCamera()
        {
                IsOpenVideo = true;
        }

        private void CaptureFrame()
        {
                if (PreviewImage != null &amp;&amp; IsOpenVideo)
                {
                        Images.Add(PreviewImage);
                }
        }

        private void StopCamera()
        {
                IsOpenVideo = false;
        }
}
</code></pre>
<hr>
<h5 id="通过上面的完整使用代码可以看出我们前面注册的视频控件的两个属性isopencamera和currentbitmap直接暴露给了父控件通过事件修改isopencamera的值就能实现视频的开启和关闭采集图像则只需要将currentbitmap当前帧画面保存起来存入images集合中给listbox显示出来即完成了采图功能">通过上面的完整使用代码可以看出,我们前面注册的视频控件的两个属性<code>IsOpenCamera</code>和<code>CurrentBitmap</code>直接暴露给了父控件,通过事件修改<code>IsOpenCamera</code>的值就能实现视频的开启和关闭。采集图像则只需要将<code>CurrentBitmap</code>当前帧画面保存起来,存入<code>Images</code>集合中给ListBox显示出来即完成了采图功能。</h5>
<h5 id="相信大家看到这应该都能理解里面的原理了通过捕捉摄像头的帧画面一帧一帧更新到image控件上其实和动画漫画一样">相信大家看到这应该都能理解里面的原理了,通过捕捉摄像头的帧画面,一帧一帧更新到Image控件上,其实和动画、漫画一样。</h5>
<hr>
<h5 id="鉴于上期便利性在这补充一点相对于wpf来讲avalonia可以更方便的给按钮绑定键盘key来触发事件只需要加上hotkeykey即可">鉴于上期便利性在这补充一点,相对于WPF来讲,Avalonia可以更方便的给按钮绑定键盘Key来触发事件,只需要加上<code>HotKey="Key"</code>即可</h5>
<img src="https://img2024.cnblogs.com/blog/923811/202504/923811-20250413200719843-1350833800.png" alt="image" loading="lazy">
<h5 id="可以看到我在这绑定了f5f6f10键当然也可以绑定复合按键例如hotkeyctrlf5">可以看到,我在这绑定了F5、F6、F10键,当然,也可以绑定复合按键,例如<code>HotKey="Ctrl+F5"</code>。</h5>
<hr>
<h5 id="好了本文就讲到这后续博主还会出一些自定义控件的合集我本人是有计划想手搓一个word文档编辑器的但现在还是想法不确定能不能行这是个工作量很大的工作祝我好运吧由于平时要上班博主大概率是在周末更新">好了,本文就讲到这,后续博主还会出一些自定义控件的合集,我本人是有计划想手搓一个word文档编辑器的,但现在还是想法,不确定能不能行,这是个工作量很大的工作,祝我好运吧。由于平时要上班,博主大概率是在周末更新。</h5>
<h5 id="都看到这了不点个赞再走吗">都看到这了,不点个赞再走吗</h5>
</li>
</ul><br><br>
来源:https://www.cnblogs.com/OrdinaryLT/p/18823614
頁: [1]
查看完整版本: Avalonia跨平台实战(三),自定义控件之Camera控件