VBA的DoEvents在做什么? —— 理解界面卡死的原因与事件处理

VBA・Officeカテゴリを表すパンダのイラスト VBA・Office

关于本文
本文查阅了Microsoft Learn的VBA DoEvents官方文档,并深入到Excel VBA事件处理与重入的视角进行了整理与总结。

验证状态:📘 已确认官方规范・Excel实机未验证
DoEvents并非是将长时间运行的处理转变为真正异步处理的功能。

在Excel VBA中执行较长的循环时,界面可能会出现看起来卡死的情况。调用DoEvents可以使程序更容易响应操作,这是因为VBA暂时将控制权交给了操作系统,并创造了处理等待中事件的机会。

这里重要的一点是,不要将DoEvents视为“实现异步化的指令”。

没有DoEvents会发生什么

sequenceDiagram
    participant U as User
    participant E as Excel UI
    participant V as VBA

    U->>E: Click / repaint等のイベント
    V->>V: 長いloopを継続
    Note over E,V: VBAが処理を占有
    E-->>U: 反応しにくく見える

当VBA持续进行长时间的处理,而UI端处理事件的机会减少时,重新绘制(重绘)和对输入的响应就会变慢。

调用DoEvents后控制权会返回

在Microsoft Learn中,DoEvents被解释为让操作系统能够处理其他event的execution yield函数。

sequenceDiagram
    participant V as VBA
    participant O as Excel / OS event processing
    participant U as User

    V->>V: loopを実行
    V->>O: DoEvents
    U->>O: input / repaint
    O->>O: queue中のeventを処理
    O-->>V: controlを戻す
    V->>V: loopの続きを実行

这并不意味着DoEvents前后处理主体会移动到不同的thread中。

最小示例

Sub DemoDoEvents()

    Const MaxCount As Long = 5000000
    Dim i As Long

    On Error GoTo CleanUp

    For i = 1 To MaxCount
        If i Mod 10000 = 0 Then
            Application.StatusBar = _
                "Processing: " & Format(i / MaxCount, "0%")
            DoEvents
        End If
    Next i

CleanUp:
    Application.StatusBar = False

End Sub

这里并非在每个loop中都调用DoEvents,而是以一定的间隔向UI返回控制权。

虽然方便,但存在“重入”的问题

在DoEvents执行期间,可能会执行其他的event procedure。

例如,从Button_Click开始执行一项长处理,如果在途中的DoEvents期间再次点击了相同的Button,则相同的处理可能会重叠开始。

flowchart TB
    A[Button_Click] --> B[長い処理]
    B --> C[DoEvents]
    C --> D{同じbuttonが再度押された?}
    D -->|No| E[元の処理へ戻る]
    D -->|Yes| F[同じevent procedureへ再入]
    F --> G[状態の競合・二重実行]

Microsoft Learn也提醒开发者注意,在通过DoEvents交出control的期间,应避免相同的procedure被重新执行。

简单的防止重入

对于现有宏的小规模对策,可以使用运行中flag(标志)。

Private mRunning As Boolean

Sub LongTask()

    Const MaxCount As Long = 5000000
    Dim i As Long

    If mRunning Then
        MsgBox "已经在运行中。"
        Exit Sub
    End If

    mRunning = True
    On Error GoTo CleanUp

    For i = 1 To MaxCount
        If i Mod 10000 = 0 Then
            Application.StatusBar = _
                "Processing: " & Format(i / MaxCount, "0%")
            DoEvents
        End If
    Next i

CleanUp:
    Application.StatusBar = False
    mRunning = False

End Sub

这并非thread-safe(线程安全)的并发处理控制。它只是为了让Excel VBA更难重复启动相同操作的简单状态管理。

区分DoEvents与异步处理

flowchart LR
    A[DoEvents] --> B[イベント処理へ一時的にcontrolを渡す]
    C[非同期I/O] --> D[待ち時間をcallback/event等で扱う]
    E[別process/thread] --> F[処理主体そのものを分ける]
    B -. 同じではない .- D
    B -. 同じではない .- F

对于HTTP通信等外部I/O等待时间较长的处理,采用目标API所提供的async mode或使用event/callback进行设计的思路往往更为本质。

DoEvents不是“将同步处理转换为async的魔法”。

与Windows的message loop的关系在多大程度上是一样的?

应避免将VBA/Excel的内部实现简单地与Win32典型的GetMessage/DispatchMessage loop画等号。

另一方面,作为学习Windows GUI一边处理message/event一边运行这一概念的入口,DoEvents是一个有趣的题材。

flowchart TB
    A[Windows GUIのevent/message] --> B[Excel application]
    B --> C[VBA event procedure]
    C --> D[長い同期処理]
    D --> E[DoEvents]
    E --> B

不要断言“VBA的DoEvents内部代码就是这张图本身”,而是将其作为理解事件驱动的概念图来对待。

何时使用

DoEvents在小型现有宏中想要实现以下功能时往往非常有用:

  • 想要更新StatusBar等进度

  • 想要接受取消操作

  • 不希望UI显得完全没有响应

另一方面,对于长时间、复杂的处理,状态共享较多的处理,以及以外部I/O等待为核心的处理,应考虑其他设计方案。

小结

  • DoEvents会暂时将control交给OS来处理event

  • 它本身并非异步处理

  • 在某些场景下可以改善UI的响应

  • 注意DoEvents期间的重入问题

  • 面对较长的I/O等待,请考虑async API或其他设计

  • 可用作学习Windows event/message处理的入口,但切勿将会内部实现过度简化

官方信息・一手资料

GitHub示例

ライセンス:本記事のテキスト/コードは特記なき限り CC BY 4.0 です。引用の際は出典URL(本ページ)を明記してください。
利用ポリシー もご参照ください。

コメント

标题和URL已复制