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

时间:2013-11-28 15:30:26

标签: excel vba 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

6 个答案:

答案 0 :(得分:6)

@CoolBlue:崩溃的机制是什么:究竟是为了让Excel崩溃?

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

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

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

现在了解一些细节。

当你在VBA中调用一个函数时,只需写下名字 - 让我们称之为'CheckMyFile()' - 这就是你在VBA中需要知道的全部内容。如果没有任何名为'CheckMyFile'的调用,或者它被声明你的调用无法看到它,编译器或运行时引擎将在编译和运行之前以断点或警告的形式引发错误。

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

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

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

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

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

如果Excel和VBA处于“忙碌”状态,或者当它尝试使用该功能指针时不可用,它可能只是放弃调用而不执行任何操作:您可能很幸运,只需要一次。但它可能会在Excel.exe进程中调用操作系统的愤怒。

如果调用导致错误,并且代码没有处理该错误,VBA会将错误提交给调用者 - 并且由于调用者不是VBA,它可能无法处理那:它将从操作系统中寻求'帮助'。

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

这些假设是:

  1. 该指针后面肯定会有一个有效的函数;
  2. 在被叫时它绝对可用;
  3. ...它不会给来电者带来任何错误。
  4. 通过API调用,调用者 操作系统,并且它对检测到错误的响应将关闭您。

    所以这是一个非常简单的过程大纲 - 一个'为什么'而不是一个'什么'的解释。

    对于C ++开发人员来说,没有过度简化的完整解释。如果你真的想深入了解答案,你必须学会​​用指针编程;并且你必须熟练掌握内存分配,异常,错误指针的后果以及操作系统用来管理正在运行的应用程序和检测无效操作的机制的概念和实践。

    VBA旨在保护您免受该知识的影响,并简化编写应用程序的任务。

答案 1 :(得分:5)

VBA中Windows Timer API的指针安全和64位声明:

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

Option Explicit
Option Private Module
#If VBA7 And Win64 Then ' 64 bit Excel under 64-bit windows ' Use LongLong and LongPtr
Private Declare PtrSafe Function SetTimer Lib "user32" _ (ByVal hwnd As LongPtr, _ ByVal nIDEvent As LongPtr, _ ByVal uElapse As LongLong, _ ByVal lpTimerFunc As LongPtr _ ) As LongLong
Public Declare PtrSafe Function KillTimer Lib "user32" _ (ByVal hwnd As LongPtr, _ ByVal nIDEvent As LongPtr _ ) As LongLong Public TimerID As LongPtr

#ElseIf VBA7 Then ' 64 bit Excel in all environments ' Use LongPtr only, LongLong is not available
Private Declare PtrSafe Function SetTimer Lib "user32" _ (ByVal hwnd As LongPtr, _ ByVal nIDEvent As Long, _ ByVal uElapse As Long, _ ByVal lpTimerFunc As LongPtr) As LongPtr
Private Declare PtrSafe Function KillTimer Lib "user32" _ (ByVal hwnd As LongPtr, _ ByVal nIDEvent As Long) As Long
Public TimerID As LongPtr
#Else ' 32 bit 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
Public Declare Function KillTimer Lib "user32" _ (ByVal hwnd As Long, _ ByVal nIDEvent As Long) As Long
Public TimerID As Long
#End If

' Call the timer as: ' SetTimer 0&, 0&, lngMilliseconds, AddressOf TimerProc

#If VBA7 And Win64 Then ' 64 bit Excel under 64-bit windows ' Use LongLong and LongPtr ' Note that wMsg is always the WM_TIMER message, which actually fits in a Long
Public Sub TimerProc(ByVal hwnd As LongPtr, _ ByVal wMsg As LongLong, _ ByVal idEvent As LongPtr, _ ByVal dwTime As LongLong) On Error Resume Next
KillTimer hwnd, idEvent ' Kill the recurring callback here, if that's what you want to do ' Otherwise, implement a lobal KillTimer call on exit
' **** YOUR TIMER PROCESS GOES HERE ****

End Sub

#ElseIf VBA7 Then ' 64 bit Excel in all environments
' Use LongPtr only
Public Sub TimerProc(ByVal hwnd As LongPtr, _ ByVal wMsg As Long, _ ByVal idEvent As LongPtr, _ ByVal dwTime As Long) On Error Resume Next
KillTimer hwnd, idEvent ' Kill the recurring callback here, if that's what you want to do ' Otherwise, implement a lobal KillTimer call on exit
' **** YOUR TIMER PROCESS GOES HERE ****

End Sub

#Else ' 32 bit Excel
Public Sub TimerProcInputBox(ByVal hwnd As Long, _ ByVal wMsg As Long, _ ByVal idEvent As Long, _ ByVal dwTime As Long) On Error Resume Next
KillTimer hwnd, idEvent ' Kill the recurring callback here, if that's what you want to do ' Otherwise, implement a lobal KillTimer call on exit
' **** YOUR TIMER PROCESS GOES HERE ****
End Sub

#End If

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

这个Timer API的完整工作示例,包括使用窗口的hwnd参数,可在Excellerando网站上找到:

Using the VBA InputBox for passwords and hiding the user's keyboard input with asterisks.




脚注:

这已作为单独的回复发布,我对与调用Timer API相关的系统错误的解释没有仔细的错误处理:它是一个单独的主题,StackOverflow将受益于一个单独的,可搜索的答案Windows Timer API的指针安全和64位声明。

网上有API声明的坏例子;在32位Windows环境中安装的VBA7(支持安全指针类型)的常见情况很少见(不支持64位' LongLong'整数)

答案 2 :(得分:4)

  

我在各个地方读到API定时器在VBA中存在风险

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

他们有风险吗?是的,但他们是tight rope walking。一个错误的举动,你就完成了。仅SetTimer API而不是几乎任何API都不是这种情况。

我在2009年创建了一个示例,使用SetTimer API在Excel中创建启动画面。这是LINK

现在,如果您解压缩文件并直接打开Excel文件,那么您将看到Excel崩溃。要使其工作,请按 SHIFT 键,然后打开Excel以使宏不运行。接下来改变图像的路径。新路径将是您从zip文件中提取的图像的路径。更改路径后,只需保存并关闭文件即可。下次运行时,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都不会失败。

答案 3 :(得分:2)

@CoolBlue我写了你上面发布的代码。确切地说,API可以无法预测地发挥作用,至少与普通代码相比。但是,如果你的代码足够健壮(遵循@Siddharth Rout'上面的评论),那么它就不再是一个预测。事实上,在开发过程中会出现这种不可预测性。

例如,在我上面创建的翻转弹出窗口的第一次迭代中,我不小心在IF语句中键入了KillTimer。基本上,现在EndTimer存在的地方我写了KillTimer。我不假思索地这样做了。我知道我有一个程序可以结束计时器,但我暂时将EndTimer与KillTimer混淆了。

所以这就是我提出这个问题的原因:通常,当你在Excel中犯这种错误时,你会收到运行时错误。但是,因为您正在使用API​​,所以您只会收到非法错误,并且整个Excel应用程序都会无响应并退出。所以,如果你在开始计时器之前没有保存,你会失去一切(这基本上是第一次发生在我身上的事情)。更糟糕的是,因为您没有收到运行时错误,所以您不会立即知道哪一行导致了错误。在这样的项目中,您必须预期几个非法错误(以及随后重新加载Excel)来诊断错误。有时这可能是一个痛苦的过程。但这是您在使用API​​时发生的典型调试情况。错误没有被直接突出显示 - 非法错误似乎是随机发生的 - 这就是为什么许多人将API描述为不可预测和风险的原因。

但只要您能找到并诊断出错误,它们就没有风险。在我上面的代码中,我相信我已经创建了一个基本封闭的表单解决方案。没有人可以引入任何可能导致问题的错误。 (不要把它当成挑战人士。)

只是为了给你一些避免错误的具体指导原则:

  • 如果您启动计时器,请确保稍后将其终止。如果在计时器被杀之前有一个Excel运行时错误,它可能会永远持续下去并占用你的记忆。每次调用TimerProc时,使用控制台(Debug.Print)写一行。如果你的代码执行完毕后它仍然在你的控制台中嘀嗒一声,那么你就有了一个失控的计时器。退出Excel并在发生这种情况时返回。
  • 不要使用多个计时器。使用ONE计时器来处理多个计时元素。
  • 不要在没有杀死旧计时器的情况下开始新的计时器。
  • 最重要的是:在您朋友的计算机上进行测试,以确保它能够在不同的平台上运行。

另外,为了清楚起见:使用API​​计时器并同时编辑单元格没有问题。没有关于计时器会排除你的能力编辑工作表上的任何内容。

答案 4 :(得分:0)

我也遇到过Excel在输入值时崩溃并找到此贡献的事实。大! 我添加这一行后,我的问题就解决了:

On Error Resume Next 

到“TimerProc”。

答案 5 :(得分:0)

使用API​​计时器,一旦我将时间间隔设置得太短,Excel就会崩溃,因为它在安排下一个任务之前没有完成上一个定时任务。使用ontime不会发生这种情况,因为您在 完成TimerProc之后设置了ontime。

也许有可能在Timerproc中杀死计时器的第一件事,并在结束之前设置一个新的计时器。

您应该注意,Killtimer有时实际上会失败,从而使计时器保持活动状态并继续永久调用该过程。因此,必须使用带有反馈控制的机枪代码来确保它真的死了。

//pseudo code :
start_kill = timer()
While still_alive= (negative result of killtimer) do
Still_ailve = KillTimer TimerID.
If timer - start_kill > 10 then msgbox "not dead     find a bigger gun" Exit sub
Wend

当然,您需要超时才能退出此循环。