音量控制程序制作手记 (及全部源码)
音量控制程序SmartVolume制作手记《上》 ==================================== 很久以来,我对Win2000的音量控制深恶痛绝。有时候音量不合适要调节的时候,我会单击托盘栏上的小喇叭,下一步不是拖动拉杆,而是耐心地等待......只听一阵硬盘哗哗作响,鼠标处的光标变成沙漏,大约五秒左右,会跳出那个小小的调整音量窗口。我长出一口气,感慨一番,做一次调整。每次都是如此,烦啊。 夜深人静的时候,有时候运行一个程序,会突然爆发出一阵巨大的声音,怕影响家人熟睡的我,就开始手忙脚乱了!前面的音量调节肯定是不能用了,因为反应太慢。我跳起来,去找音箱后面的开关。平时开关就难找得出奇,何况是这种紧急情况?于是拨音箱电源线。整个世界清静了。我坐下来喘口气,心里暗自庆幸没有错拨到主机电源线。 想起从前用win98的“好日子”。98下的音量调节出来得很快,不用有这等担心。可是也有一样不好,深夜里,想听听音乐,当然要把音量调得小而又小。可是,当我把那个TraceBar拉到最底端的时候,坏了!音量突然变成最大!又是一阵手忙脚乱,以及过后大骂微软。 下了无数次的决心,要找一个音量控制软件或者自己写一个。上网粗略地找了一下,没有合适的,还是自己写吧。这就是做程序员的好处。呵呵。 麻烦的是,我不知道控制音量的API。上网查吧。从google上找到一篇文章, 如何控制计算机系统音量http://gamepower.myst.com.cn/tech/vb_clvol.htm),一看,是VB源码,里面有几个API的声明和调用。好啊,这么容易就找到了。先看它用到什么API吧:auxGetNumDevs,auxGetDevCaps,auxSetVolume,auxGetVolume,看起来应该是这几个。 用windows自带的查找文件功能在所有Delphi源码和控件源码里查找有auxSetVolume的*.pas的文件,只有mmsystem.pas里有。打开一看,果然有相关的声明。怪我对delphi了解不够,竟然忘记了先查一下Delphi源程序。 可惜没有找到示例程序。查一下Delphi自带的Windows SDK帮助,好象不够详细;再上MSDN查一下,发现二者内容差不多。我试着在Delphi里运行这一句 ShowMessage(Inttostr(auxGetNumDevs)); // use mmsystem; 发现结果竟然是0。看来,这里还是有问题,aux就是辅助设备的意思,大概是这里不对。我要调整windows的主音量,而不是wave, midi,大概也不是aux。多媒体方面的知识我并不太了解,只能这样猜测。 印象中好象看到过有监视API的软件。如果真的能在调整音量的时候监视它调用了哪些API,不就解决了吗?于是用“监视 API”做关键字在Google上查。原来这个软件叫APIspy。国内的网站居然难以下载,用 apispy download继续查google。最终从国外站点下载了版本2.5。(http://madmat.hypermart.net/apis3225.exe) APIspy可以指定并运行一个程序,在运行过程中监视API调用。怎么知道windows调节音量的程序名呢? 用Sound做关键字在MSDN里查询,找到一篇文章讲到 The SndVol32 program (sndvol32.exe) controls both the volume settings for various sound sources (such as wave, CD, and synthesizer) and the master volume setting. 原来是sndvol32.exe,我轻易地在system32里找到了它。用apispy打开,那个run按钮居然是灰的。我试着单击旁边的imports,出现一个对话框,显示sndvol32程序调用的各dll的api的名字。有advapi32.dll, comctl32.dll, ... 最后是winmm.dll。我只关心声音控制,当然是把双击每一个winmm的api把它们添加到监视列表里。 现在run按钮可用了。单击它,出现了Volume Control窗口,同时apispy显示出调用的那些api。我拉动主音量调节杆,监视窗口又有了变化。在监视结果里面,我发现了这样几个调用:mixerGetControlDetails, mixerGetLineInfo。Mixer是混音器的意思,有戏。 从MSDN上继续用新找到的API查,又找出了几个相关的来。 UINT mixerGetNumDevs(VOID); MMRESULT mixerSetControlDetails( HMIXEROBJ hmxobj, LPMIXERCONTROLDETAILS pmxcd, DWORD fdwDetails ); 好复杂啊!(详见 http://msdn.microsoft.com/library/default.asp?url=/library/EN-US/multimed/mmfunc_8hkf.asp) 试着在Delphi里调用mixerGetNumDevs,结果是1。运行下面代码 procedure TForm1.Button1Click(Sender: TObject); var NumDevs : integer; dcaps : TAuxCaps; i:integer; begin NumDevs := mixerGetNumDevs; for i := 0 to NumDevs - 1 do begin mixerGetDevCaps(i, @dcaps, sizeof(dcaps)); Listbox1.Items.Add(dcaps.szPname) end; end; 结果是SiS 7012 Wave,那是我的声卡。一切正确。 我试着用mixerSetControlDetails设置音量,却总是不能得其门而入。再次上网找示范用例吧。不知为什么,google上不去了,用百度查询mixerSetControlDetails,找到一篇文章,名字是《音量调节及静音》(http://www.csdn.net/develop/article/17/17257.shtm),作者是Haofei。往下一看,是一个完整的Delphi单元,有四个函数获取音量GetVolume(DN)、设置音量SetVolume(DN,Value)、获取静音GetVolumeMute(DN)及设置静音SetVolumeMute(DN,Value)。 放到自己程序里,写了两行代码,完全正确。 本来是想自己写的,想不到有人已经把事情做完了。既是高兴,又是失望。关于音量调整的技术探索,到此为止。 剩下的工作没有什么难度,很快就可以写出自己的音量控制程序了。 尾声:在搜索过程中,还找到了hubdog的主页上也有类似的一篇文章《Delphi 4下编写Audio Mixer Control》(http://hubdog.myrice.com/Recommend/rcAudioMixer.htm),看来,有不少人做过同样的尝试,我只是个迟到者。 本来,问题解决也就完了,但我在整个过程中,越来越想把自己的经历写出来,供初学的朋友参考吧。 ---------------------------------------------------------------------- 音量控制程序SmartVolume制作手记《下》 ==================================== 接下来,就到了实地编程的阶段了。 我所希望的功能是: 1. 按一个热键比如Ctrl-Alt-小键盘减号,会打开/关闭声音。 2. 按热键Ctrl-Alt-上、下方向键调节音量大小。 还有一个小小的难题需要解决,那就是全局热键。我知道LMD控件组里有LMDGlobalHotkey可以做到,不过这一次想试试自己解决这个问题。 我可以去LMD的源码或上网找资料,不过记得好象是Register打头的Api,从SDK帮助中一查,果然是RegisterHotKey和UnregisterHotKey,同时要用到消息WM_HOTKEY。 大体看了一下API帮助,再经过简单的分析,写了一段程序如下,运行通过。看来很简单嘛。自己以前把这些技术想得太复杂了: procedure TForm1.Button2Click(Sender: TObject); begin RegisterHotKey(handle,1,MOD_ALT or MOD_CONTROL,VK_SUBTRACT); //Ctrl-Alt-小键盘减号 RegisterHotKey(handle,2,MOD_ALT or MOD_CONTROL,VK_DOWN); //Ctrl-Alt-方向键下 end; procedure TForm1.WMHotKey(var Message: TWMHotkey); begin case Message.HotKey of 1: showmessage('1'); 2: showmessage('2'); else Showmessage(inttostr(message.HotKey)); end; end; procedure TForm1.Button3Click(Sender: TObject); begin UnregisterHotKey(handle,1); UnregisterHotKey(handle,2); end; 下一步是得到用户自己定义的HOTKEY。Delphi有一个原生的THotkey控件,就是做这件事的。不过看了帮助,THotkey是为TMenuItem服务的。要把它转成API使用的参数,还真的颇费一番周折。通过看THotkey的源码,加上一通试验,才算搞定。 procedure TForm1.Button2Click(Sender: TObject); var Modifiers:integer; HK:longint; begin Modifiers:=0; if hkShift in Hotkey1.Modifiers then Modifiers:=Modifiers or MOD_SHIFT; if hkCtrl in Hotkey1.Modifiers then Modifiers:=Modifiers or MOD_CONTROL; if hkAlt in Hotkey1.Modifiers then Modifiers:=Modifiers or MOD_ALT; HK := SendMessage(Hotkey1.Handle, HKM_GETHOTKEY, 0, 0); // 从源码里学来的 Win32Check(RegisterHotKey(handle,1,Modifiers,HK and $FF)); end; THotkey的参数保存应该很容易的。只需要保存两个数字 byte(Hotkey1.Modifiers) 和 Hotkey1.Hotkey。 想来想去,Delphi的THotkey用着还是不爽,自己写一个组件吧。名字叫TGlobalHotKey。 我写组件的经验很少,只是知道大体的方法。 比如这次在增删一些属性的时候,发现Object Inspector里没有及时反应出来。打开DclUser.dpk,编译一下,就有了。 哈哈,做完了。以下是GlobalHotKey控件源码 //------------------- GlobalHotKey.pas 开始 ---------------------- // 作者:安富国 http://anjo.delphibbs.com 2003.5 unit GlobalHotKey; interface uses SysUtils, Classes, Controls, ComCtrls, IniFiles, Windows, CommCtrl, Messages; type TGlobalHotKey = class(THotKey) private FGlobalID: integer; FOnExecute: TNotifyEvent; procedure SetGlobalID(const Value: integer); { Private declarations } protected { Protected declarations } procedure WMHotKey(var Message: TWMHotkey); message WM_HOTKEY; public { Public declarations } // 保存hotkey到ini文件中 procedure LoadFromIni(Ini:TIniFile); // 从ini文件里取出hotkey procedure SaveToIni(Ini:TIniFile); // 查询windows有没有注册过这个Hotkey。如果没有,返回true。 function QueryGlobalHotkey:boolean; // 注册为全局Hotkey function RegisterGlobalHotkey:boolean; // 注销全局Hotkey procedure UnregisterGlobalHotkey; // 与另一个THotKey比较 function EqualTo(AHotKey:THotKey):boolean; published { Published declarations } property GlobalID:integer read FGlobalID write SetGlobalID default 1; property OnExecute: TNotifyEvent read FOnExecute write FOnExecute; end; procedure Register; implementation procedure Register; begin RegisterComponents('System', [TGlobalHotKey]); end; { TGlobalHotKey } function TGlobalHotKey.EqualTo(AHotKey: THotKey): boolean; begin result:=(Modifiers=AHotKey.Modifiers) and (HotKey=AHotKey.HotKey); end; procedure TGlobalHotKey.LoadFromIni(Ini: TIniFile); var SectionName:string; begin SectionName:='GlobalKey_'+name; Modifiers:=THKModifiers(byte(ini.ReadInteger(SectionName,'Modifiers',byte(Modifiers)))); HotKey:=ini.ReadInteger(SectionName,'Hotkey',HotKey); end; function TGlobalHotKey.QueryGlobalHotkey: boolean; begin result:=RegisterGlobalHotkey; if result then UnregisterGlobalHotkey; end; function TGlobalHotKey.RegisterGlobalHotkey: boolean; var AModifiers:integer; HK:longint; begin AModifiers:=0; if hkShift in Modifiers then AModifiers:=AModifiers or MOD_SHIFT; if hkCtrl in Modifiers then AModifiers:=AModifiers or MOD_CONTROL; if hkAlt in Modifiers then AModifiers:=AModifiers or MOD_ALT; HK := SendMessage(Handle, HKM_GETHOTKEY, 0, 0); result:=RegisterHotKey(handle,GlobalID,AModifiers,HK and $FF); end; procedure TGlobalHotKey.SaveToIni(Ini: TIniFile); var SectionName:string; begin SectionName:='GlobalKey_'+name; ini.WriteInteger(SectionName,'Modifiers',byte(Modifiers)); ini.WriteInteger(SectionName,'Hotkey',HotKey); end; procedure TGlobalHotKey.SetGlobalID(const Value: integer); begin FGlobalID := Value; end; procedure TGlobalHotKey.UnregisterGlobalHotkey; begin UnregisterHotKey(handle,GlobalID); end; procedure TGlobalHotKey.WMHotKey(var Message: TWMHotkey); begin if Assigned(FOnExecute) then FOnExecute(Self); end; end. //------------------- GlobalHotKey.pas 结束 ------------------------ 在GlobalHotKey里面,我给原来的THotkey加了调用RegisterHotKey时用到的参数GlobalID和响应Hotkey按下的事件onExecute。 按理说,TGlobalHotKey也许应该做成非可视的控件,不过我想,如果不想看到它,可以把Visible属性设为False,效果也一样。 按照事先构想的SmartVolume的功能,它需要运行时缩小到右下角任务栏。这是一个标准的TrayIcon应用。有关TrayIcon的文章,自打学Delphi以来,就见到过无数篇,而我平时一般用Rxlib里的相应的控件RxTrayIcon,完善、方便。本想不用任何控件完成SmartVolume,看了看TrayIcon的资料,好麻烦!还是沿用Rxlib吧。 前后经过两天多的工作,程序完成了。完全实现了前面构想的两个功能,而且可以用户自定义热键,调用sndvol32.exe,每次热键调整后,窗口会自动出现三秒种后消失。 虽然本人生平编写过无数的程序,但是这一个我认为是最完美的…… :)