Ken Getz
This month Ken takes on three topics, all of which involve the Windows API. The topics include how to determine which version of Windows is running, how to retrieve a list of CD track lengths, and how to place a form directly on top of another open form. This month's examples -- QA960816.MDB (Access 2) and QA960832.MDB (Access 95) -- are available in the accompanying Download file.
Lots of things in my application must work differently in Windows 3.x, Windows 95, Windows NT 3.5x (with the old shell), and Windows NT 4.0 (with the new shell). How can I determine the host operating system? There doesn't appear to be any way, from within Access, to gather this information. Can you suggest a method that will work no matter what the operating system?
You're right: Access doesn't provide a way to figure all this out. To discern what operating system is hosting your Access session, you'll need to dig into the Windows API. What's more, the solution is different, depending on whether you're running Access 2 or Access 95. Access 2 can only call 16-bit API functions, and the 16-bit API provides the GetVersion function. This function provides only the version of Windows and the version of DOS that are currently running. From Access 95 (and other 32-bit applications), you can call the GetVersionEx function. GetVersionEx provides more information, including the exact build number of the operating system.
Retrieving OS information in Access 2
In the basOSVersionInfo module, I've included sample code to retrieve all the operating system information. The next few paragraphs explain the functionality.
To use GetVersion in Access 2, you must first include a declaration for the external function, like this:
Declare Function GetVersion Lib "Kernel" () As Long |
The function returns a long integer with four pieces of information encoded in its two bytes. If you look at the long integer four bits at a time, you'll find these values:
DOS Major Revision Number DOS Minor Revision Number Windows Minor Revision Number Windows Major Revision Number |
Therefore, to use GetVersion you'll need code that takes the long integer and breaks it up into the four usable pieces of information. To make this possible, you'll also need to include this simple data type declaration:
Type tagOSInfo intWindowsMajor As Integer intWindowsMinor As Integer intDOSMajor As Integer intDOSMinor As Integer End Type |
The OSVersion procedure takes as a parameter a variable of the tagOSInfo type and fills in the various members with information. The caller can then use the information directly from the structure. The OSVersion function that follows does its work by calling GetVersion and then breaking up the four pieces of information by masking off four bits at a time and shifting them to the right as necessary:
Sub OSVersion (udtOSInfo As tagOSInfo)
' Use GetVersion API call to find the ' current OS version.
Dim lngVersion As Long Const conByte0 = &HFF Const conByte1 = &HFF00& Const conByte2 = &HFF0000 Const conByte3 = &HFF000000
lngVersion = GetVersion()
' Mask and shift bytes, as necessary. ' Use division here to emulate shifting, ' because Access doesn't supply a shift operator.
udtOSInfo.intWindowsMajor = _ (lngVersion And conByte0) / (2 ^ 0) udtOSInfo.intWindowsMinor = _ (lngVersion And conByte1) / (2 ^ 8) udtOSInfo.intDOSMinor = _ (lngVersion And conByte2) / (2 ^ 16) udtOSInfo.intDOSMajor = _ (lngVersion And conByte3) / (2 ^ 24) End Sub |
Don't worry if you don't follow all the masking and shifting -- just use the OSVersion procedure to do the work. Once you call OSVersion, you can use the values it fills in to determine which operating system you're using, as shown in Table 1.
Table 1. 16-bit criteria for determining the operating system version.
Operating System |
intWindowsMajor |
intWindowsMinor |
Windows 3.x |
3 |
<50 |
Windows 95 |
3 |
95 |
Windows NT 3.5x |
3 |
50 or 51 |
Windows NT 4 |
4 |
0 |
(GetVersion also retrieves information about the DOS version, if that's of interest to your application.) To make this simple to use, basOSVersion includes five functions: IsWindows3x, IsWindows95, IsWindowsNT, IsWindowsNTOldShell, and IsWindowsNTNewShell. Use these functions to help determine the operating system. In each case the code calls OSVersion to retrieve the information and then makes decisions based on the data in Table 1. For example, the IsWindowsNT function looks like this:
Function IsWindowsNT () As Integer Dim udtOSInfo As tagOSInfo
Call OSVersion(udtOSInfo)
' Check for Windows 3.50 or greater (3.51, too) ' or Windows 4.x If udtOSInfo.intWindowsMajor = 3 And _ (udtOSInfo.intWindowsMinor = 50 Or _ udtOSInfo.intWindowsMinor = 51) Then IsWindowsNT = True ElseIf udtOSInfo.intWindowsMajor = 4 And _ udtOSInfo.intWindowsMinor = 0 Then IsWindowsNT = True Else IsWindowsNT = False End If End Function |
Retrieving OS information in Access 95
Under Access 95 you can use 32-bit API calls and the task of retrieving operating system information is a lot simpler. Call the GetVersionEx function to fill in a data structure with all the information you need. To use GetVersionEx, you must declare the function and the data structure it needs in order to do its work. In addition, you must supply constants for the three possible values representing the current operating system platform (All code in this section comes from basOSVersionInfo):
Type OSVERSIONINFO dwOSVersionInfoSize As Long dwMajorVersion As Long dwMinorVersion As Long dwBuildNumber As Long dwPlatformId As Long szCSDVersion As String * 128 End Type
Declare Function GetVersionEx Lib "kernel32" _ Alias "GetVersionExA" _ (lpVersionInformation As OSVERSIONINFO) As Long
' dwPlatformId defines: ' ' Win32s not going to happen for Access 95. Public Const VER_PLATFORM_WIN32s = 0 ' Windows 95 Public Const VER_PLATFORM_WIN32_WINDOWS = 1 ' Windows NT Public Const VER_PLATFORM_WIN32_NT = 2 |
Given the function declaration and the supporting data type and constants, you're all set to go. To retrieve the information, call GetVersionEx directly. (Before calling GetVersionEx, you must fill the dwOSVersionInfoSize member of the data structure with the size of the data structure. See the sample code for examples.) Look at the dwPlatformID member of the data structure to determine the operating system and use the dwMajor/MinorVersion members to find out which version of NT is running. The sample module contains IsWindows95, IsWindowsNT, IsWindowsNTOldShell, and IsWindowsNTNewShell functions. These functions wrap up the calls to GetVersionEx and return a Boolean indicating the operating system in use.
For example, the following function, IsWindowsNTNewShell, calls GetVersionEx and then checks the members of the data structure to figure out whether NT 4.0 is running:
Function IsWindowsNTNewShell() As Boolean Dim udtOSVersionInfo As OSVERSIONINFO
udtOSVersionInfo.dwOSVersionInfoSize = _ Len(udtOSVersionInfo) Call GetVersionEx(udtOSVersionInfo)
' Assume it's not Windows NT 4.0 IsWindowsNTNewShell = False With udtOSVersionInfo If (.dwPlatformId = VER_PLATFORM_WIN32_NT) Then If .dwMajorVersion = 4 And _ .dwMinorVersion = 0 Then IsWindowsNTNewShell = True End If End If End With End Function |
I need to be able to determine the timings of all the tracks on an audio CD. I've seen CD players for Windows that provide a list of the track timings, so I know it must be possible to retrieve this information. I've dug around in the Windows API, but I can't find the information. Can you help?
This one sure was fun to figure out! Windows provides several levels of control over multimedia devices, but the Media Control Interface (MCI) is the simplest. In researching the answer to this question, I learned a lot about MCI, and it's a very rich interface, full of keywords, commands, and options. In the interest of not turning this response into a full article, I'll limit the coverage here to just answering the original question. If you're interested, however, I suggest you find information on MCI and all its power (I used the MSDN subscription CD to figure this all out.)
To be completely honest, if you were interested in writing a full-blown audio CD interface in an Access application, I'd suggest using the MCI control that ships with Visual Basic 4.0 (it's available in both 16- and 32-bit versions, and from my limited testing, it looks like the 16-bit version works with Access 2). It can provide all the information discussed in this answer, and it's a lot simpler. On the other hand, if you're only interested in retrieving track timings, you may not care to distribute the MCI control with its attendant overhead.
In order to use the MCI interface, you must declare the two API functions, mciSendString and mciGetErrorString. The declarations are slightly different for the Win16 and Win32 API, and you'll find these declarations in basCDAudio in both sample databases.
Using the MCI interface involves creating text strings representing instructions or questions for the multimedia device (cdaudio, in this case), and then sending the string to the device using the mciSendString API call. You must also send a text buffer that is ready to receive the response and the length of that buffer. Once it's done its work, mciSendString fills in the buffer with the requested information, and returns 0 on success or an error code on failure. If an error occurs, you can call the mciGetErrorString function to retrieve a text message that describes the error.
Using MCI boils down to constructing the text strings that describe the information you want to retrieve or the instruction you want executed. The full syntax is too broad for even a long article, much less for this limited space but, in general, the text strings direct a question or a command at a specific device. To ensure that there's an audio CD in your drive, you can use the string "status cdaudio media present." The function will place "true" into the output buffer if there's a CD there, and "false" otherwise. To retrieve the full length of the CD, you can use the string "status cdaudio length." The function call places the length, in minutes, at the beginning of the output buffer. To retrieve the length of a specific track, you must first tell the device driver that you want times reported in mm:ss:ff format (minutes:seconds:frames, where a frame is approximately 1/75th second) using the "set cdaudio time format msf" string, and then retrieve the length with a string like "status cdaudio length track 1," replacing the track number with the track number whose length you need.
All the values returned by mciSendString in the string buffer come back with a trailing Null character (Chr$(0)). To use the return value in Access, you must truncate the return value at that character. The sample database includes a TrimNull function, which you can use to truncate a string at the first Chr$(0) it finds.
To make it simple to retrieve all the necessary information, I've included a number of functions in basCDAudio (in both the Access 2 and Access 95 sample databases) that encapsulate calls to mciSendString. Table 2 lists the functions and their uses.
Table 2. CD Audio support functions.
Function |
Purpose |
GetCDLength |
Returns the length of the CD, in minutes |
GetTrackLen |
Given a track number, returns the length of that track in mm:ss:ff format |
GetNumberOfTracks |
Returns the number of tracks on the CD |
Though the sample functions are all quite similar, GetTrackLen directly answers the question asked, so I'll show that one here. Each time the code calls mciSendString, it checks the return value. If that value is 0, no error occurred and it can go on about its work. If it requested information, the function uses the TrimNull function to pull the text from the buffer it passed to mciSendString. If all else fails, the code calls mciGetErrorString to retrieve the text of the last error message:
Function GetTrackLen (intTrack As Integer) As Variant ' intTrack: specific track number ' fMilliseconds: return time in milliseconds? Dim strBuff As String Dim lngRet As Long Dim intLen As Integer
Const MAX_LEN = 255
intLen = MAX_LEN
' Assume failure GetTrackLen = 0
strBuff = Space(intLen) ' Is there a disk in the audio drive? lngRet = mciSendString( _ "status cdaudio media present", strBuff, intLen, _ 0) If lngRet = 0 Then If TrimNull(strBuff) = "true" Then strBuff = Space(intLen) lngRet = mciSendString( _ "set cdaudio time format msf", strBuff, _ intLen, 0) lngRet = mciSendString( _ "status cdaudio length track " & intTrack, _ strBuff, intLen, 0) If lngRet <> 0 Then strBuff = Space(intLen) intLen = mciGetErrorString(lngRet, strBuff, _ intLen) MsgBox TrimNull(strBuff) Else GetTrackLen = TrimNull(strBuff) End If End If End If End Function |
To demonstrate the features shown here, try out the sample form frmCDAudio (see Figure 1). This form retrieves information about an audio CD and lists the total time, the number of tracks, and timings for each track.
Figure 1
Other CD audio devices (FlexiCD in Windows 95, or the CD audio player in all versions of Windows) open the CD device unshared, so your code won't be able to access it to retrieve information at the same time. If your code, or the sample form, refuses to notice the audio CD in the drive, make sure that no other CD audio player is trying to access the drive at the same time. If you're running FlexiCD or the CD player, shut it down in order to try out this code.
As part of an application, I need to be able to place a particular form right on top of another open form. I can find the MoveSize action, but I can't find a way to find out where a form is on the screen. This seems like a real oversight in Access. Can you help me place a form at a specific location on the screen relative to another form?
Yes, it's odd that there's no way to find out exactly where a form is on the screen. Access provides a way to place a form at a given position (the MoveSize method of the DoCmd object), but no way to retrieve the coordinates of a form. Without using the Windows API, there would be no way to accomplish this task. I had previously worked out a solution to this problem for training materials I wrote for Application Developers Training Company and I've used that same solution here, with their permission.
The code that achieves this goal will use a user-defined typethe tagRect structure. But to solve this problem, and to do much work with the Windows API at all, you have to understand a number of concepts first.
In Windows, every "window" has a unique long integer that Windows can use when it needs to refer to that window. This number, which is its window handle, is assigned when Windows first creates the window, is guaranteed to be unique in the current environment, and to be non-zero. This window handle is usually called the hWnd for the window. Use the hWnd property of a form to retrieve this value.
Some API calls work with coordinates in terms of the entire screen and others work with coordinates that are dependent on the parent of the current window.
These are the key facts:
•The function you'll use to retrieve the coordinates of the window (GetWindowRect) works in terms of the entire screen.
•The function you'll use that sets the position of a window (MoveWindow) works in terms of the parent window's area, which means you'll have to convert from screen coordinates to Access' window coordinates.
•The parent of all normal forms in Access is the MDI Client window, which is a child of the main Access window. (For pop-up forms the parent is the main Access window, not the MDI Client window, but you'll disregard these windows for now).
•To calculate the position of a form within the Access window, subtract the position of the MDI Client window from the position of the form you're working with. This provides the position of the form within the MDI Client window.
•In Windows 95 (and Windows NT 4.0), the MDI Client window has a two-pixel border. Under Windows NT with the old shell, there is no border. Therefore, under Windows 95 and NT 4.0, you must also subtract two from your calculation to find the exact position.
If you want to manipulate positions of forms in Access, you'll need to work from the outside in, as shown in Figure 2.

Figure 2
Here's one last challenge: how do you find the hWnd for the MDI Client window? Your form has an hWnd property, but there's no comparable property for the MDI Client window. The trick here is to use the GetParent API function. Given a window handle, it returns the window handle of the requested window's parent (for a normal form, that'll be the MDI Client window). If you get 0 back from GetParent, you know that your window has no parent.
Retrieving a form's coordinates
To retrieve a form's position relative to its parent (the MDI Client window), you must first find its position on the screen, then find the position of the MDI Client window, and subtract the two (see Figure 2). The same goes for its vertical position. To do this, use the GetWindowRect API function. It takes a window handle and a tagRect structure and fills in the structure with the coordinates of the requested window. (The following code is for Access 95, but the code in Access 2 is quite similar. In both cases, you'll find the code in the basWindowPos module.):
Private Type udtRect lngLeft As Long lngTop As Long lngRight As Long lngBottom As Long End Type
Private Declare Function GetParent Lib "user32" _ (ByVal Hwnd As Long) As Long Private Declare Function GetWindowRect Lib "user32" _ (ByVal Hwnd As Long, lpRect As udtRect) As Long Private Declare Function MoveWindow Lib "user32" _ (ByVal Hwnd As Long, ByVal x As Long, ByVal y As Long, ByVal nWidth As Long, _ ByVal nHeight As Long, ByVal bRepaint As Long) _ As Long
Sub GetFormSize(frm As Form, rct As udtRect)
' Fill in rct with the coordinates of the window. ' This function will work correctly ONLY ' for NORMAL windows in Access -- not for popups. ' To keep things simple, we disregarded that case.
Dim hWndParent As Long Dim rctParent As udtRect Dim intLeft As Integer Dim intTop As Integer
' For Windows 95 and Windows NT New Shell, ' the MDIClient window border is 2 pixels ' wide, so you have to account for that. ' These should be 0 for WinNT Old Shell. If IsWindows95() Or IsWindowsNTNewShell() Then intTop = 2 intLeft = 2 Else intTop = 0 intLeft = 0 End If
' Find the position of the window in question, ' in relation to its parent window (the ' Access desktop, the MDIClient window). hWndParent = GetParent(frm.Hwnd)
' Get the coordinates of the current window and ' its parent. GetWindowRect frm.Hwnd, rct
' Subtract off the left and top parent ' coordinates, since you need coordinates ' relative to the parent for the ' MoveWindow function call. GetWindowRect hWndParent, rctParent With rct .lngLeft = _ .lngLeft - rctParent.lngLeft - intLeft .lngTop = _ .lngTop - rctParent.lngTop - intTop .lngRight = _ .lngRight - rctParent.lngLeft - intLeft .lngBottom = _ .lngBottom - rctParent.lngTop - intTop End With End Sub |
Setting a form's position
To set a form's position you have to call the MoveWindow API procedure. You provide MoveWindow with the window handle, its left and top coordinates, and the width and height you'd like. Remember, MoveWindow does its work in relation to your window's parent, not the whole screen. Why aren't you using the MoveSize method here? Access provides this method, and it's useful within Access, but it uses twips as its measurement (GetWindowRect uses pixels), and you have to actually select the window before placing it. This can be unattractive. I've decided to use the MoveWindow API call here instead because it uses the same coordinates as GetWindowRect and doesn't require you to select the window.
The following procedure, SetFormSize, accepts a form reference and a rectangle data structure. It places the form at the location specified in the rectangle structure. Once again this code is for Access 95, but there's a similar version in the Access 2 sample database:
Sub SetFormSize(frm As Form, rct As udtRect)
Dim intWidth As Integer Dim intHeight As Integer Dim intSuccess As Integer
With rct intWidth = (.lngRight - .lngLeft) intHeight = (.lngBottom - .lngTop)
' No sense even trying if either is less than 0. If (intWidth > 0) And (intHeight > 0) Then Call MoveWindow(frm.Hwnd, _ .lngLeft, .lngTop, _ intWidth, intHeight, True) End If End With End Sub |
Testing it out
To try out the technique described here, either call the TestSize subroutine or the less general TestIt. TestIt will open frtTryIt and frmHello, and then cause frmHello to directly overlay frmTryIt. You can also open frmTryIt manually and use the command button to open the second form then cause it to overlay frmTryIt. The following listing shows how you might use the GetFormSize and SetFormSize procedures:
Sub TestSize(strForm1 As String, strForm2 As String)
' Place strForm2 directly on top of strForm1. Dim rct As udtRect
DoCmd.OpenForm strForm1 DoCmd.OpenForm strForm2 MsgBox "Now we're going to set the positions!"
Call GetFormSize(Forms(strForm1), rct) Call SetFormSize(Forms(strForm2), rct) End Sub
Sub TestIt() ' Test out TestSize, with two specific forms. Call TestSize("frmTryIt", "frmHello") End Sub |
Get the download called getz199608.exe in the Smart Access Bronze Collection