какое событие возникает, если окно модема пользователя excel-vba вернулось в фокус? - PullRequest
2 голосов
/ 02 октября 2019

У меня немодальная пользовательская форма в проекте Excel VBA. Пользовательская форма загружается кнопкой при нажатии на электронную таблицу ( не кнопка active-x, если это актуально). Из-за немодального пользователь может работать с Excel или даже с другими приложениями, а затем переключиться обратно в окно формы. Мне нужно событие, которое запускается, если окно формы снова становится активным окном. Я думал, что UserForm_Activate должен выполнить эту работу, но это не так (не работает UserForm_GotFocus, но для пользовательских форм событие GotFocus отсутствует)?). Есть ли какое-либо событие, которое запускается, если пользователь переключается обратно на немодальную форму пользователя (или в случае, если нет: есть ли какой-нибудь известный обходной путь)? Или у меня есть какая-то странная ошибка, и Activate должен сработать?

Вот весь код, который я использовал для целей тестирования:

' standard module:

Sub BUTTON_FormLoad()
    ' associated as macro triggered by button click on a sheet
    UserForm1.Show vbModeless
End Sub


' UserForm1:

Private Sub UserForm_Activate()
    ' does not fire if focus comes back
    Debug.Print "Activated"
End Sub

Private Sub UserForm_GotFocus()
    ' does not fire if focus comes back
    ' wrong code - no GotFocus event for userforms?
    Debug.Print "Focussed"
End Sub

Private Sub UserForm_Click()
    ' only fires if clicked *inside* form
    ' does not fire eg if user clicks top of form window
    Debug.Print "Clicked"
End Sub

Где найти документацию userformМероприятия? Его нет на странице UserForm .

Ответы [ 4 ]

2 голосов
/ 02 октября 2019

Событие Activate не срабатывает при переключении между приложением и немодальной пользовательской формой. Это умышленно.

Как я уже упоминал в комментариях

Вы можете достичь того, что хотите, создав подкласс пользовательской формы и перехватывая события рабочего листа, но это очень грязно.

Вот очень простой пример. Образец файла можно загрузить по адресу Здесь

READ ME FIRST :

  1. Это просто базовый пример. Пожалуйста, закройте все файлы Excel перед тестированием.
  2. Если пользователь непосредственно щелкает элемент управления в пользовательской форме, и вы также хотите запустить activate code, то вам также придется с этим справиться.
  3. Если вы счастливы, изменитеЭто соответствует вашим потребностям.

Поместите код в модуль

Option Explicit

Private Declare Function CallWindowProc Lib "user32" Alias "CallWindowProcA" _
(ByVal lpPrevWndFunc As Long, ByVal hwnd As Long, ByVal msg As Long, _
ByVal wParam As Long, ByVal lParam As Long) As Long

Private Declare Function SetWindowLong& Lib "user32" Alias "SetWindowLongA" _
(ByVal hwnd As Long, ByVal nIndex As Long, ByVal dwNewLong As Long)

Private Const GWL_WNDPROC = (-4)
Private WinProcOld As Long
Private Const WM_NCLBUTTONDOWN = &HA1

Public formWasDeactivated As Boolean

'~~> Launch the form
Sub LaunchMyForm()
    Dim frm As New UserForm1
    frm.Show vbModeless
End Sub

'~~> Hooking the Title bar in case user clicks on the title bar
'~~> to activate the form
Public Function WinProc(ByVal hwnd As Long, ByVal wMsg As Long, ByVal wParam As Long, ByVal lParam As Long) As Long
    If wMsg = WM_NCLBUTTONDOWN Then
        '~~> Ignoring unnecessary clicks to the title bar
        '~~> by checking if the form was deactivated
        If formWasDeactivated = True Then
            formWasDeactivated = False
            MsgBox "Form Activated"
        End If
    End If

    WinProc = CallWindowProc(WinProcOld&, hwnd&, wMsg&, wParam&, lParam&)
End Function

'~~> Subclass the form
Sub SubClassUserform(hwnd As Long)
    WinProcOld& = SetWindowLong(hwnd, GWL_WNDPROC, AddressOf WinProc)
End Sub

Sub UnSubClassUserform(hwnd As Long)
    SetWindowLong hwnd, GWL_WNDPROC, WinProcOld&
    WinProcOld& = 0
End Sub

Создайте форму пользователя. Давайте назовем это Userform1. Добавьте командную кнопку в форму. Давайте назовем это CommandButton1

Поместите код в пользовательскую форму

Option Explicit

Private Declare Function FindWindow Lib "user32.dll" _
Alias "FindWindowA" (ByVal lpClassName As String, _
ByVal lpWindowName As String) As Long

Dim hwnd As Long

Private Sub UserForm_Initialize()
    hwnd = FindWindow(vbNullString, Me.Caption)
    SubClassUserform hwnd
End Sub

'~~> Userform Click event
Private Sub UserForm_Click()
    '~~> Ignoring unnecessary clicks
    '~~> by checking if the form was deactivated
    If formWasDeactivated = True Then
        formWasDeactivated = False
        MsgBox "Form Activated"
    End If
End Sub

'~~> Unload the form
Private Sub CommandButton1_Click()
    '~~> In case hwnd gets reset for whatever reason.
    hwnd = FindWindow(vbNullString, Me.Caption)
    UnSubClassUserform hwnd

    Unload Me
End Sub

Поместите этот код в Workbook область кода

Option Explicit

'~~> Checking if the form was deactivated
'~~> Add more events if you want

Private Sub Workbook_SheetActivate(ByVal Sh As Object)
    formWasDeactivated = True
End Sub

Private Sub Workbook_SheetSelectionChange(ByVal Sh As Object, ByVal Target As Range)
    formWasDeactivated = True
End Sub

Пожалуйста, не стесняйтесь добавлять больше событий книги. Я использовал только Workbook_SheetActivate и Workbook_SheetSelectionChange

И, наконец, добавил кнопку формы на лист и назначил ей макрос LaunchMyForm. И мы сделали

В действии

enter image description here

1 голос
/ 02 октября 2019

Насколько я знаю, такого события в VBA нет. Из документации:

События Активировать и Деактивировать происходят только при перемещении фокуса в приложении. Перемещение фокуса к или от объекта в другом приложении не вызывает ни одно из событий.

Однако API-интерфейсы Windows могут обрабатывать событие с помощью hook . Проблема с Win API в VBA состоит в том, что VBA не обрабатывает ошибки, поэтому Excel будет аварийно завершать работу, если / когда код обнаружит ошибку;так что они могут быть разочаровывающими для разработчика. С чисто личной точки зрения мне нравится сводить код внутри процедур подключения к минимуму и передавать любые значения классу, который затем может инициировать события - это по крайней мере минимизирует сбои. Также важно помнить, что нужно отцепиться перед завершением сеанса.

Базовая реализация ловушки Win API будет выглядеть примерно так:

В объекте класса (здесь он называется cHookHandler)

Option Explicit

Public Event HookWindowActivated()
Public Event HookIdChanged()

Private mHookId As LongPtr
Private mTargetWindows As Collection

Public Property Get HookID() As LongPtr
    HookID = mHookId
End Property

Public Property Let HookID(RHS As LongPtr)
    mHookId = RHS
    RaiseEvent HookIdChanged
End Property

Public Sub AttachHook()
    modHook.AttachHook Me
End Sub

Public Sub DetachHook()
    modHook.DetachHook
End Sub

Public Sub AddTargetWindow(className As String, Optional windowTitle As String)
    Dim v(1) As String

    'Creates an array of [0 => className, 1=> windowTitle]
    'which is stored in a collection and tested for in
    'your hook callback.
    v(0) = className
    v(1) = windowTitle
    mTargetWindows.Add v

End Sub

Public Sub TestForTargetWindowActivated(className As String, windowTitle As String)
    Dim v As Variant

    'Tests if the callback window is one that we're after.
    For Each v In mTargetWindows
        If v(0) = className Then
            If v(1) = "" Or v(1) = windowTitle Then
                'Fires the event that our target window has been activated.
                RaiseEvent HookWindowActivated
                Exit Sub
            End If
        End If
    Next
End Sub

Private Sub Class_Initialize()
    Set mTargetWindows = New Collection
End Sub

Private Sub Class_Terminate()
    modHook.DetachHook
End Sub

Код модуля (здесь модуль называется modHook)

Option Explicit

Private Declare PtrSafe Function UnhookWindowsHookEx Lib "user32" _
    (ByVal hHook As LongPtr) As Long

Private Declare PtrSafe Function SetWindowsHookEx Lib "user32" _
    Alias "SetWindowsHookExA" _
    (ByVal idHook As Long, _
    ByVal lpfn As LongPtr, _
    ByVal hmod As LongPtr, _
    ByVal dwThreadId As Long) As LongPtr

Private Declare PtrSafe Function CallNextHookEx Lib "user32" _
    (ByVal hHook As LongPtr, _
    ByVal ncode As Long, _
    ByVal wParam As LongPtr, _
    lParam As Any) As LongPtr

Private Declare PtrSafe Function GetClassName Lib "user32" _
    Alias "GetClassNameA" _
    (ByVal hwnd As LongPtr, _
    ByVal lpClassName As String, _
    ByVal nMaxCount As Long) As Long

Private Declare PtrSafe Function GetWindowText Lib "user32" _
    Alias "GetWindowTextA" _
    (ByVal hwnd As LongPtr, _
    ByVal lpString As String, _
    ByVal cch As Long) As Long

Private Declare PtrSafe Function GetCurrentThreadId Lib "kernel32" () As Long

Private Const WH_CBT As Long = 5
Private Const HCBT_ACTIVATE As Long = 5

Private mHookHandler As cHookHandler

Public Sub AttachHook(hookHandler As cHookHandler)
    Set mHookHandler = hookHandler
    mHookHandler.HookID = SetWindowsHookEx(WH_CBT, AddressOf CBTCallback, 0, GetCurrentThreadId)
End Sub

Private Function CBTCallback(ByVal lMsg As Long, _
                             ByVal wParam As LongPtr, _
                             ByVal lParam As LongPtr) As LongPtr
    Dim className As String, windowTitle As String

    If mHookHandler Is Nothing Then Exit Function

    If lMsg = HCBT_ACTIVATE Then
        className = GetClassText(wParam)
        windowTitle = GetWindowTitle(wParam)
        If Not mHookHandler Is Nothing Then
            mHookHandler.TestForTargetWindowActivated className, windowTitle
        End If
    End If
    CBTCallback = CallNextHookEx(mHookHandler.HookID, lMsg, ByVal wParam, ByVal lParam)
End Function

Public Sub DetachHook()
    Dim ret As Long

    If mHookHandler Is Nothing Then Exit Sub

    ret = UnhookWindowsHookEx(mHookHandler.HookID)
    If ret = 1 Then
        mHookHandler.HookID = 0
    End If
End Sub

Private Function GetWindowTitle(wParam As LongPtr) As String
    Dim tWnd As String
    Dim lWnd As Long

    tWnd = String(100, Chr(0))
    lWnd = GetWindowText(wParam, tWnd, 100)
    tWnd = Left(tWnd, lWnd)

    GetWindowTitle = tWnd
End Function

Private Function GetClassText(wParam As LongPtr) As String
    Dim tWnd As String
    Dim lWnd As Long

    tWnd = String(100, Chr(0))
    lWnd = GetClassName(wParam, tWnd, 100)
    tWnd = Left(tWnd, lWnd)

    GetClassText = tWnd
End Function

И в этом примере все события регистрируются вUserform

В этом простом примере две кнопки на Userform присоединяют и отсоединяют хук, но вы, вероятно, вызываете подпрограммы откуда-то еще (возможно, пользовательская форма Initialize иTerminate событий). Userform также имеет метку, отображающую HookId, который я использую во время разработки - для производственного кода вы, вероятно, этого не захотите, поэтому вы можете оставить этот бит вне.

Option Explicit

Private WithEvents mHookHandler As cHookHandler

Private Sub btnHook_Click()
    mHookHandler.AttachHook
End Sub

Private Sub btnUnhook_Click()
    mHookHandler.DetachHook
End Sub

Private Sub mHookHandler_HookIdChanged()
    lblHook.Caption = mHookHandler.HookID
End Sub

Private Sub mHookHandler_HookWindowActivated()
    Debug.Print "I've been activated!"
End Sub

Private Sub UserForm_Initialize()
    Set mHookHandler = New cHookHandler

    mHookHandler.AddTargetWindow "ThunderDFrame", "UserForm1"
End Sub

Private Sub UserForm_Terminate()
    Set mHookHandler = Nothing
End Sub
0 голосов
/ 02 октября 2019

Событие не существует, и вы можете использовать хуки Windows для достижения желаемого результата. На мой взгляд, это прямой ответ, а все остальное - обходной путь [если только он не был опубликован Сиддхартом Раутом, в таком случае ТАК является прямым ответом]

0 голосов
/ 02 октября 2019

Попробуй это. событие происходит после появления формы, поэтому спрячьте wb внутри события инициализации.

    Private Sub UserForm_Initialize() 
Set WB = ThisWorkbook Windows(WB.Name).Visible = False
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...