Would you like to create forms that are the right size no matter what screen resolutions your users are using? Here's how.
When you install Windows, you must choose a screen driver that determines whether your monitor displays 640x480 (standard VGA), 800x600 (Super VGA), 1024x768 (Super VGA or 8514/a) or 1280x768 pixels. These numbers refer to the number of picture elements (or pixels) in the horizontal and vertical directions. If you create forms that look fine on your screen running at 1024x768, those same forms may be far too large for a user working at 640x480 (still the most popular screen resolution, since it's the resolution supported by most laptop and portable computers). Similarly, if you create forms at 640x480, someone who's working at 1280x1024 will see them as very small forms. (Though some people with large displays may prefer this.) For the majority of users, you should deliver forms scaled to the user's screen resolution.
One unattractive solution to this problem is to create multiple versions of your forms, one for each screen resolution that you care to support. To do this you have to update each of those forms individually whenever you make changes to the form.
This article offers a better solution. Using the code explained here, you'll be able to scale forms as they load, allowing them to look good at any screen resolution. In addition, I'll include code that you can attach to a form's OnResize event that will allow users to resize a form and all of its controls at runtime. (All of the code discussed in this article can be found accompanying Download file.)
Understanding screen resolutions
Before you can understand the solution to the screen resolution issue, you first have to understand the problem. Figure 1 shows a scale image of the four standard Windows screen resolutions, superimposed. As you can see, a form that appears full-screen at 640x480 will take up only a small portion of a 1280x1024 screen, and a full-screen form at 1024x768 will be too large for a screen at 800x600.
The difference in the number of pixels is only one of two issues you need to consider in scaling forms. You also need to think about the size of the pixels -- the number of pixels per logical inch of screen space. Individual screen drivers control the size of pixels in relation to what Windows thinks an "inch" is. Windows provides API calls to gather all this information, which I'll discuss later. You also need to be concerned about the number of twips per pixel. (A twip is equivalent to 1/1440 inch.) Practical experience shows that 640x480 screens use 15 twips per pixel, while all other VGA screen resolutions use 12 twips per pixel. In other words, at low resolution VGA, 100 pixels take up 1500 twips (a little more than one logical inch), while at higher resolutions, 100 pixels take up 1200 twips (a little less than one logical inch). Therefore, you need to take both ratios -- the number of pixels and the number of twips per pixel -- into account to correctly scale your forms for different resolutions. In fact, you'll need to compare the values for the pixels used and the twips per pixel for both the screen on which the form was prepared and the screen on which it will be displayed. The ratios of these values will determine how you'll scale the form.
Scaling forms as they load
To solve the problem of displaying forms so that they take up the same proportion of the screen real estate on different screen resolutions, it would seem that all you need do is calculate the ratio of the original screen dimensions to the current screen dimensions, and scale the form accordingly. Unfortunately, the calculation is further complicated by the twips per pixel issue, which I discussed previously. Since different screen resolutions use a different number of twips for each pixel, you must also take this into account when calculating the new form size. When moving from 640x480 to 1024x768, the x-axis sizing ratio isn't just 1024/640. You also have to multiply that ratio by the ratio of twips per pixel, 12/15, for the two resolutions. Therefore, the correct ratio is 1024/640 x 12/15, or 1.28.
Retrieving display information
In order to scale your forms, you'll first need to retrieve information about the current display driver. To do this, you must use a Windows' Device Context, a data structure that provides a link between the Windows API and the device driver. For this example, you can even use an Information Context, a lower-powered Device Context that is unable to write information back to the driver. Any calls to the Windows API dealing with the display driver must pass an Information Context or Display Context handle as the first parameter. To get the Information Context handle, use the following code (APINULL is a constant defined to be 0&):
intIC = glr_apiCreateIC("DISPLAY", APINULL, APINULL, APINULL)
Once you have the Information Context handle, intIC, you can obtain the information you need about the current display driver. First, you'll need to retrieve the pixel resolutions of the current display driver. You can use the Windows API function GetDeviceCaps() to obtain this information.
intScreenX = glr_apiGetDeviceCaps(intIC, HORZRES)
intScreenY = glr_apiGetDeviceCaps(intIC, VERTRES)
Next, you'll need to find out the number of pixels per logical inch both horizontally (X) and vertically (Y). Once you have those values, divide them into the number of twips per inch (TWIPS_PER_INCH = 1440) to calculate the number of twips per pixel. Again, use the Windows API function, GetDeviceCaps(), to retrieve this information.
intTwipsPerPixelX = TWIPS_PER_INCH / ==>
intTwipsPerPixelY = TWIPS_PER_INCH / ==>
Now calculate the ratio of the current screen resolution to the resolution that was active when the form was created. The original values will have been passed into this function in the parameters intX and intY.
sglFactorX = intScreenX / intX
sglFactorY = intScreenY / intY
But, as I mentioned before, this isn't accurate enough. You still need to account for the different twips per pixel readings for different display adapters. To do this, scale the x-axis and y-axis ratios of your original and current displays by the twips-per-pixel ratios for the two displays. (GetTwips() is a local function that attempts to determine the twips-per-pixel value for the original display, given the horizontal resolution of that display).
sglFactorX = sglFactorX * (intTwipsPerPixelX / ==>
sglFactorY = sglFactorY * (intTwipsPerPixelY / ==>
Armed with the values for sglFactorX and sglFactorY, you have the information you need to correctly scale the form as you open it in the new display resolution. Just multiply the form's width, height, and position in the upper left corner by that scaling factor, and your form should end up in a relative position on the screen with the new width and height.
Scaling the form's contents
Scaling the form is only part of the problem, however. Just changing the size of a container won't help much if you can't see all of its contents. Therefore, you also need a way to resize all of the controls inside the form. The function glr_ScaleForm(), called from your form's OnOpen event, calls the glr_ResizeForm() function to resize all the controls. This subroutine can also be called directly from a form's OnResize event, allowing all the controls on a form to be resized dynamically each time a user resizes the form. This can be a very striking feature, allowing users to shrink a form, but still leaving it available for use. Figure 2 shows a full-sized and scaled version of the same form
To accomplish this visual feat, attach a call to glr_ResizeForm() to your form's OnResize event. The glr_ResizeForm() function loops through all the controls on the form, scaling them by the ratio of the form's previous and current sizes. In this situation, you don't need to be concerned with any screen resolution issues -- you're just comparing the form's current and previous sizes to determine the sizing ratio.
The function first checks to make sure that the user has not completely minimized the form. In that case, Access acts as if there is no active form, so the code goes no further. The code calls the GetClientRect() API function to find the form's height, and just returns if the height is 0.
glr_apiGetClientRect frm.hWnd, rctNew
intHeight = (rctNew.intY2 - rctNew.intY1)
If intHeight = 0 Then
Once the function has found the dimensions for the current form, it calculates the current width. Then it can calculate and store the scaling factors, based on the previous sizes stored in rctOriginal.
intWidth = (rctNew.intX2 - rctNew.intX1)
sglFactorX = intWidth / (rctOriginal.intX2 - ==>
sglFactorY = intHeight / (rctOriginal.intY2 - ==>
Finally, it stores away the current form sizes, so it'll have them available on the next pass.
rctOriginal.intX1 = rctNew.intX1
rctOriginal.intX2 = rctNew.intX2
rctOriginal.intY1 = rctNew.intY1
rctOriginal.intY2 = rctNew.intY2
Once all the preliminary work has been completed, the function calls the workhorse function, SetFormSize(), whenever there's resizing to be done.
Scaling the controls
In theory, SetFormSize() does nothing more than loop through all the controls on the form, scaling their locations and sizes by the scaling factors calculated in the calling function. In practice, a number of other details aren't at first obvious. Here are some issues:
|•||The order of events is important. If your form is growing, then you must expand the section heights before you allow the controls to expand. Otherwise, the expanding controls will push out the boundaries of the sections and will invalidate the scaling. The opposite holds true when shrinking. You can't shrink section heights until after you've sized all the controls. Otherwise, you risk compressing the control locations artificially.|
|•||Controls that contain other controls must be dealt with carefully. A group can contain toggle buttons, option buttons, and check boxes. A subform can contain any control and possibly yet another subform (nested, at most, two deep). To maintain the correct scaling, you need to walk through all the controls on the form, build up an array containing information about all the container controls, scale all the controls on the form, and then scale the containers. In addition, if you run across a subform, you must recursively call the function again, scaling all the controls on that subform. If that subform contains a subform, you must call the function once more to handle that final subform. Once the function has handled all the controls on the form, it loops through the array of containers and scales them correctly.|
|•||On some controls, heights and fonts don't need to be scaled. For example, you can't really change the height of a check box. You can try, but it won't look very good. Several controls don't even have a FontName property. The SetFormSize subroutine calls the ChangeHeight() and ChangeFont() functions to find if it should bother trying to change the particular property at all.|
|•||You only want to move forms when they're first loaded. After that, it should be up to the user. Therefore, the code that positions the form itself should only be called if the subroutine was called from glr_ScaleForm().|
Steps to successful scaling
Due to limitations of the technology, the methodology presented here is far from perfect. Each time Access fires off the OnResize event, the function calculates the current control or font size based on the previous size. This recursive calculation will inevitably lead to roundoff errors. Once your user compresses the form down beyond readability, attempts to expand it will often result in an unreadable mess. One alternative method would have been to store the original size of each control on the form. Then, at each resize attempt, you could compare the current form size to the original size, scaling each control accordingly. This method might be more accurate, but would slow down the process. In the tradeoff of speed against accuracy, speed won again.
In any case, there are steps you must take to make this code work:
1. Use TrueType fonts for each control that you will scale. This code only scales the fonts in labels, buttons, and text, combo and list boxes. Unfortunately, the default font used in all controls isn't scaleable. You'll either need to modify your form defaults, or select all the controls and change the font once you're finished designing. On the other hand, beware of using fonts that won't be available on your users' machines. All copies of Windows 3.1 ship with Arial and Times Roman fonts: Choosing one of these for your buttons, labels, list, combo and text boxes will guarantee a certain level of success.
2. You should not design forms at 1280x1024 and expect them to look good at 640x480. By the time forms get scaled that far down, they'll be difficult to read. If you use 800x600 or 1024x768 for development, your forms should look reasonable at most resolutions.
3. Don't attempt to mix the AutoCenter property with a call to glr_ScaleForm() called from the OnOpen event. The AutoCenter property will attempt to center the form before it's resized, and Access will place the form somewhere you don't expect it.
4. The code that handles the form resizing isn't meant to work with more than one form at a time. Since it maintains the previous size of the form in a global structure, it can only handle one form at a time. If you need to open multiple scaleable forms at the same time, you'll need to modify the code to handle multiple storage structures.
Expanding forms can make your applications work in a more reasonable fashion on Windows installations with a variety of screen resolutions. In addition, allowing users to resize their forms with scaleable controls will make your apps "sparkle." To avoid problems, though, make sure you completely understand the limitations of the technology, and don't any expect resizing miracles.
Download is called SA200406.zip and it is in Access 2 format. Read about old download on this page