Preambule
When trying to position a UserForm at a particular Pixel position (stored in a POINTAPI
Type structure), one must convert the Pixel coordinates to Point coordinates to be able to set the UserForm.Left
and UserForm.Top
VBA properties. Let's call this coefficient K.
From my tests, I came to understand that in my case, GetWindowRect
and the VBA positioning properties of a UserForm (Left
, Top
, Width
, Height
) includes the shadows around the window (of class "ThunderDFrame") containing the MSForm UserForm control. To really get the rectangle of the window delimited by the borders, DwnGetWindowAttribute(hWnd, DWMWA_EXTENDED_FRAME_BOUNDS, rcOutRECT, LenB(rcOutRECT)
Win API must be used.
The origin of the coordinate system to position a UserForm is pixel (0; 0), so no need to worry about ActiveWindow.PointsToScreenPixelsX
/ ActiveWindow.PointsToScreenPixelsY
and the offset between the top left corner of the Excel window and the top left corner of the worksheet grid (at least not until Range.Left
, Range.Top
, etc. properties come into play). However, it is interesting to note that ActiveWindow.PointsToScreenPixelsX
does not behave like ActiveWindow.ActivePane.PointsToScreenPixelsX
. The first one works with pixels has input, not points, as the second does. The real name of the method should rather be ActiveWindow.WorksheetPixelsXToScreenPixelsX
. You can verify it easily:
ActiveWindow.PointsToScreenPixelsX(1) - ActiveWindow.PointsToScreenPixelsX(0)
returns 1, while if it was really doing a convertion it should return something greater than 1 since 1 Point occupy several Pixels on screen. (not really 1/K either because of integer rounding of pixels)
Problem
Considering the zoom factor is 1 to simplify my MCV Example, the coefficients to determine the .Left
and .Top
properties in Points of the UserForm from the (x; y) position in Screen Pixels at which we want it displayed should be :
72 / GetDeviceCaps(GetDC(0), LOGPIXELSX)
72 / GetDeviceCaps(GetDC(0), LOGPIXELSY)
which is
- 0.75 for a 96 DPI legacy display (I have tried it with a PC using Win 7 + Excel 2007
- 0.375 with my Surface Pro 4 tablet running on Win 10 64-bit with Excel 2016 32-bit
Now the problem is that on my tablet, while the above calculation returns 0.375, the correct coefficient to position a UserForm at a given Pixel position (obtained from GetCursorPos
Win API for example) by converting it to the corresponding Point position is 0.35. I have no idea where that value comes from...???
Current progress
On the tablet:
reg key HKEY_CURRENT_USER\Control Panel\Desktop\WindowMetrics\AppliedDPI
indicates 192 and 72 / 192 = 0.375
I have also tried some function from the High DPI Reference in MSDN's Windows Destop App UI Reference:
GetDPIForWindow
(I tried with Application.Hwnd and UserForm's window handle)GetDPIForMonitor
but everything rightfully returns 192.
The Minimal, Complete and Verifiable Example
The following allows me to retrieve the mysterious K = 0.35 coefficient on my tablet, but returns 0.75 on the other computer, as expected.
Module1.bas
Private Declare Function GetWindowRect Lib "user32" (ByVal hWnd As Long, rcWindowRect As RECT) As Long
Private Declare Function GetCursorPos Lib "user32" (ptCursorPoint As POINTAPI) As Long
Private Type RECT
Left As Long
Top As Long
Right As Long
Bottom As Long
End Type
Private Type POINTAPI
X As Long
Y As Long
End Type
Sub test()
Dim rcUsfWindowRect As RECT
UserForm1.Show vbModeless
lRet& = GetWindowRect(UserForm1.hWnd, rcUsfWindowRect)
dblUsfRectWidth# = rcUsfWindowRect.Right - rcUsfWindowRect.Left
dblUsfRectHeight# = rcUsfWindowRect.Bottom - rcUsfWindowRect.Top
Debug.Print UserForm1.Width / dblUsfRectWidth
End Sub
UserForm1
Private Declare Function FindWindowA Lib "user32" (ByVal lpClassName As String, ByVal lpWindowName As String) As Long
Public hWnd As Long
Private Sub UserForm_Initialize()
hWnd = FindWindowA("ThunderDFrame", UserForm1.Caption)
End Sub