Digging Deeper Mapping Visual Basic Events to Windows Messages
Kicking Off Your Own Events
PostMessage
SendMessage
Setting Tab Stops
Performing Magic with Multiline Edits
Finding a String in a Listbox
Dropping Down a Listbox Automatically
Summary
7 Events
by Bill Hatfield
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.
Introducing the Event-Driven Paradigm
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.
An Example
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.
Objects and Events
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.
Table 7.1. Visual Basic common object events.
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.
A Shift of Control
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.
Looking at Event-Driven Gotchas
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.
Listing 7.1. The LostFocus event of txtName.
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.
Listing 7.2. The LostFocus event of txtPhone.
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.
Table 7.2. Actions and events 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.
Application Startup
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.
Application Shutdown
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:
All the MDI child windows receive a QueryUnload event, each in turn, followed by the MDI frame's QueryUnload. In this event, each of the windows can decide whether it is OK for the application to close. This event often is used to query the user to see
whether she wants to save data or cancel the close. Any of the windows can choose to cancel the close. And, in doing so, all the sheets and the frame stay open.
If none of the child windows cancel to the close, the Unload event occurs for each child window, and then it closes.
After all the child windows are closed, the frame receives an Unload event and subsequently closes. Because the MDI frame is usually the last window up at this stage, the application ends.
NOTE
Can't you cancel the Unload in the Unload event? Actually, yes, you can. But if you do it there, you won't know the current state of the application. Some or all of the other child windows already may have been unloaded by that time. Using the QueryUnload for canceling ensures that everything is left as it was.
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.
WARNING
Be careful with End. It kills the application right away. No QueryUnloadnot even an Unload eventoccurs. So if you have housekeeping chores or any other code in the Unload of your forms, it will not be executed.
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.
Digging Deeper Mapping Visual Basic Events to Windows Messages
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.
Table 7.3. Windows messages and how they are captured in 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.
Table 7.4. Windows messages associated with Visual Basic controls.
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
Kicking Off Your Own Events
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 controlcan 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.
PostMessage
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.
Listing 7.3. The PostMessage declare statement.
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.
Listing 7.4. Additional declarations.
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).
Listing 7.5. The EndTask function.
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.
Listing 7.6. The Form Load event.
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
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:
Setting tab stops in listboxes
Getting a single line of text from a multiline text box
Finding a string in a listbox
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.
Listing 7.7. The SendMessage() declare statement.
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.
Setting Tab Stops
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.
Listing 7.8. Setting the tab stops in a listbox.
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
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.
Performing Magic with Multiline Edits
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
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:
The number of the line you want to retrieve
The string to be filled with the contents of the line
The length of that string
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.
Listing 7.9. Placing the length of the string in the first two bytes.
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
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.
Listing 7.10. The ScrollText subroutine.
Sub ScrollText (txtSubject As TextBox, nHorizontal As Integer, nVertical As _Integer)
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.
Finding a String in a Listbox
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.
Listing 7.11. Using SendMessage() to find a string in a listbox.
Function FindStringInList(sString As String, lstBox As ListBox) As Boolean
Rem This subroutine finds the string that was passed to it and then
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).
Listing 7.12. GotFocus drops down the listbox automatically.
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.
Summary
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.