Introduction
One of the simplest ways to implement multi-tasking in Visual Basic is to create a
separate
executable program to do each task and simply use the Shell command to run them as
necessary. The only problem with this is that, once a program is running, you need to
communicate with it in order to control its operation.
One way of doing this is using the RegisterWindowMessage
and SendMessage
API
calls to create your own particular window messages and to send them between windows thus
allowing you to create two or more programs that communicate with each other.
In this example, the server has the job of watching a printer queue and sending a
message to every interested client, whenever an event (job added, driver changed,
job printed etc.) occurs.
Specifying your own unique messages
Windows communicate with each other by sending each other, standard Windows messages such
as WM_CLOSE
to close and terminate the window. There are a large number of standard messages
which cover most of the standard operations that can be performed by and to different windows.
However if you want to implement your own custom communication, you need to create your own
custom messages. This is done with the RegisterWindowMessage
API call:
Private Declare Function RegisterWindowMessage Lib "user32" Alias _
"RegisterWindowMessageA" (ByVal lpString As String) As Long
This API call takes a unique string and registers it as a defined Windows message,
returning a system wide unique identifier for that message as a result. Thereafter any
call to RegisterWindowMessage
in any application that specifies the same string, will
return the same unique message ID. Because this value is constant during each session,
it is safe to store it in a global variable to speed up execution thus:
Public Const MSG_CHANGENOTIFY = "MCL_PRINT_NOTIFY"
Public Function WM_MCL_CHANGENOTIFY() As Long
Static msg As Long
If msg = 0 Then
msg = RegisterWindowMessage(MSG_CHANGENOTIFY)
End If
WM_MCL_CHANGENOTIFY = msg
End Function
Since this message needs to be known to every application that is using it to
communicate, it is a good idea to put this into a shared code module common to all projects.
Creating windows to listen for these messages
To create a window in Visual Basic, you usually use the form designer and add a new form
to your project. However, since our communications window has no visible component nor
interaction with the user, this is a bit excessive. Instead we can use the CreateWindowEx
API call to create a window, solely for our communication:
Private Declare Function CreateWindowEx _
Lib "user32" Alias "CreateWindowExA"
(ByVal dwExStyle As Long, _
ByVal lpClassName As String, _
ByVal lpWindowName As String, _
ByVal dwStyle As Long, _
ByVal x As Long, _
ByVal y As Long, _
ByVal nWidth As Long, _
ByVal nHeight As Long, _
ByVal hWndParent As Long, _
ByVal hMenu As Long, _
ByVal hInstance As Long, _
lpParam As Any) As Long
If this call is successful, it returns a unique window handle which can be used
to refer to that window. This can be used in SendMessage
calls to send a message to it.
In a typical client/server communication, you need to create one window for the client(s) and one window for the server.
Again this can be done with a bit of code common to each application:
Public Const WINDOWTITLE_CLIENT = "Merrion Computing IPC - Client"
Public Const WINDOWTITLE_SERVER = "Merrion Computing IPC - Server"
Public Function CreateCommunicationWindow(ByVal client As Boolean) As Long
Dim hwndThis As Long
Dim sWindowTitle As String
If client Then
sWindowTitle = WINDOWTITLE_CLIENT
Else
sWindowTitle = WINDOWTITLE_SERVER
End If
hwndThis = CreateWindowEx(0, "STATIC", sWindowTitle,_
0, 0, 0, 0, 0, 0, 0, App.hInstance, ByVal 0&)
CreateCommunicationWindow = hwndThis
End Function
Obviously for your own applications you should use different text for the
WINDOWTITLE_CLIENT
and WINDOWTITLE_SERVER
than above, to ensure that
your window names are unique.
Processing the custom messages
As it stands, you have a custom message and have created a window to which you can
send that message. However, as this message is entirely new
to the window it does not do anything when it receives it. To actually process the
message you need to subclass the window to intercept and react to the message yourself.
To subclass the window, you create a procedure that processes Windows messages and
substitute this for the default message handling procedure of that window.
Your procedure must have
the same parameters and return type as the default window procedure:
Private Declare Function CallWindowProc Lib
"user32" Alias "CallWindowProcA" (ByVal lpPrevWndFunc
As Long, ByVal hwnd As Long, ByVal msg As Long, ByVal
wParam As Long, ByVal lParam As Long) As Long
Public Function VB_WindowProc(ByVal hwnd As Long, _
ByVal wMsg As Long, ByVal wParam As Long,_
ByVal lParam As Long) As Long
If wMsg = WM_MCL_CHANGENOTIFY Then
Else
VB_WindowProc = CallWindowProc(hOldProc, hwnd, _
wMsg, wParam, lParam)
End If
End Function
You then need to inform Windows to substitute this procedure for the existing
window procedure. To do this you call SetWindowLong
to change the address
of the procedure as stored in the GWL_WINDPROC
index.
Public Const GWL_WNDPROC = (-4)
Public Declare Function SetWindowLongApi Lib "user32" _
Alias "SetWindowLongA" _
(ByVal hwnd As Long, ByVal nIndex As Long, _
ByVal dwNewLong As Long) As Long
hOldProc = SetWindowLongApi(hwndThis, _
GWL_WNDPROC, AddressOf VB_WindowProc)
You keep the address of the previous window procedure address in hOldProc
in order to pass on all the messages that you don't deal with for
default processing. It is a good idea to set the window procedure back to this
address before closing the window.
Sending the custom messages
There are two steps to sending the custom message to your server window: First you need
to find the window handle of that window using the FindWindowEx
API call.
Then you need to send the message using the SendMessage
API call.
Public Declare Function SendMessageLong Lib "user32" Alias "SendMessageA" _
(ByVal hwnd As Long, ByVal wMsg As Long, ByVal wParam _
As Long, ByVal lParam As Long) As Long _
Public Declare Function FindWindow Lib "user32" Alias "FindWindowA" _
(ByVal lpClassName As String, ByVal lpWindowName As String) As Long
Dim hwndTarget As Long
hwndTarget = FindWindow(vbNullString, WINDOWTITLE_SERVER)
If hwndTarget <> 0 Then
Call SendMessageLong(hwnd_Server, _
WM_MCL_CHANGENOTIFY, 0,0)
End If
This will send the WM_MCL_CHANGENOTIFY
message to the server window and return
when it has been processed.