音量控制程序开发历程
需求激发灵感
最近玩游戏,激烈的游戏音效常常太过震撼,为此经常要切换音量。但每次都从游戏画面切回到桌面不仅麻烦,对于我这台破电脑又卡得不行。回想以前在笔记本上玩游戏调整音量都用快捷键(笔记本的特殊功能键),自然就有了这么个想法,用快捷键快速调节音量。
想法有了,接下来就定了个小开发计划。整个程序大体上有这么几个重点:
1.控制音量调节的API函数。
2.注册系统热键。能用Ctrl+Alt+↑和Ctrl+Alt+↓来控制音量增减。
3.希望可以在系统托盘里,不占用任务栏。
寻找控制音量的API的曲折过程
音量控制对高手来讲应该不算什么,但我头一次接触多媒体控制还是到处碰壁。首先Google一下,再MSDN,发现可以用来控制音量的还真不少。Directshow、waveOut一族函数里的waveOutSetVolume,aux一族里面的auxSetVolume等。
于是随便选个auxSetVolume来试试吧。根据MSDN里面的函数说明,很快完成了一段增减音量的代码,可是不论我怎么调试,都不能控制音量,查看返回值是说没有合适的设备。又上网找原因,在CSDN里有一段问答也遇到了和我一样的状况,有高手说是因为此函数和集成声卡之间有矛盾,集成声卡常常会找不到设备。郁闷!竟然会这样。我一想,现在有几个电脑不是集成声卡?既然它可能会有问题(我也不知道到底是不是这个问题),算了,那么多函数可选,我再换一个吧。
因为看CSDN里讨论说directshow和waveOutSetVolume都比较适合针对所编写的程序调节音量,也就是只调整一个程序的音量其他程序不变,而我想要的是调整整个系统音量。继续在网上寻找,最后在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指定调用实例句柄,这个参数我不太明白。
fdwOpen与uMxId配套使用,指示uMxId是什么标识类型。
2.mixerGetLineControls获得关联音频线路的一个或多个控制器。
MMRESULT mixerGetLineControls(
HMIXEROBJ hmxobj,
LPMIXERLINECONTROLS pmxlc,
DWORD fdwControls
);
pmxlc是一个MIXERLINECONTROLS结构的指针,里面是与要控制的音频线路有关的信息。
fdwControls对前两个参数做出了一些标示。
3.mixerGetControlDetails获取指定控制器的详细信息。
MMRESULT mixerGetControlDetails(
HMIXEROBJ hmxobj,
LPMIXERCONTROLDETAILS pmxcd,
DWORD fdwDetails
);
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,用向导建立一个叫Volume的MFC对话框工程,不用改其他设置项。
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函数是RegisterHotKey和UnregisterHotKey。
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设定系统音量。
最后不要忘记在程序销毁时注销掉热键。在资源窗口的对话框上点右键,类向导,然后选择给CVolumeDlg的WM_DESTROY消息添加函数,并且编辑代码。
在OnDestroy函数中加入两行反注册函数
UnregisterHotKey(GetSafeHwnd(), 1688);
UnregisterHotKey(GetSafeHwnd(), 1689);
编译运行试试,按Ctrl+Alt+↑或Ctrl+Alt+↓看看音量是否有增减?如果显示注册热键失败,可能是热键冲突,就需要换一个组合试试。
隐藏窗口到托盘
经过之前的实验,我们想要的功能都基本具备了,但是这样一个小程序却要占用任务栏很大一块空间,看起来很不爽,把它放到托盘中去,需要的时候再叫出来这样会更方便。
windows用来控制托盘图标的API:Shell_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,很漂亮。可能有人会问怎么对话框没有最小化按钮?很简单,在资源窗口的对话框上右键,属性,把最小化框选上就有了。
后记
这篇东西对高手来讲没什么技术含量,但是也许有像我一样的新手会从中有所收获,第一次写这么长的文章,很感慨。同时也感谢那些乐于共享自己知识的高手,是他们的无私才给了我们通过网络学习的机会。
最后欢迎批评交流,转帖请注明出处。
没有评论:
发表评论