C#中ViewModel属性传入Dispatcher后WriteableBitmap更新失效问题
首先,你当前遇到画面不显示的核心问题是C#引用类型的参数传递逻辑导致的:
在Utils.InjectBitmap方法中,Dest参数是按值传递的——哪怕你在方法内部创建了新的WriteableBitmap并赋值给Dest,这个修改只会改变方法内部的局部变量引用,完全不会同步到外部的VM.FrameDataNew。所以外部的VM.FrameDataNew始终保持null状态,自然无法渲染画面。
另外还有个隐藏的资源泄漏问题:你注释掉了Src.UnlockBits(data),这会导致Bitmap资源无法正常释放,必须恢复这行代码。
第一步:修复InjectBitmap方法
有两种可靠的方式解决参数传递的问题:
方式1:用ref关键字传递参数
修改方法签名,让Dest按引用传递,这样方法内部对Dest的引用修改会直接同步到外部变量:
public static class Utils { /// <summary> /// Inject the source Bitmap into the Destination WriteableBitmap /// </summary> /// <param name="Src">The source Bitmap</param> /// <param name="Dest">The destination WriteableBitmap (passed by reference)</param> public static void InjectBitmap(Bitmap Src, ref WriteableBitmap Dest) { if ((Dest == null) || (Dest.Width != Src.Width) || (Dest.Height != Src.Height)) Dest = new WriteableBitmap(Src.Width, Src.Height, Src.HorizontalResolution, Src.VerticalResolution, PixelFormats.Bgr24, null); BitmapData data = Src.LockBits(new Rectangle(0, 0, Src.Width, Src.Height), ImageLockMode.ReadOnly, System.Drawing.Imaging.PixelFormat.Format24bppRgb); try { Dest.Lock(); try { // Copy the bitmap's data directly to the on-screen buffers CopyMemory(Dest.BackBuffer, data.Scan0, (data.Stride * data.Height)); // Moves the back buffer to the front. Dest.AddDirtyRect(new Int32Rect(0, 0, data.Width, data.Height)); } finally { Dest.Unlock(); } } finally { Src.UnlockBits(data); // 必须解锁,避免资源泄漏 } } [DllImport("Kernel32.dll", EntryPoint = "RtlMoveMemory")] public static extern void CopyMemory(IntPtr Destination, IntPtr Source, int Length); }
调用时需要加上ref关键字:
private void PipeLine_BitmapCaptured(object sender, Bitmap NewBitmap) { Dispatcher.Invoke(() => { Utils.InjectBitmap(NewBitmap, ref VM.FrameDataNew); }); }
方式2:让方法返回更新后的WriteableBitmap
如果不想用ref,可以让方法返回新的或已更新的WriteableBitmap,外部直接赋值:
public static class Utils { public static WriteableBitmap InjectBitmap(Bitmap Src, WriteableBitmap Dest) { if ((Dest == null) || (Dest.Width != Src.Width) || (Dest.Height != Src.Height)) Dest = new WriteableBitmap(Src.Width, Src.Height, Src.HorizontalResolution, Src.VerticalResolution, PixelFormats.Bgr24, null); BitmapData data = Src.LockBits(new Rectangle(0, 0, Src.Width, Src.Height), ImageLockMode.ReadOnly, System.Drawing.Imaging.PixelFormat.Format24bppRgb); try { Dest.Lock(); try { CopyMemory(Dest.BackBuffer, data.Scan0, (data.Stride * data.Height)); Dest.AddDirtyRect(new Int32Rect(0, 0, data.Width, data.Height)); } finally { Dest.Unlock(); } } finally { Src.UnlockBits(data); } return Dest; } [DllImport("Kernel32.dll", EntryPoint = "RtlMoveMemory")] public static extern void CopyMemory(IntPtr Destination, IntPtr Source, int Length); }
调用方式:
private void PipeLine_BitmapCaptured(object sender, Bitmap NewBitmap) { Dispatcher.Invoke(() => { VM.FrameDataNew = Utils.InjectBitmap(NewBitmap, VM.FrameDataNew); }); }
第二步:实现多视频流的ObservableCollection方案
直接用ObservableCollection<WriteableBitmap>并不是最优解——因为WriteableBitmap本身不实现INotifyPropertyChanged,当你更新集合中某个项的内容时,UI无法自动感知到变化。更好的方案是创建一个封装WriteableBitmap的子ViewModel:
1. 创建子ViewModel类
public class VideoStreamViewModel : INotifyPropertyChanged { private WriteableBitmap frameData; public WriteableBitmap FrameData { get => frameData; set { frameData = value; OnPropertyChanged(nameof(FrameData)); } } // 可选:添加流名称、ID等标识属性,方便UI区分 public string StreamName { get; set; } public event PropertyChangedEventHandler PropertyChanged; protected virtual void OnPropertyChanged(string propertyName) { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } }
2. 在主ViewModel中添加ObservableCollection
public class MainViewModel : INotifyPropertyChanged { private ObservableCollection<VideoStreamViewModel> videoStreams; public ObservableCollection<VideoStreamViewModel> VideoStreams { get => videoStreams; set { videoStreams = value; OnPropertyChanged(nameof(VideoStreams)); } } public MainViewModel() { VideoStreams = new ObservableCollection<VideoStreamViewModel>(); // 初始化时可按需添加空流项,或后续根据管线动态添加 } // 实现INotifyPropertyChanged接口 public event PropertyChangedEventHandler PropertyChanged; protected virtual void OnPropertyChanged(string propertyName) { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } }
3. 绑定UI到集合
在XAML中用ItemsControl或ListView显示多个视频流,每个项绑定到VideoStreamViewModel.FrameData:
<ItemsControl ItemsSource="{Binding VideoStreams}"> <ItemsControl.ItemTemplate> <DataTemplate> <StackPanel Margin="10"> <TextBlock Text="{Binding StreamName}" Margin="0 0 0 5" /> <Image Source="{Binding FrameData}" Stretch="Uniform" Width="320" Height="240" /> </StackPanel> </DataTemplate> </ItemsControl.ItemTemplate> <ItemsControl.ItemsPanel> <ItemsPanelTemplate> <WrapPanel /> <!-- 用WrapPanel实现流式布局,也可替换为Grid等自定义布局 --> </ItemsPanelTemplate> </ItemsControl.ItemsPanel> </ItemsControl>
4. 处理多管线的帧更新
每个管线的BitmapCaptured事件需要知道对应更新哪个VideoStreamViewModel实例。可以通过事件参数传递流ID,或者在创建管线时将ViewModel关联到管线的Tag属性:
// 创建新管线时,先添加对应的ViewModel到集合 var camera1Vm = new VideoStreamViewModel { StreamName = "Camera 1" }; VM.VideoStreams.Add(camera1Vm); // 创建管线并绑定事件,把ViewModel存入Tag属性 var pipeline1 = new CustomGstreamerPipeline(); pipeline1.Tag = camera1Vm; pipeline1.BitmapCaptured += MultiPipeline_BitmapCaptured; // 多流的事件处理方法 private void MultiPipeline_BitmapCaptured(object sender, Bitmap NewBitmap) { var pipeline = sender as CustomGstreamerPipeline; var targetVm = pipeline.Tag as VideoStreamViewModel; if (targetVm == null) return; Dispatcher.Invoke(() => { // 用修复后的InjectBitmap方法更新 Utils.InjectBitmap(NewBitmap, ref targetVm.FrameData); // 或用返回值方式: // targetVm.FrameData = Utils.InjectBitmap(NewBitmap, targetVm.FrameData); }); }
这样既解决了当前单流的显示问题,又为多流场景提供了符合MVVM规范、可扩展的高效实现方案。
内容的提问来源于stack exchange,提问作者MrVanx

