You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在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显示。

  1. 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>
  1. 自定义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;
}
  1. 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);
    }
}
  1. 窗口加载时传递句柄
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问题。

  1. ViewModel定义绑定属性
private ImageSource _cameraSource1;
public ImageSource CameraSource1
{
    get => _cameraSource1;
    set => SetProperty(ref _cameraSource1, value); // 需实现INotifyPropertyChanged
}

// 同理定义CameraSource2、CameraSource3、CameraSource4
  1. 注册帧回调并转换帧数据
// 启动预览时注册回调
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);
}
  1. 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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.19 04:20:28