音量控制程序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,每次热键调整后,窗口会自动出现三秒种后消失。
虽然本人生平编写过无数的程序,但是这一个我认为是最完美的……
:)
继续阅读《音量控制程序制作手记 (及全部源码)》的全文内容...
--------------------------
新闻:
鲍尔默:不解Google为何推出两款操作系统网站导航:
博客园首页 新闻 .NET频道 社区 博问 闪存 找找看