An Assortment of Windows API Tricks

<< Click to Display Table of Contents >>

Navigation:  Smart Access 1996-2006 > Aug-1996 >

An Assortment of Windows API Tricks

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.

199608_kg1 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.

199608_kg2
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