Class Act

Resolving Class Envy

Creating a ScreenInfo Class for Use in VB6 or VBA6

By Ken Getz and Mike Gilbert

When you switch development environments often, working one day in Visual Basic 6.0 (VB6) and the next in Excel 2000, for example, you may find yourself suffering from a true cognitive dissonance: built-in objects you used yesterday in VB6 simply don't exist in Office's implementation of VBA. This happened to us recently when working on a project that required both Office and VB6. We grew comfortable using the Screen object in Visual Basic, which allowed us to retrieve information about the user's environment, and required the same functionality when working in Access and Excel. Among other things, we needed to know how big the screen was, and what fonts were available.

We finally grew tired of fighting the differences, and wrote the ScreenInfo class we needed. We gave it a unique name, different from VB's built-in Screen object, so Visual Basic developers could use it as well without conflicts. The class features functionality beyond that of the standard Screen object, so even VB developers will find this class new and useful.

The ScreenInfo class presented in this article uses techniques that aren't available in Office 97, such as Enums and the AddressOf operator. You can work around both these issues, if you must, in Office 97, but this class was written with VB5, VB6, and Office 2000 (VBA 6.0) developers in mind. To demonstrate some of the features of the ScreenInfo class, we've included a sample form (in Excel 2000 and VB6) that exercises some of the properties and methods (see FIGURE 1). This sample form lists all the installed screen fonts and the available display modes, and allows you to select the ones you want (available for download; see end of article for details).

FIGURE 1: This sample form demonstrates many ScreenInfo properties and methods.

The features of the ScreenInfo class can be divided into three major areas: display information, display modes, and screen-font information. The descriptions that follow also separate the properties and methods into these groupings. FIGURE 2 lists the methods and properties of the ScreenInfo class.

Property/Method Return Type Input Value Description
BitsPerPixel Integer Returns number of bits used displaying each pixel. Screen can display 2 ^ BitsPerPixel colors.
Height* Single Returns height of full screen in twips.
Width* Single Returns width of full screen in twips.
HeightInPixels Long Returns height of full screen in pixels.
WidthInPixels Long Returns width of full screen in pixels.
TwipsPerPixelX* Single Returns width of pixel in twips.
TwipsPerPixelY* Single Returns height of pixel in twips.
VerticalRefresh Integer Returns the vertical refresh speed, in Hz.
ChangeDisplayMode

Long

Integer indicating index within array of available display modes Change current display mode, with a bit of user interface; if new mode requires rebooting, or the mode change fails, the method provides a MsgBox call.
CurrentDisplayMode

Integer Returns index into array of available display modes that represents current display mode.
DisplayModesCount Integer Returns number of available display modes.
DisplayModesText

String

Integer indicating index in array of display modes Given an index, returns text describing a specific display mode, e.g. 640 by 480 pixels, 256 colors, 60 Hz.
SetDisplayMode

Long

Integer indicating index in array of display modes Changes the display mode, with no user interface. Returns the error code, if any, from the API call. ChangeDisplayMode calls this function to do its work.
FontCount* Integer Returns number of available screen fonts.
FontInfo

Variant

Integer indicating index within array of available fonts. Given an index and an enumerated value, returns a specific piece of information about a particular screen font. For more information see the LOGFONT structure in MSDN, and the LogFontInfo enumeration in the ScreenInfo class module.
Fonts*

String

Integer indicating index within array of available fonts. Given an index, returns name of requested screen font.

FIGURE 2: The properties and methods of the ScreenInfo class (*emulations of properties in VB's Screen object).

Using ScreenInfo

To use the ScreenInfo class in any VBA 6.0 host (which includes VB6 and Office 2000), you'll need to add the ScreenInfo class (ScreenInfo.cls) and the basEnumFonts module (EnumFonts.bas) to your project. We'll explain later why this example requires two modules. (Both modules are available for download; see end of article for details.)

Once you've added the modules to your project, use ScreenInfo just like any other class. That is, create a variable of type ScreenInfo, and instantiate it:

Dim scr As ScreenInfo
Set scr = New ScreenInfo

When you instantiate the object, its Initialize event procedure does a bit of work. It retrieves a list of all the installed fonts, retrieves a list of available display modes, and gathers information about the current display status. Later sections discuss exactly how to use each property, but if your intent is to fill a list box with all the screen fonts, you could write simple code like this:

lstFonts.Clear
For i = 0 To scr.FontCount - 1
  lstFonts.AddItem scr.Fonts(i)
Next I

Information About the Current Display Mode

Several of ScreenInfo's properties retrieve information about the current display mode. These are all simple properties, returning a single value. The following list describes each property:

BitsPerPixel: Windows requires some amount of screen memory to display each "dot" (pixel) on the screen. This property, the number of bits required for each pixel, is often referred to as the "color depth" of the display mode. The more bits used per pixel, the higher the color depth, and the more available colors you have to work with.

Height, Width: These two properties return screen measurements in twips. (The names of the properties, which might otherwise be "HeightInTwips" and "WidthInTwips," are so named to be consistent with properties of the VB Screen object.) These properties can be useful when you're working with other coordinates in twips in VBA code. For a 1024x768 screen, these two properties will return values like 15360 and 11520. Use the TwipsPerPixelX and TwipsPerPixelY properties to convert between twips and pixels.

HeightInPixels, WidthInPixels: Often, you'll need to know the screen resolution measured in pixels (that is, a measurement like a width of 1024 and a height of 768 — a standard Windows screen size). These two properties return the size of the screen, measured in pixels. Use the TwipsPerPixelX and TwipsPerPixelY properties to convert between twips and pixels.

TwipsPerPixelX, TwipsPerPixelY: These two properties provide the ratio between twips and pixels. The screen driver maintains this information for the current screen mode.

VerticalRefresh: The vertical refresh rate of the display mode controls how quickly the display can move from one scan line to the next. In general, the higher this number, the less flickering you see on your display. Most screen drivers offer multiple vertical refresh rates for a given resolution.

Retrieving the information for these properties requires a number of Windows API function calls. Most of these function calls require you to supply a "device context," i.e. a structure that describes the attributes of a particular output device to Windows. You can retrieve a device context using the GetDC API function, and you must remember to release that device context (often referred to as a hDC, or "handle to a Device Context") when you're done with it, using the ReleaseDC function. The Windows API provides several ways to retrieve the device context for the Screen object, but the simplest we've found is to call GetDC passing in a constant that informs the function to retrieve the device context of the top-most desktop window:

Const HWND_DESKTOP = 0

Dim hDC As Long
   
' Get the device context for the full screen.
hDC = GetDC(HWND_DESKTOP)

' Later, release the device context.
Call ReleaseDC(HWND_DESKTOP, hDC)

To retrieve the width and height of the screen in pixels, the class calls the GetSystemMetrics API function, requesting these two particular bits of information:

Private Enum gsmIndex
  SM_CXSCREEN = 0
  SM_CYSCREEN = 1
End Enum

mintWidthInPixels = GetSystemMetrics(SM_CXSCREEN)
mintHeightInPixels = GetSystemMetrics(SM_CYSCREEN)

To retrieve the bits/pixel ratio (color depth) and the vertical refresh rate, the class calls the GetDeviceCaps API function:

Private Enum DeviceCaps
  LOGPIXELSX = 88
  LOGPIXELSY = 90
  BITSPIXEL = 12
  VREFRESH = 116
End Enum

mintBitsPerPixel = GetDeviceCaps(hDC, BITSPIXEL)
mintVerticalRefresh = GetDeviceCaps(hDC, VREFRESH)

To calculate the twips-per-pixel ratio, the class calls a private function, TwipsPerPixel, passing in the hDC value, along with the value to request from GetDeviceCaps (LOGPIXELSX or LOGPIXELSY). Note that GetDeviceCaps returns its value in pixels per inch, so the code converts from pixels/inch to twips/pixel by dividing twips/inch (1440) by the pixels/inch return value. If you remember your high school math, (twips/inch)/(pixels/inch) returns a value measured in twips per pixel:

Private Const conTwipsPerInch = 1440

msngTwipsPerPixelX = TwipsPerPixel(hDC, LOGPIXELSX)
msngTwipsPerPixelY = TwipsPerPixel(hDC, LOGPIXELSY)

' Calculate number of twips per pixel in the vertical or
' horizontal direction. Used by Class_Initialize.
Private Function TwipsPerPixel(hDC As Long, _
                               lngDirection As Long)
  Dim lngTemp As Long

  lngTemp = GetDeviceCaps(hDC, lngDirection)
  If lngTemp > 0 Then
    ' We need twips/pixel. Take 1440 twips/inch divided by
    ' lngTemp pixels/inch to get twips/pixel.
    TwipsPerPixel = conTwipsPerInch / lngTemp
  End If

End Function

For each of the properties calculated in the previous code fragments, the ScreenInfo class provides a Property Get procedure to expose the property value. The Width and Height properties are calculated based on other properties, so their code looks like this:

Public Property Get Height() As Single
  ' Return height of the screen, in twips (just as with 
  ' VB's Screen.Height property).
  Height = mintHeightInPixels * msngTwipsPerPixelY
End Property

Public Property Get Width() As Single
  ' Return width of the screen, in twips (just as with 
  ' VB's Screen.Width property).
  Width = mintWidthInPixels * msngTwipsPerPixelX
End Property

(Note: For complete information on retrieving information on Windows and its devices, visit http://www.microsoft.com/msdn, or use the MSDN CDs to investigate the GetSystemMetrics and GetDeviceCaps functions.)

Listing and Changing Display Modes

Almost all Windows display drivers support multiple display modes. These modes differ in screen resolution (width and height), color depth, and vertical refresh rate. Windows makes it possible to enumerate all available display modes so you can provide a list of modes to users. In addition, you can specify a new screen mode, and Windows will attempt to switch to that mode.

To accomplish these tasks, you'll need two Windows API functions (EnumDisplaySettings and ChangeDisplaySettings) and a user-defined type, the DEVMODE structure, a conglomerate of information about any type of output device. The standard DEVMODE structure, used throughout Windows to handle printers, displays, and other output devices, contains approximately 25 fields, including many that pertain only to printers (paper size, default bin, etc.).

As a practical matter, you only need five of the fields when working with display devices. These fields are stored contiguously in DEVMODE, which makes them handy to work with. We can "factor out" those five fields, make our own little structure, and work with that instead. Because we're keeping a big array full of information on available display modes, it's better to use the smallest amount of memory possible — using the smaller DisplayMode instead of DEVMODE saves time and memory. FIGURE 3 shows the definitions for DisplayMode and DEVMODE.

Private Const CCHDEVICENAME = 32
Private Const CCHFORMNAME = 32

Private Type DisplayMode
  dsBitsPerPel As Long
  dsPelsWidth As Long
  dsPelsHeight As Long
  dsDisplayFlags As Long
  dsDisplayFrequency As Long
End Type

Private Type DEVMODE
  dmDeviceName(1 To CCHDEVICENAME) As Byte
  dmSpecVersion As Integer
  dmDriverVersion As Integer
  dmSize As Integer
  dmDriverExtra As Integer
  dmFields As Long
  dmOrientation As Integer
  dmPaperSize As Integer
  dmPaperLength As Integer
  dmPaperWidth As Integer
  dmScale As Integer
  dmCopies As Integer
  dmDefaultSource As Integer
  dmPrintQuality As Integer
  dmColor As Integer
  dmDuplex As Integer
  dmYResolution As Integer
  dmTTOption As Integer
  dmCollate As Integer
  dmFormName(1 To CCHFORMNAME) As Byte
  dmUnusedPadding As Integer
  dmDisplayMode As DisplayMode
End Type

FIGURE 3: The DisplayMode structure "factors out" the important parts of the larger DEVMODE structure.

To enumerate the available display settings, you must call the EnumDisplaySettings API function, passing it an empty string (indicating that you want information on the current display device), the index of the mode you're querying, and a DEVMODE structure to contain the returned information. The function returns a non-zero value if successful. If it fails, there are no more display settings, and you know you're done looping through all the modes.

In ScreenInfo, the GetAllDisplayModes function does the work of building an array of all available screen modes, as well as storing information about each in a local array of DisplayMode structures. The function returns that array as its return value. (Note: the ability to return an array as the return value of a function is new in VBA 6.0. This alters the way we've written code for years. Rather than using global arrays, or passing arrays as ByRef parameters, you can now simply return an array. Very clean!) The function takes three steps.

Step 1. It loops through the available display modes, using EnumDisplaySettings (later code increments the value of mintDispModeCount):

' Ask for each mode in turn, starting with 0.
Do
  lngRetval = EnumDisplaySettings( _
                vbNullString, mintDispModeCount, dm)

  ' Code from step 2 removed here.
Loop While lngRetval <> 0

Step 2. If EnumDisplaySettings signaled that it succeeded, it stores the display settings values from DEVMODE into the array of DisplayMode structures, and increments the counter variable:

If lngRetval <> 0 Then
  ' Store mode information and go on to the next one.
  aDisplayModes(mintDispModeCount) = dm.dmDisplayMode
  mintDispModeCount = mintDispModeCount + 1
End If

Step 3. After "visiting" each display mode, the function resizes the output array so it contains exactly the right number of elements:

If mintDispModeCount > 0 Then
  ReDim Preserve aDisplayMode(0 To mintDispModeCount - 1)
End If

When it's done, the function returns the array full of DisplayMode structures, and the class can use that as needed. By the way, this function uses a common technique for filling an array, resizing as it does its work. That is, it starts with an empty array, and includes error-handling code that traps for the "subscript out of range" error. When that error occurs, the error handler adds an arbitrary number of new elements (we chose 100 in this example). This keeps the array happy for a while, and when it runs out again, the error handler adds more. Finally, on the way out of the function, the code resizes the array to fit the actual number of elements, as shown in step 3. Using this technique avoids the many expensive ReDim Preserve... statements that would otherwise be necessary. Once it's done, it's filled in the array of DisplayMode structures, and the count of items, as well. Given that information, the class can also provide its DisplayModesCount property. FIGURE 4 shows the entire GetAllDisplayModes function.

' Retrieve all display settings, and add them to the array
' named aDisplayModes. This doesn't require a callback 
' function so you can do the work here in the class module.
Private Function GetAllDisplayModes() As DisplayMode()
  Dim lngRetval As Long
  Dim dm As DEVMODE
  Dim aDisplayModes() As DisplayMode
  
  On Error GoTo HandleErrors
  
  Do
    ' Ask for each mode in turn, starting with 0.
    lngRetval = EnumDisplaySettings( _
     vbNullString, mintDispModeCount, dm)
    If lngRetval <> 0 Then
      ' Store away the mode information,
      ' and go on to the next one.
      aDisplayModes(mintDispModeCount) = dm.dmDisplayMode
      mintDispModeCount = mintDispModeCount + 1
    End If
  Loop While lngRetval <> 0

  ' Knock output array back to the last-filled-in entry.
  If mintDispModeCount > 0 Then
    ReDim Preserve aDisplayModes(0 To mintDispModeCount-1)
  End If

  GetAllDisplayModes = aDisplayModes

ExitHere:
  Exit Function
  
HandleErrors:
  Select Case Err.Number
    ' Subscript out of range. Resize the array to hold more
    ' items. The 100 is arbitrary, but should be enough for
    ' only one pass on many machines.
    Case 9
      ReDim Preserve _
        aDisplayModes(0 To mintDispModeCount + 100)
      Resume
    Case Else
      ' Stop enumerating. Something's wrong!
      mintDispModeCount = 0
      Resume ExitHere
  End Select

End Function

FIGURE 4: The GetAllDisplayModes function fills an array with information for all available display modes.

There's one small problem: There's no API call that tells you which is the current display mode. You'll almost always need this bit of information, so the ScreenInfo class contains a function, OriginalDisplayMode, that does the work for you. This procedure, called from the class's Initialize event procedure, returns an integer indicating which element in the array of available display modes is currently selected (it returns -1 if, for some reason, none of the available modes match the current mode). To do its work, the function must compare four values (screen width and height, color depth, and refresh rate) for each display mode with the current settings. If it finds a match, the function jumps out of its loop and returns the current index. FIGURE 5 shows the entire function.

' Return the index within maDisplayModes of the current
' display settings. This requires looping through each item
' until we find one that matches the current width, height,


' color depth, and vertical refresh. Returns the correct
' index, or -1 (indicating that it didn't find a match).
Private Function OriginalDisplayMode() As Integer
  Dim i As Integer
  Dim fOK As Boolean
  
  fOK = False
  
  For i = LBound(maDisplayModes) To UBound(maDisplayModes)
    With maDisplayModes(i)
      If .dsPelsWidth = mintWidthInPixels And _
       .dsPelsHeight = mintHeightInPixels And _
       .dsBitsPerPel = mintBitsPerPixel And _
       .dsDisplayFrequency = mintVerticalRefresh Then
        fOK = True
        Exit For
      End If
    End With
  Next i

  If fOK Then
    OriginalDisplayMode = i
  Else
    ' Using -1 makes it easy to set the ListIndex property
    ' of a list box to this value: -1 means to not select
    ' anything in the list box.
    OriginalDisplayMode = -1
  End If

End Function

FIGURE 5: The OriginalDisplayMode function returns the index of the original display mode.

Most of the time, you'll want some descriptive way to refer to the display modes. Although the choice of how to display this information was arbitrary, we decided to use the same format Windows NT uses when you ask it to list all the display modes. We've included a DisplayModesText property that returns a description of a display mode, given an integer index. The code (shown in FIGURE 6) must check for "subscript out of range" errors, because there's nothing keeping you from requesting a number that doesn't exist. To see this property in use, refer again to FIGURE 1. (Note that this function converts from bits per pixel into either a number of colors, or a text string, such as "True Color." These choices were made only to match the way Windows NT displays this information. You can modify this procedure so that it outputs the data any way you like.)

' Given an integer, return the text of the display item
' corresponding to that index. For example, the output will
' look like this: 1024 by 768 pixels, 256 colors, 60Hz.
Public Property Get DisplayModesText(Item As Integer) _
  As String

  On Error GoTo HandleErrors
  Dim strOut As String
  
  With maDisplayModes(Item)
    strOut = .dsPelsWidth & " by " & _
             .dsPelsHeight & " pixels, "
    Select Case .dsBitsPerPel
      Case 32
        strOut = strOut & " True Color, "
      Case Else
        strOut = strOut & " " & (2 ^ .dsBitsPerPel) & _
         " colors, "
    End Select
    strOut = strOut & .dsDisplayFrequency & "Hz"
  End With

  DisplayModesText = strOut

ExitHere:
  Exit Property
  
HandleErrors:
  Select Case Err.Number
    Case 9
      ' Subscript out of range; return an empty string.
      DisplayModesText = ""
      Resume ExitHere
    Case Else
      Err.Raise Err.Number, Err.Source, _
       Err.Description, Err.HelpFile, Err.HelpContext
  End Select

End Property

FIGURE 6: The DisplayModesText property converts the internal information about a display mode into descriptive text.

The sample form accompanying this article includes code that takes advantage of these capabilities. When the form loads, it runs the following code, using the ScreenInfo object it already instantiated to retrieve a list of all display modes:

For i = 0 To scr.DisplayModesCount - 1
  lstModes.AddItem scr.DisplayModesText(i)

Next i

lstModes.ListIndex = scr.CurrentDisplayMode

What if you want to change the display mode? Windows makes that possible, as well, if the current display driver supports it. When you call the ChangeDisplaySettings API function, Windows will ask the driver to change its display properties. If possible, the driver will switch modes without restarting Windows. If it can't change without restarting, the ChangeDisplaySettings function returns a value indicating that you'll need to restart Windows to see the new settings. If it's possible to change settings without restarting, you can tell the function not to write the new settings to the registry, so they'll be effective for this session only. Of course, if your changes require restarting, they'll need to be written to the registry so they'll be there after you restart.

To call the ChangeDisplayModes API function, you'll need to send the function a DEVMODE structure, filled with information about the new display mode, and a flag indicating what you want the function to do. For this flag parameter, choose one or more of the CDS_... constants using the Or (or "+") operator (see the ScreenInfo class for the full list.) This example uses the CDS_RESET and CDS_UPDATEREGISTRY constants, indicating that Windows should reset the screen mode and save the new settings in the registry. The problem, then, is setting up the DEVMODE structure before calling the function. You must satisfy these requirements before calling ChangeDisplayModes:

Set the dmSize member of the DEVMODE structure so it contains the size of the structure.

Set the dmDriverExtra member of the DEVMODE structure so it contains 0.

Copy the display mode information from the array of available display modes into the DEVMODE structure.

Set the dmFields member of the DEVMODE structure to have one bit set for each of the properties you've set (you've set five properties: bits per pixel, width in pixels, height in pixels, display flags, and display frequency). The DM_CHANGEALL flag tells the DEVMODE structure that you've changed all those properties.

ChangeDisplayModes returns a value indicating how well it did. For a list of possible return values, check out the DISP_CHANGE... constants in the ScreenInfo class. In general, there's only a few outcomes of calling ScreenInfo: it succeeds (DISP_CHANGE_SUCCESSFUL), it fails (any of the DISP_CHANGE... constants that are less than 0), or it indicates that you must restart Windows to have your changes take effect (DISP_CHANGE_RESTART). The SetDisplayMode function (see FIGURE 7) does this work for you, and returns one of the DISP_CHANGE... constants as its return value. Pass it the index of the display mode you want to use (from the array of available display modes); it does the rest of the work.

' Change the display mode, no questions asked. If you want
' to wrap changing screen modes in your own UI, this is the
' function to call. This function is called by the 
' ChangeDisplayMode method.
Public Function SetDisplayMode(Item As Integer) As Long
  Dim dm As DEVMODE
  
  ' Set up DEVMODE structure with appropriate settings.
  With dm
    .dmSize = LenB(dm)
    .dmDriverExtra = 0
    .dmDisplayMode = maDisplayModes(Item)
    .dmFields = dm.dmFields Or DM_CHANGEALL
  End With
  ' Call the API, telling it to go ahead and make the
  ' changes, and update the registry so they're persisted.
  SetDisplayMode = ChangeDisplaySettings( _
                     dm, CDS_RESET Or CDS_UPDATEREGISTRY)
End Function

FIGURE 7: Use the SetDisplayMode function to handle calls to the ChangeDisplaySettings API function.

The ChangeDisplayMode method takes this one step further: It calls SetDisplayMode for you, then indicates the results using MsgBox. FIGURE 8 shows the ChangeDisplayMode method; feel free to change this to match your needs.

Public Function ChangeDisplayMode(Item As Integer) As Long
  Dim dcReturn As DispChange
  
  dcReturn = SetDisplayMode(Item)
  Select Case dcReturn
    Case DISP_CHANGE_BADFLAGS, DISP_CHANGE_BADMODE, _
         DISP_CHANGE_BADPARAM, DISP_CHANGE_FAILED, _
         DISP_CHANGE_NOTUPDATED
      MsgBox "Unable to change to the selected mode."
    Case DISP_CHANGE_RESTART
      Call MsgBox( _
        "The new display settings won't take effect " & _
        "until you restart Windows.", _
        vbOKOnly + vbInformation)
    Case DISP_CHANGE_SUCCESSFUL
      ' Don't do anything.
  End Select

  ChangeDisplayMode = dcReturn
End Function

FIGURE 8: The ChangeDisplayMode method wraps up the call to the SetDisplayMode method, handling the return value for you.

(Note: For complete information on enumerating and changing display modes, visit http://www.microsoft.com/msdn, or use the MSDN CDs to investigate the ChangeDisplaySettings and EnumDisplaySettings functions.)

Working with Fonts

The VB Screen object provides two font-related properties: FontCount and Fonts. The FontCount property returns the number of installed screen fonts, and the Fonts property returns the name of a screen font, given an index into the internal array of available fonts. The ScreenInfo class includes both these properties, as well as the additional FontInfo property. This new property retrieves information about the font beside its name, such as its weight, whether it's italic, and others.

Using these properties is simple. Once you've instantiated the ScreenInfo object, code in the Initialize event procedure of the class fills an internal array of font information. You can then use the FontCount, Fonts, and FontInfo properties to retrieve information about a font. For example, the following code from frmScreenDemo fills a list box with information about each of the installed screen fonts:

Dim strOut As String
Dim i As Integer

Set scr = New ScreenInfo
  
lstFonts.Clear
For i = 0 To scr.FontCount - 1
  strOut = ""
  If CBool(scr.FontInfo(i, lfiType) And _
           TRUETYPE_FONTTYPE) Then
    strOut = strOut & "T"
  End If
  strOut = strOut & vbTab & scr.FontInfo(i, lfiWeight)
  strOut = strOut & vbTab & scr.Fonts(i)
  lstFonts.AddItem strOut
Next I

First, note the loop that visits each font in turn, using an index in a loop. The loop uses the FontCount property to terminate the loop, and the Fonts property to retrieve the name of each font:

For i = 0 To scr.FontCount - 1
  ' code removed...
  strOut = strOut & vbTab & scr.Fonts(i)
  ' code removed...
Next I

The form also uses the FontInfo property, which allows you to retrieve all the information about the font that Windows makes available, including height, width, orientation, weight, italic, underline, strikethrough, and others. (The FontInfo property makes available all the fields from the standard API LOGFONT structure; see the MSDN documentation for more information.) The FontInfo property accepts two parameters: an index indicating the font in which you're interested, and an enumerated value indicating the piece of information you'd like to retrieve. The LogFontInfo enumeration in ScreenInfo includes the possible pieces of information you can retrieve. The example form uses the lfiWeight value from this enumeration to retrieve the weight of each font.

You'll need to visit the documentation for the LOGFONT structure for information on all the possible values, but this example uses the lfiType value to retrieve the type of the font, so it requires a bit of "air-time" here. The type of font includes three possible values, stored as individual bits, one or more of which might be set. The three possible values are included in the FontType enumeration:

Public Enum FontType
  DEVICE_FONTTYPE = &p
  RASTER_FONTTYPE = &H1
  TRUETYPE_FONTTYPE = &b
End Enum

Because one or more of these values might be applied to the font, you'll need to use the And operator to determine if a particular bit has been set. To check for TrueType fonts, you might write code like the sample form:

If CBool(scr.FontInfo(i, lfiType) And _
         TRUETYPE_FONTTYPE) Then
  strOut = strOut & "T"
End If

Using these properties is simple. Enumerating all the installed fonts to provide the data for the properties is, unfortunately, not simple. To provide a list of installed screen fonts, you must call the EnumFonts API function, which requires a callback function, and, therefore, the AddressOf operator. The AddressOf operator is old hat to VB developers, but is new for Office (and other VBA host) developers in this version. This topic requires an article all its own, so all we'll say here is that when you call the EnumFonts function, you must supply the memory address of a function that Windows will call to process each font, in turn. As Windows loops through each of the installed fonts, it calls your supplied function, passing specific information to the function. In this case, the callback function adds the font information that's passed to it to the array of fonts in which it's storing information. The documentation for the EnumFonts API function supplies the exact layout for the parameters to your callback function. Unfortunately, the description in MSDN is aimed at C++ developers, and if you convert the C++ information to VBA incorrectly, you're guaranteed to crash. The moral is, when working with callback functions and AddressOf, make sure you save all your work each time you test, until you've got all the kinks worked out.

There are some constraints you should be aware of when using any API function that requires a callback function, including:

The callback function must be public in a standard module. (This is why this example requires two modules: the ScreenInfo class module, and the basEnumFonts standard module. The callback function must exist in a standard module.)

The function "signature" for the callback function is specified by the function that calls it. It's imperative that you declare the callback function correctly, and there's no pre-created list of callback functions.

Most API functions that use callback functions expect the callback function to return one value to indicate that they succeeded, and a different value to indicate that an error has occurred. That way, Windows knows whether to continue calling the callback function.

In the ScreenInfo object's Initialize event procedure, you'll find the following code, which calls the Windows API EnumFonts function:

Call EnumFonts(hDC, vbNullString, _
               AddressOf EnumFontsProc, 0)

This function requires four parameters:

The screen's device context.

A string containing the name of the font face to enumerate. If you want all font names, send vbNullString.

The address of the callback function.

Any data you want to pass through to the callback. This example uses 0.

The EnumFontsProc callback procedure, in basEnumerateFonts, adds each font to the array of available fonts as it's called by Windows, once for each installed screen font. The procedure's declaration must look like this:

Public Function EnumFontsProc(lf As LOGFONT, _
  tm As TEXTMETRIC, ByVal lngType As Long, _
  lngParam As Long) As Long

Note the required use of the ByVal keyword. Normally, in VBA, you can choose whether to declare a parameter by value. When you use a callback function, the original programmer determines whether it's expecting to send a value or an address. If you remove ByVal when Windows expects it, you're doomed to crash.

The basEnumerateFonts module also contains two more public procedures: GetFontCount and GetFonts. These procedures exist only because we have an intellectual problem with using global variables. The GetFontCount function returns the number of installed fonts, and the GetFonts procedure returns the array of fonts. The ScreenInfo class calls these procedures to gather the information it needs in its Initialize event procedure.

So What Have You Got?

The ScreenInfo class (and its associated module, basEnumerateFonts) provide most of the functionality of the VB Screen object. In addition, ScreenInfo adds many methods and properties of its own. If you want to investigate the details of the inner workings of ScreenInfo, you'll need to dig into a number of Windows API functions. If you want to use ScreenInfo, however, all you need to do is import the two modules and go for it.

You may want to add other properties and methods to this class. For example, the GetSystemMetrics and GetDeviceCaps API functions can return many other bits of information about the display device. In addition, you might want to investigate the SystemParametersInfo API function, which can return even more information about your display environment. But the real point of this class is that you can easily create classes that encapsulate a great deal of complex code, in this case relying on the Windows API. From the perspective of users of the class, it doesn't matter that there's a ton of API code buried inside. As we've noted many times previously in this column, classes are a great way to hide the tricky code — write it once, and never think about it again. What's more, you can use this class in any VBA 6.0 host, e.g. VB6, Office 2000, or any other VBA host that's incorporated the new version of VBA.

If you're determined, you can get this class to work in Office 97, or VB5. To get it to work in either version, you'll need to find two specific situations that use new VBA 6.0 features:

If we've returned an array or user-defined type (UDT) as the return value from a function, you'll need to pass the array or UDT as a parameter to the function instead.

If we've assigned the value of one UDT to equal another, you'll need to write code to assign the values of each field individually.

In addition, if you want to use this in Office 97, you'll need to take two extra steps:

Review the article "Call Me Back" by Ken Getz and Michael Kaplan in the May, 1998 MOD. The article describes a technique you can use in Office 97 to emulate the AddressOf operator that's built into VB5 or higher, or any VBA 6.0 host.

Replace all enumerated values (using the Enum keyword) with constants, and replace any enumerated data types in declarations with Long.

We've provided the demonstration and code for this article in Excel format. Although we did our preparation in Excel 2000, the sample can be opened in Excel 97 as well. Unfortunately, the code won't run correctly in Excel 97, because we've used new features that don't exist in that version. The example and documentation here will appear, in modified form, in Access 2000 Developer's Handbook (Ken Getz, Paul Litwin, and Mike Gilbert) due from SYBEX in 1999.

Download source code for this article here.

Ken Getz (KenG@mcwtech.com) is a Senior Consultant with MCW Technologies, a Microsoft Solution Provider focusing on Visual Basic and the Office and BackOffice suites. Mike Gilbert (mike_gilbert@hotmail.com) is Technical Product Manager for Office Developer Edition and VBA Licensing at Microsoft. They are also co-authors of VBA Developer's Handbook [SYBEX, 1997], and Access 97 Developer's Handbook — with Paul Litwin [SYBEX, 1997].