I'm insecure in my knowledge of fonts, particularly with regard to Hi-DPI Aware scaling of fonts. I decided to put together a test app to see how stuff works, that is, how fonts scale in relation to the controls that contain text. I suppose one way of describing my uncertainty was whether fonts scale at the same ratio as Hi-DPI Scaling Factors (I don't believe they do). My idea was as follows. Suppose I create a text string such as this...
"PowerBASIC : Compile Without Compromise!"
There are 40 characters in that string. Now suppose I put an edit control on a Form/Window/Dialog and size it so that the above string fits exactly in the edit control with not one pixel extra in any direction. In other words, I could use GetTextExtentPoint32() to determine the exact pixel height and width of that string, and I could use GetWindowRect(), GetClientRect(), and GetSystemMetrics() to come up with the exact size of the edit control for my CreateWindowEx() call to create the edit control to contain that text exactly.
The code for that is posted just below (the whole program is at the end of this post). Here is what I did. In my WM_CREATE handler code for the app I retrieved DPI settings and created a "Lucida Console" Bold Font of size 12.0...
hDC = GetDC(Wea.hWnd)
dpiX = GetDeviceCaps(hDC, %LOGPIXELSX)
dpiY = GetDeviceCaps(hDC, %LOGPIXELSY)
rxRatio = dpiX/96
ryRatio = dpiY/96
hFont = CreateFont
( _
-MulDiv(12.0, dpiY, 72), _
0, _
0, _
0, _
%FW_BOLD, _
0, _
0, _
0, _
%DEFAULT_CHARSET, _
0, _
0, _
0, _
0, _
"Lucida Console"
)
When I first ran that code I had my computer set at its standard resolution which was (its a laptop) 1920 X 1200, and in Control Panel, of the three Font sizes available (small, medium, and large), I had it set to 'small', which was the default. My dpiX and dpiY readings were 96 with rxRatio and ryRatio both 1.0 - so there was no scaling. If you build and run the below code the app will TextOut() a lot of size information and the top two lines of that are as follows...
1) rxRatio = 1.0000 X DPI Scaling Factor
2) ryRatio = 1.0000 Y DPI Scaling Factor
Next in my WM_CREATE code are lines to determine the display height and width of the text string...
"PowerBASIC : Compile Without Compromise!"
...which works out to 16 pixels high and 440 pixels wide, and GetSystemMetrics() calls to get the 3D Border widths of edit controls created with a 3D extended style...
GetTextExtentPoint32(hDC, szText, Len(szText), sz)
iCx3DBorder = GetSystemMetrics(%SM_CXEDGE)
iCy3DBorder = GetSystemMetrics(%SM_CYEDGE)
iWidth = iCx3DBorder * 2 + sz.cx
iHeight = iCy3DBorder * 2 + sz.cy
So for example, if my text string needs 440 pixels, and the GetSystemMetrics() calls indicate a border width of 2 pixels, then I'm going to need an edit control 440 pixels wide plus 2 pixels each for the left and right borders of the control, which comes to 444 pixels. The same logic applied to the height yields a requirement of 20 pixels. So my CreateWindowEx() call for the edit control, if I want to locate its top left edge 10 pixels to the right of the left edge of the containing form and 10 pixels down from the top, and I want it to exactly contain my text string with no room to spare as determined above, would be this...
hCtl = CreateWindowEx _
( _
%WS_EX_CLIENTEDGE, _
"edit", _
szText, _
%WS_CHILD Or %WS_VISIBLE, _
SizX(10), _
SizY(10), _
SizX(444), _
SizY(20), _
Wea.hWnd, _
%ID_TEXT, _
Wea.hInst, _
ByVal 0 _
)
Note that my SizX() and SizY() terms above are simple one line macros to multiply the number contained within by the rxRatio and ryRatio DPI Scaling Factors. Since those scaling factors are 1.0 when not running in a high DPI resolution altered state, they have no effect on the numbers contained within the above CreateWindowEx() call. When I run the below code of my app it indeed does work out perfectly and the edit control is sized perfectly to contain the text string exactly. This is the output from the run which displays right below the edit control...
1) rxRatio = 1.0000 X DPI Scaling Factor
2) ryRatio = 1.0000 Y DPI Scaling Factor
3) dwWndWidth = 444 Edit Control Total Width
4) dwWndHeight = 20 Edit Control Total Height
5) iCx3DBorder = 2 Edit Control 3D Border Width
6) iCy3DBorder = 2 Edit Control 3D Border Height
7) dwClientWidth = 440 Edit Control Client Rect Width
8) dwClientHeight = 16 Edit Control Client Rect Height
9) tm.tmHeight = 16 Char Total Height At Font Size
10) tm.tmAveCharWidth = 11 Char Total Width At Font Size
11) sz.cx = 440 Pixel Width of Edit Control Text
12) sz.cy = 16 Pixel Height of Edit Control Text
The variables dwWndWidth and dwWndHeight are the cx and cy parameters of the CreateWindowEx() call to create the edit control (444 wide and 20 high), and in the WM_PAINT handler code where the TextOut()s are located were actually acquired by GetWindowRect() calls which retrieved those values. The iCx3DBorder and iCy3DBorder values were acquired by GetSystemMetrics() to get the pixel sizes of the edit control borders. That works out to 2 pixels per border - so 4 across for the left/right border, and 4 up/down for the top and bottom border. The dwClientWidth and dwClientHeight variables are the dwWndWidth and dwWndHeight numbers reduced by the border widths as just described, and were actually acquired by GetClientRect(). A call to GetTextMetrics() indicated my CreateFont() call for the "Lucida Console" Bold font of 12 Logical Units (Font Size 12.0) resulted in a tmHeight of 16 pixels with a tmAveCharWidth of 11 pixels. This all adds up perfectly as the MulDiv() macro call in CreateFont() resolves to this arithmetic...
MulDiv(12.0, dpiY, 72) = (12.0 * 96) / 72 = 20
...and so we see how the client rectangle within the edit control needs to be 20 pixels high, and we need a client width of 440 pixels because 40 characters times 11 pixels width for the fixed pitch Lucida Console font yields 440 pixels. So everything is working perfectly - almost.
A bit of a problem results when the above code is run at one of the non-standard resolution or font size settings alterable through Control Panel. How much of a problem I'll let you all decide. On the positive side, if you call SetProcessDPIAware(), there is no clipping of text, no detracting 'visual artifacts' resulting from font virtualization, the text output is clear and not fuzzy and everything looks otherwise OK. Except the text string, which we took great pains to size perfectly to just fit within the edit control, no longer is sized perfectly to fit within the edit control. There is quite a bit of white space to the right of the string between it and the right border of the edit control. So in other words, the font did not scale at the same factor as the other elements of the application according to the DPI Scaling Factors. Here are the results of a program run where I set the display to a larger font in Control Panel than the default...
1) rxRatio = 1.5000 X DPI Scaling Factor
2) ryRatio = 1.5000 Y DPI Scaling Factor
3) dwWndWidth = 666 Edit Control Total Width
4) dwWndHeight = 30 Edit Control Total Height
5) iCx3DBorder = 2 Edit Control 3D Border Width
6) iCy3DBorder = 2 Edit Control 3D Border Height
7) dwClientWidth = 662 Edit Control Client Rect Width
8) dwClientHeight = 26 Edit Control Client Rect Height
9) tm.tmHeight = 24 Char Total Height At Font Size
10) tm.tmAveCharWidth = 15 Char Total Width At Font Size
11) sz.cx = 600 Pixel Width of Edit Control Text
12) sz.cy = 24 Pixel Height of Edit Control Text
As you can see above we now have a DPI Scaling Factor of 1.5. The client rectangle within the edit control capable of receiving text is now 662 pixels. However, a call to GetTextExtentPoint32() reveals that only 600 pixels are needed to display the 40 character text string at whatever the altered DPI setting had done to the Font. So naturally we have that 62 pixel blank area to the right of the string. The only conclusion one can come to is that fonts do not scale at the DPI Scaling Factors acquired like so...
Local rxRatio,ryRatio As Double
Local dpiXdpiY As Long
dpiX = GetDeviceCaps(hDC, %LOGPIXELSX)
dpiY = GetDeviceCaps(hDC, %LOGPIXELSY)
rxRatio = dpiX/96
ryRatio = dpiY/96
Either that or my CreateFont() call as provided above is not set up correctly. In looking at that again...
hDC = GetDC(Wea.hWnd)
dpiX = GetDeviceCaps(hDC, %LOGPIXELSX)
dpiY = GetDeviceCaps(hDC, %LOGPIXELSY)
rxRatio = dpiX/96
ryRatio = dpiY/96
hFont = CreateFont
( _
-MulDiv(12.0, dpiY, 72), _ 'Specifies the height, in logical units, of the font's character cell or character.
0, _ 'Specifies the average width, in logical units, of characters in the requested font.
0, _
0, _
%FW_BOLD, _
0, _
0, _
0, _
%DEFAULT_CHARSET, _
0, _
0, _
0, _
0, _
"Lucida Console"
)
...the first parameter where I'm using the MulDiv() call specifically is referring to the height of the font - not the width. The second parameter refers to the width, and I can see I left that blank. So maybe that's the source of my problem. I don't know. As I said, I don't fully understand this. I will say though that putting in a value of 15 for that second parameter reduces the white space. But its still not exact. And putting in a 16 clips the string in the edit control.
What I'm wondering and beginning to believe might be true is that these things aren't suppossed to scale exactly. In the documentation on CreateFont() are these words...
For all height comparisons, the font mapper looks for the largest font that does not exceed the requested size.
and these...
Specifies the average width, in logical units, of characters in the requested font. If this value is zero, the font mapper chooses a closest match value. The closest match value is determined by comparing the absolute values of the difference between the current device's aspect ratio and the digitized aspect ratio of available fonts.
Note that it is possible to make everything match up perfectly if one specifically codes sizing code. In the app I'm posting below you'll see this CreateWindowEx() call commented out...
iCx3DBorder = GetSystemMetrics(%SM_CXEDGE)
iCy3DBorder = GetSystemMetrics(%SM_CYEDGE)
iWidth = iCx3DBorder * 2 + sz.cx
iHeight = iCy3DBorder * 2 + sz.cy
'hCtl = CreateWindowEx _
( _
%WS_EX_CLIENTEDGE, _
"edit", _
szText, _
%WS_CHILD Or %WS_VISIBLE, _
SizX(10), _
SizY(10), _
iWidth, _
iHeight, _
Wea.hWnd, _
%ID_TEXT, _
Wea.hInst, _
ByVal 0 _
)
If you uncomment that one and comment out the active one below it and run the app you'll see what I was hoping would occur but doesn't through the DPI Scaling Factors. That it doesn't possibly makes the point better that Fonts don't scale as per the DPI Scaling Factors.
I'd appreciate any clarifications corrections or opinions anyone might have regarding this or my program. Here is the test program where these things can be examined and experimented with...
#Compile Exe
#Dim All
%UNICODE = 1
#If %Def(%UNICODE)
Macro ZStr = WStringz
Macro BStr = WString
#Else
Macro ZStr = Asciiz
Macro BStr = String
#EndIf
Macro SizX(x) = x*rxRatio
Macro SizY(y) = y*ryRatio
#Include "Windows.inc"
%ID_TEXT = 1500
Type WndEventArgs
wParam As Long
lParam As Long
hWnd As Dword
hInst As Dword
End Type
Type MessageHandler
wMessage As Long
dwFnPtr As Dword
End Type
Declare Function FnPtr(wea As WndEventArgs) As Long
Declare Function SetProcessDpiAware() As Long
Function SetMyProcessDpiAware() As Long
Local hInstance,pFn,blnReturn As Dword
hInstance=LoadLibrary("user32.dll")
If hInstance Then
pFn=GetProcAddress(hInstance,"SetProcessDPIAware")
If pFn Then
Call Dword pFn Using SetProcessDpiAware To blnReturn
End If
End If
Function=blnReturn
End Function
Function fnWndProc_OnCreate(Wea As WndEventArgs) As Long
Local iCx3DBorder,iCy3DBorder,iWidth,iHeight,dpiX,dpiY As Long
Local pCreateStruct As CREATESTRUCT Ptr
Local hCtl,hDC,hFont,hTmp As Dword
Local rxRatio,ryRatio As Single
Local szText As ZStr*64
Local sz As SIZE
pCreateStruct=Wea.lParam : Wea.hInst=@pCreateStruct.hInstance
hDC=GetDC(Wea.hWnd)
dpiX=GetDeviceCaps(hDC, %LOGPIXELSX)
dpiY=GetDeviceCaps(hDC, %LOGPIXELSY)
rxRatio = dpiX/96
ryRatio = dpiY/96
Call MoveWindow(Wea.hWnd, SizX(200), SizY(100), SizX(775), SizY(475), %FALSE)
hFont=CreateFont(-MulDiv(12.0, dpiY, 72),0,0,0,%FW_BOLD,0,0,0,%DEFAULT_CHARSET,0,0,0,0,"Lucida Console")
SetWindowLong(Wea.hWnd,0,hFont)
hTmp=SelectObject(hDC,hFont)
szText="PowerBASIC : Compile Without Compromise!"
GetTextExtentPoint32(hDC,szText,Len(szText),sz)
iCx3DBorder=GetSystemMetrics(%SM_CXEDGE)
iCy3DBorder=GetSystemMetrics(%SM_CYEDGE)
iWidth = iCx3DBorder * 2 + sz.cx
iHeight = iCy3DBorder * 2 + sz.cy
'hCtl=CreateWindowEx(%WS_EX_CLIENTEDGE,"edit",szText,%WS_CHILD Or %WS_VISIBLE,SizX(10),SizY(10),iWidth,iHeight,Wea.hWnd,%ID_TEXT,Wea.hInst,ByVal 0)
hCtl=CreateWindowEx(%WS_EX_CLIENTEDGE,"edit",szText,%WS_CHILD Or %WS_VISIBLE,SizX(10),SizY(10),SizX(444),SizY(20),Wea.hWnd,%ID_TEXT,Wea.hInst,ByVal 0)
SendMessage(hCtl,%WM_SETFONT,hFont,0)
SelectObject(hDC,hTmp)
ReleaseDC(Wea.hWnd,hDC)
fnWndProc_OnCreate=0
End Function
Function fnWndProc_OnPaint(Wea As WndEventArgs) As Long
Local dwWndHeight,dwWndWidth,dwClientHeight,dwClientWidth As Dword
Local iBkMode,dpiX,dpiY,iCx3DBorder,iCy3DBorder As Long
Local szBuffer,szText As ZStr*80
Local rxRatio,ryRatio As Double
Local hDC,hFont,hTmp As Dword
Local ps As PAINTSTRUCT
Local tm As TEXTMETRIC
Local hCtl As Dword
Local rc As RECT
Local sz As SIZE
hDC=BeginPaint(Wea.hWnd,ps)
iBkMode=SetBkMode(hDC,%TRANSPARENT)
dpiX=GetDeviceCaps(hDC, %LOGPIXELSX)
dpiY=GetDeviceCaps(hDC, %LOGPIXELSY)
rxRatio = dpiX/96
ryRatio = dpiY/96
hFont=GetWindowLong(Wea.hWnd,0)
hTmp=SelectObject(hDC,hFont)
' Output rxRatio/ryRatio, i.e., DPI Scaling Factors
szBuffer="1) rxRatio = " & Format$(rxRatio,"#.0000") & " X DPI Scaling Factor" : TextOut(hDC,SizX(10),SizY(55),szBuffer,Len(szBuffer))
szBuffer="2) ryRatio = " & Format$(ryRatio,"#.0000") & " Y DPI Scaling Factor" : TextOut(hDC,SizX(10),SizY(85),szBuffer,Len(szBuffer))
' Get HWND of Edit Control And Get Overall Size Of Edit Control As When It Was Created With CreateWindowEx() in fnWndProc_OnCreate().
hCtl=GetDlgItem(Wea.hWnd,%ID_TEXT)
GetWindowRect(hCtl,rc)
dwWndWidth=rc.Right-rc.Left
szBuffer="3) dwWndWidth = " & LTrim$(Str$(dwWndWidth)) & " Edit Control Total Width" : TextOut(hDC, SizX(10), SizY(115), szBuffer, Len(szBuffer))
dwWndHeight=rc.Bottom-rc.Top
szBuffer="4) dwWndHeight = " & LTrim$(Str$(dwWndHeight)) & " Edit Control Total Height" : TextOut(hDC, SizX(10), SizY(145), szBuffer, Len(szBuffer))
' Get From GetSystemMetrics() The 3D Size of the Edit Control Borders, Which Reduces The Usable Space Inside The Edit Control For Text By Several Pixels.
iCx3DBorder=GetSystemMetrics(%SM_CXEDGE)
iCy3DBorder=GetSystemMetrics(%SM_CYEDGE)
szBuffer="5) iCx3DBorder = " & LTrim$(Str$(iCx3DBorder)) & " Edit Control 3D Border Width" : TextOut(hDC,SizX(10), SizY(175), szBuffer, Len(szBuffer))
szBuffer="6) iCY3DBorder = " & LTrim$(Str$(iCy3DBorder)) & " Edit Control 3D Border Height" : TextOut(hDC,SizX(10), SizY(205), szBuffer, Len(szBuffer))
' Get The Usable Space Inside The Edit Control For Text, which numbers can be gotten from GetClientRect().
GetClientRect(hCtl,rc)
dwClientWidth=rc.Right-rc.Left
szBuffer="7) dwClientWidth = " & LTrim$(Str$(dwClientWidth)) & " Edit Control Client Rect Width" : TextOut(hDC, SizX(10), SizY(235), szBuffer, Len(szBuffer))
dwClientHeight=rc.Bottom-rc.Top
szBuffer="8) dwClientHeight = " & LTrim$(Str$(dwClientHeight)) & " Edit Control Client Rect Height" : TextOut(hDC, SizX(10), SizY(265), szBuffer, Len(szBuffer))
' Get The Size, i.e., TEXTMETRICS, Of The Font Currently Selected Into The Device Context.
Call GetTextMetrics(hDC,tm)
szBuffer="9) tm.tmHeight = " & LTrim$(Str$(tm.tmHeight)) & " Char Total Height At Font Size" : TextOut(hDC, SizX(10), SizY(295), szBuffer, Len(szBuffer))
szBuffer="10) tm.tmAveCharWidth = " & LTrim$(Str$(tm.tmAveCharWidth)) & " Char Total Width At Font Size" : TextOut(hDC, SizX(10), SizY(325), szBuffer, Len(szBuffer))
' Get The Text Out Of The Edit Control And Determine Its Length And Height In Pixels With GetTextExtentPoints32().
GetWindowText(GetDlgItem(Wea.hWnd,%ID_TEXT),szBuffer,64)
GetTextExtentPoint32(hDC,szBuffer,Len(szBuffer),sz)
szBuffer="11) sz.cx = " & LTrim$(Str$(sz.cx)) & " Pixel Width of Edit Control Text" : TextOut(hDC,SizX(10), SizY(355), szBuffer, Len(szBuffer))
szBuffer="12) sz.cy = " & LTrim$(Str$(sz.cy)) & " Pixel Height of Edit Control Text" : TextOut(hDC,SizX(10), SizY(385), szBuffer, Len(szBuffer))
' Close Out, Restore Device Context, And Exit Procedure.
Call SetBkMode(hDC,iBkMode)
SelectObject(hDC,hTmp)
Call EndPaint(Wea.hWnd,ps)
fnWndProc_OnPaint=0
End Function
Function fnWndProc_OnDestroy(Wea As WndEventArgs) As Long
Local blnFree As Long
Local hFont As Dword
hFont=GetWindowLong(Wea.hWnd,0)
blnFree=DeleteObject(hFont)
Call PostQuitMessage(0)
Function=0
End Function
Sub AttachMessageHandlers()
Dim MsgHdlr(2) As Global MessageHandler 'Associate Windows Message With Message Handlers
MsgHdlr(0).wMessage=%WM_CREATE : MsgHdlr(0).dwFnPtr=CodePtr(fnWndProc_OnCreate)
MsgHdlr(1).wMessage=%WM_PAINT : MsgHdlr(1).dwFnPtr=CodePtr(fnWndProc_OnPaint)
MsgHdlr(2).wMessage=%WM_DESTROY : MsgHdlr(2).dwFnPtr=CodePtr(fnWndProc_OnDestroy)
End Sub
Function fnWndProc(ByVal hWnd As Long,ByVal wMsg As Long,ByVal wParam As Long,ByVal lParam As Long) As Long
Local Wea As WndEventArgs
Register iReturn As Long
Register i As Long
For i=0 To 2
If wMsg=MsgHdlr(i).wMessage Then
Wea.hWnd=hWnd: Wea.wParam=wParam: Wea.lParam=lParam
Call Dword MsgHdlr(i).dwFnPtr Using FnPtr(Wea) To iReturn
fnWndProc=iReturn
Exit Function
End If
Next i
fnWndProc=DefWindowProc(hWnd,wMsg,wParam,lParam)
End Function
Function WinMain(ByVal hInstance As Long, ByVal hPrevIns As Long, ByVal lpCmdLn As ZStr Ptr, ByVal iShowWnd As Long) As Long
Local szAppName As ZStr*16
Local wc As WNDCLASSEX
Local hWnd As Dword
Local Msg As tagMsg
SetMyProcessDpiAware() : Call AttachMessageHandlers()
szAppName = "Font2" : wc.lpszClassName = VarPtr(szAppName)
wc.lpfnWndProc = CodePtr(fnWndProc) : wc.hInstance = hInstance
wc.hCursor = LoadCursor(%NULL, ByVal %IDC_ARROW) : wc.hbrBackground = %COLOR_BTNFACE+1
wc.cbSize = sizeof(WNDCLASSEX) : wc.cbWndExtra = 4
Call RegisterClassEx(wc)
hWnd=CreateWindowEx(%WS_EX_CLIENTEDGE,szAppName,szAppName,%WS_OVERLAPPEDWINDOW,0,0,0,0,0,0,hInstance,ByVal 0)
Call ShowWindow(hWnd,iShowWnd)
While GetMessage(Msg,%NULL,0,0)
TranslateMessage Msg
DispatchMessage Msg
Wend
Function=msg.wParam
End Function