New window, move to second screen (using VBA)

Formula11

Active Member
Joined
Mar 1, 2005
Messages
468
Office Version
  1. 365
Platform
  1. Windows
I was looking into a macro to open a second window of an Excel document, on a second screen with a dual monitor setup.
Using excel command New Window, or something like ActiveWindow.NewWindow in VBA.
- The current Excel workbook may be on either monitor 1 or 2. If on monitor 1, open new window on monitor 2, if on monitor 2, open new window on monitor 1.
- The primary monitor may be Left or Right monitor, if this makes a difference.

In doing some research it looks like APIs are used.
 
As you said, this is getting a bit complicated... Referencing workbook windows can prove rather difficult specially, if you have several workbooks and each workbook has more than one window ... We cannot really rely on the windows captions nor can we rely on their indexes because they all keep changing depending on how many windows are currently opened or which one happens to be the active window when the code is called up.

The only workaround I have managed to come up with is by storing each window in the Running Object Table via temporary workbook names.

Here is the logic I followed:

Sub OpenNewWindowInSecondMonitor(ByVal wb As Workbook) will open new windows in the second monitor.

Sub CloseWindow(ByVal wbk As Workbook, ByVal WindowIndex As Long) will close a window based on the WindowIndex ... The 'WindowIndex' argument follows the chronological order in which the window was opened, from first to last... WindowIndex=1 refers to the original default window.

Again, the chronological order of the WindowIndex argument is key to specifying which window of which workbook is to be closed... for example, If you have a workbook and you open a new window via the code, it will open in the second monitor... When closing the new window, simply pass 2 (second window) to the CloseWindow routine (1 being the original window) ... If you then open another window, just pass 3 to the closeWindow routine and so on so forth... Same for each workbook if you have more than one, you just specify the workbook in the wb argument..


Run the Open_Window_Test and Close_Window_Test SUBS.

In a Standard Module:
VBA Code:
Option Explicit

Private Type POINTAPI
  x As Long
  y As Long
End Type

Private Type RECT
   Left As Long
   Top As Long
   Right As Long
   Bottom As Long
End Type

Private Type WINDOWPLACEMENT
  Length As Long
  flags As Long
  showCmd As Long
  ptMinPosition As POINTAPI
  ptMaxPosition As POINTAPI
  rcNormalPosition As RECT
End Type

Private Type MONITORINFO
   cbSize As Long
   rcMonitor As RECT
   rcWork As RECT
   dwFlags As Long
End Type

Private Type GUID
    Data1 As Long
    Data2 As Integer
    Data3 As Integer
    Data4(7) As Byte
End Type

Private Type uData
    #If Win64 Then
        hOriginalWindow As LongLong
        hNewWindow As LongLong
    #Else
        hOriginalWindow As Long
        hNewWindow As Long
    #End If
End Type

Private Type ROT_DATA
    sGUID As String
    OLEInstance As Long
End Type

#If VBA7 Then
    Private Declare PtrSafe Function GetSystemMetrics Lib "user32" (ByVal nIndex As Long) As Long
    Private Declare PtrSafe Function GetMonitorInfo Lib "user32" Alias "GetMonitorInfoA" (ByVal hMonitor As LongPtr, lpmi As MONITORINFO) As Long
    Private Declare PtrSafe Function MonitorFromWindow Lib "user32" (ByVal hwnd As LongPtr, ByVal dwFlags As Long) As LongPtr
    Private Declare PtrSafe Function EnumDisplayMonitors Lib "user32" (ByVal hDc As LongPtr, lprcClip As Any, ByVal lpfnEnum As LongPtr, dwData As uData) As Long
    Private Declare PtrSafe Function MoveWindow Lib "user32" (ByVal hwnd As LongPtr, ByVal x As Long, ByVal y As Long, ByVal nWidth As Long, ByVal nHeight As Long, ByVal bRepaint As Long) As Long
    Private Declare PtrSafe Function SetWindowPlacement Lib "user32" (ByVal hwnd As LongPtr, ByRef lpwndpl As WINDOWPLACEMENT) As Long
    Private Declare PtrSafe Function CLSIDFromString Lib "ole32" (ByVal lpsz As LongPtr, pclsid As Any) As Long
    Private Declare PtrSafe Function RegisterActiveObject Lib "OleAut32" (ByVal pUnk As IUnknown, rclsid As Any, ByVal dwFlags As Long, pdwRegister As Long) As Long
    Private Declare PtrSafe Function GetActiveObject Lib "OleAut32" (rclsid As Any, ByVal pvReserved As LongPtr, ppunk As Any) As Long
    Private Declare PtrSafe Function CoDisconnectObject Lib "ole32.dll" (ByVal pUnk As IUnknown, pvReserved As Long) As Long
    Private Declare PtrSafe Function RevokeActiveObject Lib "oleaut32.dll" (ByVal dwRegister As Long, ByVal pvReserved As Long) As Long
    Private Declare PtrSafe Function CoCreateGuid Lib "ole32" (pguid As GUID) As Long
    Private Declare PtrSafe Function StringFromGUID2 Lib "ole32" (rguid As GUID, ByVal lpsz As LongPtr, ByVal cchMax As Long) As Long
#Else
    Private Declare Function GetSystemMetrics Lib "user32" (ByVal nIndex As Long) As Long
    Private Declare Function GetMonitorInfo Lib "user32" Alias "GetMonitorInfoA" (ByVal hMonitor As Long, lpmi As MONITORINFO) As Long
    Private Declare Function MonitorFromWindow Lib "user32" (ByVal hWnd As Long, ByVal dwFlags As Long) As Long
    Private Declare Function EnumDisplayMonitors Lib "user32" (ByVal hDc As Long, lprcClip As Any, ByVal lpfnEnum As Long, dwData As uData) As Long
    Private Declare Function MoveWindow Lib "user32" (ByVal hwnd As Long, ByVal x As Long, ByVal y As Long, ByVal nWidth As Long, ByVal nHeight As Long, ByVal bRepaint As Long) As Long
    Private Declare Function SetWindowPlacement Lib "user32" (ByVal hwnd As Long, ByRef lpwndpl As WINDOWPLACEMENT) As Long
    Private Declare Function CLSIDFromString Lib "ole32" (ByVal lpsz As Long, pclsid As Any) As Long
    Private Declare Function RegisterActiveObject Lib "OleAut32" (ByVal pUnk As IUnknown, rclsid As Any, ByVal dwFlags As Long, pdwRegister As Long) As Long
    Private Declare Function GetActiveObject Lib "OleAut32" (rclsid As Any, ByVal pvReserved As Long, ppunk As Any) As Long
    Private Declare Function CoDisconnectObject Lib "ole32.dll" (ByVal pUnk As IUnknown, pvReserved As Long) As Long
    Private Declare Function RevokeActiveObject Lib "oleaut32.dll" (ByVal dwRegister As Long, ByVal pvReserved As Long) As Long
    Private Declare Function CoCreateGuid Lib "ole32" (pguid As GUID) As Long
    Private Declare Function StringFromGUID2 Lib "ole32" (rguid As GUID, ByVal lpsz As Long, ByVal cchMax As Long) As Long
#End If



Sub Open_Window_Test()

    'Open a new window of the activeworkbook in the second monitor.
    OpenNewWindowInSecondMonitor wbk:=ActiveWorkbook
  
End Sub
 
Sub Close_Window_Test()

    '///////////////////////////////////////////////////////////
    '   The 'WindowIndex' argument follows the chronological
    '   order in which the window was opened, from first to last.
    '   (1) is the original window.
    '///////////////////////////////////////////////////////////

    'This will close the second activeworkbook window:(WindowIndex:=2)
    CloseWindow wbk:=ActiveWorkbook, WindowIndex:=2  '<<== change WindowIndex to suit.

End Sub



'_______________________________________________ helper functions _________________________________________________

Private Sub CloseWindow(ByVal wbk As Workbook, ByVal WindowIndex As Long)
    Dim oWind As Window
  
    Set oWind = GetWindowObjFromROT(wbk, WindowIndex)
    If Not oWind Is Nothing Then
        Call CoDisconnectObject(oWind, 0)
        On Error Resume Next
            oWind.Close
            Call DisconnectFromROT(wbk, WindowIndex)
        On Error GoTo 0
        If Err.Number Then MsgBox "Window doesn't exist or already closed."
    End If

End Sub

Private Sub OpenNewWindowInSecondMonitor(ByVal wbk As Workbook)

    Const SM_CMONITORS = 80
    Dim oOriginWind As Window, oNewWindow As Window
    Dim uRotData As ROT_DATA, dwData As uData
  
    With wbk
        If GetSystemMetrics(SM_CMONITORS) > 1 Then
            If wbk.Windows.Count = 1 Then
                Set oOriginWind = .Windows(1)
                dwData.hOriginalWindow = .Windows(1).hwnd
                uRotData = AddWindowObjToROT(oOriginWind)
                .Names.Add "_Wind1", uRotData.sGUID, False
                .Names.Add "_Wind1OleID", uRotData.OLEInstance, False
            End If
            Set oNewWindow = .NewWindow
            dwData.hNewWindow = oNewWindow.hwnd
            uRotData = AddWindowObjToROT(oNewWindow)
            .Names.Add "_Wind" & wbk.Windows.Count, uRotData.sGUID, False
            .Names.Add "_Wind" & wbk.Windows.Count & "OleID", uRotData.OLEInstance, False
            Call EnumDisplayMonitors(ByVal 0, ByVal 0, AddressOf Monitorenumproc, dwData)
        End If
    End With

End Sub

#If Win64 Then
    Private Function Monitorenumproc( _
        ByVal hMonitor As LongLong, _
        ByVal hDc As LongLong, _
        lpRect As RECT, _
        lParam As uData _
    ) As Long
#Else
    Private Function Monitorenumproc( _
        ByVal hMonitor As Long, _
        ByVal hDc As Long, _
        lpRect As RECT, _
        lParam As uData _
    ) As Long
#End If

    Const MONITOR_DEFAULTTONEAREST = &H2&
    Const SW_SHOWNORMAL = 1
    Dim uMI As MONITORINFO
    Dim uWP As WINDOWPLACEMENT
  
    uMI.cbSize = LenB(uMI)
    Call GetMonitorInfo(hMonitor, uMI)
    If MonitorFromWindow(lParam.hOriginalWindow, MONITOR_DEFAULTTONEAREST) <> hMonitor Then
        uWP.Length = Len(uWP)
        uWP.showCmd = SW_SHOWNORMAL
        Call SetWindowPlacement(lParam.hNewWindow, uWP)
        With uMI.rcMonitor
            Call MoveWindow(lParam.hNewWindow, .Left, .Top, .Right - .Left, .Bottom - .Top, True)
        End With
        Monitorenumproc = 0
    Else
        Monitorenumproc = 1
    End If
End Function


Private Function AddWindowObjToROT(ByVal Wind As Window) As ROT_DATA
    Const S_OK = 0&
    Const ACTIVEOBJECT_WEAK = 1
    Dim ClassID(0 To 3) As Long
    Dim lOLEInstance As Long
    Dim sGUID As String

    sGUID = CreateGUID
    If CLSIDFromString(StrPtr(sGUID), ClassID(0)) = S_OK Then
        If RegisterActiveObject(Wind, ClassID(0), ACTIVEOBJECT_WEAK, lOLEInstance) = S_OK Then
            AddWindowObjToROT.sGUID = sGUID
            AddWindowObjToROT.OLEInstance = lOLEInstance
        End If
    End If
End Function

Private Function GetWindowObjFromROT(ByVal wb As Workbook, ByVal WinIndex As Long) As Window
    Const S_OK = 0&
    Dim pUnk As IUnknown
    Dim ClassID(0 To 3) As Long
    Dim sGUID As String
  
    On Error Resume Next
        sGUID = Evaluate(wb.Names("_Wind" & WinIndex).Name)
    On Error GoTo 0
    If Len(sGUID) Then
        If CLSIDFromString(StrPtr(sGUID), ClassID(0)) = S_OK Then
            If GetActiveObject(ClassID(0), 0, pUnk) = S_OK Then
                wb.Names("_Wind" & WinIndex).Delete
                wb.Names("_Wind" & WinIndex & "OleID").Delete
                Set GetWindowObjFromROT = pUnk
            End If
        End If
    Else
        MsgBox "Window doesn't exist or already closed."
    End If
End Function

Private Sub DisconnectFromROT(ByVal wb As Workbook, ByVal WindowIndex As Long)
    Const S_OK = 0&
    Dim sGUID As String
  
    sGUID = Evaluate(wb.Names("_Wind" & WindowIndex & "OleID").Name)
    If RevokeActiveObject(CLng(Evaluate(wb.Names("_Wind" & WindowIndex & "OleID").Name)), 0) = S_OK Then
        'success.
    End If
End Sub

Private Function CreateGUID() As String
  Dim uGUID As GUID
 
  Call CoCreateGuid(uGUID)
  CreateGUID = Space$(38)
  Call StringFromGUID2(uGUID, StrPtr(CreateGUID), 39)
End Function

Function NameExists(ByVal wb As Workbook, ByVal sName As String) As Boolean
    Dim oName As Name
    On Error Resume Next
    Set oName = wb.Names(sName)
    NameExists = Not oName Is Nothing
End Function

Private Sub auto_close()
    Dim oName As Name
    For Each oName In Application.Names
        If InStr(oName.Name, "_Wind") Or InStr(oName.Name, "OleID") Then
            On Error Resume Next
                Call RevokeActiveObject(Replace([oName], "=", ""), 0)
            On Error GoTo 0
            oName.Delete
        End If
    Next
    ThisWorkbook.Save
End Sub
 
Upvote 0

Excel Facts

Test for Multiple Conditions in IF?
Use AND(test, test, test, test) or OR(test, test, test, ...) as the logical_test argument of IF.
Hi Jaafar, this is quite a lot of effort that you've put in.
I does work on the second instance and onward, the active window closes though instead of the one just created (the remaining window alternately goes from screen 1 to 2).
In looking at it the opposite thing seems to happen with multiple windows, what you think is the active window may not be the case.
Again thanks very much for the effort put in.
 
Upvote 0
Hi Jaafar, this is quite a lot of effort that you've put in.
I does work on the second instance and onward, the active window closes though instead of the one just created (the remaining window alternately goes from screen 1 to 2).
In looking at it the opposite thing seems to happen with multiple windows, what you think is the active window may not be the case.
Again thanks very much for the effort put in.
The code works for me in office 2016 as advertised.
 
Upvote 0
OK that's great to hear then.
I will bear that in mind when I get a new setup and try again.
Again, great effort.
 
Upvote 0
Hi Jaafar is their a way you can actually select the screen to position the Excel window on? For example I have a laptop screen and 2 external monitors plugged in, how would I choose this screen? I've modified the code to toggle the Excel window between the laptop screen and I think the default external scree screen as seems to always toggle to this but not the other screen. The goal is to have a dropdown on a userform and select the screen to move the Excel window to - I'm working in a 64bit environment. I've included what I've modified to below.

VBA Code:
Private Declare PtrSafe Function GetSystemMetrics Lib "user32" (ByVal nIndex As Long) As Long '*Remove_Line_Resource*
Private Declare PtrSafe Function GetMonitorInfo Lib "user32" Alias "GetMonitorInfoA" (ByVal hMonitor As LongPtr, lpMI As MONITORINFO) As Long '*Remove_Line_Resource*
Private Declare PtrSafe Function MonitorFromWindow Lib "user32" (ByVal hwnd As LongPtr, ByVal dwFlags As Long) As LongPtr '*Remove_Line_Resource*
Private Declare PtrSafe Function EnumDisplayMonitors Lib "user32" (ByVal hDC As LongPtr, lprcClip As Any, ByVal lpfnEnum As LongPtr, dwData As uData) As Long '*Remove_Line_Resource*
Private Declare PtrSafe Function MoveWindow Lib "user32" (ByVal hwnd As LongPtr, ByVal x As Long, ByVal y As Long, ByVal nWidth As Long, ByVal nHeight As Long, ByVal bRepaint As Long) As Long '*Remove_Line_Resource*
Private Declare PtrSafe Function SetWindowPlacement Lib "user32" (ByVal hwnd As LongPtr, ByRef lpwndpl As WINDOWPLACEMENT) As Long '*Remove_Line_Resource*

'https://www.mrexcel.com/board/threads/new-window-move-to-second-screen-using-vba.1213373/post-5929963
Private Type POINTAPI
  x As Long
  y As Long
End Type

Private Type RECT
   Left As Long
   Top As Long
   Right As Long
   Bottom As Long
End Type

Private Type WINDOWPLACEMENT
  Length As Long
  flags As Long
  showCmd As Long
  ptMinPosition As POINTAPI
  ptMaxPosition As POINTAPI
  rcNormalPosition As RECT
End Type

Private Type MONITORINFO
   cbSize As Long
   rcMonitor As RECT
   rcWork As RECT
   dwFlags As Long
End Type

Private Type uData
    #If Win64 Then
        hOriginalWindow As LongLong
        hNewlWindow As LongLong
    #Else
        hOriginalWindow As Long
        hNewlWindow As Long
    #End If
End Type
Sub toggleAppWindow()

Const SM_CMONITORS = 80

If (GetSystemMetrics(SM_CMONITORS) > 1 And CBool(ScreenParameter("isprimary", ThisWorkbook.Windows(1).hwnd))) Then
    Call MoveSecondMonitor
ElseIf GetSystemMetrics(SM_CMONITORS) > 1 And CBool(ScreenParameter("isprimary", ThisWorkbook.Windows(1).hwnd)) = False Then
    ResetAppPosition
ElseIf ThisWorkbook.Windows(1).Top <> 0 And GetSystemMetrics(SM_CMONITORS) < 1 Then
    ResetAppPosition
ElseIf ThisWorkbook.Windows(1).Top = 0 And GetSystemMetrics(SM_CMONITORS) < 1 Then
    MsgBox "App Window is already Set to default location", vbOKOnly + vbInformation, "Window Position"
ElseIf GetSystemMetrics(SM_CMONITORS) = 1 Then
    MsgBox "App Window is already Set to default location, no seperate screen connected!", vbOKOnly + vbInformation, "Window Position"
Else
    MsgBox "App Window is already Set to default location", vbOKOnly + vbInformation, "Window Position"
End If

End Sub
Sub ResetAppPosition()

'Ensure on Primary Screen
With ThisWorkbook.Windows(1)
    .WindowState = xlNormal
    .Top = 0
    .Left = 0
    .WindowState = xlMaximized
    .Activate
End With

End Sub
Sub MoveSecondMonitor()
    OpenNewWindowInSecondMonitor ThisWorkbook
    AutoZoom
    ThisWorkbook.Windows(1).WindowState = xlMaximized
End Sub
  
  
Sub OpenNewWindowInSecondMonitor(ByVal wb As Workbook, Optional toggleWin As Boolean)
    Const SM_CMONITORS = 80
    Dim dwData As uData
    Dim oNewWindow As Window
    
    If GetSystemMetrics(SM_CMONITORS) > 1 And CBool(ScreenParameter("isprimary", ThisWorkbook.Windows(1).hwnd)) Then
        dwData.hOriginalWindow = wb.Windows(1).hwnd

            'Toggle Window to and from default to external screen
            Set oNewWindow = wb.Windows(1) 'Wb.Windows(1).NewWindow
            dwData.hNewlWindow = wb.Windows(1).hwnd 'oNewWindow.hwnd
            Call EnumDisplayMonitors(ByVal 0, ByVal 0, AddressOf MonitorEnumProc, dwData)
         
    End If
    
End Sub
  
  
#If Win64 Then
    Function MonitorEnumProc( _
        ByVal hMonitor As LongLong, _
        ByVal hDC As LongLong, _
        lpRect As RECT, _
        lParam As uData _
    ) As Long
#Else
    Function MonitorEnumProc( _
        ByVal hMonitor As Long, _
        ByVal hDC As Long, _
        lpRect As RECT, _
        lParam As uData _
    ) As Long
#End If

    Const MONITOR_DEFAULTTONEAREST = &H2&
    Const MONITOR_DEFAULTTOPRIMARY = &H1&
    Const SW_SHOWNORMAL = 1
    Dim uMI As MONITORINFO
    Dim uWP As WINDOWPLACEMENT
    
    uMI.cbSize = LenB(uMI)
    Call GetMonitorInfo(hMonitor, uMI)
    'If MonitorFromWindow(lParam.hOriginalWindow, MONITOR_DEFAULTTONEAREST) <> hMonitor Then
    If MonitorFromWindow(lParam.hOriginalWindow, MONITOR_DEFAULTTOPRIMARY) <> hMonitor Then
        uWP.Length = Len(uWP)
        uWP.showCmd = SW_SHOWNORMAL
        Call SetWindowPlacement(lParam.hNewlWindow, uWP)
        With uMI.rcMonitor
            Call MoveWindow(lParam.hNewlWindow, .Left, .Top, .Right - .Left, .Bottom - .Top, True)

        End With
        MonitorEnumProc = 0
    Else
        MonitorEnumProc = 1
    End If
    
End Function

Next code used to determine if on the primary screen
VBA Code:
Private Declare PtrSafe Function GetSystemMetrics Lib "user32" (ByVal nIndex As Long) As Long '*Remove_Line_Resource*
Private Declare PtrSafe Function GetDeviceCaps Lib "gdi32" (ByVal hDC As LongPtr, ByVal nIndex As Long) As Long '*Remove_Line_Resource*
Private Declare PtrSafe Function GetDC Lib "user32" (ByVal hwnd As LongPtr) As LongPtr '*Remove_Line_Resource*
Private Declare PtrSafe Function ReleaseDC Lib "user32" (ByVal hwnd As LongPtr, ByVal hDC As LongPtr) As Long '*Remove_Line_Resource*
Private Declare PtrSafe Function GetActiveWindow Lib "user32" () As LongPtr '*Remove_Line_Resource*
Private Declare PtrSafe Function MonitorFromWindow Lib "user32" (ByVal hwnd As LongPtr, ByVal dwFlags As Long) As LongPtr '*Remove_Line_Resource*
Private Declare PtrSafe Function GetMonitorInfo Lib "user32" Alias "GetMonitorInfoA" (ByVal hMonitor As LongPtr, ByRef lpMI As MONITORINFOEX) As Boolean '*Remove_Line_Resource*
Private Declare PtrSafe Function CreateDC Lib "gdi32" Alias "CreateDCA" (ByVal lpDriverName As String, ByVal lpDeviceName As String, ByVal lpOutput As String, lpInitData As LongPtr) As LongPtr '*Remove_Line_Resource*
Private Declare PtrSafe Function DeleteDC Lib "gdi32" (ByVal hDC As LongPtr) As Long '*Remove_Line_Resource*



' https://wellsr.com/vba/2019/excel/calculate-ScreenParameter-size-and-other-display-details-with-vba/
' This module includes Private declarations for certain Windows API functions
' plus code for Public Function ScreenParameter, which returns metrics for the ScreenParameter displaying ActiveWindow
' This module requires VBA7 (Office 2010 or later)
' DEVELOPER: J. Woolley (for wellsr.com)

Private Const SM_CMONITORS              As Long = 80    ' number of display monitors
Private Const MONITOR_CCHDEVICENAME     As Long = 32    ' device name fixed length
Private Const MONITOR_PRIMARY           As Long = 1
Private Const MONITOR_DEFAULTTONULL     As Long = 0
Private Const MONITOR_DEFAULTTOPRIMARY  As Long = 1
Private Const MONITOR_DEFAULTTONEAREST  As Long = 2
Private Type RECT
    Left As Long
    Top As Long
    Right As Long
    Bottom As Long
End Type
Private Type MONITORINFOEX
   cbSize As Long
   rcMonitor As RECT
   rcWork As RECT
   dwFlags As Long
   szDevice As String * MONITOR_CCHDEVICENAME
End Type
Private Enum DevCap     ' GetDeviceCaps nIndex (video displays)
    HORZSIZE = 4        ' width in millimeters
    VERTSIZE = 6        ' height in millimeters
    HORZRES = 8         ' width in pixels
    VERTRES = 10        ' height in pixels
    BITSPIXEL = 12      ' color bits per pixel
    LOGPIXELSX = 88     ' horizontal DPI (assumed by Windows)
    LOGPIXELSY = 90     ' vertical DPI (assumed by Windows)
    COLORRES = 108      ' actual color resolution (bits per pixel)
    VREFRESH = 116      ' vertical refresh rate (Hz)
End Enum

Public Function ScreenParameter(Item As String, Optional ActiveWindowID As LongPtr) As Variant

' Return display ScreenParameter Item for monitor displaying ActiveWindow
' Patterned after Excel's built-in information functions CELL and INFO
' Supported Item values (each must be a string, but alphabetic case is ignored):
' HorizontalResolution or pixelsX
' VerticalResolution or pixelsY
' WidthInches or inchesX
' HeightInches or inchesY
' DiagonalInches or inchesDiag
' PixelsPerInchX or ppiX
' PixelsPerInchY or ppiY
' PixelsPerInch or ppiDiag
' WinDotsPerInchX or dpiX
' WinDotsPerInchY or dpiY
' WinDotsPerInch or dpiWin ' DPI assumed by Windows
' AdjustmentFactor or zoomFac ' adjustment to match actual size (ppiDiag/dpiWin)
' IsPrimary ' True if primary display
' DisplayName ' name recognized by CreateDC
' Update ' update cells referencing this UDF and return date/time
' Help ' display all recognized Item string values
' EXAMPLE: =ScreenParameter("pixelsX")
' Function Returns #VALUE! for invalid Item
    Dim xHSizeSq As Double, xVSizeSq As Double, xPix As Double, xDot As Double
    Dim hwnd As LongPtr, hDC As LongPtr, hMonitor As LongPtr
    Dim tMonitorInfo As MONITORINFOEX
    Dim nMonitors As Integer
    Dim vResult As Variant
    Dim sItem As String
    Application.Volatile
    nMonitors = GetSystemMetrics(SM_CMONITORS)
    
    If nMonitors < 2 Then
        nMonitors = 1                                       ' in case GetSystemMetrics failed
        hwnd = 0
    Else
        If ActiveWindowID <> 0 Then
            hwnd = ActiveWindowID
        Else
            hwnd = GetActiveWindow()
        End If
        
        hMonitor = MonitorFromWindow(hwnd, MONITOR_DEFAULTTONULL)
        If hMonitor = 0 Then
            Debug.Print "ActiveWindow does not intersect a monitor"
            hwnd = 0
        Else
            tMonitorInfo.cbSize = Len(tMonitorInfo)
            If GetMonitorInfo(hMonitor, tMonitorInfo) = False Then
                Debug.Print "GetMonitorInfo failed"
                hwnd = 0
            Else
                hDC = CreateDC(tMonitorInfo.szDevice, 0, 0, 0)
                If hDC = 0 Then
                    Debug.Print "CreateDC failed"
                    hwnd = 0
                End If
            End If
        End If
    End If
    If hwnd = 0 Then
    
        If ActiveWindowID <> 0 Then
        hDC = GetDC(ActiveWindowID) 'XL_Screen
        Else
        hDC = GetDC(hwnd)
        End If
        
        tMonitorInfo.dwFlags = MONITOR_PRIMARY
        tMonitorInfo.szDevice = "PRIMARY" & vbNullChar
    End If
    sItem = Trim(LCase(Item))
    Select Case sItem
    Case "horizontalresolution", "pixelsx"                  ' HorizontalResolution (pixelsX)
        vResult = GetDeviceCaps(hDC, DevCap.HORZRES)
    Case "verticalresolution", "pixelsy"                    ' VerticalResolution (pixelsY)
        vResult = GetDeviceCaps(hDC, DevCap.VERTRES)
    Case "widthinches", "inchesx"                           ' WidthInches (inchesX)
        vResult = GetDeviceCaps(hDC, DevCap.HORZSIZE) / 25.4
    Case "heightinches", "inchesy"                          ' HeightInches (inchesY)
        vResult = GetDeviceCaps(hDC, DevCap.VERTSIZE) / 25.4
    Case "diagonalinches", "inchesdiag"                     ' DiagonalInches (inchesDiag)
        vResult = Sqr(GetDeviceCaps(hDC, DevCap.HORZSIZE) ^ 2 + GetDeviceCaps(hDC, DevCap.VERTSIZE) ^ 2) / 25.4
    Case "pixelsperinchx", "ppix"                           ' PixelsPerInchX (ppiX)
        vResult = 25.4 * GetDeviceCaps(hDC, DevCap.HORZRES) / GetDeviceCaps(hDC, DevCap.HORZSIZE)
    Case "pixelsperinchy", "ppiy"                           ' PixelsPerInchY (ppiY)
        vResult = 25.4 * GetDeviceCaps(hDC, DevCap.VERTRES) / GetDeviceCaps(hDC, DevCap.VERTSIZE)
    Case "pixelsperinch", "ppidiag"                         ' PixelsPerInch (ppiDiag)
        xHSizeSq = GetDeviceCaps(hDC, DevCap.HORZSIZE) ^ 2
        xVSizeSq = GetDeviceCaps(hDC, DevCap.VERTSIZE) ^ 2
        xPix = GetDeviceCaps(hDC, DevCap.HORZRES) ^ 2 + GetDeviceCaps(hDC, DevCap.VERTRES) ^ 2
        vResult = 25.4 * Sqr(xPix / (xHSizeSq + xVSizeSq))
    Case "windotsperinchx", "dpix"                          ' WinDotsPerInchX (dpiX)
        vResult = GetDeviceCaps(hDC, DevCap.LOGPIXELSX)
    Case "windotsperinchy", "dpiy"                          ' WinDotsPerInchY (dpiY)
        vResult = GetDeviceCaps(hDC, DevCap.LOGPIXELSY)
    Case "windotsperinch", "dpiwin"                         ' WinDotsPerInch (dpiWin)
        xHSizeSq = GetDeviceCaps(hDC, DevCap.HORZSIZE) ^ 2
        xVSizeSq = GetDeviceCaps(hDC, DevCap.VERTSIZE) ^ 2
        xDot = GetDeviceCaps(hDC, DevCap.LOGPIXELSX) ^ 2 * xHSizeSq + GetDeviceCaps(hDC, DevCap.LOGPIXELSY) ^ 2 * xVSizeSq
        vResult = Sqr(xDot / (xHSizeSq + xVSizeSq))
    Case "adjustmentfactor", "zoomfac"                      ' AdjustmentFactor (zoomFac)
        xHSizeSq = GetDeviceCaps(hDC, DevCap.HORZSIZE) ^ 2
        xVSizeSq = GetDeviceCaps(hDC, DevCap.VERTSIZE) ^ 2
        xPix = GetDeviceCaps(hDC, DevCap.HORZRES) ^ 2 + GetDeviceCaps(hDC, DevCap.VERTRES) ^ 2
        xDot = GetDeviceCaps(hDC, DevCap.LOGPIXELSX) ^ 2 * xHSizeSq + GetDeviceCaps(hDC, DevCap.LOGPIXELSY) ^ 2 * xVSizeSq
        vResult = 25.4 * Sqr(xPix / xDot)
    Case "isprimary"                                        ' IsPrimary
        vResult = CBool(tMonitorInfo.dwFlags And MONITOR_PRIMARY)
    Case "displayname"                                      ' DisplayName
        vResult = tMonitorInfo.szDevice & vbNullChar
        vResult = Left(vResult, (InStr(1, vResult, vbNullChar) - 1))
    Case "update"                                           ' Update
        vResult = Now
    Case "help"                                             ' Help
        vResult = "HorizontalResolution (pixelsX), VerticalResolution (pixelsY), " _
            & "WidthInches (inchesX), HeightInches (inchesY), DiagonalInches (inchesDiag), " _
            & "PixelsPerInchX (ppiX), PixelsPerInchY (ppiY), PixelsPerInch (ppiDiag), " _
            & "WinDotsPerInchX (dpiX), WinDotsPerInchY (dpiY), WinDotsPerInch (dpiWin), " _
            & "AdjustmentFactor (zoomFac), IsPrimary, DisplayName, Update, Help"
    Case Else                                               ' Else
        vResult = CVErr(xlErrValue)                         ' return #VALUE! error (2015)
    End Select
    If hwnd = 0 Then
        ReleaseDC hwnd, hDC
    Else
        DeleteDC hDC
    End If
    ScreenParameter = vResult
End Function
 
Upvote 0
@Handyman84
... The goal is to have a dropdown on a userform and select the screen to move the Excel window to

I have written this code without being able to test it on multiple monitors as I only have one ... So, as shown in the image below, the only monitor name that is displayed in the combobox is \\.\DISPLAY1 which is obviously my [Primary Monitor]

File Demo:
ExcelToMonitor.xlsm

monitors.png


Basically, the code is supposed to retrieve the internal names of all the enabled monitors (inc the primary monitor), get their respective monitor handles and pass the handle you want to the PositionWindowOnMonitor routine.

The arguments in the PositionWindowOnMonitor routine above expect to be passed the hwnd of the window to be positionned, the Handle to the target Monitor and the new Left & Top screen positions (in pixels) of the window in relation to the chosen monitor.

Note that due to the Single Document Interface (SDI) that came with excel 2013, if you have more than one workbook open, the code will only move the window of the active workbook.


1- API Worker code in a Standard Module:
VBA Code:
Option Explicit

#If Win64 Then
    Private Const NULL_PTR = 0^
#Else
    Private Const NULL_PTR = 0&
#End If

#If VBA7 Then
    Private Declare PtrSafe Sub CopyMemory Lib "kernel32" Alias "RtlMoveMemory" (Destination As Any, Source As Any, ByVal Length As LongPtr)
    Private Declare PtrSafe Function GetMonitorInfo Lib "user32" Alias "GetMonitorInfoA" (ByVal hMonitor As LongPtr, lpmi As MONITORINFOEX) As Long
    Private Declare PtrSafe Function EnumDisplayMonitorsString Lib "user32" Alias "EnumDisplayMonitors" (ByVal hDc As LongPtr, lprcClip As Any, ByVal lpfnEnum As LongPtr, dwData() As String) As Long
    Private Declare PtrSafe Function EnumDisplayMonitorsUDT Lib "user32" Alias "EnumDisplayMonitors" (ByVal hDc As LongPtr, lprcClip As Any, ByVal lpfnEnum As LongPtr, dwData As uData) As Long
    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
#Else
    Private Enum LongPtr
        [_]
    End Enum
    Private Declare Sub CopyMemory Lib "kernel32" Alias "RtlMoveMemory" (Destination As Any, Source As Any, ByVal Length As LongPtr)
    Private Declare Function GetMonitorInfo Lib "user32" Alias "GetMonitorInfoA" (ByVal hMonitor As LongPtr, lpmi As MONITORINFOEX) As Long
    Private Declare Function EnumDisplayMonitorsString Lib "user32" Alias "EnumDisplayMonitors" (ByVal hDc As LongPtr, lprcClip As Any, ByVal lpfnEnum As LongPtr, dwData() As String) As Long
    Private Declare Function EnumDisplayMonitorsUDT Lib "user32" Alias "EnumDisplayMonitors" (ByVal hDc As LongPtr, lprcClip As Any, ByVal lpfnEnum As LongPtr, dwData As uData) As Long
    Private Declare 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
#End If

Private Type POINTAPI
  X As Long
  Y As Long
End Type

Private Const CCHDEVICENAME As Long = 32&
Private Type uData
    MonitorHnwd As LongPtr
    MonitorName(0& To CCHDEVICENAME - 1&) As Byte
End Type

Private Type MONITORINFOEX
    cbSize As Long
    rcMonitor(0& To 3&) As Long
    rcWork(0& To 3&) As Long
    dwFlags As Long
    szDevice(0& To CCHDEVICENAME - 1&) As Byte
End Type

Private lMonitorsCounter As Long


' _________________________________ PUBLIC ROUTINES ___________________________________________

Public Sub PositionWindowOnMonitor( _
    ByVal hwnd As LongPtr, _
    ByVal hMonitor As LongPtr, _
    ByVal LeftPixel As Long, _
    ByVal TopPixel As Long _
)
    Const SWP_NOSIZE = &H1, SWP_SHOWWINDOW = &H40
 
    LeftPixel = LeftPixel + MonitorLeftPix(hMonitor)
    TopPixel = TopPixel + MonitorTopPix(hMonitor)
    Call SetWindowPos(hwnd, NULL_PTR, LeftPixel, TopPixel, 0&, 0&, SWP_SHOWWINDOW + SWP_NOSIZE)
End Sub

Public Function GetMonitorsNames() As String()
  Call EnumDisplayMonitorsString(ByVal NULL_PTR, ByVal 0&, AddressOf MonitorNamesEnumProc, GetMonitorsNames)
  lMonitorsCounter = 0&
End Function

Public Function MonitorName2Handle(ByVal MonitorName As String) As LongPtr
    Dim tData As uData
    If Len(MonitorName) Then
        Call CopyMemory(tData.MonitorName(0&), ByVal StrPtr(MonitorName), LenB(MonitorName))
        Call EnumDisplayMonitorsUDT(NULL_PTR, ByVal 0&, AddressOf MonitorName2HandleProc, tData)
        MonitorName2Handle = tData.MonitorHnwd
    End If
End Function

' _________________________________ PRIVATE ROUTINES ___________________________________________

Private Function MonitorLeftPix(hMonitor As LongPtr) As Long
    Dim uMI As MONITORINFOEX
    uMI.cbSize = Len(uMI)
    If GetMonitorInfo(hMonitor, uMI) Then
        MonitorLeftPix = uMI.rcMonitor(0&)
    End If
End Function

Private Function MonitorTopPix(hMonitor As LongPtr) As Long
    Dim uMI As MONITORINFOEX
    uMI.cbSize = Len(uMI)
    If GetMonitorInfo(hMonitor, uMI) = 0& Then
        MonitorTopPix = uMI.rcMonitor(1&)
    End If
End Function

Private Function MonitorName2HandleProc( _
    ByVal hMonitor As LongPtr, _
    ByVal hDc As LongPtr, _
    ByVal lpRect As Long, _
    dwData As uData _
) As Long

    Dim sMonitorName As String, lNullCharPos As Long
    Dim sMonitorNames() As String, vName As Variant
 
    sMonitorNames = GetMonitorsNames
    sMonitorName = dwData.MonitorName
    lNullCharPos = InStr(sMonitorName, vbNullChar) - 1&
    If lNullCharPos Then
        sMonitorName = Left$(sMonitorName, lNullCharPos)
        For Each vName In sMonitorNames
            vName = Replace(vName, " [Primary Monitor]", "")
            If vName = sMonitorName Then
                dwData.MonitorHnwd = hMonitor
                MonitorName2HandleProc = 0&
                Exit Function
            End If
        Next vName
    End If
    MonitorName2HandleProc = 1&
End Function
 
Private Function MonitorNamesEnumProc( _
    ByVal hMonitor As LongPtr, _
    ByVal hDc As LongPtr, _
    ByVal lpRect As Long, _
    dwData() As String _
) As Long

    Const MONITORINFOF_PRIMARY = &H1
    Dim uMI As MONITORINFOEX
    Dim sPrimary As String, sMonitorName As String, lNullCharPos As Long
 
    uMI.cbSize = Len(uMI)
    If GetMonitorInfo(hMonitor, uMI) Then
        sMonitorName = uMI.szDevice
        lNullCharPos = InStr(sMonitorName, vbNullChar) - 1&
        If lNullCharPos Then
            ReDim Preserve dwData(lMonitorsCounter)
            dwData(lMonitorsCounter) = Left$(sMonitorName, lNullCharPos)
            If uMI.dwFlags = MONITORINFOF_PRIMARY Then
                sPrimary = " [Primary Monitor]"
                sPrimary = StrConv(sPrimary, vbFromUnicode)
                dwData(lMonitorsCounter) = dwData(lMonitorsCounter) & sPrimary
            End If
            lMonitorsCounter = lMonitorsCounter + 1&
        End If
    End If
    MonitorNamesEnumProc = 1&
End Function


2- Code Usage in the UserForm Module: (As per the attached file demo)
VBA Code:
Option Explicit

Private Sub UserForm_Initialize()
    ComboBox1.List = GetMonitorsNames
    ComboBox1.Value = Me.ComboBox1.List(0)
    TextBox1 = 0&: TextBox2 = 0&
End Sub

Private Sub CommandButton1_Click()

    #If VBA7 Then
        Dim hMonitor As LongPtr
    #Else
        Dim hMonitor As Long
    #End If
 
    hMonitor = MonitorName2Handle(Replace(ComboBox1.Value, " [Primary Monitor]", ""))
    Application.WindowState = xlNormal
    Call PositionWindowOnMonitor(Application.hwnd, hMonitor, CLng(TextBox1), CLng(TextBox2))
    Application.WindowState = IIf(CheckBox1.Value, xlMaximized, xlNormal)
 
End Sub
 

Attachments

  • monitors.png
    monitors.png
    5 KB · Views: 6
Last edited:
Upvote 0
Hi Jaafar have tried and will only move to the primary screen. If Excel is on the the other displays it will move to the primary display only. The userform all works fine the dropdown is populated fine.

Not sure what the issue is but will not move to another screen apart from the primary display. See recorded screenshot


Thanks Handyman84
 
Last edited:
Upvote 0
Hi Jaafar have tried and will only move to the primary screen. If Excel is on the the other displays it will move to the primary display only. The userform all works fine the dropdown is populated fine.

Not sure what the issue is but will not move to another screen apart from the primary display. See recorded screenshot


Thanks Handyman84
Do you have more than one workbook open?
What happens if the excel window is not maximized? (ie:; WindowState = xlNormal)
 
Upvote 0
Only one workbook is open (ExcelToMonitor.xlsm) and xlNormal is true.
Having one single monitor, it is hard for me to test\debug the code... Hopefully, if I can borrow a second monitor and plug it then I will be able to further test.

Edit: Which versions of excel and windows do you have ? including bitness that is.
 
Upvote 0

Forum statistics

Threads
1,223,894
Messages
6,175,252
Members
452,623
Latest member
Techenthusiast

We've detected that you are using an adblocker.

We have a great community of people providing Excel help here, but the hosting costs are enormous. You can help keep this site running by allowing ads on MrExcel.com.
Allow Ads at MrExcel

Which adblocker are you using?

Disable AdBlock

Follow these easy steps to disable AdBlock

1)Click on the icon in the browser’s toolbar.
2)Click on the icon in the browser’s toolbar.
2)Click on the "Pause on this site" option.
Go back

Disable AdBlock Plus

Follow these easy steps to disable AdBlock Plus

1)Click on the icon in the browser’s toolbar.
2)Click on the toggle to disable it for "mrexcel.com".
Go back

Disable uBlock Origin

Follow these easy steps to disable uBlock Origin

1)Click on the icon in the browser’s toolbar.
2)Click on the "Power" button.
3)Click on the "Refresh" button.
Go back

Disable uBlock

Follow these easy steps to disable uBlock

1)Click on the icon in the browser’s toolbar.
2)Click on the "Power" button.
3)Click on the "Refresh" button.
Go back
Back
Top