Creating the NonClientMetrics Class

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:

  1. Create the Font objects: In the declarations section of the module, the code includes the following declarations:
    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
  2. Copy the data into the Font objects: In the Initialize event procedure, the class needs to copy the data from each LogFont structure to each separate Font object. The NonClientMetrics class contains a private procedure, SetFontInfo, that copies all the necessary data:
    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
  3. Copy the data back from the Font objects: In the SaveSettings method of the class, the code must perform the reverse of the set of steps in the Initialize event procedure. Here, it must retrieve all the font information from the various Font objects, filling in the appropriate LogFont structures in the typNonClientMetrics structure. It uses the private GetFontInfo procedure to move the data back from the Font objects. Finally, it calls the SystemParametersInfo function to send the information back to Windows:
    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.

Working with the Face Name

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.

Working with Point Sizes

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.

© 1997 by SYBEX Inc. All rights reserved.