qidao123.com技术社区-IT企服评测·应用市场
标题:
Avalonia跨平台实战(三),自定义控件之Camera控件
[打印本页]
作者:
火影
时间:
2025-4-14 00:10
标题:
Avalonia跨平台实战(三),自定义控件之Camera控件
上文讲到Avalonia中比较多的便利性,今天我们来讲一下自定义控件
研究了个把礼拜Avalonia,发现生态并不是很完善
首先是国内.NET人数少,且市场占据率也低,导致Avalonia相关的文档和讲授视频也少
其次是对于Avalonia这个新事务来讲,控件库不完善,固然官方提供了很多控件库,也有其他一些控件库,但是照旧有很多控件没有,例如,富文本编辑器,word,报表,流媒体控件好像官方必要付费才能使用.....
在这个情况下,由于本身行业和之前项目的关系,有效到流媒体控件,必要调用摄像头来出现影像,但是找了一圈发现并没有满足的控件....
那我们应该怎么办呢,没办法,只能手撸一个,话不多说,先上效果图,左边是开启的视频窗口,右侧为收罗的帧画面
那这个效果是怎么实现的呢,我们必要了解几个点
影像是什么
首先我们必要知道相机或者说摄像头捕获的影像是什么,是一帧一帧的画面,你也可以理解为照片,一帧即一张照片。那知道了这个我们就清楚,影像无非就是连续帧画面播放出来的效果,也就是一帧一帧的画面切换,形成了我们眼中看到的视频影像
怎样自定义控件
自定义控件分两种,第一种就是用空模板从零开始创建一个控件,第二个就是基于已有控件来定义本身想要的用户控件
话不多说,开干,这里操作影像使用的库是OpenCvSharp4,在你的项目中引入下面的包,根据本身平台引入对应的runtime包,这里我使用的是win平台测试
[/code][size=2]首先我们新建一个UserControl,放入一个Image控件[/size]
[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]
[size=2]完备代码如下[/size]
[code]public partial class Camera : UserControl
{
private VideoCapture? _capture;//视频捕捉器
private CancellationTokenSource? _cancellationTokenSource;//线程令牌
private bool _isRunning;//视频状态
public static readonly StyledProperty<bool> IsOpenCameraProperty =
AvaloniaProperty.Register<Camera, bool>(
nameof(IsOpenCamera), defaultValue: false);
public static readonly StyledProperty<WriteableBitmap> CurrentBitmapProperty =
AvaloniaProperty.Register<Camera, WriteableBitmap>(
nameof(CurrentBitmap));
public event EventHandler<string>? CameraErrorOccurred;
public Camera()
{
InitializeComponent();
this.GetObservable(IsOpenCameraProperty).Subscribe(OnIsOpenCameraChanged);
}
public bool IsOpenCamera
{
get => GetValue(IsOpenCameraProperty);
set => SetValue(IsOpenCameraProperty, value);
}
public WriteableBitmap CurrentBitmap
{
get => GetValue(CurrentBitmapProperty);
set => SetValue(CurrentBitmapProperty, value);
}
private void OnIsOpenCameraChanged(bool isOpen)
{
if (isOpen)
StartCamera();
else
StopCamera();
}
/// <summary>
/// 开启摄像头
/// </summary>
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(() => CaptureLoop(_cancellationTokenSource.Token));
}
/// <summary>
/// 关闭摄像头
/// </summary>
private void StopCamera()
{
if (!_isRunning) return;
_cancellationTokenSource?.Cancel();
_capture?.Release();
_capture?.Dispose();
_isRunning = false;
}
/// <summary>
/// 捕获帧画面更新到Image控件上
/// </summary>
/// <param name="token"></param>
private void CaptureLoop(CancellationToken token)
{
using var mat = new Mat();
while (!token.IsCancellationRequested && _capture!.IsOpened())
{
_capture.Read(mat);
if (mat.Empty())
continue;
var bitmap = ConvertMatToBitmap(mat);
Dispatcher.UIThread.InvokeAsync(() =>
{
CurrentBitmap = bitmap;
VideoImage.Source = bitmap;
});
Thread.Sleep(30); // 控制帧率
}
}
/// <summary>
/// 用户控件销毁时释放资源
/// </summary>
/// <param name="e"></param>
protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnDetachedFromVisualTree(e);
StopCamera();
}
/// <summary>
/// 将帧画面转换为Bitmap
/// </summary>
/// <param name="mat"></param>
/// <returns></returns>
private static WriteableBitmap ConvertMatToBitmap(Mat mat)
{
using var ms = mat.ToMemoryStream();
ms.Seek(0, SeekOrigin.Begin);
return WriteableBitmap.Decode(ms);
}
}
复制代码
这里可以看到,我们定义了IsOpenCamera来控制是否开启摄像头,CurrentBitmap为当前帧画面。
我们还需监听一下这个IsOpenCamera的状态来控制视频的捕捉,在构造函数中有这么一句代码
public Camera()
{
InitializeComponent();
this.GetObservable(IsOpenCameraProperty).Subscribe(OnIsOpenCameraChanged);
}
复制代码
在构造函数中我们需注入属性的监听来执行某些变乱
在开启摄像头变乱StartCamera中我们使用了线程来循环执行视频捕捉变乱,通过捕捉每一帧的画面,更新到Image控件上,实现视频的实时预览。
接下来,我们在别的地方使用这个控件
View代码
<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:>
<Design.DataContext>
<vm:HomeViewModel></vm:HomeViewModel>
</Design.DataContext>
<Grid ColumnDefinitions="*,300">
<Grid Grid.Column="0" RowDefinitions="50,300,*">
<StackPanel Spacing="20" Grid.Row="0" Orientation="Horizontal">
<Button Content="开始摄像头" HotKey="F5" Command="{Binding StartCameraCommand}" Margin="0,0,0,10" Width="150" />
<Button Content="关闭摄像头" HotKey="F6" Command="{Binding StopCameraCommand}" Margin="0,0,0,10" Width="150" />
<Button Content="采集图像" HotKey="F10" Command="{Binding CaptureFrameCommand}" Margin="0,0,0,10" Width="150" />
</StackPanel>
<StackPanel Orientation="Horizontal" Grid.Row="1">
<local:Camera x:Name="CameraVideo"
IsOpenCamera="{Binding IsOpenCamera,Mode=TwoWay}"
CurrentBitmap="{Binding PreviewImage,Mode=TwoWay}" />
</StackPanel>
</Grid>
<Grid Grid.Column="1">
<ScrollViewer VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Auto">
<ListBox ItemsSource="{Binding Images}">
<ListBox.ItemTemplate>
<DataTemplate>
<Image Source="{Binding}" Height="260" Stretch="Uniform" />
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</ScrollViewer>
</Grid>
</Grid>
</UserControl>
复制代码
ViewModel代码
public class HomeViewModel : ViewModelBase
{
private WriteableBitmap? _previewImage;
public WriteableBitmap? PreviewImage
{
get => _previewImage;
set => this.RaiseAndSetIfChanged(ref _previewImage, value);
}
private ObservableCollection<WriteableBitmap> _images = [];
public ObservableCollection<WriteableBitmap> Images
{
get => _images;
set => this.RaiseAndSetIfChanged(ref _images, value);
}
public ReactiveCommand<Unit, Unit> StartCameraCommand { get; }
public ReactiveCommand<Unit, Unit> StopCameraCommand { get; }
public ReactiveCommand<Unit, Unit> CaptureFrameCommand { get; }
private bool _isOpenVideo = false;
public bool IsOpenVideo
{
get => _isOpenVideo;
set => 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 && IsOpenVideo)
{
Images.Add(PreviewImage);
}
}
private void StopCamera()
{
IsOpenVideo = false;
}
}
复制代码
通过上面的完备使用代码可以看出,我们前面注册的视频控件的两个属性IsOpenCamera和CurrentBitmap直接暴露给了父控件,通过变乱修改IsOpenCamera的值就能实现视频的开启和关闭。收罗图像则只必要将CurrentBitmap当前帧画面保存起来,存入Images集合中给ListBox体现出来即完成了采图功能。
相信大家看到这应该都能理解里面的原理了,通过捕捉摄像头的帧画面,一帧一帧更新到Image控件上,其实和动画、漫画一样。
鉴于上期便利性在这补充一点,相对于WPF来讲,Avalonia可以更方便的给按钮绑定键盘Key来触发变乱,只必要加上HotKey="Key"即可
可以看到,我在这绑定了F5、F6、F10键,当然,也可以绑定复合案件,例如HotKey="Ctrl+F5"。
好了,本文就讲到这,后续博主还会出一些自定义控件的合集,我本人是有筹划想手搓一个word文档编辑器的,但如今照旧想法,不确定能不能行,这是个工作量很大的工作,祝我好运吧。由于平时要上班,博主大概率是在周末更新。
都看到这了,不点个赞再走吗
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
欢迎光临 qidao123.com技术社区-IT企服评测·应用市场 (https://dis.qidao123.com/)
Powered by Discuz! X3.4