VBA中的API定时器 – 如何使安全

我在各种地方读到,API定时器在VBA中有风险,如果在定时器运行时编辑一个单元格,将导致Excel崩溃。 但是由于Jordan Goldmeier的http://optionexplicitvba.wordpress.com这个代码似乎没有这个问题。 它使用计时器淡出popup窗口,在淡入淡出时,我可以单击并在单元格和公式栏中input文本,而不会出现任何问题。

什么时候API计时器安全,什么时候不是? 有一些具体的原则可以帮助我理解吗? 什么是崩溃的机制:究竟发生了什么事情,使Excel崩溃?

Option Explicit Public Declare Function SetTimer Lib "user32" ( _ ByVal HWnd As Long, _ ByVal nIDEvent As Long, _ ByVal uElapse As Long, _ ByVal lpTimerFunc As Long) As Long Public Declare Function KillTimer Lib "user32" ( _ ByVal HWnd As Long, _ ByVal nIDEvent As Long) As Long Public TimerID As Long Public TimerSeconds As Single Public bTimerEnabled As Boolean Public iCounter As Integer Public bComplete As Boolean Public EventType As Integer Public Sub Reset() With Sheet1.Shapes("MyLabel") .Fill.Transparency = 0 .Line.Transparency = 0 .TextFrame2.TextRange.Font.Fill.ForeColor.RGB = RGB(0, 0, 0) End With Sheet1.Shapes("MyLabel").Visible = msoTrue End Sub Sub StartTimer() iCounter = 1 Reset TimerID = SetTimer(0&, 0&, 0.05 * 1000&, AddressOf TimerProc) End Sub Sub EndTimer() KillTimer 0&, TimerID bTimerEnabled = False bComplete = True End Sub Sub TimerProc(ByVal HWnd As Long, ByVal uMsg As Long, _ ByVal nIDEvent As Long, ByVal dwTimer As Long) On Error Resume Next Debug.Print iCounter If iCounter > 50 Then With Sheet1.Shapes("MyLabel") .Fill.Transparency = (iCounter - 50) / 50 .Line.Transparency = (iCounter - 50) / 50 .TextFrame2.TextRange.Font.Fill.ForeColor.RGB = _ RGB((iCounter - 50) / 50 * 224, _ (iCounter - 50) / 50 * 224, _ (iCounter - 50) / 50 * 224) End With End If If iCounter > 100 Then Sheet1.Shapes("MyLabel").Visible = msoFalse EndTimer End If iCounter = iCounter + 1 End Sub Public Function ShowPopup(index As Integer) Sheet1.Range("Hotzone.Index").Value = index iCounter = 1 If bTimerEnabled = False Then StartTimer bTimerEnabled = True Reset Else Reset End If With Sheet1.Shapes("MyLabel") .Left = Sheet1.Range("Hotzones").Cells(index, 1).Left + _ Sheet1.Range("Hotzones").Cells(index, 1).Width .Top = Sheet1.Range("Hotzones").Cells(index, 1).Top - _ (.Height / 2) End With Sheet1.Range("a4:a6").Cells(index, 1).Value = index End Function 

@CoolBlue: 崩溃的机制是什么:究竟发生了什么事情使Excel崩溃?

我可以给你一个扩展的Siddarth Rout的答案,但不是一个完整的解释。

API调用不是VBA:它们存在于VBA的error handling程序之外,当事情出错时,它们将不执行任何操作,或者调用内存中不存在的资源,或尝试读取(或写入)到外部内存为Excel.exe指定的内存空间

当发生这种情况时,操作系统将进入并closures应用程序。 我们曾经把这称为“一般保护错误”,这仍然是对过程的有用描述。

现在了解一些细节。

当你在VBA中调用一个函数时, 只需要写出名字 – 我们称之为“CheckMyFile()” – 这就是你在VBA中需要知道的一切。 如果没有任何东西叫做“CheckMyFile”来调用,或者声明了你的调用看不见的地方,编译器或者运行时引擎会在编译和运行之前以断点或者警告的forms产生一个错误。

在幕后,有一个与string“CheckMyFile”相关联的数字地址:我正在简化一点,但是我们把这个地址称为函数指针 – 跟随这个地址,我们得到一个存储定义的结构化内存块的函数参数,它们的存储值的空间,以及在这些地址之后的地址将这些参数引导到为执行VBA而创build的function结构中,并将值返回给函数输出的地址。

事情可能会出错,VBA做了很多工作,以确保当出现问题时,所有这些优雅地折叠起来。

如果你给那个不是VBA的函数指针 – 一个外部的应用程序或者(比如说) 一个API定时器调用 – 你的函数仍然可以被调用,它仍然可以运行,并且一切都会工作。

但是最好在这个指针后面有一个有效的函数。

如果没有,外部应用程序将调用它自己的error handling程序,它们不会像VBA一样宽容。

如果Excel和VBA处于“忙碌”状态,或者在尝试使用该函数指针时不可用,则可能只是放弃呼叫而不做任何事情:您可能是幸运的,就是这样一次。 但是它可能会在Excel.exe进程中调用操作系统的愤怒。

如果调用导致错误,并且该错误不是由您的代码处理的,VBA会将错误提交给调用者,而由于调用者不是VBA,它可能无法处理:它会要求操作系统的“帮助”。

如果是一个API调用,它是为那些被认为在调用代码中放置error handling和应急pipe理的开发人员编写的。

这些假设是:

  1. 这个指针后面肯定会有一个有效的函数。
  2. 当它被调用时它肯定是可用的;
  3. …这将不会给调用者带来任何错误。

通过API调用,调用者操作系统,它对检测到错误的响应将closures您。

这是一个非常简单的过程纲要 – 一个“为什么”,而不是“什么”的解释。

没有过多的简单说明,完整的解释是针对C ++开发者的。 如果你真的想要深入的答案,你必须学会​​用指针编程; 而且必须熟悉内存分配,exception,坏指针的后果以及操作系统用于pipe理正在运行的应用程序以及检测无效操作的机制的概念和实践。

VBA的存在是为了保护您免受这些知识的束缚并简化编写应用程序的任务。

我在不同的地方读到,API定时器在VBA中是有风险的

那么这个陈述应该是I read in various places that API timers are risky吗? 而我之所以说这是因为这些API可以在VB6 / VBA / VB.Net等中使用。

那么他们有风险吗? 是的,但是紧紧的绳索走路也是如此。 一个错误的举动,你完成了。 而不是只有SetTimer API但几乎所有的API。

我在2009年创build了一个示例,它使用SetTimer API在Excel中创build启animation面。 这是LINK 。

现在,如果你提取的文件,你直接打开Excel文件,那么你会看到,Excel崩溃。 要使其工作,请按SHIFT键,然后打开Excel以使macros不运行。 接下来改变图像的path。 新path将是您从zip文件中提取的图像的path。 一旦你改变path,只需保存并closures文件。 下次运行时,Excel不会崩溃。

这是Excel文件中的代码

 Public Declare Function SetTimer Lib "user32" ( _ ByVal HWnd As Long, ByVal nIDEvent As Long, _ ByVal uElapse As Long, ByVal lpTimerFunc As Long) As Long Public Declare Function KillTimer Lib "user32" ( _ ByVal HWnd As Long, ByVal nIDEvent As Long) As Long Public TimerID As Long, TimerSeconds As Single, tim As Boolean Dim Counter As Long Sub StartTimer() '~~ Set the timer. TimerSeconds = 1 TimerID = SetTimer(0&, 0&, TimerSeconds * 1000&, AddressOf TimerProc) End Sub Sub EndTimer() On Error Resume Next KillTimer 0&, TimerID End Sub Sub TimerProc(ByVal HWnd As Long, ByVal uMsg As Long, _ ByVal nIDEvent As Long, ByVal dwTimer As Long) If tim = False Then UserForm1.Image1.Picture = LoadPicture("C:\temp\1.bmp") tim = True Else UserForm1.Image1.Picture = LoadPicture("C:\temp\2.bmp") tim = False End If Counter = Counter + 1 If Counter = 10 Then EndTimer Unload UserForm1 End If End Sub 

什么时候API计时器安全,什么时候不是? 有一些宽泛的原则可以帮助我理解吗?

所以这一切归结为一个事实。 你的代码有多健壮 。 如果你的代码处理每个场景,那么SetTimer API或者其他任何API都不会失败。

@CoolBlue我写了你上面张贴的代码。 确实,API至less和普通的代码相比,可以发生不可预测的变化。 然而,如果你的代码足够强大(从上面的@Siddharth Rout的评论),那么它不再是一个预测。 事实上,这种不可预测性来自于开发过程。

例如,在上面创build的翻转popup窗口的第一个迭代中,我偶然在IF语句中键入了KillTimer。 基本上,EndTimer现在存在,我已经写了KillTimer。 我没有想到这一点。 我知道我有一个会结束计时器的程序,但是我一时间把EndTimer和KillTimer混淆了。

所以这就是为什么我提出这个问题:通常,当你在Excel中犯这种types的错误,你会收到一个运行时错误。 但是,由于您正在使用API​​,因此只会收到非法错误,整个Excel应用程序将无法响应并退出。 所以,如果你在启动计时器之前没有保存,你会失去所有的东西(这实际上是第一次发生的事情)。 更糟糕的是,因为您没有收到运行时错误,您不会立即知道哪一行导致错误。 在这样的项目中,您必须预料到几个非法错误(以及随后重新载入Excel)来诊断错误。 有时候,这可能是一个痛苦的过程。 但是,这是一个典型的debugging情况,发生在使用API​​进行工作时。 错误没有被直接强调 – 而且非法错误似乎是随机发生的,这就是为什么许多人将API描述为不可预测和有风险的原因。

但只要你能find和诊断错误,他们就没有风险。 在我上面的代码中,我相信我已经创build了一个基本封闭的表单解决scheme。 有没有什么错误可以引入,以后会导致一个问题。 (不要把这当成是一个挑战的人)

只是给你一些具体的指导方针,以避免错误:

  • 如果你启动一个计时器,确保你以后杀死它。 如果您在计时器死亡之前有一个Excel运行时错误,它可能会永远持续下去并吃掉你的记忆。 每次调用TimerProc时,使用控制台(Debug.Print)写一行。 如果在你的代码完成执行之后它仍然在你的控制台中滴答滴答,那么你有一个失控的计时器。 退出Excel并在发生这种情况时再回来。
  • 不要使用多个定时器。 使用一个定时器来处理多个时间元素。
  • 不要在没有杀死旧的计时器的情况下启动新的计时器。
  • 最重要的是:testing你的朋友的电脑,以确保它可以在不同的平台上工作。

另外,要明确一点: 使用API​​定时器和同时编辑单元没有问题。 定时器没有任何东西可以阻止你编辑表单上的任何东西。

VBA中Windows定时器API的指针安全和64位声明:

按照承诺,这里是Timer API的32位和64位API声明,使用LongLong和安全指针types:

选项显式
选件专用模块 
#如果VBA7和Win64那么64位窗口下的64位Excel '使用LongLong和LongPtr
私人声明PtrSafe函数SetTimer Lib“user32”_ (ByVal hwnd As LongPtr,_ ByVal nIDEvent As LongPtr,_ ByVal uElapse As LongLong,_ ByVal lpTimerFunc As LongPtr _ )作为LongLong
公开声明PtrSafe函数KillTimer Lib“user32”_ (ByVal hwnd As LongPtr,_ ByVal nIDEvent As LongPtr _ )作为LongLong 公开TimerID作为LongPtr

#ElseIf VBA7然后'在所有环境中的64位Excel '只使用LongPtr,LongLong不可用
私人声明PtrSafe函数SetTimer Lib“user32”_ (ByVal hwnd As LongPtr,_ ByVal nIDEvent As Long,_ ByVal uElapse As Long,_ ByVal lpTimerFunc As LongPtr)As LongPtr
私人声明PtrSafe函数KillTimer Lib“user32”_ (ByVal hwnd As LongPtr,_ ByVal nIDEvent As Long)一样长
公开TimerID作为LongPtr
#Else'32位Excel
Private Declare Function SetTimer Lib“user32”_ (ByVal hwnd As Long,_ ByVal nIDEvent As Long,_ ByVal uElapse As Long,_ ByVal lpTimerFunc As Long)As Long
公共声明函数KillTimer Lib“user32”_ (ByVal hwnd As Long,_ ByVal nIDEvent As Long)一样长
公共TimerID只要
#万一

'调用定时器为: SetTimer 0&,0&,lngMilliseconds,AddressOf TimerProc

#如果VBA7和Win64那么64位窗口下的64位Excel使用LongLong和LongPtr '请注意,wMsg总是WM_TIMER消息,实际上它适合于一个Long
Public Sub TimerProc(ByVal hwnd As LongPtr,_ ByVal wMsg As LongLong,_ ByVal idEvent As LongPtr,_ ByVal dwTime As LongLong) 在错误恢复下一步
KillTimer hwnd,idEvent'如果这是你想要做的事情,就杀死这个循环的callback 否则,在退出时执行一个KillTimer调用
'****你的定时器进程在这里****

结束小组

#ElseIf VBA7然后'在所有环境中的64位Excel
'只使用LongPtr
Public Sub TimerProc(ByVal hwnd As LongPtr,_
ByVal wMsg只要,_
ByVal idEvent As LongPtr,_
ByVal dwTime As Long)
在错误恢复下一步
KillTimer hwnd,idEvent'如果这是你想要做的事情,就杀死这个循环的callback
否则,在退出时执行一个KillTimer调用
'****你的定时器进程在这里****

结束小组

#Else'32位Excel
Public Sub TimerProcInputBox(ByVal hwnd As Long,_
ByVal wMsg只要,_
ByVal idEvent As Long,_
ByVal dwTime As Long)
在错误恢复下一步
KillTimer hwnd,idEvent'如果这是你想要做的事情,就杀死这个循环的callback
否则,在退出时执行一个KillTimer调用
'****你的定时器进程在这里****
结束小组

#万一

在上面的示例代码中,hwnd参数设置为零,如果您从VBA调用此参数,而不是将该调用与(比如说)input框或表单相关联,则该参数应始终为零。

这个Timer API的一个完整的例子,包括在窗口中使用hwnd参数,可以在Excellerand网站上find:

使用VBA InputBoxinput密码并用星号隐藏用户的键盘input。

脚注:

这已经作为一个单独的答复发布给我解释与调用Timer API相关的系统错误而没有仔细的error handling:这是一个单独的主题,并且StackOverflow将受益于一个单独的,可search的答案与指针安全和64- Windows定时器API的位声明。

在网上有很多API声明的例子。 并且在32位Windows环境(不支持64位'LongLong'整数)上安装VBA7(支持安全指针types)的常见情况的例子很less。

我也遇到了Excel进入一个值时崩溃的事实,并且发现了这个贡献。 大! 我的问题很快就解决了:

 On Error Resume Next 

到“TimerProc”。