如何在WPF MVVM中将基于hWnd的CCTV流显示到指定ImageSource?
问题描述
我有一款可在WinForms中正常运行的IP摄像头(监控)SDK,目前正在使用WPF MVVM架构开发项目。
我曾尝试使用<WindowsFormsHost>搭配PictureBox来显示摄像头画面,虽然画面能显示,但经查找得知这会导致叠加的TextBlock无法正常显示。我也试过AirSpace方案,但出现错误且需安装大量DLL,因此不得不放弃。
以下是未使用WindowsFormsHost的WPF原始设计:
MainWindow.xaml
<Grid> <Image x:Name="MainCamera" Source="{Binding MAINCAMERASOURCE}"/> <TextBlock Text="Camera 255.255.255.255 Offline"/> <Grid>
ViewModel
internal void OnWindowHandleAvailable(IntPtr handleCameraMain) { CameraMain_Handle = handleCameraMain; } private IntPtr StartRealPlayCamera { IntPtr RealPayID = NetSDK.NETClient.RealPlay(LoginID, ChannelID, CameraMain_Handle, EM_RealPlayType.Realplay); }
MainWindow.xaml.cs
private void GiveWindowHandleToViewModel() { var viewModel = this.DataContext as ViewModel.MainMenuViewModel; if (viewModel == null) return; var windowHandle = this.GetWindowHandle(); if (windowHandle == IntPtr.Zero) return; viewModel.OnWindowHandleAvailable(windowHandle); } private IntPtr GetWindowHandle() { var window = Window.GetWindow(this); return new System.Windows.Interop.WindowInteropHelper(window).EnsureHandle(); }
我将窗口的hWnd传递给ViewModel以调用摄像头SDK,但结果是摄像头画面占据了整个窗口。我需要在主窗口中显示4路CCTV画面,请问如何将这些CCTV流指向指定的ImageSource?
解决方案
要实现多路摄像头画面绑定到指定WPF控件且不覆盖窗口,核心是避免传递整个窗口句柄,而是给每个画面分配独立的渲染容器,或通过SDK帧回调转换为WPF原生ImageSource。
方案1:用HwndHost给每个摄像头分配独立句柄
WPF原生的HwndHost可以创建Win32子窗口,把这个子窗口句柄传给SDK的RealPlay,每个摄像头画面会限制在对应的HwndHost区域内,同时不影响上层TextBlock显示。
- XAML布局(4路摄像头)
<Grid> <Grid.ColumnDefinitions> <ColumnDefinition Width="*"/> <ColumnDefinition Width="*"/> </Grid.ColumnDefinitions> <Grid.RowDefinitions> <RowDefinition Height="*"/> <RowDefinition Height="*"/> </Grid.RowDefinitions> <!-- 摄像头1 --> <local:CameraHwndHost x:Name="CameraHost1" Grid.Row="0" Grid.Column="0"/> <TextBlock Grid.Row="0" Grid.Column="0" Text="Camera 1 Offline" Margin="10" Foreground="Red"/> <!-- 摄像头2 --> <local:CameraHwndHost x:Name="CameraHost2" Grid.Row="0" Grid.Column="1"/> <TextBlock Grid.Row="0" Grid.Column="1" Text="Camera 2 Offline" Margin="10" Foreground="Red"/> <!-- 摄像头3 --> <local:CameraHwndHost x:Name="CameraHost3" Grid.Row="1" Grid.Column="0"/> <TextBlock Grid.Row="1" Grid.Column="0" Text="Camera 3 Offline" Margin="10" Foreground="Red"/> <!-- 摄像头4 --> <local:CameraHwndHost x:Name="CameraHost4" Grid.Row="1" Grid.Column="1"/> <TextBlock Grid.Row="1" Grid.Column="1" Text="Camera 4 Offline" Margin="10" Foreground="Red"/> </Grid>
- 自定义CameraHwndHost类
public class CameraHwndHost : HwndHost { public IntPtr HostHandle { get; private set; } protected override HandleRef BuildWindowCore(HandleRef hwndParent) { // 创建Win32子窗口作为SDK渲染容器 HostHandle = CreateWindowEx(0, "static", "", WS_CHILD | WS_VISIBLE, 0, 0, (int)ActualWidth, (int)ActualHeight, hwndParent.Handle, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero); return new HandleRef(this, HostHandle); } protected override void DestroyWindowCore(HandleRef hwnd) { DestroyWindow(hwnd.Handle); } // 导入Win32 API [DllImport("user32.dll", CharSet = CharSet.Unicode)] private static extern IntPtr CreateWindowEx(int dwExStyle, string lpClassName, string lpWindowName, int dwStyle, int x, int y, int nWidth, int nHeight, IntPtr hWndParent, IntPtr hMenu, IntPtr hInstance, IntPtr lpParam); [DllImport("user32.dll")] private static extern bool DestroyWindow(IntPtr hWnd); private const int WS_CHILD = 0x40000000; private const int WS_VISIBLE = 0x10000000; }
- ViewModel中管理多路句柄与启动预览
// 保存4个摄像头的句柄 private IntPtr _cameraHandle1, _cameraHandle2, _cameraHandle3, _cameraHandle4; public void SetCameraHandle(int cameraIndex, IntPtr handle) { switch(cameraIndex) { case 1: _cameraHandle1 = handle; break; case 2: _cameraHandle2 = handle; break; case 3: _cameraHandle3 = handle; break; case 4: _cameraHandle4 = handle; break; } } // 启动对应摄像头预览 public void StartCameraPreview(int cameraIndex, int loginId, int channelId) { IntPtr handle = cameraIndex switch { 1 => _cameraHandle1, 2 => _cameraHandle2, 3 => _cameraHandle3, 4 => _cameraHandle4, _ => IntPtr.Zero }; if(handle != IntPtr.Zero) { NetSDK.NETClient.RealPlay(loginId, channelId, handle, EM_RealPlayType.Realplay); } }
- 窗口加载时传递句柄
private void Window_Loaded(object sender, RoutedEventArgs e) { var viewModel = DataContext as MainMenuViewModel; if(viewModel == null) return; viewModel.SetCameraHandle(1, CameraHost1.HostHandle); viewModel.SetCameraHandle(2, CameraHost2.HostHandle); viewModel.SetCameraHandle(3, CameraHost3.HostHandle); viewModel.SetCameraHandle(4, CameraHost4.HostHandle); // 示例:启动各摄像头预览 viewModel.StartCameraPreview(1, LoginId1, ChannelId1); viewModel.StartCameraPreview(2, LoginId2, ChannelId2); viewModel.StartCameraPreview(3, LoginId3, ChannelId3); viewModel.StartCameraPreview(4, LoginId4, ChannelId4); }
方案2:通过SDK帧回调转换为ImageSource
如果SDK支持获取原始视频帧(RGB/YUV格式),可以注册帧回调,将帧数据转换成WPF原生的WriteableBitmap,直接绑定到Image的Source属性,完全避开句柄和Airspace问题。
- ViewModel定义绑定属性
private ImageSource _cameraSource1; public ImageSource CameraSource1 { get => _cameraSource1; set => SetProperty(ref _cameraSource1, value); // 需实现INotifyPropertyChanged } // 同理定义CameraSource2、CameraSource3、CameraSource4
- 注册帧回调并转换帧数据
// 启动预览时注册回调 public void StartCameraPreview(int cameraIndex, int loginId, int channelId) { // 假设SDK支持设置帧回调,传递摄像头索引作为用户数据 NetSDK.NETClient.SetRealPlayFrameCallback(loginId, channelId, (frameData, frameSize, width, height, format, userData) => { int idx = (int)userData; // 示例:RGB24格式转WriteableBitmap WriteableBitmap bitmap = new WriteableBitmap(width, height, 96, 96, PixelFormats.Rgb24, null); bitmap.Lock(); Marshal.Copy(frameData, bitmap.BackBuffer, 0, frameSize); bitmap.AddDirtyRect(new Int32Rect(0, 0, width, height)); bitmap.Unlock(); // UI线程更新属性 Application.Current.Dispatcher.Invoke(() => { switch(idx) { case 1: CameraSource1 = bitmap; break; case 2: CameraSource2 = bitmap; break; case 3: CameraSource3 = bitmap; break; case 4: CameraSource4 = bitmap; break; } }); }, (IntPtr)cameraIndex); // 传递空句柄,SDK会通过回调返回帧数据 NetSDK.NETClient.RealPlay(loginId, channelId, IntPtr.Zero, EM_RealPlayType.Realplay); }
- XAML直接绑定
<Image Source="{Binding CameraSource1}"/> <TextBlock Text="Camera 1 Offline" Margin="10" Foreground="Red"/> <!-- 其他摄像头同理 -->
注意事项
- 方案1中,TextBlock需在HwndHost之后声明,确保WPF渲染时TextBlock在画面上层。
- 方案2需注意帧处理的性能,使用
WriteableBitmap能减少UI线程开销,回调中必须切换到UI线程更新属性。 - 停止预览时务必调用SDK的
StopRealPlay等释放方法,避免内存泄漏。
内容的提问来源于stack exchange,提问作者Luiey
相关产品推荐
相关产品推荐

