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

火影  论坛元老 | 2025-4-14 00:10:51 | 显示全部楼层 | 阅读模式
打印 上一主题 下一主题

主题 1822|帖子 1822|积分 5466

上文讲到Avalonia中比较多的便利性,今天我们来讲一下自定义控件


  • 研究了个把礼拜Avalonia,发现生态并不是很完善

    首先是国内.NET人数少,且市场占据率也低,导致Avalonia相关的文档和讲授视频也少

    其次是对于Avalonia这个新事务来讲,控件库不完善,固然官方提供了很多控件库,也有其他一些控件库,但是照旧有很多控件没有,例如,富文本编辑器,word,报表,流媒体控件好像官方必要付费才能使用.....

    在这个情况下,由于本身行业和之前项目的关系,有效到流媒体控件,必要调用摄像头来出现影像,但是找了一圈发现并没有满足的控件....

    那我们应该怎么办呢,没办法,只能手撸一个,话不多说,先上效果图,左边是开启的视频窗口,右侧为收罗的帧画面

    那这个效果是怎么实现的呢,我们必要了解几个点

    • 影像是什么

      首先我们必要知道相机或者说摄像头捕获的影像是什么,是一帧一帧的画面,你也可以理解为照片,一帧即一张照片。那知道了这个我们就清楚,影像无非就是连续帧画面播放出来的效果,也就是一帧一帧的画面切换,形成了我们眼中看到的视频影像
    • 怎样自定义控件

      自定义控件分两种,第一种就是用空模板从零开始创建一个控件,第二个就是基于已有控件来定义本身想要的用户控件
    话不多说,开干,这里操作影像使用的库是OpenCvSharp4,在你的项目中引入下面的包,根据本身平台引入对应的runtime包,这里我使用的是win平台测试
    1. [/code][size=2]首先我们新建一个UserControl,放入一个Image控件[/size]
    2. [align=center][img]https://img2024.cnblogs.com/blog/923811/202504/923811-20250413190841658-353616669.png[/img][/align][size=2]接下来,我们必要定义一些须要的属性和方法,这些属性是对外暴露的,且必要注册到用户控件中,建议不了解的小同伴去官网了解一下自定义控件底子。[url=https://docs.avaloniaui.net/zh-Hans/docs/guides/custom-controls/defining-properties]Avalonia自定义控件官方文档[/url][/size]
    3. [size=2]完备代码如下[/size]
    4. [code]public partial class Camera : UserControl
    5. {
    6.         private VideoCapture? _capture;//视频捕捉器
    7.         private CancellationTokenSource? _cancellationTokenSource;//线程令牌
    8.         private bool _isRunning;//视频状态
    9.         public static readonly StyledProperty<bool> IsOpenCameraProperty =
    10.     AvaloniaProperty.Register<Camera, bool>(
    11.         nameof(IsOpenCamera), defaultValue: false);
    12.         public static readonly StyledProperty<WriteableBitmap> CurrentBitmapProperty =
    13.     AvaloniaProperty.Register<Camera, WriteableBitmap>(
    14.         nameof(CurrentBitmap));
    15.         public event EventHandler<string>? CameraErrorOccurred;
    16.         public Camera()
    17.         {
    18.             InitializeComponent();
    19.             this.GetObservable(IsOpenCameraProperty).Subscribe(OnIsOpenCameraChanged);
    20.         }
    21.         public bool IsOpenCamera
    22.         {
    23.             get => GetValue(IsOpenCameraProperty);
    24.             set => SetValue(IsOpenCameraProperty, value);
    25.         }
    26.         public WriteableBitmap CurrentBitmap
    27.         {
    28.             get => GetValue(CurrentBitmapProperty);
    29.             set => SetValue(CurrentBitmapProperty, value);
    30.         }
    31.         private void OnIsOpenCameraChanged(bool isOpen)
    32.         {
    33.             if (isOpen)
    34.                 StartCamera();
    35.             else
    36.                 StopCamera();
    37.         }
    38.         /// <summary>
    39.         /// 开启摄像头
    40.         /// </summary>
    41.         private void StartCamera()
    42.         {
    43.             if (_isRunning) return;
    44.             _capture = new VideoCapture(0);
    45.             if (!_capture.IsOpened())
    46.             {
    47.                 _capture.Dispose();
    48.                 _capture = null;
    49.                 CameraErrorOccurred?.Invoke(this, "未找到可用的摄像头或设备已被占用。");
    50.                 return;
    51.             }
    52.             _cancellationTokenSource = new CancellationTokenSource();
    53.             _isRunning = true;
    54.             Task.Run(() => CaptureLoop(_cancellationTokenSource.Token));
    55.         }
    56.         /// <summary>
    57.         /// 关闭摄像头
    58.         /// </summary>
    59.         private void StopCamera()
    60.         {
    61.                     if (!_isRunning) return;
    62.                     _cancellationTokenSource?.Cancel();
    63.                     _capture?.Release();
    64.                     _capture?.Dispose();
    65.                     _isRunning = false;
    66.         }
    67.         /// <summary>
    68.         /// 捕获帧画面更新到Image控件上
    69.         /// </summary>
    70.         /// <param name="token"></param>
    71.         private void CaptureLoop(CancellationToken token)
    72.         {
    73.             using var mat = new Mat();
    74.             while (!token.IsCancellationRequested && _capture!.IsOpened())
    75.             {
    76.                 _capture.Read(mat);
    77.                 if (mat.Empty())
    78.                     continue;
    79.                 var bitmap = ConvertMatToBitmap(mat);
    80.                 Dispatcher.UIThread.InvokeAsync(() =>
    81.                 {
    82.                     CurrentBitmap = bitmap;
    83.                     VideoImage.Source = bitmap;
    84.                 });
    85.                 Thread.Sleep(30); // 控制帧率
    86.             }
    87.         }
    88.         /// <summary>
    89.         /// 用户控件销毁时释放资源
    90.         /// </summary>
    91.         /// <param name="e"></param>
    92.         protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
    93.         {
    94.                 base.OnDetachedFromVisualTree(e);
    95.                 StopCamera();
    96.         }
    97.         /// <summary>
    98.         /// 将帧画面转换为Bitmap
    99.         /// </summary>
    100.         /// <param name="mat"></param>
    101.         /// <returns></returns>
    102.         private static WriteableBitmap ConvertMatToBitmap(Mat mat)
    103.         {
    104.                 using var ms = mat.ToMemoryStream();
    105.                 ms.Seek(0, SeekOrigin.Begin);
    106.                 return WriteableBitmap.Decode(ms);
    107.         }
    108. }
    复制代码
    这里可以看到,我们定义了IsOpenCamera来控制是否开启摄像头,CurrentBitmap为当前帧画面。

    我们还需监听一下这个IsOpenCamera的状态来控制视频的捕捉,在构造函数中有这么一句代码
    1. public Camera()
    2. {
    3.     InitializeComponent();
    4.     this.GetObservable(IsOpenCameraProperty).Subscribe(OnIsOpenCameraChanged);
    5. }
    复制代码
    在构造函数中我们需注入属性的监听来执行某些变乱

    在开启摄像头变乱StartCamera中我们使用了线程来循环执行视频捕捉变乱,通过捕捉每一帧的画面,更新到Image控件上,实现视频的实时预览。

    接下来,我们在别的地方使用这个控件


    • View代码
    1. <UserControl xmlns="https://github.com/avaloniaui"
    2.          xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    3.          xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    4.          xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    5.          mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
    6.          xmlns:local="using:GeneralPurposeProgram.Controls"
    7.          xmlns:vm="using:GeneralPurposeProgram.ViewModels.UserViewModels"
    8.          x:DataType="vm:HomeViewModel"
    9.          x:>
    10. <Design.DataContext>
    11.     <vm:HomeViewModel></vm:HomeViewModel>
    12. </Design.DataContext>
    13. <Grid ColumnDefinitions="*,300">
    14.     <Grid Grid.Column="0" RowDefinitions="50,300,*">
    15.         <StackPanel Spacing="20" Grid.Row="0" Orientation="Horizontal">
    16.             <Button Content="开始摄像头" HotKey="F5" Command="{Binding StartCameraCommand}" Margin="0,0,0,10" Width="150" />
    17.             <Button Content="关闭摄像头" HotKey="F6" Command="{Binding StopCameraCommand}" Margin="0,0,0,10" Width="150" />
    18.             <Button Content="采集图像" HotKey="F10" Command="{Binding CaptureFrameCommand}" Margin="0,0,0,10" Width="150" />
    19.         </StackPanel>
    20.         <StackPanel Orientation="Horizontal" Grid.Row="1">
    21.             <local:Camera x:Name="CameraVideo"
    22.                               IsOpenCamera="{Binding IsOpenCamera,Mode=TwoWay}"
    23.                               CurrentBitmap="{Binding PreviewImage,Mode=TwoWay}" />
    24.         </StackPanel>
    25.     </Grid>
    26.     <Grid Grid.Column="1">
    27.         <ScrollViewer VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Auto">
    28.             <ListBox ItemsSource="{Binding Images}">
    29.                 <ListBox.ItemTemplate>
    30.                     <DataTemplate>
    31.                         <Image Source="{Binding}" Height="260" Stretch="Uniform" />
    32.                     </DataTemplate>
    33.                 </ListBox.ItemTemplate>
    34.             </ListBox>
    35.         </ScrollViewer>
    36.     </Grid>
    37. </Grid>
    38. </UserControl>
    复制代码

    • ViewModel代码
    1. public class HomeViewModel : ViewModelBase
    2. {
    3.         private WriteableBitmap? _previewImage;
    4.         public WriteableBitmap? PreviewImage
    5.         {
    6.                 get => _previewImage;
    7.                 set => this.RaiseAndSetIfChanged(ref _previewImage, value);
    8.         }
    9.         private ObservableCollection<WriteableBitmap> _images = [];
    10.         public ObservableCollection<WriteableBitmap> Images
    11.         {
    12.                 get => _images;
    13.                 set => this.RaiseAndSetIfChanged(ref _images, value);
    14.         }
    15.         public ReactiveCommand<Unit, Unit> StartCameraCommand { get; }
    16.         public ReactiveCommand<Unit, Unit> StopCameraCommand { get; }
    17.         public ReactiveCommand<Unit, Unit> CaptureFrameCommand { get; }
    18.         private bool _isOpenVideo = false;
    19.         public bool IsOpenVideo
    20.         {
    21.                 get => _isOpenVideo;
    22.                 set => this.RaiseAndSetIfChanged(ref _isOpenVideo, value);
    23.         }
    24.         public HomeViewModel()
    25.         {
    26.                 StartCameraCommand = ReactiveCommand.Create(StartCamera);
    27.                 StopCameraCommand = ReactiveCommand.Create(StopCamera);
    28.                 CaptureFrameCommand = ReactiveCommand.Create(CaptureFrame);
    29.                 Images = [];
    30.         }
    31.         private void StartCamera()
    32.         {
    33.                 IsOpenVideo = true;
    34.         }
    35.         private void CaptureFrame()
    36.         {
    37.                 if (PreviewImage != null && IsOpenVideo)
    38.                 {
    39.                         Images.Add(PreviewImage);
    40.                 }
    41.         }
    42.         private void StopCamera()
    43.         {
    44.                 IsOpenVideo = false;
    45.         }
    46. }
    复制代码
    通过上面的完备使用代码可以看出,我们前面注册的视频控件的两个属性IsOpenCamera和CurrentBitmap直接暴露给了父控件,通过变乱修改IsOpenCamera的值就能实现视频的开启和关闭。收罗图像则只必要将CurrentBitmap当前帧画面保存起来,存入Images集合中给ListBox体现出来即完成了采图功能。

    相信大家看到这应该都能理解里面的原理了,通过捕捉摄像头的帧画面,一帧一帧更新到Image控件上,其实和动画、漫画一样。

    鉴于上期便利性在这补充一点,相对于WPF来讲,Avalonia可以更方便的给按钮绑定键盘Key来触发变乱,只必要加上HotKey="Key"即可

    可以看到,我在这绑定了F5、F6、F10键,当然,也可以绑定复合案件,例如HotKey="Ctrl+F5"。

    好了,本文就讲到这,后续博主还会出一些自定义控件的合集,我本人是有筹划想手搓一个word文档编辑器的,但如今照旧想法,不确定能不能行,这是个工作量很大的工作,祝我好运吧。由于平时要上班,博主大概率是在周末更新。

    都看到这了,不点个赞再走吗



免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
继续阅读请点击广告

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

正序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

火影

论坛元老
这个人很懒什么都没写!
快速回复 返回顶部 返回列表