Although its read-only properties (Caption, SmallCaption, FixedBorderX, and FixedBorderY) rely on the GetSystemMetrics function to retrieve their values, like several other classes in this chapter, the NonClientMetrics class is centered around a single data structure, the typNonClientMetrics structure:
Private Type typNonClientMetrics
cbSize As Long
lngBorderWidth As Long
lngScrollWidth As Long
lngScrollHeight As Long
lngCaptionWidth As Long
lngCaptionHeight As Long
lfCaptionFont As LogFont
lngSMCaptionWidth As Long
lngSMCaptionHeight As Long
lfSMCaptionFont As LogFont
lngMenuWidth As Long
lngMenuHeight As Long
lfMenuFont As LogFont
lfStatusFont As LogFont
lfMessageFont As LogFont
End Type
In the Initialize event procedure for the class, the code calls the SystemParametersInfo function, passing a typNonClientMetrics structure to be filled in. From then on, all the properties use the settings in this structure for retrieving and setting properties.
The problem, as you can see, is that several members of this structure aren’t simple variables; each is itself another data structure, the LogFont structure:
Const LF_FACESIZE = 32
Private Type LogFont
lfHeight As Long
lfWidth As Long
lfEscapement As Long
lfOrientation As Long
lfWeight As Long
lfItalic As Byte
lfUnderline As Byte
lfStrikeOut As Byte
lfCharSet As Byte
lfOutPrecision As Byte
lfClipPrecision As Byte
lfQuality As Byte
lfPitchAndFamily As Byte
lfFaceName(0 To LF_FACESIZE - 1) As Byte
End Type
Clearly, there is more information here than is necessary for the fonts used by the NonClientMetrics object. The question, then, was how to expose all the information you need in order to use the object but not end up with an overload of similar, but differently named, properties.
The answer, in this case, was to create a simple Font class, with the properties shown earlier in Table 9.17. This way, the NonClientMetrics class can create five instances of the class to maintain the font information it needs for each of its five font properties, and you can access those properties using objects within your NonClientMetrics object. In addition, the Font class can expose just the properties that make sense for this situation, not every portion of the LogFont data structure.
To use the Font class, the NonClientMetrics class needs to take three distinct steps:
Dim oCaptionFont As New Font
Dim oSMCaptionFont As New Font
Dim oMenuFont As New Font
Dim oStatusFont As New Font
Dim oMessageFont As New Font
Private Sub Class_Initialize()
Dim lngLen As Long
lngLen = Len(ncm)
ncm.cbSize = lngLen
Call SystemParametersInfo(SPI_GETNONCLIENTMETRICS, _
lngLen, ncm, 0)
Call SetFontInfo(ncm.lfCaptionFont, oCaptionFont)
Call SetFontInfo(ncm.lfMenuFont, oMenuFont)
Call SetFontInfo(ncm.lfMessageFont, oMessageFont)
Call SetFontInfo(ncm.lfSMCaptionFont, oSMCaptionFont)
Call SetFontInfo(ncm.lfStatusFont, oStatusFont)
End Sub
Public Sub SaveSettings()
' Save all changed settings.
Dim lngLen As Long
lngLen = Len(ncm)
ncm.cbSize = lngLen
' Need to copy all the font values back into the
' LogFont structures.
Call GetFontInfo(ncm.lfCaptionFont, oCaptionFont)
Call GetFontInfo(ncm.lfMenuFont, oMenuFont)
Call GetFontInfo(ncm.lfMessageFont, oMessageFont)
Call GetFontInfo(ncm.lfSMCaptionFont, oSMCaptionFont)
Call GetFontInfo(ncm.lfStatusFont, oStatusFont)
' Now save all the settings back to Windows.
Call SystemParametersInfo(SPI_SETNONCLIENTMETRICS, _
lngLen, ncm, SPIF_TELLALL)
End Sub
The SetFontInfo and GetFontInfo procedures are both simple, moving data from one data structure into an equivalent object and back. There are two interesting challenges along the way, however:
The next two sections deal with these issues.
In order to support international versions, VBA stores all its strings internally, using the Unicode character mapping. In Unicode, each visible character takes up two bytes of internal storage. Because Windows 95 does not support Unicode and supports only the ANSI character mapping (with one byte for each character), VBA must convert strings it sends to and from the Windows API into ANSI. This happens regardless of whether the application is running under Windows 95 or under Windows NT (which does completely support Unicode).
The problem in working with the font name in the NonClientMetrics class is that the LogFont structure retrieves the font name (also referred to as its face name) into an ANSI string, which you must convert to Unicode before manipulating the font name within VBA. Luckily, VBA provides the StrConv function, which allows you to convert strings to and from Unicode. The NonClientMetrics class uses this function to perform the conversion. (See Chapter 1 for more information on working with strings and byte arrays.)
The font name member of the LogFont structure is declared like this:
lfFaceName(0 To LF_FACESIZE - 1) As Byte
where FACESIZE is a constant with a value of 32. Why isn’t lfFaceName simply declared as a 32-byte string? The problem is that if you tell VBA that a variable contains a string, it assumes that that string is stored in Unicode and processes it as though it contained two bytes per character. Declaring the value as an array of bytes keeps VBA from mangling the string. Figure 9.16 shows the Immediate window displaying both the raw ANSI and converted Unicode versions of the value in the lfFaceName field of a LogFont structure.
Figure 9.16: ANSI and Unicode representations of the ANSI FaceName element
The problem, then, is getting the array of bytes in and out of a Unicode string. Getting it into a string is simple: because the StrConv function can take a byte array as its input, you can use it to move the byte array, converted to Unicode, directly into a string. The SetFontInfo procedure in the NonClientMetrics class does just this:
With oFont
' Code removed...
.FaceName = dhTrimNull(StrConv(lf.lfFaceName, vbUnicode))
End With
One final issue in this conversion is the removal of extra “junk” after the name of the font. The dhTrimNull function (borrowed from Chapter 1 of this book), also in the NonClientMetrics class, looks for the first null character (vbNullChar) in a string and truncates the string at that point. Because the Windows API returns strings with an embedded Null indicating the end of the string, dhTrimNull is a very useful tool whenever you’re moving strings from a Windows API function call into VBA.
The tricky issue is getting the string back into the byte array in the LogFont structure in order to send the information back to Windows. Although you can assign a variant directly into a dynamic byte array, you cannot do the same with a fixed-size array, which is exactly what LogFont contains. Therefore, to get the text back into that array, the SetFaceName procedure in NonClientMetrics must traverse the input string, byte by byte, once it’s converted the string back to ANSI.
Listing 9.12 shows the entire SetFaceName procedure. This procedure starts out by converting the Unicode string containing the new face name back into ANSI, using the StrConv function. StrConv places its return value into a dynamic byte array. (The byte array makes it fast and simple to traverse the string, one byte at a time, later on.)
abytTemp = StrConv(strValue, vbFromUnicode)
Then the code places the length of the string into the intLen variable. Because the array filled in by the StrConv function is zero-based, the number of items in the array is 1 greater than its UBound:
intLen = UBound(abytTemp) + 1
The LogFont fixed-sized array can hold only LF_FACESIZE – 1 characters, so the code next checks to make sure it’s not going to try to write more characters than that to the structure:
If intLen > LF_FACESIZE - 1 Then
intLen = LF_FACESIZE - 1
End If
Finally, it’s time to write the bytes into the structure, using a simple loop:
For intI = 0 To intLen - 1
lf.lfFaceName(intI) = abytTemp(intI)
Next intI
As the final step, the code inserts a null value (0) into the final position in the string. The Windows API expects to find this value as the string delimiter, and bypassing this step can cause trouble for your API calls. (Normally, when you pass a string to the API, you needn’t worry about this—it’s only because we’ve copied the string in, one byte at a time, that it’s an issue at all.)
Listing 9.12: Moving a Unicode String Back into an ANSI Byte Array Takes a Few Steps
Private Sub SetFaceName(lf As LogFont, strValue As String)
' Given a string, get it back into the ANSI byte array
' contained within a LOGFONT structure.
Dim intLen As String
Dim intI As Integer
Dim varName As Variant
Dim abytTemp() As Byte
abytTemp = StrConv(strValue, vbFromUnicode)
intLen = UBound(abytTemp) + 1
' Make sure the string isn’t too long.
If intLen > LF_FACESIZE - 1 Then
intLen = LF_FACESIZE - 1
End If
For intI = 0 To intLen - 1
lf.lfFaceName(intI) = abytTemp(intI)
Next intI
lf.lfFaceName(intI) = 0
End Sub
Although it’s a bit more work to get the Unicode string back into the ANSI buffer than it was to get the ANSI buffer into a Unicode string, once you’ve got the code worked out, you needn’t worry about it in the future. The NonClientMetrics class uses this code whenever you work with any of the Font objects, and should you need to use this functionality in any of your own classes (Windows uses the LogFont structure in many situations), you can lift the code from this class.
The LogFont structure maintains information about the font’s height and width in pixels rather than its point size. It’s the font’s point size that you see when you choose a font in any Windows application, however. Therefore, to make the Font object’s Size property work as you’d expect, the NonClientMetrics class must convert the font height into a standard point size.
When Windows provides the font width and height in the LogFont structure, it fills in 0 for the lfWidth member and a negative value for the lfHeight member. The negative value indicates internally that Windows should provide the closest match for the character height requested. The code in NonClientMetrics must, therefore, convert to and from that negative value in the lfHeight member of the LogFont structure.
When converting from the LogFont structure into points, the formula to use is
points = -Int(lngHeight * 72 / lngLogPixelsY)
where lngLogPixelsY is the number of pixels per logical inch on the screen. Because there are 72 points per logical inch, this calculation converts from pixels (the value in the LogFont structure) to points.
Where does the value for lngLogPixelsY come from? Windows itself provides this information, using the GetDeviceCaps API function. If you’re interested, check out the code in the CalcSize procedure in the NonClientMetrics class. This value returns, for the specific screen driver, the number of screen pixels there are per logical inch of screen real estate. (The driver itself converts from logical inches to real inches, but that isn’t part of this story.)
Converting back from points to pixels, when saving a new font size, is no more difficult. The formula for this conversion is
pixels = -Int(lngHeight * lngLogPixelsY / 72)
The CalcSize procedure in NonClientMetrics takes care of both translations for you. It’s called whenever you move font information to or from a LogFont structure.