1

Forward Notes:
• All of the code and UserForm discussed here are in an Excel Addin
• Office is 365 on a 64 bit system

I'm aiming to move the 'parent' of a Modeless UserForm to any workbook (of a given name-type) that is activated while the UserForm is active. I succeeded in doing this by reloading the UserForm when a workbook is activated using the following:

''' Code in an EventClassModule:
Private Sub ExcelApp_WorkbookActivate(ByVal wbActivated As Workbook)
 '… Conditional and Supporting Code …
    Unload UD_SearchImageNotes
    UD_SearchImageNotes.Show vbModeless

While that works, it means having to save the form fields and re-fill those after the .Show (Which seems inefficient and clunky)

While searching for a possibly better (and simpler) solution I came across a similar issue and resolution here: 58658670
I've implemented the suggested solution (with minor changes) per:

''' Code (in an EventClassModule):
Private Sub ExcelApp_WindowActivate(ByVal wbBook As Workbook, ByVal wnWindow As Window)
    Dim lpHWnd As LongPtr, lpHWndActivated As LongPtr

    lpHWndActivated = wnWindow.hwnd
    lpHWnd = FindWindowA(vbNullString, "Search Image Notes")

    If lpHWnd > 0 And wbBook.Name Like "*Image Data*" Then
        If GetParent(lpHWnd) <> lpHWndActivated Then
            SetParent lpHWnd, lpHWndActivated
        End If
    End If
procFinish: Set wbBook = Nothing: Set wnWindow = Nothing
End Sub

''' Supporting Code (in a standard module):
Public Declare PtrSafe Function SetParent Lib "user32" (ByVal hWndChild As LongPtr, ByVal hWndParent As LongPtr) As LongPtr
Public Declare PtrSafe Function GetParent Lib "user32" (ByVal hWndChild As LongPtr) As LongPtr
Public Declare PtrSafe Function FindWindowA Lib "user32" (ByVal stClassName$, ByVal stWindowName$) As LongPtr

But (sadly) the above doesn't work. The event handler works as does FindWindowA, but instead of assigning a new a parent to the UserForm, the UserForm is closed.

I read the Opening Poster's comments regarding the solution at 58658670:

"… as soon as I open another workbook from my code, it closes my Userform" and
"… managed to get it working by creating my new workbook first and then opening the workbook"

That sounds like they also saw the symptom I'm seeing but found a way around it. Sadly, I can't see how their workaround applies to my case (as I'm not creating any workbooks, simply switching between existing books).

Does anyone have any clues as to what could be causing this and/or what I might try?

Spinner
  • 1,078
  • 1
  • 6
  • 15

1 Answers1

0

Yes, even 10 years in, SDI is still frustrating XLAM devs... I too saw the 'vanishing' userform and ended up using APIs to make this work. Even with those, there are some edge cases that needed consideration:

  • The use of SetParent works well to a degree, and the issue appears to be about userform positioning. Setting both .left and .top (in VBA) for a userform isn't reliable when the userform is passed in to a subroutine as an object. Setting one or the other seemed ok, but in any case, the coordinate systems aren't what was expected. This was dealt with using the SetWindowPos API in positionUserForm().
  • A second monitor complicates things wrt form position. Even when using the SetWindowPos API call, the userform vanished when the initial calling window was on a secondary monitor. This was dealt with by detecting the current monitor for the active window when loading the form the first time, moving the window to the primary monitor, loading the form, and then moving the active window back to its original position. Additionally, if the excel window is maximized on a second monitor, the winState must be set to xlNormal before moving it. The moving of the window creates a visible blip, but only when launching from a second monitor, thanks to isWindowOnPrimaryMonitor().
  • The modeless userform itself changes (for the better) after setParent. Prior to calling setParent (through the winActivate event), the modeless form sat on top of all Excel windows, after setParent it sat inside the activeWindow. This 'first use' edge case was dealt with by calling SetParent just before .Show in ShowModeless. Only tested w/ 32bit...Hope this gets you home.

'''' Class WinActivate
Public WithEvents AppEvents As Application
Private Sub AppEvents_WindowActivate(ByVal wb As Workbook, ByVal wn As Window)
    'Change precedent object of UserForm when switching windows
    'hwndUserForm is a global that is set when a userform is loaded and set to 0 when it's unloaded
    If Val(Application.Version) >= 15 And hwndUserForm <> 0 Then
        SetParent hwndUserForm, wn.hWnd
        positionUserForm objUserForm, wn.hWnd
    End If
End Sub

''''Standard Module
Option Explicit
#If Win64 Then
    Declare PtrSafe Function SetParent Lib "user32" (ByVal hWndChild As LongPtr, ByVal hWndNewParent As LongPtr) As LongPtr
    Private Declare PtrSafe Function GetSystemMetrics32 Lib "user32" Alias "GetSystemMetrics" (ByVal nIndex As Long) As Long
    Private Declare PtrSafe Function FindWindow Lib "user32" Alias "FindWindowA" (ByVal lpClassName As String, ByVal lpWindowName As String) As LongPtr
    Private Declare PtrSafe Function SetWindowPos Lib "user32" (ByVal hWnd As LongPtr, ByVal hWndInsertAfter As LongPtr, ByVal x As Long, ByVal y As Long, ByVal cx As Long, ByVal cy As Long, ByVal wFlags As Long) As Long
    'Unsure of these 64bit declations :EnumDisplayMonitors, GetMonitorInfo, MonitorFromWindow
    Private Declare PtrSafe Function EnumDisplayMonitors Lib "user32.dll" (ByVal hdc As LongPtr, ByRef lprcClip As Any, ByVal lpfnEnum As LongPtr, ByVal dwData As LongPtr) As Boolean
    Private Declare PtrSafe Function GetMonitorInfo Lib "user32.dll" Alias "GetMonitorInfoA" (ByVal hMonitor As LongPtr, ByRef lpmi As MonitorInfoEx) As Boolean
    Private Declare PtrSafe Function MonitorFromWindow Lib "user32.dll" (ByVal hWnd As LongPtr, ByVal dwFlags As LongPtr) As LongLong
    ''''''''
    Private Declare PtrSafe Function GetProcAddress Lib "kernel32" (ByVal hModule As LongPtr, ByVal lpProcName As String) As LongPtr
#ElseIf Win32 Then
    Declare Function SetParent Lib "user32" (ByVal hWndChild As Long, ByVal hWndNewParent As Long) As Long
    Private Declare Function GetSystemMetrics32 Lib "user32" Alias "GetSystemMetrics" (ByVal nIndex As Long) As Long
    Private Declare Function FindWindow Lib "user32" Alias "FindWindowA" (ByVal lpClassName As String, ByVal lpWindowName As String) As Long
    Private Declare Function SetWindowPos Lib "user32" (ByVal hWnd As Long, ByVal hWndInsertAfter As Long, ByVal x As Long, ByVal y As Long, ByVal cx As Long, ByVal cy As Long, ByVal wFlags As Long) As Long
    Private Declare Function EnumDisplayMonitors Lib "user32.dll" (ByVal hdc As Long, ByRef lprcClip As Any, ByVal lpfnEnum As Long, ByVal dwData As Long) As Boolean
    Private Declare Function GetMonitorInfo Lib "user32.dll" Alias "GetMonitorInfoA" (ByVal hMonitor As Long, ByRef lpmi As MonitorInfoEx) As Boolean
    Private Declare Function MonitorFromWindow Lib "user32.dll" (ByVal hWnd As Long, ByVal dwFlags As Long) As Long
    Private Declare Function GetProcAddress Lib "kernel32.dll" (ByVal hModule As Long, ByVal lpProcName As String) As Long
#End If

'support SetWindowPos
Const HWND_TOPMOST = -1
Const HWND_NOTOPMOST = -2
Const SWP_NOSIZE = &H1
Const SWP_NOMOVE = &H2
Const SWP_NOACTIVATE = &H10
Const SWP_SHOWWINDOW = &H40

'support finding the primary monitor
Public Const CCHDEVICENAME = 32
Public Const MONITORINFOF_PRIMARY = &H1

'RECT anmd MonitorInfoEx, may need to be conditionally included for 64bit
'only tested on 32 bit Excel versions
Public Type RECT
    Left As Long
    Top As Long
    Right As Long
    Bottom As Long
End Type
Public Type MonitorInfoEx
    cbSize As Long
    rcMonitor As RECT
    rcWork As RECT
    dwFlags As Long
    szDevice As String * CCHDEVICENAME
End Type
Dim MonitorId() As String
'references to modal userForm and var for window switching
Dim WA As New WinActivate
Public hwndUserForm   As Long
Public objUserForm As Object

Sub ShowModeless()
    Set WA.AppEvents = Application
    SetParent hwndUserForm, ActiveWindow.hWnd
    objUserForm.Show vbModeless
End Sub

Sub load_test_form()
'the form has to be created on a window that's on the primary monitor
' for it to show consistently. So stash the left coord in case you need it
    Dim startLeft As Integer
    Dim wnState As Long
    Dim isPrimaryMonitor As Boolean
    startLeft = ActiveWindow.Left
    wnState = ActiveWindow.WindowState 'in case it's maximized on a secondary monitor and needs to be moved
    isPrimaryMonitor = isWindowOnPrimaryMonitor(ActiveWindow)
    If Not isPrimaryMonitor Then
        Dim xLeft As Long
        Dim yTop As Long
        'move the activewindow to the primary monitor (must be in normal state)
        getPrimaryMonitorInfo xLeft, yTop
        ActiveWindow.WindowState = xlNormal
        ActiveWindow.Left = xLeft
    End If
    'load the form
        Load frm_test
        Set objUserForm = frm_test
        hwndUserForm = FindWindow("ThunderDFrame", objUserForm.Caption)
        positionUserForm objUserForm, ActiveWindow.hWnd
        ShowModeless
    'reset the left and state in case it was changed
    If Not isPrimaryMonitor Then
        ActiveWindow.WindowState = xlNormal
        ActiveWindow.Left = startLeft
        ActiveWindow.WindowState = wnState
    End If
End Sub

Public Sub positionUserForm(ByRef thisform As Object, ByVal parentHwnd)
    If thisform Is Nothing Then Exit Sub
    thisform.StartUpPosition = 0
    Dim xPos As Integer
    Dim yPos As Integer
    yPos = ActiveWindow.Top + (ActiveWindow.Height - thisform.Height) / 2
    xPos = ActiveWindow.Left + (ActiveWindow.Width - thisform.Width) / 2
    SetWindowPos hwndUserForm, ActiveWindow.hWnd, xPos, yPos, 0, 0, SWP_NOSIZE 'Or HWND_TOPMOST
End Sub

Public Function isWindowOnPrimaryMonitor(ByRef wn As Window) As Boolean
   'returns true if the window passed in is on the primary monitor
   
    Dim hMonitor        As Long
    Dim apiReturnCode   As Long
    Dim MonitorInfoEx As MonitorInfoEx
    MonitorInfoEx.cbSize = Len(MonitorInfoEx)
    hMonitor = MonitorFromWindow(wn.hWnd, &H0&)
    
    'MonitorInfo.cbSize = LenB(MonitorInfo)
    apiReturnCode = GetMonitorInfo(hMonitor, MonitorInfoEx)
    If MonitorInfoEx.dwFlags And MONITORINFOF_PRIMARY Then
        isWindowOnPrimaryMonitor = True
    Else
        isWindowOnPrimaryMonitor = False
    End If
End Function

Private Sub getPrimaryMonitorInfo(xLeft As Long, yTop As Long)
    Dim i As Integer
    GetMonitorId 'get all the monitor handles
    
    Dim MonitorInfoEx As MonitorInfoEx
    MonitorInfoEx.cbSize = Len(MonitorInfoEx)
    
    For i = 1 To UBound(MonitorId)
        GetMonitorInfo CLng(MonitorId(i)), MonitorInfoEx
        With MonitorInfoEx
            If .dwFlags And MONITORINFOF_PRIMARY Then
                With .rcMonitor
                    xLeft = MonitorInfoEx.rcMonitor.Left
                    yTop = MonitorInfoEx.rcMonitor.Top
                End With
                Exit For
            End If
        End With
    Next i
End Sub

Public Function GetMonitorId()
    'populates the monitor array ids
    ReDim MonitorId(0)
    EnumDisplayMonitors &H0, ByVal &H0, AddressOf MonitorEnumProc, &H0
    GetMonitorId = UBound(MonitorId)
End Function

Public Function MonitorEnumProc(ByVal hMonitor As Long, ByVal hdcMonitor As Long, ByRef lprcMonitor As RECT, ByVal dwData As Long) As Boolean
    Dim ub As Integer
    ub = 0
    On Error Resume Next
    ub = UBound(MonitorId)
    On Error GoTo 0
    ReDim Preserve MonitorId(ub + 1)
    MonitorId(UBound(MonitorId)) = CStr(hMonitor)
    MonitorEnumProc = 1
End Function
DrPimento
  • 1
  • 2