I needed to modify my temperature controller software for a customer to enable them to communicate with it from within their own software (e.g. changing parameters, starting/stopping a program etc). This falls under the heading of "inter-process communication", where data is exchanged between different running processes or applications.
The Windows operating system makes extensive used of messages. There are hundreds of different message types, which are used for things like handling user input (keyboard and mouse), system notifications, control interactions (buttons, lists) etc. It is possible to send a message to an open window by using the SendMessage function. This takes four parameters - the destination window handle, the type of message, and two parameters which are message-specific information.
It is actually possible to define your own custom message, unique to your application. However, I felt this was needlessly complex, since Windows already provides an ideal message type for data transfer - the WM_COPYDATA message. I'm going to describe how to use this message to transfer an array of data between completely separate applications. Note that this is not intended to be an in-depth explanation - I expect you to do a bit of reading about the finer points! Also note that, although here I'm using it to transfer an array of data, you could probably use it to transfer any sort of variable, as long as the data types are the same at both ends and the pointer is assigned correctly.
Every windows message has two parameters - wParam and lParam. For the WM_COPYDATA message, wParam is a handle to the sending window, and lParam is a pointer to a data structure which contains the data to be transferred. See MSDN's page on WM_COPYDATA for details. The data structure is of type COPYDATASTRUCT and contains three members (all are data type LONG):
So, how do we use this?
This ZIP file contains VB6 source code and compiled executables for both sending and receiving applications.
Here's screenshots of the sender application, both immediately after starting and after entering some data.
The box dwData allows the user to specify the value of the dwData member of the COPYDATASTRUCT.
The Data box, along with the Add and Clear buttons, allow the user to enter a list of numerical data. This is stored internally in an array - a pointer to this array, along with its size, will be sent in the cbData and lpData members.
In order to find the window handle of the receiving application, the user enters the receiver's window title (in this case "WM_COPYDATA Receiver") and clicks Find Window - the window handle (if found) is displayed. In this case it's 50E48.
Let's take a look at the code for the sender. First, there is a seperate module (Module1.bas) which contains various function definitions:
Public Type COPYDATASTRUCT dwData As Long ' Data to be passed to the receiving application cbData As Long ' Size, in bytes, of the data pointed to by lpData lpData As Long ' Pointer to data to be passed to the receiving application (kind of the main load of data) End Type Public rechWnd As Long ' handle to receiving window ' used to identify the WM_COPYDATA message Public Const WM_COPYDATA = &H4A ' Find window Public Declare Function FindWindow Lib "user32" Alias _ "FindWindowA" (ByVal lpClassName As String, ByVal lpWindowName _ As String) As Long ' Send windows message Public Declare Function SendMessage Lib "user32" Alias _ "SendMessageA" (ByVal hwnd As Long, ByVal wMsg As Long, ByVal _ wParam As Long, lParam As Any) As Long
First, we define the COPYDATASTRUCT type, a variable to store the receiving window handle (rechWnd), and a constant which defines the WM_COPYDATA message (&H4A). Next, there's two external function declarations:
FindWindow returns the handle to a window, given the window's exact title text.
SendMessage is the most important function and is used to send a windows message to the receiver.
Now let's take a look at the form code. The Add button takes a value from the text box and adds it to the list:
Private Sub Command2_Click() ' Add item If Text2.Text <> "" Then List1.AddItem Text2.Text End Sub
The clear button simply clears the list:
Private Sub Command3_Click() ' Clear list List1.Clear End Sub
The Find Window button uses the FindWindow function to get the window handle for the receiver. It both stores the handle in the rechWnd variable and displays it.
Private Sub Command4_Click() ' Find handle to receiving window and display it rechWnd = FindWindow(vbNullString, Text3.Text) Label3.Caption = Hex$(rechWnd) End Sub
Finally, the Send Data button, where all the action happens:
Private Sub Command1_Click() ' Only proceed if we have data If List1.ListCount = 0 Then Exit Sub ' Define things Dim cds As COPYDATASTRUCT Dim buf() As Byte ReDim buf(0 To List1.ListCount - 1) ' Load buffer array For n = 0 To List1.ListCount - 1 buf(n) = List1.List(n) Next n cds.dwData = Val(Text1.Text) cds.cbData = List1.ListCount cds.lpData = VarPtr(buf(0)) ' pointer to the first byte of the array returnval = SendMessage(rechWnd, WM_COPYDATA, Me.hwnd, cds) ' send message End Sub
First, we define a COPYDATASTRUCT (cds) and an array to hold the data contained in the listbox. The array is loaded with data from the listbox. Finally, we define the various members of the COPYDATASTRUCT. dwData is simply taken from the textbox. cbData is the number of items in the data array, and is taken from the listbox count. Finally, lpData (which is the pointer to the data) is assigned using VarPtr() - this returns a pointer to the specified variable, which in this case is buf(0), the first element of the data array. Finally, SendMessage is used to pass the message to the receiving application. The four parameters of SendMessage are rechWnd (the handle to the receiving window), the WM_COPYDATA constant (to indicate that the message is a WM_COPYDATA), Me.hwnd which is the handle of the sending application (so the receiver knows where the message came from), and finally the COPYDATASTRUCT (cds). Make sense?
Now let's take a look at the receiver, which is a little bit more complicated.
Here's screenshots of the receiving application, before and after receiving some data. There is no user interaction (buttons etc) - it just hangs around waiting to receive a WM_COPYDATA message from the sender. When a message is received, it displays both the dwData value and the complete list of data sent. Note that window handle (50E48) is the same as that found by the sender using FindWindow.
After receiving data
As before, most of the action is contained in an external module (Module1.bas). Here's the code:
' CopyDataStruct Public Type COPYDATASTRUCT dwData As Long ' Data to be passed to the receiving application cbData As Long ' Size, in bytes, of the data pointed to by lpData lpData As Long ' Pointer to data to be passed to the receiving application (kind of the main load of data) End Type ' Some declarations Public Const GWL_WNDPROC = (-4) ' used in SetWindowLong to indicate which index to change Public Const WM_COPYDATA = &H4A ' used to identify the WM_COPYDATA message Global lpPrevWndProc As Long ' previous window handling procedure Global gHW As Long ' window handle 'Copies a block of memory from one location to another. Declare Sub CopyMemory Lib "kernel32" Alias "RtlMoveMemory" _ (hpvDest As Any, hpvSource As Any, ByVal cbCopy As Long) ' Calls the window procedure 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 ' Adjusts window parameters (used to change the window handling procedure) Declare Function SetWindowLong Lib "user32" Alias "SetWindowLongA" _ (ByVal hwnd As Long, ByVal nIndex As Long, ByVal dwNewLong As _ Long) As Long ' Hook our OWN window procedure Public Sub Hook() lpPrevWndProc = SetWindowLong(gHW, GWL_WNDPROC, AddressOf WindowProc) Debug.Print lpPrevWndProc End Sub ' Unhook our window procedure and return it to its original state Public Sub Unhook() Dim temp As Long temp = SetWindowLong(gHW, GWL_WNDPROC, lpPrevWndProc) End Sub ' Our own window procedure Function WindowProc(ByVal hw As Long, ByVal uMsg As Long, ByVal wParam As Long, ByVal lParam As Long) As Long If uMsg = WM_COPYDATA Then ' If a WM_COPYDATA message then call our sub to process it Call mySub(lParam) End If ' Pass to the "normal" window handler WindowProc = CallWindowProc(lpPrevWndProc, hw, uMsg, wParam, lParam) End Function ' Our own procedure - called when a WM_COPYDATA message is received Private Sub mySub(lParam As Long) ' Define things Dim cds As COPYDATASTRUCT Dim buf() As Byte ' Load received data into copydatastruct Call CopyMemory(cds, ByVal lParam, Len(cds)) ' Redim the buffer array ReDim buf(0 To (cds.cbData - 1)) ' Load the array data Call CopyMemory(buf(0), ByVal cds.lpData, cds.cbData) ' Display dwData Form1.Label4.Caption = Str$(cds.dwData) ' Display main data Form1.List1.Clear For n = 0 To cds.cbData - 1 Form1.List1.AddItem Str$(buf(n)) Next n End Sub
Every window has an associated procedure called a window procedure. This is run whenever a message is received. However, if it receives our WM_COPYDATA message, it won't know what to do with it. Therefore, we need to replace the default window procedure with our own procedure which tests for the WM_COPYDATA message and processes it if received.
As before, we have some declarations - COPYDATASTRUCT, some window handles etc. There are two important functions. CallWindowProc runs the window procedure at the specified memory location. This is used to run the default procedure if the message we receive isn't WM_COPYDATA. SetWindowLong is used to change the window procedure. Now for some action.
Hook() attaches our window procedure to the window, so any incoming messages are passed to it. lpPrevWndProc is used to store the address of the previous (default) procedure, so we can restore it on exit. AddressOf is used to get the memory address of our own window procedure.
Unhook() does the exact opposite - it restores the default window procedure and is called upon exit.
WindowProc() is our own window procedure. It checks to see what sort of message was received and, if it is a WM_COPYDATA message, passes the lParam (which is a pointer to a COPYDATASTRUCT) to another subroutine to do stuff. If the message isn't WM_COPYDATA, then it's passed along to the default window procedure by using CallWindowProc.
Finally, mySub actually interprets the data received. It first defines a COPYDATASTRUCT variable and a buffer to hold the data. Next, it uses CopyMemory to load data into the COPYDATASTRUCT, and then loads the buffer array with data (as specified by lpData and cbData). It then displays dwData, and the contents of the data array are displayed in the listbox.
The only form code is some stuff to Hook the window procedure on startup and unhook it on shutdown:
Private Sub Form_Load() gHW = Me.hwnd ' get handle of current window Label1.Caption = Hex$(gHW) Hook ' Hook our own window handler End Sub Private Sub Form_Unload(Cancel As Integer) Unhook ' unhook and return to previous handler End Sub
There's an important problem with this method of communication - WM_COPYDATA messages can be generated by applications other than the intended sender, so you must implement some means of checking where the message came from. The simplest is to stick a unique sequence of data into the data array, which is then checked to see if it matches. For example, in my temperature control software, the first 16 bytes indicate where the message came from. If the received message's data doesn't match, then it is ignored and passed to the default window procedure. This guy has a good page describing exactly this problem.
Bi-directional communication is also possible. It's a bit more lengthy, but it basically involves nothing more than combining the techniques used by the sender and receiver described above.
Windows messages are very powerful, and can be used to give users a means of interacting with a piece of software from other applications.