Обнаружение (в VBA), когда окно, содержащее экземпляр Excel, становится активным
Я вижу, как события WindowActivate запускаются на разных уровнях, когда я переключаюсь между окнами в Excel, но есть ли способ вызвать событие, когда Excel становится основным приложением? Если я щелкаю из Excel и работаю, например, некоторое время в браузере, а затем снова щелкаю в окне Excel, я не вижу запуска ни одного события. Есть ли способ обнаружить это?
Я хотел бы обновить некоторые элементы моего приложения VBA, потому что иногда я обнаруживаю, что моя функция наведения мыши, основанная на функции гипертекста, теряет способность активировать диаграммы. Я могу исправить это, сняв защиту и защитив рабочий лист, или уничтожив и повторно инициализировав подмножество моих объектов. Я хотел бы вызвать это действие на событие, которое я ищу.
Я также могу сделать это с помощью SendKeys, но это нехорошо, потому что он стирает настройки клавиатуры (например, блокировку прокрутки) из-за задокументированной ошибки в SendKeys и заставляет экран мерцать больше, чем хотелось бы.
Поскольку код будет находиться в VBA, я бы ограничил действие конкретной книгой. Если при входе в окно экземпляра Excel активна другая (пассивная) рабочая книга, никакое действие не будет инициировано, и я могу использовать событие WorkbookActivate для обновления приложения, если и когда пользователь выберет рабочую книгу, содержащую его.
4 ответа
Я считаю, что это не предусмотрено в Excel напрямую, поэтому используйте Windows API. Вы можете заниматься программированием на Win32 в VBA!
объяснение
Вы можете использовать API- интерфейс win32 SetWinEventHook, чтобы Windows сообщала вам об определенных событиях. Включая EVENT_SYSTEM_FOREGROUND, который срабатывает при изменении окна переднего плана. В приведенном ниже примере я проверяю идентификатор процесса нового окна переднего плана с идентификатором процесса Excel. Это простой способ сделать это, но он обнаружит другие окна Excel, такие как окно VBA, так же, как главное окно Excel. Это может или не может быть поведение, которое вы хотите, и может быть изменено соответствующим образом.
Вы должны быть осторожны, используя SetWinEventHook, поскольку вы передаете ему функцию обратного вызова. Вы ограничены в том, что вы можете сделать в этой функции обратного вызова, она существует вне обычного выполнения VBA, и любые ошибки внутри нее приведут к аварийному завершению аварийного завершения Excel.
Вот почему я использую Application.OnTime, чтобы сообщить о событиях. Они не гарантированы, чтобы происходить по порядку, если несколько событий запускаются быстрее, чем обновление Excel и VBA. Но это безопаснее. Вы также можете обновить коллекцию или массив событий, а затем прочитать их отдельно вне обратного вызова WinEventFunc.
Пример кода
Чтобы проверить это, создайте новый модуль и вставьте в него этот код. Затем запустите StartHook. Не забудьте запустить StopAllEventHooks перед закрытием Excel или изменением кода!! В рабочем коде вы, вероятно, добавили бы StartEventHook и StopAllEventHooks к событиям WorkBook_Open и WorkBook_BeforeClose, чтобы обеспечить их запуск в соответствующее время. Помните, что если что-то случится с кодом VBA WinEventFunc до остановки хука, Excel вылетит. Это включает изменяемый код или рабочую книгу, в которой он находится, будучи закрытым. Также не нажимайте кнопку остановки в VBA, когда активен хук. Кнопка Стоп может стереть текущее состояние программы!
Option Explicit
Private Const EVENT_SYSTEM_FOREGROUND = &H3&
Private Const WINEVENT_OUTOFCONTEXT = 0
Private Declare Function SetWinEventHook Lib "user32.dll" (ByVal eventMin As Long, ByVal eventMax As Long, _
ByVal hmodWinEventProc As Long, ByVal pfnWinEventProc As Long, ByVal idProcess As Long, _
ByVal idThread As Long, ByVal dwFlags As Long) As Long
Private Declare Function GetCurrentProcessId Lib "kernel32" () As Long
Private Declare Function GetWindowThreadProcessId Lib "user32" (ByVal hWnd As Long, lpdwProcessId As Long) As Long
Private pRunningHandles As Collection
Public Function StartEventHook() As Long
If pRunningHandles Is Nothing Then Set pRunningHandles = New Collection
StartEventHook = SetWinEventHook(EVENT_SYSTEM_FOREGROUND, EVENT_SYSTEM_FOREGROUND, 0&, AddressOf WinEventFunc, 0, 0, WINEVENT_OUTOFCONTEXT)
pRunningHandles.Add StartEventHook
End Function
Public Sub StopEventHook(lHook As Long)
Dim LRet As Long
If lHook = 0 Then Exit Sub
LRet = UnhookWinEvent(lHook)
End Sub
Public Sub StartHook()
StartEventHook
End Sub
Public Sub StopAllEventHooks()
Dim vHook As Variant, lHook As Long
For Each vHook In pRunningHandles
lHook = vHook
StopEventHook lHook
Next vHook
End Sub
Public Function WinEventFunc(ByVal HookHandle As Long, ByVal LEvent As Long, _
ByVal hWnd As Long, ByVal idObject As Long, ByVal idChild As Long, _
ByVal idEventThread As Long, ByVal dwmsEventTime As Long) As Long
'This function is a callback passed to the win32 api
'We CANNOT throw an error or break. Bad things will happen.
On Error Resume Next
Dim thePID As Long
If LEvent = EVENT_SYSTEM_FOREGROUND Then
GetWindowThreadProcessId hWnd, thePID
If thePID = GetCurrentProcessId Then
Application.OnTime Now, "Event_GotFocus"
Else
Application.OnTime Now, "Event_LostFocus"
End If
End If
On Error GoTo 0
End Function
Public Sub Event_GotFocus()
Sheet1.[A1] = "Got Focus"
End Sub
Public Sub Event_LostFocus()
Sheet1.[A1] = "Nope"
End Sub
Я изменил очень хорошее решение @AndASM для работы в 64-битной среде. Изменения были
- изменил параметры вызова функции API с параметров Long на LongLong
- включенные атрибуты PtrSafe
- заменил лист 1.[A1] = на диапазон ("a1"). значение = синтаксис
Код @andasm с модами следует
Option Explicit
Private Const EVENT_SYSTEM_FOREGROUND = &H3&
Private Const WINEVENT_OUTOFCONTEXT = 0
Private Declare PtrSafe Function SetWinEventHook Lib "user32.dll" (ByVal eventMin As Long, _
ByVal eventMax As Long, _
ByVal hmodWinEventProc As LongLong, _
ByVal pfnWinEventProc As LongLong, _
ByVal idProcess As Long, _
ByVal idThread As Long, _
ByVal dwFlags As Long) As Long
Private Declare PtrSafe Function GetCurrentProcessId Lib "kernel32" () As Long
Private Declare PtrSafe Function GetWindowThreadProcessId Lib "user32" (ByVal hWnd As Long, lpdwProcessId As Long) As Long
Private pRunningHandles As Collection
Public Function StartEventHook() As Long
If pRunningHandles Is Nothing Then Set pRunningHandles = New Collection
StartEventHook = SetWinEventHook(EVENT_SYSTEM_FOREGROUND, EVENT_SYSTEM_FOREGROUND, 0&, AddressOf WinEventFunc, 0, 0, WINEVENT_OUTOFCONTEXT)
pRunningHandles.Add StartEventHook
End Function
Public Sub StopEventHook(lHook As Long)
Dim LRet As Long
If lHook = 0 Then Exit Sub
LRet = UnhookWinEvent(lHook)
End Sub
Public Sub StartHook()
StartEventHook
End Sub
Public Sub StopAllEventHooks()
Dim vHook As Variant, lHook As Long
For Each vHook In pRunningHandles
lHook = vHook
StopEventHook lHook
Next vHook
End Sub
Public Function WinEventFunc(ByVal HookHandle As Long, ByVal LEvent As Long, _
ByVal hWnd As Long, ByVal idObject As Long, ByVal idChild As Long, _
ByVal idEventThread As Long, ByVal dwmsEventTime As Long) As Long
'This function is a callback passed to the win32 api
'We CANNOT throw an error or break. Bad things will happen.
On Error Resume Next
Dim thePID As Long
If LEvent = EVENT_SYSTEM_FOREGROUND Then
GetWindowThreadProcessId hWnd, thePID
If thePID = GetCurrentProcessId Then
Application.OnTime Now, "Event_GotFocus"
Else
Application.OnTime Now, "Event_LostFocus"
End If
End If
On Error GoTo 0
End Function
Public Sub Event_GotFocus()
Range("a1").Value = "Got Focus"
End Sub
Public Sub Event_LostFocus()
Range("a1").Value = "Nope"
End Sub
Вот идея, как добиться этого с помощью VBA: Обнаружение, когда Excel теряет фокус?
Я использую это для активации текстового поля немодальной пользовательской формы после переключения между окнами.
Код из версии @AndASM и Michael с некоторыми модификациями (StopAllEventHooks теперь работает, протестировано в Excel 32 и 64)
Это стандартный модуль (например, с названием «Hooking»):
Option Explicit
'https://stackoverflow.com/questions/20486944/detecting-in-vba-when-the-window-containing-an-excel-instance-becomes-active
'https://stackoverflow.com/questions/62245375/detecting-lost-focus-in-excel-application-workbook-or-worksheet
'https://social.msdn.microsoft.com/Forums/office/en-US/70ec18cd-2438-4c96-bbb0-97cdecd3ddbb/detect-the-event-when-someone-minimizes-or-maxmize-the-excel-application?forum=exceldev
'https://learn.microsoft.com/es-es/windows/win32/winmsg/using-hooks?redirectedfrom=MSDN
'https://stackoverflow.com/questions/6777772/call-event-in-excel-vba-when-switching-to-another-app
'https://stackoverflow.com/questions/4659457/forms-gotfocus-event-does-not-seem-to-fire/4659751#4659751
Private Const EVENT_SYSTEM_FOREGROUND = &H3&
Private Const WINEVENT_OUTOFCONTEXT = 0
#If VBA7 Then
Private Declare PtrSafe Function SetWinEventHook Lib "user32.dll" (ByVal EventMin As Long, ByVal EventMax As Long, ByVal hmodWinEventProc As LongLong, ByVal lpfnWinEventProc As LongLong, ByVal idProcess As Long, ByVal idThread As Long, ByVal dwFlags As Long) As Long
Private Declare PtrSafe Function GetCurrentProcessId Lib "kernel32" () As Long
'https://stackoverflow.com/questions/69464536/vba-codes-32bit-to-64bit
Private Declare PtrSafe Function GetWindowThreadProcessId Lib "user32" (ByVal hwnd As LongPtr, lpdwProcessId As Long) As Long
Private Declare PtrSafe Function UnhookWinEvent Lib "user32.dll" (ByVal hWinEventHook As LongPtr) As Long
#Else
'https://www.vbforums.com/showthread.php?861147-RESOLVED-help-How-to-use-SetWinEventHook-to-determine-an-application-s-window-is-created
Private Declare Function SetWinEventHook Lib "user32.dll" (ByVal EventMin As Long, ByVal EventMax As Long, ByVal hmodWinEventProc As Long, ByVal lpfnWinEventProc As Long, ByVal idProcess As Long, ByVal idThread As Long, ByVal dwFlags As Long) As Long
Private Declare Function GetCurrentProcessId Lib "kernel32" () As Long
Private Declare Function GetWindowThreadProcessId Lib "user32" (ByVal hWnd As Long, lpdwProcessId As Long) As Long
Private Declare Function UnhookWindowsHookEx Lib "user32" (ByVal hHook As Long) As Long
'https://stackoverflow.com/questions/43875909/unhookwinevent-does-not-unhook-in-vba
Private Declare Function UnhookWinEvent Lib "user32.dll" (ByVal hWinEventHook As Long) As Long
#End If
Private pRunningHandles As Collection
Public Function StartEventHook() As Long
If pRunningHandles Is Nothing Then Set pRunningHandles = New Collection
StartEventHook = SetWinEventHook(EVENT_SYSTEM_FOREGROUND, EVENT_SYSTEM_FOREGROUND, 0&, AddressOf WinEventFunc, 0, 0, WINEVENT_OUTOFCONTEXT)
pRunningHandles.Add StartEventHook
End Function
#If VBA7 Then
Public Sub StopEventHook(lHook As LongPtr)
#Else
Public Sub StopEventHook(lHook As Long)
#End If
Dim LRet As Long
If lHook = 0 Then Exit Sub
LRet = UnhookWinEvent(lHook)
End Sub
Public Sub StartHook()
StartEventHook
End Sub
Public Sub StopAllEventHooks()
Dim vHook As Variant
#If VBA7 Then
Dim lHook As LongPtr
#Else
Dim lHook As Long
#End If
For Each vHook In pRunningHandles
#If VBA7 Then
lHook = CLngPtr(vHook)
#Else
lHook = CLng(vHook)
#End If
StopEventHook lHook
Next vHook
End Sub
#If VBA7 Then
Public Function WinEventFunc(ByVal HookHandle As Long, ByVal LEvent As Long, _
ByVal hWnd As LongPtr, ByVal idObject As Long, ByVal idChild As Long, _
ByVal idEventThread As Long, ByVal dwmsEventTime As Long) As Long
#Else
Public Function WinEventFunc(ByVal HookHandle As Long, ByVal LEvent As Long, _
ByVal hWnd As Long, ByVal idObject As Long, ByVal idChild As Long, _
ByVal idEventThread As Long, ByVal dwmsEventTime As Long) As Long
#End If
'This function is a callback passed to the win32 api
'We CANNOT throw an error or break. Bad things will happen.
On Error Resume Next
Dim thePID As Long
If LEvent = EVENT_SYSTEM_FOREGROUND Then
GetWindowThreadProcessId hWnd, thePID
If thePID = GetCurrentProcessId Then
Application.OnTime Now, "Event_GotFocus"
Else
Application.OnTime Now, "Event_LostFocus"
End If
End If
On Error GoTo 0
End Function
Public Sub Event_GotFocus()
'Debug.Print "GOT FOCUS"
Activar_userform_visible
'Dim SoundName As String
'SoundName = "C:\WINDOWS\Media\Chimes.wav"
'PlayWavSound SoundName, 1
End Sub
Public Sub Event_LostFocus()
'Debug.Print "Nope"
'Dim SoundName As String
'SoundName = "C:\WINDOWS\Media\recycle.wav" 'Chimes.wav
'PlayWavSound SoundName, 1
End Sub
Создайте пользовательскую форму («UserForm1») с двумя текстовыми полями . Это модуль кода UserForm1:
Option Explicit
#If VBA7 Then
Dim MeHWnd As LongPtr, lngCurrentStyle As Long, lngNewStyle As Long
#Else
Dim MeHWnd As Long, lngCurrentStyle As Long, lngNewStyle As Long
#End If
'http://www.cpearson.com/excel/SuppressChangeInForms.htm
'https://riptutorial.com/vba/example/19036/best-practices
Private Type TView
IsCancelled As Boolean
EnableEvents As Boolean
End Type
Private this As TView
Public Property Get IsCancelled() As Boolean
IsCancelled = this.IsCancelled
End Property
Public Property Get EnableEvents() As Boolean
EnableEvents = this.EnableEvents
End Property
Private Sub UserForm_Initialize()
On Error GoTo ExceptionHandling
'If Application.Version < 9 Then
' MeHWnd = FindWindow("THUNDERXFRAME", Me.Caption) 'XL97
'Else
MeHWnd = FindWindow("THUNDERDFRAME", Me.Caption) 'XL2000+
'End If
lngCurrentStyle = GetWindowLong(MeHWnd, GWL_STYLE)
lngNewStyle = lngCurrentStyle Or WS_MINIMIZEBOX Or WS_MAXIMIZEBOX
SetWindowLong MeHWnd, GWL_STYLE, lngNewStyle
this.EnableEvents = True
CleanUp:
On Error Resume Next
Exit Sub
ExceptionHandling:
'MsgBox "Error: " & Err.Description
Resume CleanUp
'https://stackoverflow.com/a/52206311/6406135
Resume 'for debugging
End Sub
Private Sub UserForm_QueryClose(Cancel As Integer, CloseMode As Integer)
If CloseMode = VbQueryClose.vbFormControlMenu Then
Cancel = True
this.IsCancelled = True
'ListBox1.Clear
'ListBox3.Clear
StopAllEventHooks
Me.Hide
End If
End Sub
Private Sub UserForm_Layout()
'https://www.mrexcel.com/board/threads/how-can-i-tell-if-a-userform-has-been-minimized-vba.920923/
'https://stackoverflow.com/questions/52500202/unhook-scroll-when-userform-minimized
If IsIconic(MeHWnd) Then
'MsgBox "The userform : '" & Me.Name & "' has just been minimized", vbInformation
StopAllEventHooks
ElseIf IsZoomed(MeHWnd) Then
'MsgBox "The userform : '" & Me.Name & "' has just been maximized", vbInformation
Activar_userform_visible
StartHook
Else
'MsgBox "The userform : '" & Me.Name & "' has just been restored", vbInformation
Activar_userform_visible
StartHook
End If
End Sub
Код, помещенный в другой стандартный модуль (например, «Модуль1»):
#If VBA7 Then
Public Declare PtrSafe Function PlayWavSound Lib "winmm.dll" Alias "sndPlaySoundA" (ByVal lpszSoundName As String, ByVal uFlags As Long) As Long
Public Declare PtrSafe Function FindWindow Lib "user32" Alias "FindWindowA" (ByVal lpClassName As String, ByVal lpWindowName As String) As LongPtr
Public Declare PtrSafe Function GetWindowLong Lib "user32" Alias "GetWindowLongA" (ByVal hwnd As LongPtr, ByVal nIndex As Long) As Long
Public Declare PtrSafe Function SetWindowLong Lib "user32" Alias "SetWindowLongA" (ByVal hwnd As LongPtr, ByVal nIndex As Long, ByVal dwNewLong As Long) As Long
Public Declare PtrSafe Function IsIconic Lib "user32" (ByVal hwnd As LongPtr) As Long
Public Declare PtrSafe Function IsZoomed Lib "user32" (ByVal hWnd As LongPtr) As Long
#Else
Public Declare Function PlayWavSound Lib "winmm.dll" Alias "sndPlaySoundA" (ByVal LpszSoundName As String, ByVal uFlags As Long) As Long
Public Declare Function FindWindow Lib "user32" Alias "FindWindowA" (ByVal lpClassName As String, ByVal lpWindowName As String) As Long
Public Declare Function GetWindowLong Lib "user32" Alias "GetWindowLongA" (ByVal hWnd As Long, ByVal nIndex As Long) As Long
Public Declare Function SetWindowLong Lib "user32" Alias "SetWindowLongA" (ByVal hWnd As Long, ByVal nIndex As Long, ByVal dwNewLong As Long) As Long
Public Declare Function IsIconic Lib "user32" (ByVal hWnd As Long) As Long
Public Declare Function IsZoomed Lib "user32" (ByVal hWnd As Long) As Long
#End If
'Siddharth Rout: https://stackoverflow.com/questions/20555050/minimize-userform-when-macro-in-it-is-running/20558520#20558520
'https://answers.microsoft.com/en-us/msoffice/forum/all/excel-vba-how-to-exclude-minimize-and-esc-button/0617ee7c-6c49-4127-8dba-3f8eb04acb9d
Public Const WS_MINIMIZEBOX As Long = &H20000
Public Const WS_MAXIMIZEBOX As Long = &H10000
Public Const GWL_STYLE As Long = -16
Sub Show_Userform1()
Dim frm As UserForm1
Set frm = New UserForm1
frm.Show vbModeless
End Sub
Sub Activar_userform_visible()
Dim objLoop As Object, Pausa As Single, Inicio As Single
Pausa = 0.2
For Each objLoop In VBA.UserForms
If (objLoop.Name = "UserForm1" Or objLoop.Name = "UserForm1") And objLoop.Visible = True Then
'https://stackoverflow.com/questions/28042521/set-focus-back-to-the-application-window-after-showing-userform
AppActivate objLoop.Caption
Inicio = Timer
Do While Timer < Inicio + Pausa
DoEvents
Loop
CreateObject("WScript.Shell").SendKeys "{TAB}", True
CreateObject("WScript.Shell").SendKeys "+{TAB}", True
End If
Next objLoop
End Sub