Visual Basic is easy to learn because it takes difficult Windows concepts and reduces them to easy-to-understand abstractions. Similarly, ActiveX controls are easy to program because they wrap program complexity in the easy-to-understand Visual Basic paradigm of properties, events, and methods.
There is a price to pay for this simplification, though. Using Visual Basic represents a trade-off between ease of use and programming power. Fortunately, Visual Basic enables you to delve deeper into the world of Windows through the use of the Windows Application Programming Interface, or API.
Most of the topics surrounding low-level programming with the
Windows API are beyond the scope of this book. The quintessential
reference to the Windows API for Visual Basic users clocked in
at over 1500 pages in its last incarnation. But hopefully this
chapter will point you in the right direction and give you an
idea of what's possible outside the realm of plain vanilla Visual
Basic.
NOTE |
If you've used Visual Basic to make calls to the Windows API before, congratulations. You'll have a leg up on this chapter. But whether you've used the Windows API or not, I have tried to include examples in this chapter that will be meaningful and relevant to control creation. For VB programmers, the best reference to the Windows API is Daniel Appleman's Visual Basic Programmer's Guide to the Win32 API (Ziff-Davis Press, 1996). If you ever plan on doing serious work with Visual Basic, you need this book. (There is also a 16-bit version of the book if you're still working with 16-bit Windows.) |
Anything that happens in Windows ultimately takes place as a result of a program calling an element of the Windows API.
The Windows API is comprised of a number of dynamic-link libraries, or DLLs. In 32-bit Windows, the core Windows API DLLs are:
There are a few other APIs, and more are being added all the time to handle new operating system extensions such as e-mail, networking, and new types of hardware peripherals.
In order to use a Windows API procedure in Visual Basic, you must first declare it. API calls are declared in the Declarations section of a module. The generic syntax of an API call looks like this:
Declare Function TheAPICall "gdi32" (param1 As String) As Long
This made-up example is a declaration to an API function called
TheAPICall. This function exists in the file gdi.dll. It is a
function that returns a long integer. And it takes one parameter,
called param1, a String.
TIP |
By convention, you do not spell out the entire name of the DLL in which the API is contained for Windows system APIs. However, for calls to functions in non-system DLLs, you usually will spell out the full DLL filename and, optionally, a directory path to the DLL. By the way, there's no difference between calling a Windows API DLL and calling a DLL that is not a part of the Windows DLL. The Declare statement works for both types of calls. |
One thing you notice fairly quickly about Windows DLL calls is that they're usually quite strongly typed; that is, all the parameters and function calls have data types. This is because the language they're written in (C or C++) is itself strongly typed. Additionally, there is no such thing as a variant in the world of DLLs; the variant is a Visual Basic animal.
You want to double- and triple-check your function declarations
when placing calls to DLLs in Visual Basic. Mistakes in the declaration
of API calls is one of the fastest ways to crash your program.
TIP |
The wise Visual Basic programmer almost never types API function declarations in directly. Instead copy and paste them in as needed from a reference library of DLL calls. Windows 4.0 contains a utility called API Viewer that enables you to list, view, and copy Visual Basic declarations for Windows API calls. The beta version of Visual Basic 5.0 used for this book did not contain a new version of this utility, but it's a safe bet that Microsoft will include it in the final release. There are also third-party component libraries that expose the Windows API in an object-oriented fashion. I haven't used any of these extensively, so I can't recommend any of them, but if you're interested, you might want to check out Sheridan Software's WinAPI Oblets (http://www.shersoft.com/products/oblets/obgen.htm) or Desaware's Spyworks (http://www.desaware.com/desaware/spyhome.htm). Desaware, by the way, is Daniel Appleman's company; he's the guy who wrote the tremendous book on using Windows API calls in Visual Basic that I plugged earlier in this chapter. |
To get you started on a project that makes a simple API call, you'll create an enhanced command button control. The button you create will make a clicking sound (using Windows' multimedia API) whenever it is clicked. To do this:
Const SND_SYNC = &H0 Const SND_ASYNC = &H1 Const SND_NODEFAULT = &H2 Const SND_LOOP = &H8 Const SND_NOSTOP = &H10 Private Declare Function sndPlaySound Lib "WINMM.DLL" _ Alias "sndPlaySoundA" _ (ByVal lpszSoundName As String, ByVal uFlags As Long) As Long
Public Event Click()
Private Sub Command1_Click() SoundName$ = "c:\windows\clicksnd.wav" wFlags% = SND_ASYNC Or SND_NODEFAULT x% = sndPlaySound(SoundName$, wFlags%) End Sub
Private Sub UserControl_InitProperties() Caption = Extender.Name End Sub Public Property Get Caption() As String Caption = Command1.Caption End Property Public Property Let Caption(ByVal NewCaption As String) Command1.Caption = NewCaption End Property Private Sub UserControl_Resize() Command1.Width = Width Command1.Height = Height End Sub
To test the enhanced button control, close the designer and place
an instance of the control on an EXE project form. Run the EXE
project, then click the button. You should be able to hear a clicking
sound each time you click the button (assuming your PC has a sound
card installed).
NOTE |
The clicking sound, incidentally, was made by me. I did it by clucking into a microphone. I'm a master of the stupid sound effect. Impressed? I knew you would be. |
You may find it useful to include resource files along with your control. A resource file is a file containing one or more resources-a piece of data that is compiled along with your control or VB application. A resource can be a string, a bitmap, a sound file-nearly any kind of data.
Because it requires a sound file to work properly, the SoundButton control is a prime candidate for the use of a resource file. If the .WAV file it uses for its click sound is ever moved or deleted, the control will bite the dust. Including the sound as a resource, rather than a file, means that it will be far less likely that the control will fail to make its pleasant, soothing click sound each time it is clicked.
In order to provide a resource for use with your application, you must first compile it with a special utility called a resource compiler.
The full version of Visual Basic 4.0 contains a resource compiler,
rc.exe. (It doesn't get copied to your hard disk when you install
VB; it resides on the Visual Basic CD-ROM in the \tools\resource
folder.) There are 16- and 32-bit versions of this compiler; make
sure you use the 32-bit one when creating resources to be included
in your ActiveX control.
NOTE |
It's likely that rc.exe or a tool like it will be included with the full version of Visual Basic 5.0, although I can't tell you for sure whether that's the case, because as I write this, the full version of VB 5 hasn't been released yet. (By the way, hello all you people in the future! How's the weather up there?) A resource compiler was not scheduled to be included with the Control Creation Edition of VB5 as of this writing. However, the compiler that ships with Visual C++ is the exact same one that ships with Visual Basic, so if you have access to VC++, you can use those and everything should work just peachy. Also note that resource files created by 16- and 32-bit compilers are different. This means that you won't be able to use resource files created in the resource compiler of the 16-bit version of Visual C++ in Visual Basic. |
In order to compile a resource, you must have a script that describes what should go into the file. This file can be created in a text editor such as Notepad. The file can contain references to more than one resource, although this example only requires the sound file clicksnd.wav.
The resource file is on the CD-ROM that accompanies this book as clicksnd.rc. Here's what's in the file:
// clicksnd.rc // Jeffrey P. McManus (jeffreyp@sirius.com) // December 12, 1996 // WAV resources CLICKSND WAVE DISCARDABLE "Clicksnd.wav"
The first few lines of the .RC file are comments, set off by double slashes. The last line of the file indicates which file to include in the compiled resource file. CLICKSND is the resource ID of the resource; it's what you'll later use to retrieve the resource from the compiled resource file in your program.
Although our example .RC file only contains one resource, you can add as many more resources as you want, as long as each one has its own description in the .RC file and no two resources have the same resource ID.
The next entry, WAVE, indicates that the file that's being included is a .WAV audio resource. If this resource were a string, you'd use the descriptor STRING.
DISCARDABLE indicates to the host application that it is OK to remove this resource from memory during the course of the program's execution.
Finally, Clicksnd.wav is the name of the sound file that is to be compiled into the resource file.
Now that you have your sound file and your resource script set up, you need to compile it into a resource file. To do this:
f:\tools\resource\rc32\rc.exe -r c:\resource\clicksnd.rc
NOTE |
The file clicksnd.wav needs to be in the same folder as the resource script clicksnd.rc. |
To use the resource in your control, you must first add it to your control's project. To do this:
Next, you'll have to change the declaration you used for the API call sndPlaySound previously. This is because you're going to pass a byte array to it instead of a string (the name of the .WAV file on disk). So the parameter should be declared As Any instead of As String.
Additionally, you remove the ByVal before the SoundName because you're no longer passing the parameter by value; instead, you're passing a reference to the array that contains the actual data. (In the declaration, I changed the name of the parameter from SoundName to SoundData to reflect this.) The new version of the declaration should look like this:
Private Declare Function sndPlaySound _ Lib "winmm" Alias "sndPlaySoundA" _ (SoundData As Any, _ ByVal uFlags As Long) As Long
Next you need to write code to access the sound file compiled in the resource file. To begin, you'll need to declare a variable to store it in. Type the following code in the Declarations section of the UserControl's code module:
' byte array for storing binary file Private bSound() As Byte
The variable bSound is a byte array. The byte array stores the return value of the Visual Basic LoadResData function. LoadResData retrieves a binary resource from a compiled resource file and returns a byte array.
The reason bSound is declared at the module level is because it must not go out of scope before the sound is done playing. If the user chooses to play the sound asynchronously, the sound could go on for hours, long after the PlayRes procedure and all of its variables have gone out of scope.
Here's the code that actually plays the sound. The code belongs in the Click event of the constituent CommandButton in your control; you should delete or comment out the existing code before adding the new code. When you're done, the event procedure will look like this:
Private Sub Command1_Click() ' ***** vastly inferior version ' SoundName$ = "c:\windows\clicksnd.wav" ' x% = sndPlaySound(SoundName$, wFlags%) ' ***** end of vastly inferior version bSound = LoadResData("Clicksnd", "WAVE") wFlags% = SND_NODEFAULT Or SND_SYNC Or SND_MEMORY sndPlaySound bSound(0), wFlags% End Sub
Voila. If you run the EXE project test form and click, it should make the clicky sound just like it did before. But the difference is, this time the sound file does not have to exist on disk-it exists in your project, ready to be compiled into an EXE or OCX. The project no longer requires that the file clicksnd.wav exist on disk anywhere.
There are a number of other uses for resource files beyond embedding a sound file in your application. You can use resources to store graphics, for example, although it might make more sense to store graphics in a PictureBox control, since Visual Basic has many features for handling graphics built into the language.
You can also use resource files to store strings. This makes particular sense in situations where your control must be localized, or translated into international languages. For more information on localization, see Chapter 12, "Distributing Your Control."
The SoundButton control is a fairly simple demonstration of how to use a Windows API call to perform a task not normally available to a Visual Basic programmer. In this next section you'll create a control that makes use of several calls to the core Windows API.
The MenuPic control acts as a wrapper for a set of Windows API functions that control the drawing of menus. The purpose of the control is to replace a particular menu with a bitmap of your choice. This mimics the functionality of the menus in the new Visual Basic 5.0 IDE, which includes bitmaps that serve as a cue to toolbar buttons. The MenuPic control exposes three main properties:
To use the MenuPic control, you place an instance of the control on a form that has a menu, then you set the control's Menu and MenuItem properties to tell it which menu to change. Finally, you assign a graphic to the control's Picture property. The existing menu item will be replaced by the graphic of your choice (both at design-time and at runtime).
The visual design of the MenuPic control consists of nothing more than a constituent PictureBox control, called Picture1. In order to make the control appear invisible at runtime, you set the UserControl's InvisibleAtRuntime property to True.
The MenuPic's Picture property is delegated to Picture1's Picture property; the picture stored in the PictureBox is assigned to the menu through an API call.
Once you've created the MenuPic control with its constituent PictureBox control, you can begin entering code. Begin by entering a code module to the project. Code modules are where you store global declarations (such as API declarations) that are accessible from any part of your project.
To add a code module to your project:
You can now begin entering API call declarations in the MenuAPIs code window. Here are the API call declarations for the MenuPic project:
Public Const MF_BITMAP = &H4 Public Const MF_BYPOSITION = &H400 Declare Function GetMenu Lib "user32" (ByVal hwnd As Long) As Long Declare Function GetSubMenu Lib "user32" (ByVal hMenu As Long, _ ByVal nPos As Long) As Long Declare Function GetMenuItemID Lib "user32" (ByVal hMenu As Long, _ ByVal nPos As Long) As Long Declare Function ModifyMenuByNum Lib "user32" Alias "ModifyMenuA" _ (ByVal hMenu As Long, ByVal nPosition As Long, _ ByVal wFlags As Long, ByVal wIDNewItem As Long, _ ByVal lpstring As Long) As Long
Here's a brief summary of what these API calls do:
Because it has to be called from several places in the control, I put the menu-changing code into a private subroutine, called ChangeMenu. This code belongs in the UserControl's code window. Here is the code for the ChangeMenu subroutine:
Private Sub ChangeMenu() If Picture1.Picture = 0 Then 'nothing to do yet Exit Sub End If Dim lngTopMenuHandle As Long Dim lngSubMenuHandle As Long Dim lngMenuID As Long Dim result As Long ' Get a handle to this form's top menu lngTopMenuHandle = GetMenu(Extender.Parent.hwnd) ' Get a handle to its submenu ' Syntax: GetSubMenu(top_menu_handle, entry_position) lngSubMenuHandle = GetSubMenu(lngTopMenuHandle, mlngMenu) ' Gets the menu ID ' Syntax: GetMenuItemID(menu_handle, entry_position) lngMenuID = GetMenuItemID(lngSubMenuHandle, mlngMenuItem) ' Stick the picture in the menu ' Syntax: ModifyMenuBynum(menu_handle, entry_position, _ ' flags, ID_new_item, string) result = ModifyMenuBynum(lngSubMenuHandle, mlngMenuItem, _ MF_BITMAP Or MF_BYPOSITION, _ lngMenuID, _ Picture1.Picture) Debug.Print "ChangeMenu: Success: " & result End Sub
TIP |
When you're writing subroutines like ChangeMenu, remember to make them private, rather than public. When you make a public subroutine in a UserControl, it is exposed to the user as a method of your control. |
In order to get access to a menu item so the ModifyMenuByNum function can do its work, ChangeMenu has to do the following, in order:
You can see that the ChangeMenu procedure starts by getting the window handle of the control's parent by inspecting the HWnd property of the Extender's Parent object. This is a powerful trick that enables you to access properties of the form on which your control resides.
After ChangeMenu has retrieved the window handle of your control's parent form, it feeds it to the GetMenu API call. GetMenu returns the handle to the menu that resides on the form.
Once you've got a handle to the menu, you can feed that to the GetSubMenu API, which returns a handle to the individual menu.
You then feed GetSubMenu's return value to the GetMenuItemID API call, which gives you the ID number of the item in the submenu you're interested in messing with.
Finally you can do the actual work-calling the ModifyMenuByNum function to make the change to the menu.
Complicated, ain't it? Almost makes you wish there were an ActiveX control to encapsulate all these hairy function calls.
The Menu and MenuItem properties are simple long integers that have no direct effect on the control's appearance (they serve only to supply parameters to the ModifyMenuByNum API call; there are no new concepts here, so they shouldn't require any explanation).
To implement these properties, enter the following code into MenuPic's code window::
' Declarations Private mlngMenu As Long Private mlngMenuItem As Long ' The meat and potatoes Public Property Get Menu() As Long Menu = mlngMenu End Property Public Property Let Menu(ByVal New_Menu As Long) mlngMenu = New_Menu PropertyChanged "Menu"
More Fun With Modifymenubynum |
By the way, ModifyMenuByNum can be used to make other kinds of changes to menus, as well. For example, you can use it to change the text of a menu. But you probably wouldn't want to do that, and here's why: changes you make to menus using the Windows API short-circuit the Visual Basic Menu object you normally use to create and manage menus in VB. To demonstrate this, create a menu with the caption Ringbo, then use the MenuPic control to assign the Photon Lock graphic to it. Then use the Immediate window to inspect the Caption property of the menu. You should be able to see that the caption is still officially Ringbo. Watch out for this little anomaly when you start monkeying with menus in Visual Basic forms. |
ChangeMenu End Property Public Property Get MenuItem() As Long MenuItem = mlngMenuItem End Property Public Property Let MenuItem(ByVal New_MenuItem As Long) mlngMenuItem = New_MenuItem PropertyChanged "MenuItem" ChangeMenu End Property
There are, of course, a number of additional enhancements you could make to this control. For example, making the user choose what menu to change by having them type in a number isn't the most elegant way of going about it. Ideally, you'd let the user pick from a list of available menus; this might be best done in a custom property sheet.
You've already placed calls to ChangeMenu in the Property Let procedure. This causes the menu to change when the user assigns a Picture to the MenuPic property. However, ChangeMenu must also be called in a few other situations as well.
The "normal" time when the menu needs to be changed is when the application is first started. If you were programming this as a normal VB application, you might put the ChangeMenu code in the form's Load event. But since you're working with a UserControl instead of a form, you have to do it a little differently.
If you put a call to ChangeMenu in the UserControl's Paint event, the menu will be changed at design time. But because the control is invisible at runtime, it raises no Paint events then. The menu won't be changed at runtime. So you have a problem.
My solution to this problem was to take advantage of the fact
that the UserControl's container undergoes a palette shift at
the time the container application is run. You can trap this event
by placing code into the AmbientChanged event of the UserControl.
NOTE |
Using the AmbientChanged event in this way seems like an inelegant way to accomplish what you're trying to do with this control. There may very well be a better way to do it, but it worked reliably for me. Maybe a future enhancement to control creation in Visual Basic will include a new UserControl event that will correspond more closely with the initialization of the container application. |
The code looks like this:
Private Sub UserControl_Paint() ' changes the menu at design time only ChangeMenu End Sub Private Sub UserControl_AmbientChanged(PropertyName As String) ' this is a hack, but it works. If PropertyName = "Palette" Then ChangeMenu End If End Sub
To test the MenuPic control, do the following:
This example gives you a sense of how easy you can make it for users to access advanced functionality. By wrapping the functionality of Windows API calls in an ActiveX control, you provide new features for users while keeping them rooted in the world of Visual Basic objects, properties, and methods.
In this chapter you learned how to place calls to the Windows API in Visual Basic, as well as some of the pitfalls of API calls particular to control creation. In the next chapter, you'll learn how to compile and distribute your control to users.