2010年11月22日星期一

音量控制程序开发历程

音量控制程序开发历程

需求激发灵感

最近玩游戏,激烈的游戏音效常常太过震撼,为此经常要切换音量。但每次都从游戏画面切回到桌面不仅麻烦,对于我这台破电脑又卡得不行。回想以前在笔记本上玩游戏调整音量都用快捷键(笔记本的特殊功能键),自然就有了这么个想法,用快捷键快速调节音量。
想法有了,接下来就定了个小开发计划。整个程序大体上有这么几个重点:
1.控制音量调节的API函数。
2.注册系统热键。能用Ctrl+Alt+↑和Ctrl+Alt+↓来控制音量增减。
3.希望可以在系统托盘里,不占用任务栏。

寻找控制音量的API的曲折过程

音量控制对高手来讲应该不算什么,但我头一次接触多媒体控制还是到处碰壁。首先Google一下,再MSDN,发现可以用来控制音量的还真不少。DirectshowwaveOut一族函数里的waveOutSetVolumeaux一族里面的auxSetVolume等。
于是随便选个auxSetVolume来试试吧。根据MSDN里面的函数说明,很快完成了一段增减音量的代码,可是不论我怎么调试,都不能控制音量,查看返回值是说没有合适的设备。又上网找原因,在CSDN里有一段问答也遇到了和我一样的状况,有高手说是因为此函数和集成声卡之间有矛盾,集成声卡常常会找不到设备。郁闷!竟然会这样。我一想,现在有几个电脑不是集成声卡?既然它可能会有问题(我也不知道到底是不是这个问题),算了,那么多函数可选,我再换一个吧。
因为看CSDN里讨论说directshowwaveOutSetVolume都比较适合针对所编写的程序调节音量,也就是只调整一个程序的音量其他程序不变,而我想要的是调整整个系统音量。继续在网上寻找,最后在51cto里找到一篇(http://book.51cto.com/art/200805/72107.htm)用的是混音器mixer系列的函数,它是直接针对windows下的混音器,可以直接调整windows音量,和鼠标点击右下角的小喇叭作用是一样的。确定目标就是它了。

控制音量的函数

控制音量的过程很简单,打开混音器设备mixer,然后获取当前音量或是设置当前音量。主要用到这样几个函数:

1.mixerOpen打开混音器设备。

MMRESULT mixerOpen(
  LPHMIXER phmx,
  UINT uMxId,
  DWORD dwCallback,
  DWORD dwInstance,
  DWORD fdwOpen
);
phmx指向一个HMIXER类型的指针,返回已打开的混音设备的标识句柄,不能为空。
uMxld指定要打开的混音器设备标识,这一标识可以是设备句柄,也可以是设备标识号(数字),具体是什么由fdwOpen指定。
dwCallback指定回调窗口句柄,就是指所打开的设备状态变化时,产生的消息传递给哪个窗口。
dwInstance指定调用实例句柄,这个参数我不太明白。
fdwOpenuMxId配套使用,指示uMxId是什么标识类型。

2.mixerGetLineControls获得关联音频线路的一个或多个控制器。

MMRESULT mixerGetLineControls(
    HMIXEROBJ hmxobj,
    LPMIXERLINECONTROLS pmxlc,
    DWORD fdwControls
);
hmxobj指定要获取控制的混音器设备对象句柄,就是mixerOpen的第一个参数返回的值。
pmxlc是一个MIXERLINECONTROLS结构的指针,里面是与要控制的音频线路有关的信息。
fdwControls对前两个参数做出了一些标示。

3.mixerGetControlDetails获取指定控制器的详细信息。

MMRESULT mixerGetControlDetails(
    HMIXEROBJ hmxobj,
    LPMIXERCONTROLDETAILS pmxcd,
    DWORD fdwDetails
);
hmxobj指定要获取控制的混音器设备对象句柄,就是mixerOpen的第一个参数返回的值。
pmxcd是一个MIXERCONTROLDETAILS结构的指针,里面是与要控制的音频线路有关的信息。
fdwControls对前两个参数做出了一些标示。

4. mixerSetControlDetails获取指定控制器的详细信息。

MMRESULT mixerSetControlDetails(
    HMIXEROBJ hmxobj,
    LPMIXERCONTROLDETAILS pmxcd,
    DWORD fdwDetails
);
hmxobj指定要获取控制的混音器设备对象句柄,就是mixerOpen的第一个参数返回的值。
pmxcd是一个MIXERCONTROLDETAILS结构的指针,里面是与要控制的音频线路有关的信息。
fdwControls对前两个参数做出了一些标示。

有了这样几个函数之后还需要注意,这些函数都要用到头文件“mmsystem.h”,而这个头文件又要用到“windows.h”,编译的时候还要加上库文件“winmm.lib”。

建立工程

先来试试这些函数能不能控制音量。不要嫌我啰嗦,一步一步来。
1.打开VC,用向导建立一个叫VolumeMFC对话框工程,不用改其他设置项。


2. 在对话框上添加一个滑标控件,就是Slider。在它上面右键,类向导。在成员变量里,对IDC_SLIDER1添加变量,变量类型选择control,变量名m_control

3.打开头文件VolumeDlg.h,在开头添加
#pragma comment(lib,"winmm.lib")
#include "windows.h"
#include "mmsystem.h"
在类声明中添加
HMIXER m_hMixer;
DWORD m_controlid;
VolumeDlg.cpp中,找到对话框初始化函数OnInitDialog,添加
MIXERLINE mxl;
MIXERCONTROL mxc;
MIXERLINECONTROLS mxlc;

mixerOpen(&m_hMixer,0,(DWORD)this->GetSafeHwnd(),
                    NULL,MIXER_OBJECTF_MIXER | CALLBACK_WINDOW);
mxl.cbStruct = sizeof(MIXERLINE);
mxl.dwComponentType =MIXERLINE_COMPONENTTYPE_DST_SPEAKERS;
mixerGetLineInfo((HMIXEROBJ)m_hMixer,&mxl,
                    MIXER_OBJECTF_HMIXER|MIXER_GETLINEINFOF_COMPONENTTYPE);
mxlc.cbStruct=sizeof(MIXERLINECONTROLS);
mxlc.dwLineID=mxl.dwLineID;
mxlc.dwControlType=MIXERCONTROL_CONTROLTYPE_VOLUME;
mxlc.cControls=1;//一般为1
mxlc.cbmxctrl=sizeof(MIXERCONTROL);
mxlc.pamxctrl=&mxc;
mixerGetLineControls((HMIXEROBJ)m_hMixer,&mxlc,
                    MIXER_OBJECTF_HMIXER|MIXER_GETLINECONTROLSF_ONEBYTYPE);
m_controlid=mxc.dwControlID;
m_control.SetRange(mxc.Bounds.lMinimum,mxc.Bounds.lMaximum);
MIXERCONTROLDETAILS_SIGNED mxcdVolume;
MIXERCONTROLDETAILS mxcd;
mxcd.cbStruct = sizeof(MIXERCONTROLDETAILS);
mxcd.dwControlID = mxc.dwControlID;
mxcd.cChannels = 1;
mxcd.cMultipleItems = 0;
mxcd.cbDetails = sizeof(MIXERCONTROLDETAILS_SIGNED);
mxcd.paDetails = &mxcdVolume;
mixerGetControlDetails((HMIXEROBJ)m_hMixer,&mxcd,
                    MIXER_OBJECTF_HMIXER|MIXER_GETCONTROLDETAILSF_VALUE);
m_control.SetPos(mxcdVolume.lValue);
代码解释:先由mixerOpen打开混音器设备并返回设备句柄m_hMixer。第二个参数0是混音器编号,如果你的电脑有多个混音器那么编号从零开始依次递加。接下来用了一个mixerGetLineInfo函数主要是为了获取音频输出线路的可调范围和ID号,就是mxl.dwLineID。接下来mixerGetLineControls函数用前面得到的dwControlID来获得对混音器的控制。最后的mixerGetControlDetails获取具体音量,通过SetPos显示在窗口中。
接下来为滑杆控件添加事件处理函数,滑杆移动时触发WM_HSCROLL消息,该函数不能通过类向导添加,需要手动添加,在头文件Volume.h中添加类成员函数
afx_msg void OnHScroll(UINT nSBCode, UINT nPos, CScrollBar* pScrollBar);
volume.cpp中添加消息映射,就是在BEGIN_MESSAGE_MAP(CVolumeDlg, CDialog)END_MESSAGE_MAP()中间加上一句
ON_WM_HSCROLL()
并且在后面加上函数定义
void CVolumeDlg::OnHScroll(UINT nSBCode, UINT nPos, CScrollBar* pScrollBar)
{
                    DWORD val;
                    val=((CSliderCtrl*)pScrollBar)->GetPos();
                    MIXERCONTROLDETAILS_UNSIGNED mxcdVolume = {val};
                    MIXERCONTROLDETAILS mxcd;
                    mxcd.cbStruct = sizeof(MIXERCONTROLDETAILS);
                    mxcd.dwControlID = m_controlid;
                    mxcd.cChannels = 1;
                    mxcd.cMultipleItems = 0;
                    mxcd.cbDetails = sizeof(MIXERCONTROLDETAILS_UNSIGNED);
                    mxcd.paDetails = &mxcdVolume;
                   
                    mixerSetControlDetails((HMIXEROBJ)m_hMixer,&mxcd,
                         MIXER_OBJECTF_HMIXER|MIXER_SETCONTROLDETAILSF_VALUE);
                    CDialog::OnHScroll(nSBCode, nPos, pScrollBar);
}
代码解释:此部分在手动移动滑杆时触发。先通过GetPos获得滑块位置,然后用mixerSetControlDetails来调整系统音量。
4.当系统音量改变时我们的程序也应该跟着变动。系统会在音量改变时向程序发送MM_MIXM_CONTROL_CHANGE消息,我们则在程序中添加对此消息的响应。类似于上一步添加WM_HSCROLL消息,也是在头文件加入声明
afx_msg LONG OnMixerCtrlChange(UINT wParam, LONG lParam);
然后在cpp文件中建立消息映射
ON_MESSAGE(MM_MIXM_CONTROL_CHANGE,OnMixerCtrlChange)
并且添加函数原型定义
LONG CVolumeDlg::OnMixerCtrlChange(UINT wParam, LONG lParam)
{
                    if ((HMIXER)wParam == m_hMixer && (DWORD)lParam ==m_controlid)
                    {
                         MIXERCONTROLDETAILS_UNSIGNED mxcdVolume;
                         MIXERCONTROLDETAILS mxcd;
                         mxcd.cbStruct = sizeof(MIXERCONTROLDETAILS);
                         mxcd.dwControlID = m_controlid;
                         mxcd.cChannels = 1;
                         mxcd.cMultipleItems = 0;
                         mxcd.cbDetails = sizeof(MIXERCONTROLDETAILS_UNSIGNED);
                         mxcd.paDetails = &mxcdVolume;
                        
                         mixerGetControlDetails((HMIXEROBJ)m_hMixer,&mxcd,
                                MIXER_OBJECTF_HMIXER|MIXER_GETCONTROLDETAILSF_VALUE);
                        
                         m_control.SetPos(mxcdVolume.dwValue);
                    }
                    return 0L;
}
至此整个音量控制的功能就算是具备了,编译连接后就可以通过滑块控制音量了。

热键控制

接下来就是加入热键控制机制,这样我们就可以在任何时候通过键盘来调整音量,不必再移动滑块了。注册系统热键的API函数是RegisterHotKeyUnregisterHotKey
BOOL WINAPI RegisterHotKey(
  __in_opt  HWND hWnd,
  __in      int id,
  __in      UINT fsModifiers,
  __in      UINT vk
);

BOOL WINAPI UnregisterHotKey(
  __in_opt  HWND hWnd,
  __in      int id
);
首先在cpp文件中的对话框初始化部分OnInitDialog加入
RegisterHotKey(GetSafeHwnd(),1688,MOD_ALT|MOD_CONTROL,VK_UP);
RegisterHotKey(GetSafeHwnd(),1689,MOD_ALT|MOD_CONTROL,VK_DOWN);
第一个参数是注册热键的窗口句柄;第二个是热键ID号,不能和已有的重复;第三个和第四个组成一套组合键,表示按Ctrl+Alt+↑或Ctrl+Alt+↓。
热键注册好了以后就要给它们添加响应函数,跟前面添加消息响应函数类似。在头文件中添加函数声明
afx_msg LONG OnHotKey(WPARAM wParam,LPARAM lParam);
cpp文件中添加消息映射
ON_MESSAGE(WM_HOTKEY,OnHotKey)
以及函数原型
LONG CVolumeDlg::OnHotKey(WPARAM wParam,LPARAM lParam)
{
                    if(wParam==1688)
                    {
                         int val;
                         val=m_control.GetPos();
                         val+=(m_control.GetRangeMax()-m_control.GetRangeMin())/10;
                         if(val>=m_control.GetRangeMax())
                                val=m_control.GetRangeMax();
                         MIXERCONTROLDETAILS_UNSIGNED mxcdVolume = {val};
                         MIXERCONTROLDETAILS mxcd;
                         mxcd.cbStruct = sizeof(MIXERCONTROLDETAILS);
                         mxcd.dwControlID = m_controlid;
                         mxcd.cChannels = 1;
                         mxcd.cMultipleItems = 0;
                         mxcd.cbDetails = sizeof(MIXERCONTROLDETAILS_UNSIGNED);
                         mxcd.paDetails = &mxcdVolume;            
                         mixerSetControlDetails((HMIXEROBJ)m_hMixer,&mxcd,
                                MIXER_OBJECTF_HMIXER|MIXER_SETCONTROLDETAILSF_VALUE);
                    }
                    if(wParam==1689)
                    {
                         int val;
                         val=m_control.GetPos();
                         val-=(m_control.GetRangeMax()-m_control.GetRangeMin())/10;
                         if(val<=m_control.GetRangeMin())
                                val=m_control.GetRangeMin();
                         MIXERCONTROLDETAILS_UNSIGNED mxcdVolume = {val};
                         MIXERCONTROLDETAILS mxcd;
                         mxcd.cbStruct = sizeof(MIXERCONTROLDETAILS);
                         mxcd.dwControlID = m_controlid;
                         mxcd.cChannels = 1;
                         mxcd.cMultipleItems = 0;
                         mxcd.cbDetails = sizeof(MIXERCONTROLDETAILS_UNSIGNED);
                         mxcd.paDetails = &mxcdVolume;     
                         mixerSetControlDetails((HMIXEROBJ)m_hMixer,&mxcd,
                                MIXER_OBJECTF_HMIXER|MIXER_SETCONTROLDETAILSF_VALUE);
                    }
                    return 0;
}
此部分在热键被按下时触发,首先条件语句判断热键是↑还是↓,然后获取当前音量值并给它加减音量范围的十分之一,判断未超出可调范围,最后用mixerSetControlDetails设定系统音量。
最后不要忘记在程序销毁时注销掉热键。在资源窗口的对话框上点右键,类向导,然后选择给CVolumeDlgWM_DESTROY消息添加函数,并且编辑代码。
OnDestroy函数中加入两行反注册函数
UnregisterHotKey(GetSafeHwnd(), 1688);
UnregisterHotKey(GetSafeHwnd(), 1689);
编译运行试试,按Ctrl+Alt+↑或Ctrl+Alt+↓看看音量是否有增减?如果显示注册热键失败,可能是热键冲突,就需要换一个组合试试。

隐藏窗口到托盘

经过之前的实验,我们想要的功能都基本具备了,但是这样一个小程序却要占用任务栏很大一块空间,看起来很不爽,把它放到托盘中去,需要的时候再叫出来这样会更方便。
windows用来控制托盘图标的APIShell_NotifyIcon
BOOL Shell_NotifyIcon(
  __in  DWORD dwMessage,
  __in  PNOTIFYICONDATA lpdata
);
我们首先要把图标加入到托盘中去。在头文件中加入成员变量
NOTIFYICONDATA nd;
OnInitDialog中加入
nd.cbSize = sizeof (NOTIFYICONDATA);
nd.hWnd = m_hWnd;
nd.uID = IDR_MAINFRAME;
nd.uFlags = NIF_ICON|NIF_MESSAGE|NIF_TIP;
nd.uCallbackMessage= MYM_NOTIFYICON;
nd.hIcon = m_hIcon;
strcpy(nd.szTip, "音量控制");
Shell_NotifyIcon(NIM_ADD, &nd);
然后添加对托盘的操作,让它在最小化时隐藏窗口,在双击时弹出窗口,这需要我们重载windowProc函数。先在头文件中加入
#define MYM_NOTIFYICON WM_USER+1
然后在类视图中右键CVolume类,增加虚函数,然后选windowProc增加并编辑。添加如下代码。
switch(message)
{
case MYM_NOTIFYICON:        //如果是用户定义的消息
                    if(lParam==WM_LBUTTONDBLCLK)   //鼠标双击时主窗口出现
                    AfxGetApp()->m_pMainWnd->ShowWindow(SW_SHOW);
                    break;
case WM_SYSCOMMAND:                  //如果是系统消息
                    if(wParam==SC_MINIMIZE)             //接收到最小化消息时主窗口隐藏
                    {
                    AfxGetApp()->m_pMainWnd->ShowWindow(SW_HIDE);
                         return 0;
                    }
                    break;
}                  
最后也不要忘了在程序销毁时,清除掉托盘图标。在OnDestroy中加入
nd.cbSize = sizeof (NOTIFYICONDATA);
nd.hWnd = m_hWnd;
nd.uID = IDR_MAINFRAME;
nd.uFlags = NIF_ICON|NIF_MESSAGE|NIF_TIP;
nd.uCallbackMessage = MYM_NOTIFYICON;
nd.hIcon = m_hIcon;
Shell_NotifyIcon(NIM_DELETE, &nd);
编译运行一下,点最小化是不是就隐藏到系统托盘中去了,再双击托盘窗口就又出来了。哈哈,大功告成,通过替换IDR_MAINFRAME还可以改变托盘图标的样子,我把它换成了我的专属LOGO,很漂亮。可能有人会问怎么对话框没有最小化按钮?很简单,在资源窗口的对话框上右键,属性,把最小化框选上就有了。

后记

这篇东西对高手来讲没什么技术含量,但是也许有像我一样的新手会从中有所收获,第一次写这么长的文章,很感慨。同时也感谢那些乐于共享自己知识的高手,是他们的无私才给了我们通过网络学习的机会。
关于系统托盘VC知识库(VCKBASE)上有篇文章讲得很好,很值得一看,地址是:http://www.vckbase.com/document/viewdoc/?id=492
最后欢迎批评交流,转帖请注明出处。

阅读全文...