Excel KillTimer如何/为什么使现有WM_计时器消息无效?

Excel KillTimer如何/为什么使现有WM_计时器消息无效?,excel,vba,winapi,timer,windows-messages,Excel,Vba,Winapi,Timer,Windows Messages,我读到一个关于如何安排TIMERPROC立即运行的问题。我对此很感兴趣,因为我需要在调用方同步执行完代码后立即运行一个方法 所以我想到了这个骨架: Private Sub asyncProc(ByVal HWnd As LongPtr, ByVal message As WindowsMessage, ByVal timerID As LongPtr, ByVal tickCount As Long) Debug.Print "asyncProc called (should

我读到一个关于如何安排TIMERPROC立即运行的问题。我对此很感兴趣,因为我需要在调用方同步执行完代码后立即运行一个方法

所以我想到了这个骨架:

Private Sub asyncProc(ByVal HWnd As LongPtr, ByVal message As WindowsMessage, ByVal timerID As LongPtr, ByVal tickCount As Long)
    Debug.Print "asyncProc called (should be called second)"
End Sub

Private Sub syncProc()
    Debug.Print "syncProc called (should be called first)"
End Sub

Sub test()
    If tryScheduleProc(AddressOf asyncProc, New Collection) Then
        syncProc
    Else
        Debug.Print "Unable to schedule proc"
    End If
End Sub
。。。当然,下一步是设计
tryScheduleProc
函数,该函数将设置对
asyncProc
的调用,调用时使用
新集合
对象的地址提供的唯一id(实际上这是回调的参数)

测验 我的第一个方法是复制链接答案中列出的方法;即将
WM_TIMER
消息发布到
应用程序.HWnd
的消息队列:

Private Function tryScheduleProc(byVal timerProc As LongPtr, ByVal arg As Object) As Boolean
    Debug.Print "Scheduling..."
    tryScheduleProc = PostMessage(Application.HWnd, WM_TIMER, objPtr(arg), timerProc)
End Function
不管那是什么印刷品

Scheduling...
syncProc called (should be called first)
i、 e.
asyncProc
从未被调用


考虑到消息可能有问题,我切换到生成计时器消息的传统方式,
SetTimer
,只是这次延迟很短:

Private Function tryScheduleProc(ByVal timerProc As LongPtr, ByVal arg As Object) As Boolean
    Debug.Print "Scheduling..."
    Debug.Print "Create Timer:"; SetTimer(Application.HWnd, objPtr(arg), 0, timerProc)
    tightLoopDelay 100 'wait 100ms for timer to have elapsed definitely
    
    Dim tempMsg As tagMSG
    tryScheduleProc = PeekMessage(tempMsg, Application.HWnd, WM_TIMER, WM_TIMER, PM_NOREMOVE) 'force message to be posted
    KillTimer Application.HWnd, objPtr(arg)
End Function
这里,
SetTimer
在队列经过时在队列上创建一个标志,并且
PeekMessage
强制对该标志进行求值-这让我可以销毁计时器,因为
KillTimer
不会删除现有的消息(在
KillTimer
呼叫后,我又看了一眼,证实了这一点-消息在队列中)

但是,立即窗口中的输出再次显示消息没有导致
asyncProc
运行

Scheduling...
Create Timer: 482245352 
syncProc called (should be called first)

我的困惑在于:如果我在回调过程中调用了
KillTimer
,那么一切都正常:

Private Sub asyncProc(ByVal HWnd As LongPtr, ByVal message As WindowsMessage, ByVal timerID As LongPtr, ByVal tickCount As Long)
    Debug.Print "asyncProc called (should be called second)"
    KillTimer HWnd, timerID
End Sub
输出

Scheduling...
Create Timer: 477790208 
syncProc called (should be called first)
asyncProc called (should be called second)
现在,据我所知,线程消息队列的状态不应该根据我何时终止计时器而改变(可能计时器会再次运行,这取决于我的代码执行的速度,但是当线程繁忙时,其他进程没有机会改变队列,是吗?)。打印队列将确认它仍按预期包含一条计时器消息


因此,我认为正在发生的是,当与WM_TIMER消息关联的timerID不再对应于live TIMER时,WM_TIMER消息以某种方式被标记为无效。为了确认这一点,我尝试了原始的PostMessage方法,但使用了一个临时计时器来验证消息的ID:

Private Function tryScheduleProc(ByVal timerProc As LongPtr, ByVal arg As Object) As Boolean
    Debug.Print "Scheduling..."
    'make a validation timer - this won't expire for a long time
    Debug.Print "Create a validation timer:"; SetTimer(Application.HWnd, objPtr(arg), &H7FFFFFFF, timerProc) 'max interval time
    tryScheduleProc = PostMessage(Application.HWnd, WM_TIMER, objPtr(arg), timerProc)
End Function
这和以前一样有效

Scheduling...
Create a validation timer: 482246472 
syncProc called (should be called first)
asyncProc called (should be called second)
…但这不是一个很好的解决方案,因为我仍然需要记住在回调过程中终止计时器,即使它没有被用来生成消息

结论 我认为我的问题在于Excel中消息循环的实现,或者是
DispatchMessage
函数。我不知道这两个函数在内部是什么样子,或者它们是否在任何地方都有文档记录,但我认为Excel的消息循环实现可能是罪魁祸首,因为它几乎完全是关于这个主题的(孤立计时器消息)未提及此问题。此外,on
DispatchMessage
上还显示

MSG结构必须包含有效的消息值

但我认为这并不是说timerID必须仍然存在,只是tagMSG.message必须有一个有效值(这可能是对c的错误解释)。如果归咎于
DispatchMessage
,则也可能是错误的。但可能观察到的行为完全来自其他地方-我对WinApi&messages非常陌生,因此对我得出的结论没有信心


不管怎样,我想知道是否有人能够解释导致这种行为的原因,并确实建议在代码运行后立即调用方法,而不必对异步或同步方法的外观有太多限制?

我无法真正解释发生了什么,但有一个简单的解决方案不能解决这个问题根本不涉及计时器。将
tryScheduleProc
存储
timerProc
arg
在内存中的某个列表中,然后
PostMessage()
将自定义消息发送到
Application.HWnd
,并让消息处理程序在调用存储过程的列表中循环并从列表中删除它们。@RemyLebeau您所指的消息处理程序,是我通过Excel控制的吗?如果是我调用的“消息循环”然后我不确定我是否可以修改它;我不能在switch语句中添加一个case,它说“在WM_APP+1上,消息做了废话…”。我之所以使用计时器消息,是因为TimerProc参数允许我绕过标准窗口过程并提供自己的处理程序-如果有另一种消息我可以
Post
,让我提供自己的回调,那么这将是一个很好的选择。不确定这是否是您想要的?
WM_Timer
m消息不会发布到队列中,它们是在调用
PeekMessage
GetMessage
时动态生成的,如果计时器已过期且队列中没有其他消息保留。@JonathanPotter这不是问题所在,但我不认为;我已经(1):使用带有PM_NOREMOVE的
peek消息
强制计时器的标志评估消息并将其放入队列或(2):使用
PostMessage
手动将WM_定时器消息放入队列。我可以通过
Kill
关闭定时器来确认消息是否在队列中-这应该会取消自动生成标志-然后再次
Peek
排队;无论哪种方式消息都是排队的。我的问题是,如果我继续
Dispatch
手动发送消息,或者让消息loop do it for me,无论出于何种原因,TIMERPROC只有在计时器存在的情况下才会被调用。@Greedo或,您可以简单地发布自己的消息,其中包含一个函数指针,类似于
WM_timer
,然后让您的消息处理程序直接执行该函数。不需要列表。我无法真正解释发生了什么,但有一个简单的方法我同意