WEBINAR: On-demand webcast
How to Boost Database Development Productivity on Linux, Docker, and Kubernetes with Microsoft SQL Server 2017 REGISTER >
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 to control its operation. One way of doing this is by 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, and so on) 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 that 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:
'\\ Declaration to register custom messages Private Declare Function RegisterWindowMessage Lib "user32" Alias _ "RegisterWindowMessageA" (ByVal lpString As String) As Long
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
Note: Because 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, because 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, _ '\\ The window class, e.g. "STATIC","BUTTON" etc. ByVal lpClassName As String, _ '\\ The window's name (and caption if it has one) 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 that 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
Note: 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, because 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 substitutes 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 '\\ --[VB_WindowProc]--------------------------------- '\\ 'typedef LRESULT (CALLBACK* WNDPROC)(HWND, '\\ UINT, WPARAM, LPARAM); '\\ Parameters: '\\ hwnd - window handle receiving message '\\ wMsg - The window message (WM_..etc.) '\\ wParam - First message parameter '\\ lParam - Second message parameter 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 '\\Respond to the custom message here Else '\\Pass the message to the previous '\\window procedure to handle it 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 '\\ Use (after creating the window...) hOldProc = SetWindowLongApi(hwndThis, _ GWL_WNDPROC, AddressOf VB_WindowProc)
You keep the address of the previous window procedure address in hOldProc 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 by using the FindWindowEx API call. Then, you need to send the message by using the SendMessage API call.
'\\ Declarations 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 '\\ use.... 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.