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,而非重新赋值,避免破坏绑定关系。
相关产品推荐
相关产品推荐

