Requires: GfaWinx.lg32
Constant that can be used in a MessageProc event sub to respond to DPI changes for a form.
Const WM_DPICHANGED = 0x2E0
The WM_DPICHANGED message is only sent in a dpi-aware application. An application is dpi-aware if it has a manifest that includes the dpi awareness tag or when the program uses the DpiAwareness function, see Remarks. The WM_DPICHANGED message is sent to a window if for that window the DPI has changed. The possible causes for the change in DPI is a window that is moved to a new monitor that has a different DPI or if the DPI of the monitor hosting the window changes.
Message parameters:
wParam | The HIWORD of the wParam contains the Y-axis value of the new dpi of the window. The LOWORD of the wParam contains the X-axis value of the new DPI of the window. For example, 96, 120, 144, or 192. The values of the X-axis and the Y-axis are identical for Windows apps. |
lParam | A pointer to a RECT structure that provides a suggested size and position of the current window scaled for the new DPI. The expectation is that apps will reposition and resize windows based on the suggestions provided by lParam when handling this message. |
If the application processes this message, it should return zero. In the Form_MessageProc event sub set RetVal% to 0 and Valid? to True.
To properly scale the GFA-BASIC's graphical output commands both the form's scale and fontsize must be set. To draw the shapes according to the current DPI setting, the form's ScaleWidth and ScaleHeight properties are changed to reflect the current DPI. In the following example, the form's Scale* properties are initially set using the helper command ScaleXYWHToDpi frm, dpi, x0, y0, width, height. If the width and height parameters are omitted, the ScaleWidth is set to _X * (96/DPI) and ScaleHeight is set to _Y * (96/DPI). The example shows how to set ScaleWidth and ScaleHeight to a fixed number of units in X- and Y- direction. The coordinates must always be in 96 DPI units (the units as they are used on a 96 DPI display). So rather than using the Scale* properties in a DPI-aware program use ScaleXYWHToDpi.
To properly display the text, the fontsize as used on 96 dots-per-inch display is saved and scaled to the DPI using ScaleToDpiSng().
An application should not process WM_DPICHANGED when executing LoadFormPos and running the app from inside the IDE. The result of LoadFormPos might be a WM_DPICHANGED if the form is positioned on a different display with a different resolution than the IDE's display. To prevent the handling of WM_DPICHANGED test the gfawinx's exported variable LoadingFormPos in the WM_DPICHANGED handler, this variable is set to True when LoadFormPos is executing. This problem does not happen with a compiled to EXE application.
'
' WM_DPICHANGED (dpi-aware)
' Form.WhatsThisHelpID stores the current dpi
'
$Library "gfawinx"
DpiAwareness()
OpenW Hidden 1, 0, 0, 300, 300, ~15
' A form uses DEFAULT_GUI_FONT, but on hi-dpi display
' Windows might use a differently sized default font.
Me.FontSize = 8.25 * 96 / WinDpi(SysDpi) ' reset to form's default
' TODO: perform initialization
FormScaleForDpi(Me) ' for actual screen
LoadFormPos(Me)
Win_1.Show
Do
Sleep
Until Me Is Nothing
Sub Win_1_Destroy
SaveFormPos Win_1 ' \\HKCU\Software\UserName\AppName\Win_1Placement
EndSub
Sub Win_1_Paint
' GB's graphical commands use correct fontsize and scaling
Text 2, 2, "Hello DPI aware GB program"
Box 0, 0, 260, 30
EndSub
Sub Win_1_MessageProc(hWnd%, Mess%, wParam%, lParam%, retval%, ValidRet?)
Local Int dpi
If Mess% == WM_DPICHANGED
If FormScaleForDpi(Me, wParam%, lParam%)
' TODO: update other dpi-dependent resources
dpi = LoWord(wParam%)
EndIf
retval% = 0 : ValidRet? = True ' tell OS we handled it
EndIf
EndSub
' FormScaleForDpi is used in all dpi-aware samples
Function FormScaleForDpi(ByVal frm As Form, Optional newdpi As Int, Optional lParam As Int) As Bool
Local i As Int, c As Control, rc As RECT, prevDpi As Int, newrc As Pointer To RECT, _
tb As Tab, w As Int, h As Int
With frm
' Set initial newdpi for the form and the scale factors for a LoadForm
If .WhatsThisHelpID == 0 ' WhatsThisHelpID stores the current dpi
If Exist(":{" + .Name) ' a Form-Editor form
.WhatsThisHelpID = WinDpi(0) ' initial dpi is system newdpi
.ScaleWidth = .ScaleWidth * 96 / .WhatsThisHelpID ' set initial scale factors
.ScaleHeight = .ScaleHeight * 96 / .WhatsThisHelpID
Else ' created by OpenW or Form command
.WhatsThisHelpID = 96 ' coord space is 96
EndIf
EndIf
prevDpi = .WhatsThisHelpID
newdpi = LoWord(newdpi)
If newdpi == 0 Then newdpi = WinDpi(.hWnd)
Exit Func If newdpi == prevDpi ' don't continue if equal
' Size the form, except if it is a child window
If !(GetWindowLong(.hWnd, GWL_STYLE) %& WS_CHILD)
If lParam ' size to the OS suggested size
Pointer newrc = lParam ' ptr to RECT in lParam
SizeW .hWnd, newrc.Right - newrc.Left, newrc.Bottom - newrc.Top
Else ' scale based on the current client size
GetClientSize .hWnd, w, h ' can't use _X _Y, they are for ME only
AdjustW .hWnd, Scale(w, newdpi, prevDpi), Scale(h, newdpi, prevDpi)
EndIf
EndIf
.FontSize = .FontSize * newdpi / prevDpi
.ScaleWidth = .ScaleWidth * prevDpi / newdpi
.ScaleHeight = .ScaleHeight * prevDpi / newdpi
' Scale position and size of the Ocx windowed controls
For Each c In frm.Controls
If !(TypeOf(c) Is ImageList || TypeOf(c) Is Timer || _
TypeOf(c) Is CommDlg || TypeOf(c) Is TrayIcon) ' not the windowless OCX-es
' First set fontsize, but not all OCX-es have a Font property
If !(TypeOf(c) Is ProgressBar || TypeOf(c) Is Slider || TypeOf(c) Is Image || TypeOf(c) Is RichEdit)
c.fontsize = c.fontsize * newdpi / prevDpi
ElseIf TypeOf(c) Is RichEdit ' Requires LoadRichEdit50W!
SendMessage c.hwnd, WM_DPICHANGED, MakeLong(newdpi, newdpi), 0
EndIf
' Set new position and size for OCX-controls
GetWinPos(c.hwnd, rc) ' relative to parent
~SetWindowPos(c.hwnd, 0, _
Scale(rc.Left, newdpi, prevDpi), Scale(rc.Top, newdpi, prevDpi), _
Scale(rc.Right - rc.Left, newdpi, prevDpi), _
Scale(rc.Bottom - rc.Top, newdpi, prevDpi), SWP_NOZORDER) ' new pos and size
If TypeOf(c) Is Form ' Scale OCX-es of a child Form Ocx
c.WhatsThisHelpID = prevDpi
~FormScaleForDpi(c, newdpi)
ElseIf TypeOf(c) Is TabStrip ' Scale TabStrip's attached forms
For Each tb In c.Tabs
If TypeOf(tb.Ocx) Is Form
tb.Ocx.WhatsThisHelpID = prevDpi
~FormScaleForDpi(tb.Ocx, newdpi)
EndIf
Next
EndIf
EndIf
Next
.WhatsThisHelpID = newdpi ' store new newdpi
EndWith
FormScaleForDpi = True
EndFunc
By moving the form from one display to the other will show the results of the example. The example can only be tested if the program is run on or moved to a high-resolution display.
The form's _MessageProc event sub processes the WM_DPICHANGED message. When using two displays, of which one is a high-res screen, the message can be processed as documented when the high-res screen is to the right of the normal 96DPI screen. However, if the high-res screen is situated to the left of the 96DPI display the window movement to the high-res screen is messed up. Not only is the movement far from smooth, but the advised new dimension of the form can become corrupted. The result is unknown beforehand: the window might have the expected size or it is either too small or too large. To prevent the corruption the example introduces two static helper variables to help in a smooth movement and use the suggested size when done. The fInSizing boolean prevents the handling of WM_DPICHANGED if they are the result of SizeW. The CurDpi static variable prevents the handling if the processing and scaling for a given DPI is already done. To understand this we'll examine the situation where a window from a normal 96DPI screen is moved to a high-res screen at the left. Once the window is moved to the left screen and about 30% of the window is located at the left screen Windows sends a WM_DPICHANGED with the DPI for that screen (144 DPI, 192DPI). The default processing is to size - enlarge - the window to the new DPI. However, by enlarging it the major part of the window will return to the 96 DPI screen and a new WM_DPICHANGED with 96 dpi is sent to the window recursively. If the handler would call SizeW again the window would actually be enlarged and will be redrawn in that size. In the meanwhile, the window is being dragged to the left a new WM_DPICHANGED with 192 dpi is sent to inform that again 30% of the window is situated on the high-res screen. The WM_DPICHANGED handler will resize for 192 dpi and the whole cycle starts again. The fInSizing static variable prevents the recursive execution of the WM_DPICHANGED handler. The CurDpi variable holds the actual dpi (144, 192) the handler used to scale the window. If WM_DPICHANGED is finally called again with the same new dpi the handling is blocked, because the suggested size of the window might have become corrupted. To understand the use of these variables uncomment the Debug commands inside the WM_DPICHANGED handler.
There are two ways to make the GB program dpi-aware.
DpiAwareness, FontSize, ScaleToDpiSng, WinDpi, LoadFormPos, SaveFormPos, ScaleWidth, ScaleHeight, ScaleXYWHToDpi, WorkWidth, WorkHeight.
{Created by Sjouke Hamstra; Last updated: 03/03/2022 by James Gaite}