Visual Basic is a Visual Development Environment (VDE). In fact, it was the first VDE for Windows. A VDE is more than a programming language. It is a complete environment tailored to the needs of a programmer writing applications for a Graphical User
Interface (GUI) environment.
If you have never programmed in a VDE and you are new to Visual Basic, then this new way of doing things will take some getting used to. Programming in Visual Basic is a very different experience from programming in a traditional language like C,
Pascal, or COBOL. Probably the biggest difference between the two is the event-driven paradigm. Let me explain.
In traditional development environments, you, as the developer, were in complete control. You determined what happened first and then what happened next. That's not to say that the users didn't have input. They did, of course. But for the most part, if
the application was at step three, for example, you knew for sure that steps one and two already had been completed. You could depend on it because there was no way the user could have gotten to step three without going through the first two steps. This
sort of certainty helped to simplify coding and error checking.
An application that makes use of the event-driven model works very differently. It starts up by doing some initialization code, just as the traditional system did, but at some point the application just stops. It doesn't do anything. The user stares at
the application and the application stares back. And nothing happens until the user makes a move. What kind of move? Well, nearly anything qualifies. She could simply move her mouse. Or she could click a menu or a button. When she does, she sets into
motion a series of events that cause the application to respond.
Let's say the user clicks a button. The operating system is monitoring very closely every move the user makes. So it doesn't fail to notice the click. The operating system also notices that the clicked button is a part of the Visual Basic application
you wrote, so it sends a message to this application. Message here is a technical term, but it means pretty much what you might think. It is as if Windows takes out its quill pen and writes a brief note that says,
Dear VB Application, The user has just clicked on the OK button that is on your main window. Love, Windows
It then drops it in the mailbox to be delivered to the application. A couple of milliseconds later when your application reads the message, it responds by generating an event on the appropriate object. The object, in this case, is the button on the main
window. And the event is Click.
Now it just so happens that when the application was written, the programmer had an inkling that the user might click on that button. So he wrote a routine that was associated with the Click event of the OK button on the main window. In fact, that is
how you will do almost all your programming in an event-driven application. You first choose an object. Then you choose an event that can happen to that object. And finally, you write a routine that will be executed at runtime when that event happens to
that object.
What are some examples of objects? The button in the earlier example was an object. But if there are text boxes, labels, drop-down listboxes, and other controls on the window, all of those are objects as well. Even the window itself is an object.
Events are simply things that can happen to an object. A given object can have many events. A text box can receive the focus (the GotFocus event). And it can lose the focus (LostFocus). Its text can be changed (Changed). It even can be clicked on like
the button was (Click). All these are events associated with a text box control. A button shares some of these events (like Click, GotFocus, and LostFocus) but not all of them. And the button may even have a few of its own events that the text box doesn't
have. Table 7.1 summarizes some often-used events common to many objects.
Event |
Event Description |
Change |
Occurs again and again as changes are made in a text control. |
Click |
Occurs when the user presses and then releases a mouse button over an object. |
DblClick |
Occurs when the user rapidly presses the mouse button twice. |
DragDrop |
Occurs when the user presses and holds the mouse button down over a dragable object, moves the mouse to a new position, and then releases the mouse button (drops the dragged object). The DragDrop event is an event of the object that was dropped on, not the one being dragged. |
DragOver |
Occurs in an object when another object is being dragged across it. |
GotFocus |
Occurs when an object receives the focus. Happens when the user tabs to or clicks to move focus to the object. |
KeyDown, KeyUp |
Occurs when the user presses (KeyDown) or releases (KeyUp) a key while an object has the focus. |
KeyPress |
Occurs when the user presses and releases a key. |
LostFocus |
Occurs when an object loses the focus. Happens when a user tabs to or clicks on another object. |
MouseDown, MouseUp |
Occurs when the user presses (MouseDown) or releases (MouseUp) a mouse button. |
MouseMove |
Occurs when the user moves the mouse across the screen. MouseMove is generated again and again dozens and dozens of times as the mouse moves from one place to another. |
Will you, as a programmer, write code to respond to all these different events? No. In fact, you probably will write code only for a small portion of them. You will simply write for the events you think should be responded to in your application.
Do all events happen because the user did something? No. Although most events ultimately happen because of user intervention, Windows itself also can generate events based on a timer, the system clock, or the changing status of a connected peripheral.
But wait. If all your code is in these little event routines and the routines are executed only when the associated event happens, then how do you know which events will happen first? The answer is simple: You don't. And that's why event-driven
programming can be more challenging than traditional development. If you are in step three, you have to check to see whether steps one and two have been done. If not, you have to decide whether they really must be done before step three. If they do, you
will have to inform your users. If not, let your users do it however they like.
This points to a subtle but very important shift in priorities. You, the developer, are no longer the one in control. The user is. In fact, the user should be the center of the universe you create. This concept, called user-centric design, is the
cornerstone to developing truly great user interfaces.
In the event-driven world, there are times when you can be writing code that looks completely harmless but causes problems when run. Look at the window in Figure 7.1.
FIGURE 7.1. A simple Visual Basic form.
The first text box is txtName and the second is txtPhone. Now suppose that the code in the LostFocus event of the first text box looks like Listing 7.1.
Private Sub txtName_LostFocus() If txtName.Text = "" Then Beep txtName.SetFocus End If End Sub
Now suppose that the LostFocus event of the second text box looks like Listing 7.2.
Private Sub txtPhone_LostFocus() If txtPhone.Text = "" Then Beep txtPhone.SetFocus End If End Sub
Looks harmless enough. If either of these is empty, you will want your application to beep when users try to tab away and force them to enter something in the text box.
But if you run this code and simply press Tab right away, what will happen? First, the focus moves to txtPhone. Then the LostFocus event of txtName is executed (because it just lost the focus to txtPhone). txtName's LostFocus checks to see whether
txtName is empty. Since txtName is empty, it beeps and sets the focus back on txtName. So far so good. But setting the focus back on txtName causes txtPhone to trigger its LostFocus event. It goes through the same process and tries to set the focus back to
it. This, in turn, triggers the txtName LostFocus event, creating an endless loop.
This example shows how easy it can be to unintentionally create bad situations in event-driven code. Remember that everything you do in code has the potential for triggering other events, and if those events trigger events that eventually trigger your
code again, you easily can create an endless loop.
The moral of this particular story is this: Be careful when using a SetFocus command in a LostFocus or GotFocus event. What are some other situations to watch out for?
Table 7.2 lists some actions and corresponding events likely to be triggered.
Action |
Event(s) Triggered |
Form Refresh |
form's_Paint |
Changing the Text property of a text box |
text box's Change |
Changing the Height or Width properties of a form |
form's_Paint and form's_Resize |
If you perform any of these actions in the event script of an event they could trigger, you risk an endless loop. If you change the height of the form in the form's Resize event, for example, you almost certainly will cause an endless loop that will
generate an Out of Stack Space error. Again, these are just a few common examples. If you get an Out of Stack Space error or some other form of endless loop, be sure to check for this kind of situation.
So if our application is event-driven and code is only written in response to events, how do things ever get started? How your application starts depends on the setting in the Options dialog box. To access the Options dialog box, choose Options from the
Tools menu. The Options tabbed dialog box appears. Now choose the Project tab. The first control is Startup Form.
Visual Basic assumes that the first thing you want to do is display a form. And, of course, this usually is correct. It further assumes that the first form you create is the one you will want to display when your application starts. This is much more
likely to be a wrong assumption. After you have created several forms in your application and then want to test it, be sure to come here to the Options dialog and verify that the correct form is specified.
But what if you don't want a window to display first thing? Suppose that you want to do some initialization or checking of the system before you actually display a form? If you click on the Startup Form drop-down listbox, you will see that one of the
options is Sub Main. This option enables you to create a subroutine named Main and to have it be the first thing executed. After it finishes its processing, it probably will show a form or two, but executing the subroutine first gives you a chance to
verify that everything is okay before you get started.
You also would use Sub Main if you wanted to create a batch application that does all its processing behind the scenes without any user interaction. This kind of application is more unusual, but it is always good to have the option.
The next logical question is how does this whole thing end? The simplest answer is that it ends when the last form of your application is unloaded. That's the way it looks from the user's perspective. But the fact is that you still can have code
executing behind the scenes long after the user thinks your application has died. So it is better to say that the application ends when the last of its components has left memory.
But that isn't very helpful from a programming perspective. How do you wrap up processing for an application in your code? The Unload command removes a form from memory. Assuming that nothing else is running, an Unload on the last form that is up shuts
down the application. So generally, you don't have to worry about anything special when you are creating a simple application. Just have each form do its own cleanup in the form's Unload event.
What about larger MDI applications? Here is the process that takes place when the user indicates that he wants to close a MDI frame:
What if you don't want to go through all this checking and verifying? You can stop your application cold in its tracks by executing the End command.
Another way to freeze your program is by using the Stop command. This is a break. It differs from End in that it leaves all your variable values intact so that you can use the Debug window to check them. You even can restart after a Stop. You will use
Stop a lot in debugging, but your final code probably will never have a need to use it.
So Visual Basic creates a visual, easy-to-use way of creating Windows applications. It receives Windows messages and converts them into events for which you can write code. But sooner or later, you are going to want to go further with your applications.
Sometimes you will hear developers talk about "hitting walls" with environments like Visual Basic. Although Visual Basic makes it easy to access the most common capabilities of Windows, it doesn't really erect many walls that prevent you from
going deeper. You just have to know how it's done.
Suppose that you want to make use of a Windows message that is not captured in Visual Basic. The first step is to be absolutely sure that it isn't captured in some way. Although many messages are captured as form or control events, some are captured
through methods as well. Table 7.3 lists window messages and how they are captured within Visual Basic.
Windows Message |
VB Event/Method/Property |
Description |
WM_ACTIVATE |
Form_Activate |
Indicates a change in the activation state |
WM_ACTIVATEAPP |
None |
Notifies applications when a new task is activated |
WM_ASKCBFORMATNAME |
GetFormat() Clipboard function |
Retrieves the name of the Clipboard format |
WM_CANCELMODE |
None |
Notifies a window to cancel internal modes |
WM_CHANGECBCHAIN |
None |
Notifies Clipboard viewer of removal from chain |
WM_CHAR |
Form_KeyPress, Form_KeyDown |
Passes keyboard events to focus window |
WM_CHARTOITEM |
None, managed by Visual Basic |
Provides listbox keystrokes to owner window |
WM_CHILDACTIVATE |
MDIChildForm_ Activate |
Notifies a child window of activation |
WM_CHOOSEFONT_GETLOGFONT |
None, managed by Visual Basic |
Retrieves LOGFONT structure for Font dialog box |
WM_CLEAR |
Used as the control object.Clear method or Text1.Text = "" |
Clears an edit or combo box |
WM_CLOSE |
Form_Unload,Form_QueryUnload |
Signals a window or application to terminate |
WM_COMMAND |
object_Click (menu or command button) |
Specifies a command message |
WM_COMMNOTIFY |
None |
Notifies a window about the status of its queues |
WM_COMPACTING |
None |
Indicates a low-memory con-dition |
WM_COMPAREITEM |
Used as the object.ListIndex method |
Determines position of combo box or listbox item. |
WM_COPY |
Same as SetText() and SetData() Clipboard methods |
Copies a selection to the Clipboard |
WM_CREATE |
Form_Load |
Indicates that a window is being created |
WM_CTLCOLOR |
None |
Indicates that a control is about to be drawn |
WM_CUT |
Used as SetText() or SetData() Clipboard methods |
Deletes a selection and copies it to the Clipboard |
WM_DDE_ACK |
|
Acknowledges the receipt of a DDE transaction |
WM_DDE_EXECUTE |
object.LinkExecute() |
Passes a command to a DDE server |
WM_DDE_INITIATE |
object.LinkMethod() |
Initiates a DDE conversation |
WM_DDE_POKE |
object.LinkPoke() |
Sends an unsolicited data item to a server |
WM_DDE_REQUEST |
object.LinkRequest() |
Requests value of a data item from a DDE server |
WM_DDE_TERMINATE |
object.LinkClose() |
Terminates a DDE conversation |
WM_DEADCHAR |
None |
Indicates when a dead key is pressed |
WM_DELETEITEM |
None |
Indicates that owner-drawn item or control is altered |
WM_DESTROY |
Form_QueryUnload |
Indicates that window is about to be destroyed |
WM_DESTROYCLIPBOARD |
None |
Notifies owner when Clipboard is emptied |
WM_DEVMODECHANGE |
None |
Indicates when device-mode settings are changed |
WM_DRAWCLIPBOARD |
None |
Indicates when Clipboard contents are changed |
WM_DRAWITEM |
None |
Indicates when owner-drawn control or menu changes |
WM_DROPFILES |
object_DragDrop |
Indicates when a file is dropped |
WM_ENABLE |
None |
Indicates when enable state of window is changing |
WM_ENDSESSION |
None |
Indicates whether the Windows session is ending |
WM_ENTERIDLE |
None |
Indicates that a modal dialog box or menu is idle |
WM_ERASEBKGND |
None, closest item is the AutoRedraw property |
Indicates when a window background needs erasing |
WM_FONTCHANGE |
None |
Indicates a change in the font-resource pool |
WM_GETDLGCODE |
None |
Allows processing of control input |
WM_GETFONT |
None |
Retrieves the font that a control is using |
WM_GETMINMAXINFO |
None, however can be linked to Form_Resize |
Retrieves minimum and maximum sizing information |
WM_GETTEXT |
Used as GetText() or GetData() Clipboard methods |
Copies the text to a corresponding window |
WM_GETTEXTLENGTH |
Same as Len(object.Text) |
Determines length of text associated with a window |
WM_HSCROLL |
object.Scroll where object is a horizontal scroll bar |
Indicates a click in a horizontal scroll bar |
WM_HSCROLLCLIPBOARD |
None |
Prompts owner to scroll Clipboard contents |
WM_ICONERASEBKGND |
None |
Notifies minimized window to fill icon background |
WM_INITDIALOG |
None |
Initializes a dialog box |
WM_INITMENU |
None |
Indicates when a menu is about to become active |
WM_INITMENUPOPUP |
Used as object.PopUpMenu method |
Indicates when a pop-up menu is being created |
WM_KEYDOWN |
object_KeyDown |
Indicates when a nonsystem key is pressed |
WM_KEYUP |
object_KeyUp |
Indicates when a nonsystem key is released |
WM_KILLFOCUS |
object_LostFocus |
Indicates window is about to lose input focus |
WM_LBUTTONDBLCLK |
object_DblClick |
Indicates double-click of left mouse button |
WM_LBUTTONDOWN |
object_MouseDown |
Indicates when left mouse button is pressed |
WM_LBUTTONUP |
object_MouseUp |
Indicates when left mouse button is released |
WM_MBUTTONDBLCLK |
object_DblClick |
Indicates double-click of middle mouse button |
WM_MBUTTONDOWN |
object_MouseDown |
Indicates when middle mouse button is pressed |
WM_MBUTTONUP |
object_MouseUp |
Indicates when middle mouse button is released |
WM_MDIACTIVATE |
MDIChildForm Activate |
Activates a new MDI child window |
WM_MDICASCADE |
Used as the MDIForm.Arrange() method |
Arranges MDI child windows in a cascade format |
WM_MDICREATE |
None |
Prompts a MDI client to create a child window |
WM_MDIDESTROY |
MDIChildForm QueryUnload, MDIChildForm_Unload |
Closes a MDI child window |
WM_MDIGETACTIVE |
None |
Retrieves data about the active MDI child window |
WM_MDIICONARRANGE |
MDIForm.Arrange() |
Arranges minimized MDI child windows |
WM_MDIMAXIMIZE |
None |
Maximizes a MDI child window |
WM_MDINEXT |
None |
Activates the next MDI child window |
WM_MDIRESTORE |
None |
Prompts a MDI client to restore a child window |
WM_MDISETMENU |
None |
Replaces the menu of a MDI frame window |
WM_MDITILE |
MDIForm.Arrange() |
Arranges MDI child windows in a tiled format |
WM_MEASUREITEM |
None |
Requests dimensions of owner-drawn control |
WM_MENUCHAR |
None |
Indicates when unknown menu mnemonic is pressed |
WM_MENUSELECT |
MenuObject_Click |
Indicates when a menu item is selected |
WM_MOUSEACTIVATE |
Form_Activate |
Indicates a mouse click in an inactive window |
WM_MOUSEMOVE |
object_MouseMove |
Indicates mouse-cursor movement |
WM_MOVE |
None |
Indicates the position of a window has changed |
WM_NCACTIVATE |
None. Visual Basic does not provide access to any non-client events |
Changes the active state of a nonclient area |
WM_NCCALCSIZE |
Used as the ScaleWidth and ScaleHeight properties |
Calculates the size of a window's client area |
WM_NCCREATE |
None |
Indicates that a nonclient area is being created |
WM_NCDESTROY |
None |
Indicates when nonclient area is being destroyed |
WM_NCHITTEST |
None |
Indicates mouse-cursor movement |
WM_NCLBUTTONDBLCLK |
None |
Indicates non-client left button double-click |
WM_NCLBUTTONDOWN |
None |
Indicates left button pressed in nonclient area |
WM_NCLBUTTONUP |
None |
Indicates left button released in nonclient area |
WM_NCMBUTTONDBLCLK |
None |
Indicates middle button nonclient double-click |
WM_NCMBUTTONDOWN |
None |
Indicates middle button pressed in nonclient area |
WM_NCMBUTTONUP |
None |
Indicates middle button released in nonclient area |
WM_NCMOUSEMOVE |
None |
Indicates mouse-cursor movement in nonclient area |
WM_NCPAINT |
None |
Indicates that a window's frame needs painting |
WM_NCRBUTTONDBLCLK |
None |
Indicates right button nonclient double-click |
WM_NCRBUTTONDOWN |
None |
Indicates right button pressed in nonclient area |
WM_NCRBUTTONUP |
None |
Indicates right button released in nonclient area |
WM_NEXTDLGCTL |
Used as the SetFocus() method |
Sets focus to a different dialog box control |
WM_PAINT |
Form_Paint |
Indicates that a window frame needs painting |
WM_PAINTCLIPBOARD |
None |
Paints the specified portion of the window |
WM_PALETTECHANGED |
None |
Indicates that focus window has realized its palette |
WM_PALETTEISCHANGING |
None |
Informs windows about change to palette |
WM_PARENTNOTIFY |
None |
Notifies parent of child-window activity |
WM_PASTE |
Used as Text1.Text =Clipboard.GetText (vbCFText) |
Inserts Clipboard data into an edit control (text box) |
WM_POWER |
None |
Indicates that the system is entering suspended mode |
WM_QUERYDRAGICON |
None |
Requests a cursor handle for a minimized window |
WM_QUERYENDSESSION |
None |
Requests that the Windows session be ended |
WM_QUERYNEWPALETTE |
None |
Enables a window to realize its logical palette |
WM_QUERYOPEN |
None |
Requests that a minimized window be restored |
WM_QUEUESYNC |
None |
Delimits CBT messages |
WM_QUIT |
Form_QueryUnload, Form_Unload |
Requests that an application be terminated |
WM_RBUTTONDBLCLK |
object_DblClick |
Indicates a double-click of right mouse button |
WM_RBUTTONDOWN |
object_MouseDown |
Indicates when the right mouse button is pressed |
WM_RBUTTONUP |
object_MouseUp |
Indicates when the right mouse button is released |
WM_RENDERALLFORMATS |
None |
Notifies owner to render all Clipboard formats |
WM_RENDERFORMAT |
None |
Notifies owner to render particular Clipboard data |
WM_SETCURSOR |
object.ScreenMouse Pointer property |
Displays the appropriate mouse cursor shape |
WM_SETFOCUS |
object_GotFocus |
Indicates when a window has gained input focus |
WM_SETFONT |
Used as the object.FontName property |
Sets the font for a control |
WM_SETREDRAW |
None |
Allows or prevents redrawing in a window |
WM_SETTEXT |
Used as the .Text or .Caption |
Sets the text of a window properties |
WM_SHOWWINDOW |
None |
Indicates that a window is about to be hidden or shown |
WM_SIZE |
Form_ReSize |
Indicates a change in window size |
WM_SIZECLIPBOARD |
None |
Indicates a change in Clipboard size |
WM_SPOOLERSTATUS |
None |
Indicates when a print job is added or removed |
WM_SYSCHAR |
None |
Indicates when a system-menu key is pressed |
WM_SYSCOLORCHANGE |
None |
Indicates when a system color setting is changed |
WM_SYSCOMMAND |
None |
Indicates when a system command is requested |
WM_SYSDEADCHAR |
None |
Indicates when a system dead key is pressed |
WM_SYSKEYDOWN |
object_KeyDown |
Indicates that Alt plus another key was pressed |
WM_SYSKEYUP |
object_KeyUp |
Indicates that Alt plus another key was released |
WM_SYSTEMERROR |
None |
Indicates that a system error has occurred |
WM_TIMECHANGE |
None |
Indicates that the system time has been set |
WM_TIMER |
TimerObject_Timer |
Indicates that the time-out interval for a timer has elapsed |
WM_UNDO |
None |
Undoes the last operation in an edit control |
WM_USER |
None |
Indicates a range of message values |
WM_VKEYTOITEM |
None |
Provides listbox keystrokes to owner window |
WM_VSCROLL |
object_Scroll |
Indicates a click where object is a vertical scroll bar |
WM_VSCROLLCLIPBOARD |
None |
Prompts the owner to scroll Clipboard contents |
WM_WINDOWPOSCHANGED |
Form_Resize |
Notifies a window of a size or position change |
WM_WINDOWPOSCHANGING |
Form_Resize |
Notifies a window of a new size or position |
WM_WININICHANGE |
None |
Notifies applications of change to WIN.INI |
As you can see, some of the Windows messages map directly to events in Visual Basic. Others map directly to methods or properties. Some loosely map to an event, method, or property that Visual Basic has chosen to handle in a more generalized way. And
finally, there are some that simply are unavailable. These same things could be said of the messages associated with controls in Table 7.4.
Windows Message |
VB Event/Method/Property |
Indicates That |
BN_CLICKED |
CommandButton _Click |
The user clicked a button |
BN_DISABLE |
CommandButton.Enabled |
A button is disabled |
BN_DOUBLECLICKED |
CommandButton _DblClick |
The user double-clicked a button |
BN_HILITE |
CommandButton _GetFocus |
The user highlighted a button |
BN_PAINT |
None |
The button should be painted |
BN_UNHILITE |
CommandButton_LostFocus |
The highlight should be removed |
CBN_CLOSEUP |
None |
The listbox of a combo box has closed |
CBN_DBLCLK |
None |
The user double-clicked a string |
CBN_DROPDOWN |
None |
The listbox of a combo box is dropping down |
CBN_EDITCHANGE |
ComboBox_Change |
The user has changed text in the edit control |
CBN_EDITUPDATE |
None |
Altered text is about to be dis- played |
CBN_ERRSPACE |
None |
The combo box is out of memory |
CBN_KILLFOCUS |
ComboBox_LostFocus |
The combo box is losing the input focus |
CBN_SELCHANGE |
ComboBox.ListFocus |
A new combo box list item is selected |
CBN_SELENDCANCEL |
None |
The user's selection should be canceled |
CBN_SELENDOK |
None |
The user's selection is valid |
CBN_SETFOCUS |
ComboBox_GotFocus |
The combo box is receiving the input focus |
EN_CHANGE |
TextBox_Change |
The display is updated after text changes |
EN_ERRSPACE |
None |
The edit control is out of memory |
EN_HSCROLL |
None |
The user clicked the scroll bar |
EN_KILLFOCUS |
TextBox_LostFocus |
The edit control is losing the input focus |
EN_MAXTEXT |
Text .MaxLength |
The insertion is truncated |
EN_SETFOCUS |
TextBox_GotFocus |
The edit control is receiving the input focus |
EN_UPDATE |
None |
The edit control is about to display altered text |
EN_VSCROLL |
Vscroll_Scroll |
The user clicked the vertical scroll bar |
LBN_DBLCLK |
ListBox_DblClick |
The user double-clicked a string |
LBN_ERRSPACE |
None |
The list box is out of memory |
LBN_KILLFOCUS |
ListBox_LostFocus |
The list box is losing the input focus |
LBN_SELCANCEL |
None |
The selection is canceled |
LBN_SELCHANGE |
ListBox_Change |
The selection is about to change |
LBN_SETFOCUS |
ListBox_GetFocus |
The list box is receiving the input focus |
Windows sends messages to applications indicating what the user is doing, and the application responds. I also mentioned that events can be triggered by timers or the system clock. So can you, as a developer, trigger events? Can you send a message to
other windows in your own application or even other applications? Well, if you just want to execute the code associated with the clicked event of your Update button, for example, you can simply code this:
Call cmdUpdate_Click()
This doesn't actually click the button (it doesn't cause the graphic to appear depressed), but it does execute the code in the Click event.
But what if you want to go further? You actually want to send a real Windows message to a window or control—can you do it? The answer is yes, but not in native Visual Basic. You'll have to resort to Win32.
The two primary Win32 functions you will use are SendMessage() and PostMessage(). These functions both do the same thing: They ship off a message to the window or control indicated (either in this application or in another one). The difference is that
SendMessage() is synchronous and PostMessage() is asynchronous. That is, SendMessage() sends the message and waits until it has arrived there and has taken effect before it returns control to your application. PostMessage(), on the other hand, simply fires
off the message and immediately returns control to your application. SendMessage(), then, works more like a function call. You know it is completely finished by the time the next line executes.
Suppose that your application wants to shut down another application. Using PostMessage, it is easy. First you have to declare the Win32 function, as shown in Listing 7.3.
Declare Function PostMessage Lib "user32" Alias "PostMessageA"
(ByVal hWnd As Long, _ ByVal wMsg As Long, _ ByVal wParam As Integer, _ lParam As Any) As Long
hWnd identifies the window to which the message is posted. If this parameter is HWND_BROADCAST, the message is posted to all top-level windows, including disabled or invisible windows.
wMsg specifies the message to be posted.
wParam specifies 16 bits of additional message-dependent information. lParam specifies 32 bits of additional message-dependent information.
The return value is nonzero if the function is successful. The only way it would return zero is if the receiving application's message queue is full. That can happen when you send the same application many messages without giving it a chance to respond.
If you run into this problem, try sprinkling a few Yield() statements around. You will use only PostMessage() with Windows. You'll see how the more flexible SendMessage() can be used with controls later.
After you declare PostMessage(), you need to declare a few other Win32 functions you'll use along the way, as shown in Listing 7.4.
Declare Function IsWindow Lib "User" (ByVal Hwnd As Integer) As Integer Declare Function GetWindow Lib "User" (ByVal Hwnd As Integer, ByVal wCmd As
_Integer) As Integer Declare Function GetWindowLong Lib "User" (ByVal Hwnd As Integer, ByVal nIndex As _Integer) As Long Declare Function PostMessage Lib "User" (ByVal Hwnd As Integer, ByVal wMsg As _Integer, ByVal wParam As Integer, ByVal lParam As Long) As Integer Declare Function FindWindow Lib "User" (ByVal lpClassName As Any, ByVal _lpWindowName As String) As Integer Const GW_OWNER = 4 Const GWL_STYLE = -16 Const WS_DISABLED = &H8000000 Const WS_CANCELMODE = &H1F Const WM_CLOSE = &H10
Listing 7.5 is the EndTask function. It receives a window handle. It verifies that the window handle is valid and that it is not disabled. Then the WM_CANCELMODE and WM_CLOSE messages are passed with no parameters (0 and 0& in the word and long
params).
Function EndTask(TargetHwnd As Integer) As Integer Dim X As Integer Dim ReturnVal As Integer ReturnVal = True If TargetHwnd = hWndMe% Or GetWindow(TargetHwnd, GW_OWNER) = hWndMe% Then End End If If IsWindow(TargetHwnd) = False Then ReturnVal = False Else If Not (GetWindowLong(TargetHwnd, GWL_STYLE) And WS_DISABLED) Then X = PostMessage(TargetHwnd, WM_CANCELMODE, 0, 0&) X = PostMessage(TargetHwnd, WM_CLOSE, 0, 0&) DoEvents End If End If EndTask% = ReturnVal End Function
Finally, Listing 7.6 is the Form Load event procedure, which uses EndTask to take down Notepad if it happens to be running when this window opens. You can use similar code wherever you need this functionality.
Sub Form_Load() Dim Hwnd As Integer Dim Y As Integer Hwnd = FindWindow(0&, "Notepad") If Hwnd = 0 Then MsgBox "NotePad is not running", vbInformation Exit Sub Else MsgBox "NotePad will now be closed", vbInformation End If Y = EndTask(Hwnd) If Y <> 0 Then MsgBox "NotePad terminated", vbInformation Else MsgBox "Error - Cannot terminate NotePad", vbInformation End If End Sub
SendMessage() is the synchronous sibling of PostMessage(). Unlike PostMessage(), SendMessage() tends to get used frequently to send messages to windows and controls within the same application. Why would you want to send a message to a control on your
own form? Well, it turns out that this is a good way to retrieve some of the functionality that Visual Basic steals away from you and to get around some of those "walls."
Here are some examples of things you can do with the appropriate SendMessage() that Visual Basic doesn't normally let you do:
Although you can implement your own code to find a string in a listbox, you don't have to. Windows already has that functionality built right in. All you have to do is access it through SendMessage()!
The declaration and parameters are very similar to PostMessage(), as shown in Listing 7.7.
Declare Function SendMessage Lib "user32" Alias "SendMessageA"
(ByVal hWnd As Long, _ ByVal wMsg As Long, _ ByVal wParam As Integer, _ lParam As Any) As Long
hWnd identifies the window to which the message will be sent. If this parameter is HWND_BROADCAST, the message is sent to all top-level windows, including disabled or invisible unowned windows.
uMsg specifies the message to be sent.
wParam specifies 16 bits of additional message-dependent information. lParam specifies 32 bits of additional message-dependent information.
So how do you put it to use? Well, normally, if you include a Chr$(9) (a tab character) in a string that you put in a listbox or a multiline edit, the control simply moves the following text to its next tab stop. Visual Basic gives you no way to set
these tab stops to be where you want them, however. But you can with SendMessage(). Listing 7.8 shows you how.
Dim nTabPos(3) As Integer Dim lResult As Long Rem Define the positions nTabPos(0) = 10 nTabPos(1) = 50 nTabPos(2) = 90 Rem Set the focus to the target listbox and fire away lstExample.SetFocus lResult = SendMessage(GetFocus(), LB_SETTABSTOPS, 0, ByVal 0&) lResult = SendMessage(GetFocus(), LB_SETTABSTOPS, 3, nTabPos(0))
First, an array is created and then filled in with the desired tab positions. Then an LB_SETTABSTOPS with a 0 and ByVal 0& is used to clear any existing tab stops. Finally, LB_SETTABSTOPS with the number of array elements (3) and the first array
element (nTabPos(0)) actually sets the tab stops.
You can set the tab stops in a multiline text box in the same way. Just send the message EM_SETTABSTOPS instead of LB_SETTABSTOPS.
Speaking of multiline text boxes, there are several useful messages you can send to a multiline text box to get information that Visual Basic does not provide. You can send the EM_GETLINECOUNT message, for example, to get the number of lines of text in
a multiline text box:
lLineCount = SendMessage(GetFocus(), EM_GETLINECOUNT, 0, ByVal 0&)
In addition, you can get the text of any individual line in a multiline text box with the message EM_GETLINE. But this message is a little tricky because it requires you to pass three parameters:
Because there are only two parameters, you will have to do a little trickery. You'll put the number of the line you want in the third parameter and the string in the fourth. But in order to tell it how long the string is, you'll have to embed the number
in the first two bytes of the string, as shown in Listing 7.9.
Dim nLineNum As Integer Dim nLineLen As Integer Dim sTemp As String Rem Set the max line length to 200 nLineLen = 200 Rem Fill The string with nulls and then paste the length to the front sTemp = String$(nLinLen, vbNullChar) sTemp = Chr$(nLinLen Mod 256) + Chr$(nLineLen \ 256) + sTemp sTemp = Left$(sTemp, SendMessage(GetFocus(), EM_GETLINE, nLinNum, ByVal sTemp))
Another handy message for multiline text boxes is EM_LINESCROLL, which enables you to scroll your text boxes horizontally and vertically. Just specify the number of characters to scroll in the fourth argument of the SendMessage() function. Place the
number of characters to scroll horizontally in the high order word (by multiplying by 65,536) and the number of lines to scroll vertically in the low order word. See Listing 7.10.
Sub ScrollText (txtSubject As TextBox, nHorizontal As Integer, nVertical As _Integer) Const EM_LINESCROLL = &HB6 Dim lResult As Long Dim lScroll As Long lScroll = (nHorizontal * 65536) + nVertical txtSubject.SetFocus lResult = SendMessage(GetFocus(), EM_LINESCROLL, 0, ByVal lScroll) End Sub
Keep in mind that this is a relative scroll. So if you use the value 3, the text box scrolls down three lines. If you use the value —65,536 (—1 times 65,536), the text box scrolls left one char-acter.
A searching capability already is built into the listbox. But Visual Basic doesn't let you get to it. SendMessage() does. The code for FindStringInList is in Listing 7.11. It receives a string and a listbox or combo box. It returns True or False,
indicating whether it succeeded in finding the string in the listbox. If it did find it, the ListIndex for the listbox or combo box is set to the place it was found.
Function FindStringInList(sString As String, lstBox As ListBox) As Boolean Rem This subroutine finds the string that was passed to it and then Rem sets the passed combo box to that value Const LB_ERR = —1 Const LB_FINDSTRING = (&H400 + 16) Dim lResult As Long Dim nZero As Integer lResult = SendMessage(lstBox.hWnd, LB_FINDSTRING, 0, ByVal sString) If lResult = LB_ERR Then FindStringInList = False Else FindStringInList = True lstBox.ListIndex = lResult End If End Function
Here's a neat trick for a drop-down list combo box (a combo box with the Style property set to 2). This code drops the list automatically when the combo box gets the focus (see Listing 7.12).
Sub Combo1_GotFocus () Rem When this control receives focus it will Rem automatically drop down Dim lResult As Long Const CB_SHOWDROPDOWN = WM_USER + 15 lResult = SendMessage(GetFocus(), CB_SHOWDROPDOWN, 1, ByVal 0&) End Sub
Sending messages in the GotFocus event for a control is a good technique because it enables you to avoid explicitly setting the focus to a control to get its hWnd.
This chapter was a whirlwind tour of event-driven programming and the use of events in
Visual Basic. I began with a description of what it means to be event-driven in comparison to using traditional techniques. After some warnings about potential pitfalls, you explored
application startup and shutdown. Then you saw the window and control events and how they applied to Visual Basic. Finally, you learned about some key Win32 functions that enable you to make use of Windows messages to get at some of the built-in
Windows functionality that Visual Basic hides.