其实就是怼一个API,一个相对比较底层的API
Private Declare Function SoftModalMessageBox Lib "user32" (pMsgBoxParams As MSGBOXDATA) As Long
这个API当然也不是最底层的API,且这个API并没有在微软官方文档中出现,其实逆向大神自行逆向出来的。
所以以下内容在MSDN是找不到的
首先看这个API的传入参数:
Private Type MSGBOXPARAMS cbSize As Long hWndOwner As Long hInstance As Long lpszText As Long lpszCaption As Long dwStyle As Long lpszIcon As Long dwContextHelpId As Long lpfnMsgBoxCallback As Long dwLanguageId As Long End Type Public Type MSGBOXDATA_Earlier 'Vista及以前的版本 params As MSGBOXPARAMS pwndOwner As Long wLanguageId As Long pidButton As Long ppszButtonText As Long cButtons As Long DefButton As Long CancelID As Long Timeout As Long End Type Public Type MSGBOXDATA_Later 'Win7及以后的版本 params As MSGBOXPARAMS unk1 As Long pwndOwner As Long wLanguageId As Long pidButton As Long ppszButtonText As Long cButtons As Long DefButton As Long CancelID As Long Timeout As Long unk2 As Long unk3(4) As Long End Type
API传入的参数在低版本(Vista及以前)和高版本中不一样,我们在声明API时可以分别声明,像下面这样,分别声明:
Private Declare Function SoftModalMessageBox_Earlier Lib "user32" Alias "SoftModalMessageBox" (pMsgBoxParams As MSGBOXDATA_Earlier) As Long Private Declare Function SoftModalMessageBox_Later Lib "user32" Alias "SoftModalMessageBox" (pMsgBoxParams As MSGBOXDATA_Later) As Long
当然也可以偷个懒,直接声明为Any:
Private Declare Function SoftModalMessageBox Lib "user32" (pMsgBoxParams As Any) As Long
只是声明为Any后,传入参数的类型就需要特别注意了,由于是高阶应用,上述对高低版本分别声明的应用我这里就不讲了,请有兴趣的自己看完自行娱乐。
观察以下MSGBOXDATA这个结构体,里面全部是Long,全部是4字节对齐的,这简直是天助我也,这不就是为了让我们用数组来代替结构体吗?O(∩_∩)O哈哈~haha
进入正题,我们封装一个函数,解决以下问题:
1、自定义按钮个数、按钮文本、自动退出时间
2、Windows版本兼容
首先解决版本兼容问题,意味着我们需要识别不同的Windows版本,这个经过再三测试,发现怼的API实际上不靠谱,直接用wmi最可靠
Public Function WindowsVersion() As Long ' Windows 10 100 ' Windows 8.1 63 ' Windows 8.0 62 ' Windows 7 61 ' Windows Vista 60 ' Windows XP 51 ' Windows 2000 50 Dim SystemSet As Object Dim System As Object Dim S As String Dim i As Long Set SystemSet = GetObject("winmgmts:").InstancesOf("Win32_OperatingSystem") For Each System In SystemSet S = System.Version Next i = InStr(S, ".") S = Left$(S, i - 1) & Mid$(S, i + 1) i = InStr(S, ".") If i Then S = Left$(S, i - 1) WindowsVersion = Val(S) End Function
获取到不同的版本之后,存储到一个全局变量里:
Public Enum WindowsVersion Earlier Later End Enum Public WinVersion As WindowsVersion
这里为啥无聊定义一个枚举,请自行体会,全部都说完了,就没意思了
然后就可以根据实际情况定义不同的参数(我这里肯定是打算定义数组的)
为了代码可读性,我当然不会直接用数字,还是打算用枚举代替结构体成员位置:
Public Enum eMsgParams cbSize hWndOwner hInstance lpszText lpszCaption dwStyle lpszIcon dwContextHelpId lpfnMsgBoxCallback '回调 dwLanguageId pwndOwner '从这里开始高版本结构体+1 wLanguageId pidButton ppszButtonText cButtons DefButton CancelID Timeout unk2_2 = 21 '回调开始位置 unk2_3 unk2_4 End Enum
枚举的特性,印象中以前好像讲过,cbSize=0,不用书写0
hWndOwner=1,以此类推
直到重新定义了unk2_2=21,流水号重新计算
距离主函数出来之前,先发一个Split函数,比VBA库里自带的Split函数快3倍
Public Sub mySplit(Expression$, ResultSplit$(), Optional Delimiter$ = " ") ' By Chris Lucas, cdl1051@earthlink.net, 20011208 Dim C&, SLen&, DelLen&, Tmp&, Results&() SLen = LenB(Expression) \ 2 DelLen = LenB(Delimiter) \ 2 ' Bail if we were passed an empty delimiter or an empty expression If SLen = 0 Or DelLen = 0 Then ReDim Preserve ResultSplit(0 To 0) ResultSplit(0) = Expression Exit Sub End If ' Count delimiters and remember their positions ReDim Preserve Results(0 To SLen) Tmp = InStr(Expression, Delimiter) Do While Tmp Results(C) = Tmp C = C + 1 Tmp = InStr(Results(C - 1) + 1, Expression, Delimiter) Loop ' Size our return array ReDim Preserve ResultSplit(0 To C) ' Populate the array If C = 0 Then ' lazy man's call ResultSplit(0) = Expression Else ' typical call ResultSplit(0) = Left$(Expression, Results(0) - 1) For C = 0 To C - 2 ResultSplit(C + 1) = Mid$(Expression, _ Results(C) + DelLen, _ Results(C + 1) - Results(C) - DelLen) Next C ResultSplit(C + 1) = Right$(Expression, SLen - Results(C) - DelLen + 1) End If End Sub
好,现在就封装目标函数:
Public Function MsgBoxEx(arrText$, strTitle$, strPrompt$, Optional ByVal DefButton As Long = 0, Optional ByVal Timeout As Long = -1, Optional ByVal CancelID As Boolean = True) Dim md_msg() As Long Dim lBtn() As Long Dim sTxt() As String Dim i&, U& If WindowsVersion() < 61 Then WinVersion = Earlier ReDim md_msg(17) As Long Else WinVersion = Later ReDim md_msg(24) As Long End If 'arrText = VBA.Split(sTxt, "|") '效率极低 mySplit arrText, sTxt, "|" '不用VBA.Split,效率太低 U = UBound(sTxt) ReDim lBtn(U) As Long For i = 0 To U '这里我的按钮定义规则是倒序,最后一个按钮是2 lBtn(i) = U - i + 2 '5 4 3 2,因为必须有2,最后一个按钮为:退出2 Next i md_msg(eMsgParams.cbSize) = 40 '固定参数 md_msg(eMsgParams.lpszText) = StrPtr(strPrompt) '提示文字 md_msg(eMsgParams.lpszCaption) = StrPtr(strTitle) '标题 md_msg(eMsgParams.dwStyle) = vbYesNo '很奇怪,必须设置这个参数,虽然没用 md_msg(eMsgParams.DefButton + WinVersion) = DefButton - 1 '不以arr的上下标为计数标准,总是从0开始,强行改为从1开始 md_msg(eMsgParams.CancelID + WinVersion) = CancelID '点击关闭按钮时,只能返回2,arrButtonID里必须有2 md_msg(eMsgParams.cButtons + WinVersion) = U + 1 '按钮数 md_msg(eMsgParams.pidButton + WinVersion) = VarPtr(lBtn(0)) 'ButtonID md_msg(eMsgParams.ppszButtonText + WinVersion) = VarPtr(sTxt(0)) 'Button文字 md_msg(eMsgParams.Timeout + WinVersion) = Timeout '超时时间,默认为无限长 MsgBoxEx = SoftModalMessageBox(md_msg(0)) End Function
其实呢eMsgParams这个枚举是可有可无的
可有,是由于对于代码可读性和可维护性来说,这个必须存在
可无,如果你不打算维护这个函数,那么完全可以将这个函数改成下面这样
Public Function MsgBoxEx(arrText$, strTitle$, strPrompt$, Optional ByVal DefButton As Long = 0, Optional ByVal Timeout As Long = -1, Optional ByVal CancelID As Boolean = True) Dim md_msg() As Long Dim lBtn() As Long Dim sTxt() As String Dim i&, U& If WindowsVersion() < 61 Then WinVersion = Earlier ReDim md_msg(17) As Long Else WinVersion = Later ReDim md_msg(24) As Long End If 'arrText = VBA.Split(sTxt, "|") '效率极低 mySplit arrText, sTxt, "|" '不用VBA.Split,效率太低 U = UBound(sTxt) ReDim lBtn(U) As Long For i = 0 To U lBtn(i) = U - i + 2 '5 4 3 2,最后一个按钮为:退出2 Next i md_msg(0) = 40 md_msg(3) = StrPtr(strPrompt) md_msg(4) = StrPtr(strTitle) md_msg(5) = vbYesNo '很奇怪,必须设置这个参数,虽然没用 md_msg(15 + WinVersion) = DefButton - 1 '不以arr的上下标为计数标准,总是从0开始,强行改为从1开始 md_msg(16 + WinVersion) = CancelID '点击关闭按钮时,只能返回2,arrButtonID里必须有2 md_msg(14 + WinVersion) = U + 1 '按钮数 md_msg(12 + WinVersion) = VarPtr(lBtn(0)) 'ButtonID md_msg(13 + WinVersion) = VarPtr(sTxt(0)) 'Button文字 md_msg(17 + WinVersion) = Timeout '超时时间,默认为无限长 MsgBoxEx = SoftModalMessageBox(md_msg(0)) End Function
调用示例:
MsgBoxEx “我的按钮|你的按钮|她的按钮|取消”, “一起摇摆”, “女士优先,默认选她”, 3, , False
不显示关闭叉叉,不设定第3个参数,等待用户选择,直接按Enter,返回3
测试1
MsgBoxEx “我的按钮|你的按钮|她的按钮|取消”, “一起摇摆”, “女士优先,默认选她”, 3, 6000, True
显示关闭叉叉,6000ms=6s后自动退出,自动退出时返回32000,点关闭和点取消都返回2
测试2
以上,如果在应用时可以这样写:
Dim var& var = MsgBoxEx("我的按钮|你的按钮|她的按钮|取消", "一起摇摆", "女士优先,默认选她", 3, 6000, True) Select Case var Case 32000 '未选择,自动退出 Case 2 '取消 Case 3 '她的按钮 Case 4 '你的按钮 Case 5 '我的按钮 End Select