VBA中的API计时器 - 如何使安全

Coo*_*lue 9 excel vba timer

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

什么时候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
Run Code Online (Sandbox Code Playgroud)

Nig*_*nan 6

@CoolBlue:崩溃的机理是什么:到底是什么使Excel崩溃?

我可以给您扩展Siddarth Rout的答案,但不能提供完整的解释。

API调用不是VBA:它们存在于VBA的错误处理程序之外,并且当发生错误时,它们将不执行任何操作,或者调用不存在的内存中的资源,或者尝试读取(或写入!)外部的内存Excel.exe的指定内存空间

发生这种情况时,操作系统将介入并关闭您的应用程序。我们曾经将其称为“一般保护错误”,但这仍然是该过程的有用描述。

现在了解一些细节。

当你调用VBA函数,只写了名字-我们称之为“CheckMyFile()” -这就是你需要在VBA内知道。如果没有要调用的“ CheckMyFile”,或者在调用看不​​到的地方声明了该内容,则编译器或运行时引擎将以断点或警告的形式发出错误,然后编译和运行。

在幕后,有一个与字符串'CheckMyFile'相关联的数字地址:我在简化一点,但是我们将该地址称为功能指针 -跟随该地址,然后到达存储定义的结构化内存块函数参数,它们存储的值的空间,以及在其后的地址,这些参数将这些参数定向到为执行VBA而创建的函数结构中,并将值返回到函数输出的地址。

事情可能会出错,VBA会做很多工作以确保在出错时所有这些都能正常折叠。

如果您将该函数的指针指向非VBA的东西(外部应用程序或API计时器调用),则您的函数仍然可以被调用,它仍然可以运行,并且一切正常。

当您将函数指针交给API时,我们将其称为“回调”,因为您调用了它的计时器函数,并且回调了您。

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

如果没有,则外部应用程序将调用其自己的错误处理程序,并且它们不会像VBA那样宽容。

如果Excel和VBA处于“忙碌”状态,或者在尝试使用该函数指针时不可用,则它可能只会挂断电话,并且什么也不做:您可能很幸运,仅此一次。但是,这可能会降低操作系统在Excel.exe进程上的愤怒。

如果回调导致错误,并且您的代码未处理该错误,则VBA会将错误引发给调用方-而且,由于调用方不是VBA,因此可能无法处理该错误:它将要求操作系统提供“帮助”。

如果是API调用,它是为假定已将错误处理和应急管理放在调用代码中的开发人员编写的。

这些假设是:

  1. 该指针后面肯定会有一个有效的函数;
  2. 它在被调用时肯定可用;
  3. ...而且不会给调用者带来任何错误。

使用API​​回调,调用者操作系统,其对检测到错误的响应将使您关机。

因此,这是该过程的非常简单的概述-对它的“为什么”而不是“什么”解释。

对于C ++开发人员,没有过多简化的完整说明。如果您确实想要更深入的答案,则必须学习使用指针进行编程的知识;并且您必须精通内存分配的概念和实践,异常,指针错误的后果以及操作系统用来管理正在运行的应用程序和检测无效操作的机制。

VBA的存在是为了使您免受这些知识的影响,并简化编写应用程序的任务。

  • @ x-yuri-让我们举一个简单的例子。'我看了一下,我可以确切地告诉你出了什么问题。这是“单元格中的单击”-这些单击中的一个或多个将工作表移至“编辑单元格”状态,该状态将暂停计算,可视化基础和事件。**在此状态下,VBA将不会响应外部回调**,并且API计时器无法正常处理该条件。不,这不仅仅是Excel窗口消息队列中的消息-它是带有函数指针的回调,并且有线程管理注意事项。 (2认同)

Nig*_*nan 5

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

正如所承诺的,以下是 Timer API 的 32 位和 64 位 API 声明,使用 LongLong 和安全指针类型:

选项显式
选项私有模块
#If VBA7 And Win64 Then ' 64 位 Windows 下的 64 位 Excel ' 使用 LongLong 和 LongPtr
私有声明 PtrSafe 函数 SetTimer Lib "user32" _ (ByVal hwnd As LongPtr, _ ByVal nIDEvent As LongPtr, _ ByVal uElapse As LongLong, _ ByVal lpTimerFunc As LongPtr _ ) 只要
公开声明 PtrSafe 函数 KillTimer Lib "user32" _ (ByVal hwnd As LongPtr, _ ByVal nIDEvent As LongPtr _ ) 只要 公共 TimerID 作为 LongPtr
#ElseIf VBA7 Then ' 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 Long
私有声明 PtrSafe 函数 KillTimer Lib "user32" _ (ByVal hwnd As LongPtr, _ ByVal
nIDEvent As Long) As Long Public TimerID As LongPtr
#Else ' 32 位 Excel
私有声明函数 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 ) As Long Public TimerID As Long
#万一

' 将计时器称为: ' SetTimer 0&, 0&, lngMilliseconds, AddressOf TimerProc

#If VBA7 And Win64 Then ' 64 位 Windows 下的 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 ' 在这里终止循环回调,如果这是你想要做的 ' 否则,在退出时执行一个 lobal KillTimer 调用
' **** 你的定时器进程在这里 ****
结束子 #ElseIf VBA7 Then ' 64 位 Excel 在所有环境中 ' 仅使用 LongPtr
Public Sub TimerProc(ByVal hwnd As LongPtr, _ ByVal wMsg As Long, _ ByVal idEvent As LongPtr, _ ByVal dwTime As Long) 出错时继续下一步
KillTimer hwnd, idEvent ' 在这里终止循环回调,如果这是你想要做的 ' 否则,在退出时执行一个 lobal KillTimer 调用
' **** 你的定时器进程在这里 ****
结束子 #Else ' 32 位 Excel
Public Sub TimerProcInputBox(ByVal hwnd As Long, _ ByVal wMsg As Long, _ ByVal idEvent As Long, _ ByVal dwTime As Long) 出错时继续下一步
KillTimer hwnd, idEvent ' 在这里终止循环回调,如果这是你想要做的 ' 否则,在退出时执行一个 lobal KillTimer 调用
' **** 你的定时器进程在这里 ****
结束子 #万一

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

Excellerando 网站上提供了此 Timer API 的完整示例,包括对窗口使用 hwnd 参数:

使用 VBA InputBox 作为密码并用星号隐藏用户的键盘输入。




**脚注:**

这已作为对我对与调用 Timer API 相关的系统错误的解释的单独回复而发布,而无需仔细处理错误:这是一个单独的主题,StackOverflow 将受益于 Pointer-Safe 和 64- 的单独且可搜索的答案Windows 计时器 API 的位声明。

网上有一些关于 API 声明的不好的例子;对于安装在 32 位 Windows 环境(不支持 64 位“LongLong”整数)上的 VBA7(支持安全指针类型)的常见情况,很少有示例。