在Windows操作系统中,通过编程方式控制声音播放设备可能并非易事。微软似乎有意为之,使得这一任务变得复杂。本文将提供一个解决方案,使用Managed C++(.NET) 来实现,但该方法也可以轻松地适应其他编程语言。
在深入讨论之前,需要明确几点:
由于微软不希望用户这样做,因此这个解决方案本质上是一种临时解决方案,一种变通方法。
任何第三方应用程序,如WindowsMedia Player,输出到声音播放音频设备时,都需要在音频设备更改后重新启动,因为它们通常在应用程序启动时只配置一次。自己的应用程序不必模仿这个限制。
只在XP上测试过这个解决方案。不知道为什么这个解决方案在Vista下不会工作,但在Vista下验证解决方案的可操作性留给读者作为练习。
因为在切换音频设备,而机器上实际可用的音频设备几乎肯定与机器不同,所以必须提供一些机器依赖的信息,以便解决方案在计算机上工作。
通过编程方式启动“声音和音频设备属性”小程序,并使用消息系统操作面板。面板实际上会在屏幕上短暂出现,但这并不是问题。在看来,通过这种方式更改音频设备比直接修改注册表更好,因为注册表总是容易出错,因为从来没有确切清楚涉及哪些注册表键。
当然,这种一般方法可以扩展到操作任何小程序或应用程序,不仅限于“声音和音频设备属性”小程序。解决方案足够小,其适应其他小程序(.cpl)和应用程序(.exe)应该很明显。
让开始吧。首先,需要确定想要切换的两个音频设备。从控制面板启动“声音和音频设备属性”小程序,转到“音频”选项卡,点击“声音播放”组合框。一个下拉列表显示了机器上可用于声音播放的音频输出设备。写下想要操作的两个设备(完全准确,所有的大写和小写字母以及标点都必须准确)。
在机器上,选择了“Realtek HD Audio output”和“CreamWare Play/Rec 1”。
在文件ACMDefines.h的顶部,用自己的选择替换选择:
#define AUDIO_DEVICE_1 _T("Realtek HD Audio output")
#define AUDIO_DEVICE_2 _T("CreamWare Play/Rec 1")
现在,构建解决方案,它应该在机器上工作。
从托管代码启动小程序(.cpl)很容易,它是一行代码:
Process::Start("rundll32.exe", "shell32.dll,Control_RunDLL mmsys.cpl");
小程序“mmsys.cpl”位于system32目录中,与其他控制面板中的小程序一起,没有什么奇怪的。故意没有立即启动显示“音频”选项卡的小程序,以便在示例代码中演示如何以编程方式选择选项卡。但是,可以将上述行更改为:
Process::Start("rundll32.exe", "shell32.dll,Control_RunDLL mmsys.cpl,,2");
结果将是小程序窗口出现,已经选择了“音频”选项卡,因此不需要在代码中执行选择所需选项卡的过程。但是,如果想利用解决方案作为一个模板,适应自己的具体需求,最好展示所有涉及的步骤。
一旦小程序成功启动,获取小程序窗口的句柄:
appletWindow = FindWindow(NULL, APPLET_WINDOW_TITLE);
从那里,它只是一个向小程序发送消息的问题,模拟键盘活动。概念很简单。困难在于获取小程序窗口中各种控件的句柄。首先,需要在小程序窗口中使用Spy++(在Visual Studio内部)来发现想要操作的控件的类型和标签。所以,启动小程序,打开Spy++,查找小程序窗口。应该看到类似以下内容:
Window xxxxxxxx "Sounds and Audio Devices Properties" #xxxxx (Dialog)
点击这个条目左侧的“+”以展开子窗口列表。其中一个子条目是'Window xxxxxxxx "&Apply" Button'。这里有两个重要的信息。一,它是一个类型的控件BUTTON。二,按钮的文本(或标签)是"&Apply"(注意实际GUI中不出现的和号)。
在示例代码中,使用调用findChildWindow()来获取这个控件(以及所有后续控件)的句柄:
applyButton = findChildWindow(appletWindow, BUTTON, APPLY_BUTTON);
BUTTON在ACMDefines.h中定义为:
#define BUTTON _T("Button")
并且APPLY_BUTTON定义为:
#define APPLY_BUTTON _T("&Apply")
现在,有了“Apply”按钮的句柄。接下来需要做的是选择“Audio”选项卡。在Spy++中,会看到一个条目,如:
Window xxxxxxxx '"' SysTabControl32
类型是SysTabControl,但没有标签。它没有标签不是问题,因为在小程序窗口中只有一个类型的控件"SysTabControl32"。以与获取“Apply”按钮句柄相同的方式获取这个控件的句柄:
tabControl = findChildWindow(appletWindow, SYS_TAB_CTRL, NULL);
一旦有了句柄,模拟键盘活动(ctrl-up/down/right/left)来选择“Audio”选项卡:
SendMessage(tabControl, WM_KEYDOWN, VK_CONTROL, 0);
//ctrl key down
SendMessage(tabControl, WM_KEYDOWN, VK_RIGHT, 0);
//right arrow key down
SendMessage(tabControl, WM_KEYUP, VK_RIGHT, 0);
//right arrow key up
SendMessage(tabControl, WM_KEYDOWN, VK_RIGHT, 0);
//right arrow key down
SendMessage(tabControl, WM_KEYUP, VK_RIGHT, 0);
//right arrow key up
SendMessage(tabControl, WM_KEYUP, VK_CONTROL, 0);
//ctrl key up
一旦显示了音频选项卡,查找想要的音频设备选择的句柄(AUDIO_DEVICE_1):
comboBox = findChildWindow(appletWindow, COMBO_BOX, AUDIO_DEVICE_1);
如果找不到句柄,查找AUDIO_DEVICE_2的句柄。示例代码假设这两个音频设备中的一个总是被选中,因此总是组合框中选定的项目。如果机器有超过两个音频设备,并且选择的两个设备都不是默认选定的项目,那么必须首先查找默认选定项目的句柄,然后从那里选择两个感兴趣的设备之一。示例代码没有这样做,它假设两个设备中的一个已经被选中。
一旦有了当前选定项目在组合框中的句柄,只需向它发送一个消息以更改为新的音频设备:
SendMessage(comboBox, CB_SELECTSTRING, (WPARAM)NONE, (LPARAM)&AUDIO_DEVICE_2);
然后,向“Apply”按钮发送消息以应用更改:
SendMessage(applyButton, BM_CLICK, 0, 0);
//apply the change
并关闭小程序窗口:
SendMessage(appletWindow, WM_CLOSE, 0, 0);
//close the applet
如果想启动一个常规应用程序(.exe)并使用模拟按键来操作它,有几种方法可以做到这一点。这里只是一个可能性:
protected:
bool launchApplication()
{
HINSTANCE returnCode;
returnCode = ShellExecute((HWND)errorBox->getWindowHandle(),
//window
"open",
//verb - action
"C:\\Program Files\\SomeProgram.exe",
//program file specification
"",
//parameters (none)
"C:\\Program Files",
//directory where opened
SW_SHOWMINNOACTIVE);
//window minimized, no focus
return ((returnCode > (HINSTANCE)32 && returnCode != (HINSTANCE)SE_ERR_NOASSOC) ? true : false);
}
对于.exe文件,只有启动方法不同,其他一切都与.cpl文件相同。
正如看到的,概念非常简单。这个编码示例的价值在于它展示了一种紧凑和通用的方法,用于从托管C++获取小程序中各种控件的句柄。