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

WPF中绑定List/ObservableCollection的ComboBox运行时刷新问题

WPF音频设备ComboBox实时更新问题解决方案

问题概述

开发WPF应用时,ComboBox的ItemSource绑定后台填充的音频设备列表,可获取默认设备,但运行时新增设备无法实时检测,需重启应用。尝试过ObservableCollection、ViewModel绑定、监听CollectionChanged事件均无效;在ComboBox的DropDownOpened事件中调用刷新方法虽能检测设备,但会丢失当前选中项。

现有代码片段

MainWindow代码

public partial class MainWindow : Window, INotifyPropertyChanged
{
    public MainWindow()
    {
        InitializeComponent();
        List<AudioDevice> devices = new List<AudioDevice>();
        HRESULT hr = HRESULT.E_FAIL;
        Guid CLSID_MMDeviceEnumerator = new Guid("{BCDE0395-E52F-467C-8E3D-C4579291692E}");
        Type MMDeviceEnumeratorType = Type.GetTypeFromCLSID(CLSID_MMDeviceEnumerator, true);
        object MMDeviceEnumerator = Activator.CreateInstance(MMDeviceEnumeratorType);
        IMMDeviceEnumerator pMMDeviceEnumerator = (IMMDeviceEnumerator)MMDeviceEnumerator;
        if (pMMDeviceEnumerator != null)
        {
            string sIdDefaultRender = null;
            string sIdDefaultCapture = null;
            IMMDevice pDefaultDevice = null;
            hr = pMMDeviceEnumerator.GetDefaultAudioEndpoint(EDataFlow.eRender, ERole.eConsole, out pDefaultDevice);
            if (hr == HRESULT.S_OK)
            {
                IntPtr hGlobal = Marshal.AllocHGlobal(260);
                hr = pDefaultDevice.GetId(out hGlobal);
                sIdDefaultRender = Marshal.PtrToStringUni(hGlobal);
                Marshal.FreeHGlobal(hGlobal);
                Marshal.ReleaseComObject(pDefaultDevice);
            }
            hr = pMMDeviceEnumerator.GetDefaultAudioEndpoint(EDataFlow.eCapture, ERole.eConsole, out pDefaultDevice);
            if (hr == HRESULT.S_OK)
            {
                IntPtr hGlobal = Marshal.AllocHGlobal(260);
                hr = pDefaultDevice.GetId(out hGlobal);
                sIdDefaultCapture = Marshal.PtrToStringUni(hGlobal);
                Marshal.FreeHGlobal(hGlobal);
                Marshal.ReleaseComObject(pDefaultDevice);
            }
            IMMDeviceCollection pDeviceCollection = null;
            hr = pMMDeviceEnumerator.EnumAudioEndpoints(EDataFlow.eAll, DEVICE_STATE_ACTIVE | DEVICE_STATE_UNPLUGGED, out pDeviceCollection);
            if (hr == HRESULT.S_OK)
            {
                uint nDevices = 0;
                hr = pDeviceCollection.GetCount(out nDevices);
                devices.Add(new AudioDevice() { Name = "System default", Direction = "Playback", Id = sIdDefaultRender, Default = true });
                for (uint i = 0; i < nDevices; i++)
                {
                    IMMDevice pDevice = null;
                    hr = pDeviceCollection.Item(i, out pDevice);
                    if (hr == HRESULT.S_OK)
                    {
                        IPropertyStore pPropertyStore = null;
                        hr = pDevice.OpenPropertyStore(STGM_READ, out pPropertyStore);
                        if (hr == HRESULT.S_OK)
                        {
                            string sFriendlyName = null;
                            string sDesc = null;
                            PROPVARIANT pv = new PROPVARIANT();
                            hr = pPropertyStore.GetValue(ref PKEY_Device_FriendlyName, out pv);
                            if (hr == HRESULT.S_OK)
                                sFriendlyName = Marshal.PtrToStringUni(pv.pwszVal);
                            hr = pPropertyStore.GetValue(ref PKEY_Device_DeviceDesc, out pv);
                            if (hr == HRESULT.S_OK)
                                sDesc = Marshal.PtrToStringUni(pv.pwszVal);
                            IntPtr hGlobal = Marshal.AllocHGlobal(260);
                            hr = pDevice.GetId(out hGlobal);
                            string sId = Marshal.PtrToStringUni(hGlobal);
                            Marshal.FreeHGlobal(hGlobal);
                            IMMEndpoint pEndpoint = (IMMEndpoint)pDevice;
                            EDataFlow eDirection = EDataFlow.eAll;
                            hr = pEndpoint.GetDataFlow(out eDirection);
                            string sDirection = "";
                            if (eDirection == EDataFlow.eRender)
                                sDirection = "Playback";
                            else if (eDirection == EDataFlow.eCapture)
                                sDirection = "Recording";
                            int nState = 0;
                            hr = pDevice.GetState(out nState);
                            if ((nState == DEVICE_STATE_ACTIVE))
                                devices.Add(new AudioDevice() { Name = sFriendlyName, Direction = sDirection, Id = sId, Default = (sId == sIdDefaultRender || (sId == sIdDefaultCapture)) });
                            Marshal.ReleaseComObject(pPropertyStore);
                        }
                        Marshal.ReleaseComObject(pDevice);
                    }
                }
                devices.Insert(devices.Count - 0, new AudioDevice() { Name = "Selected application ...", Direction = "Recording", Id = "Idlast", Default = false });
            }
            Marshal.ReleaseComObject(pDeviceCollection);
            ListCollectionView lcv = new ListCollectionView(devices);
            lcv.GroupDescriptions.Add(new PropertyGroupDescription("Direction"));
            ObservableCollection<AudioDevice> lcv2 = new(devices);
            this.cmb1.ItemsSource = lcv;
            cmb1.ItemsSource = lcv2;
        }
    }

    public class AudioDevice
    {
        public string Name { get; set; }
        public string Direction { get; set; }
        public string Id { get; set; }
        public bool Default { get; set; }
        public override string ToString()
        { return Name; }
    }
}

ViewModel代码

public class TestViewModel : INotifyPropertyChanged
{
    private ObservableCollection<AudioDevice> devices;
    public ObservableCollection<AudioDevice> Devices
    {
        get { return devices; }
        set { devices = value; NotifyPropertyChanged("Devices"); }
    }
    public event PropertyChangedEventHandler PropertyChanged;
    private void NotifyPropertyChanged(string propertyName)
    { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); }
}

XAML绑定代码

<ComboBox x:Name="cmb1" ItemsSource="{Binding Devices}" FontFamily="Segoe UI" FontSize="10" TextOptions.TextFormattingMode="Display" SnapsToDevicePixels="True" UseLayoutRounding="True" RenderOptions.BitmapScalingMode="NearestNeighbor" RenderOptions.ClearTypeHint="Enabled" Height="24.2" Width="246.5" Margin="121,7,0,0" HorizontalAlignment="Left" VerticalAlignment="Top" SelectedValuePath="Content"></ComboBox>

CollectionChanged事件代码

devices = new ObservableCollection<AudioDevice>();
this.devices.CollectionChanged += Devices_CollectionChanged;
private void Devices_CollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
{
    if (e.Action == NotifyCollectionChangedAction.Add) devices.Reverse();
    if (e.Action == NotifyCollectionChangedAction.Remove) devices.Reverse();
    if (e.Action == NotifyCollectionChangedAction.Move) devices.Reverse();
    if (e.Action == NotifyCollectionChangedAction.Reset) devices.Reverse();
    if (e.Action == NotifyCollectionChangedAction.Replace) devices.Reverse();
}

刷新方法及调用代码

public void Cmb1Update()
{
    List<AudioDevice> devices = new();
    HRESULT hr = HRESULT.E_FAIL;
    Guid CLSID_MMDeviceEnumerator = new Guid("{BCDE0395-E52F-467C-8E3D-C4579291692E}");
    Type MMDeviceEnumeratorType = Type.GetTypeFromCLSID(CLSID_MMDeviceEnumerator, true);
    object MMDeviceEnumerator = Activator.CreateInstance(MMDeviceEnumeratorType);
    IMMDeviceEnumerator pMMDeviceEnumerator = (IMMDeviceEnumerator)MMDeviceEnumerator;
    if (pMMDeviceEnumerator != null)
        .............................
}
private void cmb1_DropDownOpened(object sender, EventArgs e)
{ Cmb1Update(); }

解决方案

1. 监听音频设备硬件变化(核心)

使用MMDeviceEnumerator的RegisterEndpointNotificationCallback方法注册设备通知,实现IMMNotificationClient接口监听设备添加/移除事件,无需依赖下拉事件触发刷新。

首先实现IMMNotificationClient接口:

public class AudioDeviceNotificationClient : IMMNotificationClient
{
    private readonly Action _onDeviceChanged;

    public AudioDeviceNotificationClient(Action onDeviceChanged)
    {
        _onDeviceChanged = onDeviceChanged;
    }

    public void OnDeviceStateChanged(string pwstrDeviceId, DWORD dwNewState)
    {
        _onDeviceChanged?.Invoke();
    }

    public void OnDeviceAdded(string pwstrDeviceId)
    {
        _onDeviceChanged?.Invoke();
    }

    public void OnDeviceRemoved(string pwstrDeviceId)
    {
        _onDeviceChanged?.Invoke();
    }

    public void OnDefaultDeviceChanged(EDataFlow flow, ERole role, string pwstrDefaultDeviceId)
    {
        _onDeviceChanged?.Invoke();
    }

    public void OnPropertyValueChanged(string pwstrDeviceId, PROPERTYKEY key)
    {
        // 可选触发刷新
    }
}

在MainWindow中注册通知:

private IMMNotificationClient _notificationClient;
private IMMDeviceEnumerator _deviceEnumerator;

public MainWindow()
{
    InitializeComponent();
    // 初始化ViewModel和设备列表
    var viewModel = new TestViewModel();
    viewModel.Devices = new ObservableCollection<AudioDevice>();
    DataContext = viewModel;

    // 初始化设备枚举器并注册通知
    Guid CLSID_MMDeviceEnumerator = new Guid("{BCDE0395-E52F-467C-8E3D-C4579291692E}");
    Type MMDeviceEnumeratorType = Type.GetTypeFromCLSID(CLSID_MMDeviceEnumerator, true);
    _deviceEnumerator = (IMMDeviceEnumerator)Activator.CreateInstance(MMDeviceEnumeratorType);
    _notificationClient = new AudioDeviceNotificationClient(RefreshDeviceList);
    _deviceEnumerator.RegisterEndpointNotificationCallback(_notificationClient);

    // 初始加载设备列表
    RefreshDeviceList();
}

protected override void OnClosed(EventArgs e)
{
    base.OnClosed(e);
    // 注销通知,释放资源
    _deviceEnumerator.UnregisterEndpointNotificationCallback(_notificationClient);
    Marshal.ReleaseComObject(_deviceEnumerator);
}

2. 刷新设备列表时保留选中项

修改RefreshDeviceList方法,先记录当前选中设备的Id,刷新后重新选中对应设备:

private void RefreshDeviceList()
{
    var viewModel = (TestViewModel)DataContext;
    // 记录当前选中的设备Id
    string selectedId = null;
    if (cmb1.SelectedItem is AudioDevice selectedDevice)
    {
        selectedId = selectedDevice.Id;
    }

    // 获取最新设备列表(复用原有设备获取逻辑)
    List<AudioDevice> newDevices = GetAudioDevices();

    // 更新ObservableCollection:先清空再添加,避免破坏绑定
    viewModel.Devices.Clear();
    foreach (var device in newDevices)
    {
        viewModel.Devices.Add(device);
    }

    // 恢复选中项
    if (!string.IsNullOrEmpty(selectedId))
    {
        var targetDevice = viewModel.Devices.FirstOrDefault(d => d.Id == selectedId);
        if (targetDevice != null)
        {
            cmb1.SelectedItem = targetDevice;
        }
        else
        {
            // 原设备已移除,选中默认设备
            var defaultDevice = viewModel.Devices.FirstOrDefault(d => d.Default);
            cmb1.SelectedItem = defaultDevice ?? viewModel.Devices.FirstOrDefault();
        }
    }
}

// 提取设备获取逻辑为独立方法
private List<AudioDevice> GetAudioDevices()
{
    List<AudioDevice> devices = new List<AudioDevice>();
    HRESULT hr = HRESULT.E_FAIL;
    if (_deviceEnumerator != null)
    {
        string sIdDefaultRender = null;
        string sIdDefaultCapture = null;
        IMMDevice pDefaultDevice = null;
        hr = _deviceEnumerator.GetDefaultAudioEndpoint(EDataFlow.eRender, ERole.eConsole, out pDefaultDevice);
        if (hr == HRESULT.S_OK)
        {
            IntPtr hGlobal = Marshal.AllocHGlobal(260);
            hr = pDefaultDevice.GetId(out hGlobal);
            sIdDefaultRender = Marshal.PtrToStringUni(hGlobal);
            Marshal.FreeHGlobal(hGlobal);
            Marshal.ReleaseComObject(pDefaultDevice);
        }
        hr = _deviceEnumerator.GetDefaultAudioEndpoint(EDataFlow.eCapture, ERole.eConsole, out pDefaultDevice);
        if (hr == HRESULT.S_OK)
        {
            IntPtr hGlobal = Marshal.AllocHGlobal(260);
            hr = pDefaultDevice.GetId(out hGlobal);
            sIdDefaultCapture = Marshal.PtrToStringUni(hGlobal);
            Marshal.FreeHGlobal(hGlobal);
            Marshal.ReleaseComObject(pDefaultDevice);
        }
        IMMDeviceCollection pDeviceCollection = null;
        hr = _deviceEnumerator.EnumAudioEndpoints(EDataFlow.eAll, DEVICE_STATE_ACTIVE, out pDeviceCollection);
        if (hr == HRESULT.S_OK)
        {
            uint nDevices = 0;
            hr = pDeviceCollection.GetCount(out nDevices);
            devices.Add(new AudioDevice() { Name = "System default", Direction = "Playback", Id = sIdDefaultRender, Default = true });
            for (uint i = 0; i < nDevices; i++)
            {
                IMMDevice pDevice = null;
                hr = pDeviceCollection.Item(i, out pDevice);
                if (hr == HRESULT.S_OK)
                {
                    IPropertyStore pPropertyStore = null;
                    hr = pDevice.OpenPropertyStore(STGM_READ, out pPropertyStore);
                    if (hr == HRESULT.S_OK)
                    {
                        string sFriendlyName = null;
                        PROPVARIANT pv = new PROPVARIANT();
                        hr = pPropertyStore.GetValue(ref PKEY_Device_FriendlyName, out pv);
                        if (hr == HRESULT.S_OK)
                            sFriendlyName = Marshal.PtrToStringUni(pv.pwszVal);
                        IntPtr hGlobal = Marshal.AllocHGlobal(260);
                        hr = pDevice.GetId(out hGlobal);
                        string sId = Marshal.PtrToStringUni(hGlobal);
                        Marshal.FreeHGlobal(hGlobal);
                        IMMEndpoint pEndpoint = (IMMEndpoint)pDevice;
                        EDataFlow eDirection = EDataFlow.eAll;
                        hr = pEndpoint.GetDataFlow(out eDirection);
                        string sDirection = "";
                        if (eDirection == EDataFlow.eRender)
                            sDirection = "Playback";
                        else if (eDirection == EDataFlow.eCapture)
                            sDirection = "Recording";
                        int nState = 0;
                        hr = pDevice.GetState(out nState);
                        if ((nState == DEVICE_STATE_ACTIVE))
                            devices.Add(new AudioDevice() { Name = sFriendlyName, Direction = sDirection, Id = sId, Default = (sId == sIdDefaultRender || sId == sIdDefaultCapture) });
                        Marshal.ReleaseComObject(pPropertyStore);
                    }
                    Marshal.ReleaseComObject(pDevice);
                }
            }
            devices.Add(new AudioDevice() { Name = "Selected application ...", Direction = "Recording", Id = "Idlast", Default = false });
            Marshal.ReleaseComObject(pDeviceCollection);
        }
    }
    return devices;
}

3. 修复XAML绑定错误

原XAML中SelectedValuePath="Content"错误,AudioDevice类无Content属性,改为绑定SelectedItem或调整SelectedValuePath为Id:

<ComboBox x:Name="cmb1" 
          ItemsSource="{Binding Devices}" 
          SelectedItem="{Binding SelectedDevice}" 
          FontFamily="Segoe UI" 
          FontSize="10" 
          TextOptions.TextFormattingMode="Display" 
          SnapsToDevicePixels="True" 
          UseLayoutRounding="True" 
          RenderOptions.BitmapScalingMode="NearestNeighbor" 
          RenderOptions.ClearTypeHint="Enabled" 
          Height="24.2" Width="246.5" 
          Margin="121,7,0,0" 
          HorizontalAlignment="Left" 
          VerticalAlignment="Top">
</ComboBox>

若用ViewModel管理选中项,在TestViewModel中添加:

private AudioDevice selectedDevice;
public AudioDevice SelectedDevice
{
    get { return selectedDevice; }
    set { selectedDevice = value; NotifyPropertyChanged(nameof(SelectedDevice)); }
}

4. 移除无效的CollectionChanged逻辑

删除原代码中CollectionChanged事件内的devices.Reverse()代码,该逻辑会打乱列表顺序且无实际作用。

关键说明

  • 设备通知是Windows音频API原生监听方式,可实时捕获设备变化,比依赖下拉事件更可靠。
  • 刷新列表时通过Clear()和Add()更新ObservableCollection,而非重新赋值,避免破坏绑定关系。
相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.25 23:43:12