如何修改 Avalonia WritableBitmap 并将更改更新为 UI 上的图像控件?

问题描述 投票:0回答:1

我的

_myBitmap
类中有一个 WritableBitmap 对象
MainWindowViewModel
,我想用它来显示来自网络摄像头的实时镜头。但本质上,我要做的就是定期向 WritableBitmap 的帧缓冲区写入一些新值,如方法
_changeMyBitmapColor
所示。

预期的应用程序行为:位图的灰度级每 500 毫秒从 0 更改为 50、100、150...。

实际应用程序行为:仅当调整应用程序窗口大小或按下虚拟组合框时,位图颜色的更改才会反映在 UI 上。 颜色从 0 变为 50,并且似乎永远保持在 50。此外,仅当调整窗口大小或按下

MainWindow.axaml
中的虚拟组合框时,显示的位图才会更改其颜色。

我不知道为什么调整窗口大小或在 UI 上点击虚拟组合框会更新显示的位图。我也不明白为什么显示的位图在应用程序的整个生命周期中似乎只改变一次颜色。 从控制台日志中,可以确认 WritableBitmap

_myBitmap
的帧缓冲区每 500 毫秒成功修改一次。

这是一个可重现的示例。我实现了 ReactiveUI 方法,如本教程中所述。由于项目中有不安全的方法,所以

<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
中有一个
TryUpdateBitmap.csproj
。该项目是使用默认的 Avalonia MVVM 项目模板创建的,运行:

dotnet new avalonia.mvvm -o TryUpdateBitmap

文件:MainWindowViewModel

namespace TryUpdateBitmap.ViewModels;

using Avalonia.Media.Imaging;
using Avalonia;
using Avalonia.Platform;
using System.Threading;
using System;
using System.Threading.Tasks;
using ReactiveUI;

public class MainWindowViewModel : ReactiveObject
{
    public MainWindowViewModel()
    {
        // Assume the bitmap is of shape 640x480 with 4 channels
        _myBitmap = new WriteableBitmap(new PixelSize(640, 480), new Vector(96, 96), PixelFormat.Bgra8888, AlphaFormat.Opaque);
        Console.WriteLine("Start app");
        Task.Run(_updateMyBitmapColorPeriodicallyAsync);
    }
    public WriteableBitmap MyBitmap 
    { 
        get => _myBitmap; 
        set => this.RaiseAndSetIfChanged(ref _myBitmap, value); 
    }

    private WriteableBitmap _myBitmap;
    private  async Task _updateMyBitmapColorPeriodicallyAsync()
    {
        PeriodicTimer timer = new(TimeSpan.FromMilliseconds(500)); // Color of my bitmap pixels changes every 500ms
        int colorOption = 0;
        while (await timer.WaitForNextTickAsync())
        {
            _changeMyBitmapColor(colorOption);
            colorOption = (colorOption + 1) % 5; // there will be 3 different color options
            Console.WriteLine("Bitmap modified!");
        }
    }
    private unsafe void _changeMyBitmapColor(int colorOption)
    {
        byte[] pixelValueOptions = [50, 100, 150, 200, 225];
        using ILockedFramebuffer frameBuffer = MyBitmap.Lock();
        void* dataStart = (void*)frameBuffer.Address;
        Span<byte> buffer = new Span<byte>(dataStart, 640 * 480 * 4); // assume each pixel is 1 byte in size and my image has 4 channels
        buffer.Fill(pixelValueOptions[colorOption]); // fill the bitmap memory buffer with some values
    }
}

文件:MainWindow.axaml

<Window xmlns="https://github.com/avaloniaui"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:vm="using:TryUpdateBitmap.ViewModels"
        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"
        x:Class="TryUpdateBitmap.Views.MainWindow"
        x:DataType="vm:MainWindowViewModel"
        Icon="/Assets/avalonia-logo.ico"
        Title="IHaveGivenUpIHaveLetMyselfDownIWillRunAroundAndDesertMyself">

    <Design.DataContext>
        <vm:MainWindowViewModel/>
    </Design.DataContext>
    <Grid ColumnDefinitions="640" RowDefinitions="500">
        <Image Source="{Binding MyBitmap, Mode=TwoWay}" Grid.Column="0" Grid.Row="0"/>
        <ComboBox Grid.Column="0" Grid.Row="1"/>
    </Grid>
</Window>

非常感谢任何帮助!新年快乐。

mvvm video-streaming reactiveui avalonia writeablebitmap
1个回答
0
投票

我找到了两个可行的解决方案,都实现了

INotifyPropertyChange
接口,而不是使用
本指南
中的 ReactiveUI

所以这种方法的关键是改变属性

_myBitmap
引用的writeablebitmap对象。销毁并重新分配新的位图实例并将每个帧分配给
_myBitmap
对于性能而言可能并不理想,因此将像素以字节形式写入其中更为可取。但在这种情况下,
_myBitmap
引用的实例始终保持不变,事情就无法正常工作。

解决方案1:实施
INotifyPropertyChange

文件:MainWindowViewModel

namespace TryUpdateBitmap.ViewModels;

using Avalonia.Media.Imaging;
using Avalonia;
using Avalonia.Platform;
using System.Threading;
using System;
using System.Threading.Tasks;
using System.Collections.Generic;
using System.ComponentModel;

public class MainWindowViewModel : INotifyPropertyChanged
{
    public MainWindowViewModel()
    {
        // Assume the bitmap is of shape 640x480 with 4 channels
        _myBitmap = new WriteableBitmap(new PixelSize(640, 480), new Vector(96, 96), PixelFormat.Bgra8888, AlphaFormat.Opaque);
        _currentBitmapIndex = 0;
        _scratchBoards = [];
        _scratchBoards.Add(new WriteableBitmap(new PixelSize(640, 480), new Vector(96, 96), PixelFormat.Bgra8888, AlphaFormat.Opaque));
        _scratchBoards.Add(new WriteableBitmap(new PixelSize(640, 480), new Vector(96, 96), PixelFormat.Bgra8888, AlphaFormat.Opaque));
        Console.WriteLine("Start app");
        Task.Run(_updateMyBitmapColorPeriodicallyAsync);
    }
    public WriteableBitmap MyBitmap 
    { 
        get => _myBitmap; 
        set 
        {
            _myBitmap = value;
            OnPropertyChanged(nameof(MyBitmap));
        }
    }
    public event PropertyChangedEventHandler? PropertyChanged;
    protected virtual void OnPropertyChanged(string propertyName)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }
    private WriteableBitmap _myBitmap;
    private int _currentBitmapIndex;
    private List<WriteableBitmap> _scratchBoards;
    private  async Task _updateMyBitmapColorPeriodicallyAsync()
    {
        PeriodicTimer timer = new(TimeSpan.FromMilliseconds(500)); // Color of my bitmap pixels changes every 500ms
        int colorOption = 0;
        while (await timer.WaitForNextTickAsync())
        {
            _changeMyBitmapColor(colorOption);
            colorOption = (colorOption + 1) % 5; // there will be 3 different color options
            Console.WriteLine("Bitmap modified!");
            _currentBitmapIndex = (_currentBitmapIndex + 1) % 2;
        }
    }
    private unsafe void _changeMyBitmapColor(int colorOption)
    {
        byte[] pixelValueOptions = [50, 100, 150, 200, 225];
        using ILockedFramebuffer frameBuffer = _scratchBoards[_currentBitmapIndex].Lock();
        void* dataStart = (void*)frameBuffer.Address;
        Span<byte> buffer = new Span<byte>(dataStart, 640 * 480 * 4); // assume each pixel is 1 byte in size and my image has 4 channels
        buffer.Fill(pixelValueOptions[colorOption]); // fill the bitmap memory buffer with some values
        MyBitmap = _scratchBoards[_currentBitmapIndex];
    }
}

通过在

MyBitmap
中的两个元素之间交替
_scratchBoards
引用的 writablebitmap 实例(注意不要修改私有成员
_myBitmap
),保证每帧都会触发
set
MyBitmap
访问器,这通过
PropertyChanged
引发
OnPropertyChanged
事件,然后通知视图重新渲染显示的图像。

解决方案2:使用
CommunityToolkit

文件:MainWindowViewModel

namespace TryUpdateBitmap.ViewModels;

using Avalonia.Media.Imaging;
using Avalonia;
using Avalonia.Platform;
using System.Threading;
using System;
using System.Threading.Tasks;
using System.Collections.Generic;
using CommunityToolkit.Mvvm.ComponentModel;

public partial class MainWindowViewModel : ObservableObject
{
    public MainWindowViewModel()
    {
        // Assume the bitmap is of shape 640x480 with 4 channels
        _myBitmap = new WriteableBitmap(new PixelSize(640, 480), new Vector(96, 96), PixelFormat.Bgra8888, AlphaFormat.Opaque);
        _currentBitmapIndex = 0;
        _scratchBoards = [];
        _scratchBoards.Add(new WriteableBitmap(new PixelSize(640, 480), new Vector(96, 96), PixelFormat.Bgra8888, AlphaFormat.Opaque));
        _scratchBoards.Add(new WriteableBitmap(new PixelSize(640, 480), new Vector(96, 96), PixelFormat.Bgra8888, AlphaFormat.Opaque));
        Console.WriteLine("Start app");
        Task.Run(_updateMyBitmapColorPeriodicallyAsync);
    }
    [ObservableProperty]
    private WriteableBitmap _myBitmap;
    private int _currentBitmapIndex;
    private List<WriteableBitmap> _scratchBoards;
    private  async Task _updateMyBitmapColorPeriodicallyAsync()
    {
        PeriodicTimer timer = new(TimeSpan.FromMilliseconds(500)); // Color of my bitmap pixels changes every 500ms
        int colorOption = 0;
        while (await timer.WaitForNextTickAsync())
        {
            _changeMyBitmapColor(colorOption);
            colorOption = (colorOption + 1) % 5; // there will be 3 different color options
            Console.WriteLine("Bitmap modified!");
            _currentBitmapIndex = (_currentBitmapIndex + 1) % 2;
        }
    }
    private unsafe void _changeMyBitmapColor(int colorOption)
    {
        byte[] pixelValueOptions = [50, 100, 150, 200, 225];
        using ILockedFramebuffer frameBuffer = _scratchBoards[_currentBitmapIndex].Lock();
        void* dataStart = (void*)frameBuffer.Address;
        Span<byte> buffer = new Span<byte>(dataStart, 640 * 480 * 4); // assume each pixel is 1 byte in size and my image has 4 channels
        buffer.Fill(pixelValueOptions[colorOption]); // fill the bitmap memory buffer with some values
        MyBitmap = _scratchBoards[_currentBitmapIndex]; // note that MyBitmap is implemented by community toolkit since _myBitmap is marked as ObservableProperty
    }
}

非常感谢CommunityToolkit,所需的代码显着减少。据我所知,这种方法在底层做了与解决方案 1 类似的事情,但它抽象了很多样板文件。我们要做的就是将

MainWindowViewModel
类标记为partial,继承自
ObservableObject
并将私有属性
_myBitmap
标记为
ObservableProperty
。它将生成公共字段
MyBitmap
,并通过更改
MyBitmap
(而不是
_myBitmap
)引用的对象,它通知属性更改,并且将重新渲染显示的位图。

最后一点:

这两种提议的解决方案都有效,但可能不是最佳方法。分配了两个额外的 writeablebitmap 实例,并且在列表中的元素之间交替对象引用似乎有点有趣。 总的来说,我对 Avalonia、MVVM 或 C# 还很陌生。也许使用 ReactiveUI 方法或使用

Avalonia.Threading.Dispatch.UIThread
更简单,但我还没有让它发挥作用。还没有。
所以如果有更好的解决方案的人看到这个,请分享。非常感激!干杯!

© www.soinside.com 2019 - 2024. All rights reserved.