Click here to Skip to main content
Click here to Skip to main content

VB.NET, VB6 and C# Interprocess communication via Window Messaging

By , 16 May 2004
 

Introduction

There are quite a few articles on inter-process communication for VB6, and quite a few articles for inter-process communication in .NET. But it is an interesting mix using managed and unmanaged environments for inter-process communication. I have finally got it working after some issues with VB6. :-)

Background

My company is currently developing new applications in .NET, but we still have to support our (main company app) legacy VB6 applications. We had started to use COM-interop from VB6 to VB.NET components, but struck some bugs trying to automate applications from each other. Having a look around, I figured that Window Messaging was independent of VB and .NET environments. So then began my search of code samples and explanations of doing this. Some explained VB6 messaging, some explained .NET messaging, but couldn't find any that did both.

Using the code

For an explanation on the VB.NET sub-classing code, refer to this CodeProject article. I have taken the code straight from here and see no point in explaining it again.

VB6: Form interface:

  Private Sub Form_Load()
    ....
    'Create custom window and start listening to window messages.
    modMessaging.InitWindowMessaging
  End Sub

  Private Sub Form_Unload(Cancel As Integer)
    'Tear down custom message handling and pass 
    'back to original message handler i.e this form
    modMessaging.StopWindowMessaging
  End Sub

VB6: Messaging

  ' Function to create custom window and setup Message Listening
  Public Function InitWindowMessaging()

    '\\This statement creates the new window
    hWindowHandle = CreateWindowEx(0, "STATIC", VB6_WINDOWTITLE_SERVER, _
      0, 0, 0, 0, 0, 0, 0, App.hInstance, ByVal 0&)

    '\\ This statement sets the message handling to the
    '\\ProcessWindowMessages function defined later.
    ' We also save the address (hOldProc) of
    '\\of the previous MessageHandler so we
    ' can reset on StopWindowMessaging>
    hOldProc = SetWindowLongApi(hWindowHandle,_
       GWL_WNDPROC, AddressOf ProcessWindowMessages)

    WindowMessagingInitialised = True
  End Function

  'Function to tear down Message Handling 
  'and return to original Message Handler
  Public Function StopWindowMessaging()
    '\\This statement sets the Message Handling 
    'to be set to the address
    '\\of the previous MessageHandler which 
    'we saved before changing it to ours.
    Call SetWindowLongApi(hWindowHandle, GWL_WNDPROC, hOldProc)
  End Function

  'Function to find VB.NET window and attempt to send message
  Public Function SendMessageToVBNET()
    Dim hwndTarget As Long
    Dim MessageId As Long

    If WindowMessagingInitialised = False Then
      InitWindowMessaging
    End If

    'Get TargetWindow handle from global Window Name
    hwndTarget = VBNET_WindowHandle

    'Get MessageId from API call to RegisterMessage
    MessageId = VB6_TO_DOTNET_MessageId

    'If Window target exists, then PostMessage to target
    If
    hwndTarget <>  0 Then
        Call PostMessage(hwndTarget, MessageId, 0, 0)
    End If
  End Function

  Function to process messages. If not one of our custom messages,
  'then fall through to original Message Handler
  Private Function ProcessWindowMessages(ByVal hwnd As Long, _
    ByVal wMsg As Long, _
    ByVal wParam As Long, _
    ByVal lParam As Integer) As Long

    If wMsg = DOTNET_TO_VB6_MessageId Then
      '\\Respond to the custom message here
      MsgBox "window message received from DOTNET environment"
    Else
      '\\Pass the message to the previous 
      'window procedure to handle it
      ProcessWindowMessages = CallWindowProc(hOldProc, _
                               hwnd, wMsg, wParam, lParam)
    End If
  
  End Function

Points of Interest

One thing which I think is quite nice about this implementation is that it creates its own windows with user defined names. So you have global constants defined in VB6 and VB.NET for the window names. You can then rename your application, and the communication won't break.

The RegisterWindowMessage API call provides a system-wide MessageId. Subsequent calls to RegisterWindowMessage will return the same MessageId.

VB6: One thing to note that is vitally important is that you need to disconnect your custom Message Handler before the application closes, otherwise it starts to handle messages in the VB IDE. It was quite hard to track this problem down because it would work nicely when I ran the executable, but once inside the IDE, it would crash VB. Once I started writing debug.print statements in my Message Handler function, all became clear. Somehow, I was not handing message handling responsibilities to the MainForm.

VB.NET: Nothing much to note here. So much easier in .NET than VB. :-)

Links used while developing:

Version 3 now has VB6App2 source code. You can now use Window Messaging:

  • VB6 -> VB.NET
  • VB6 -> C#
  • VB6 -> VB6App2
  • VB6App2 -> VB6
  • VB.NET -> VB6
  • VB.NET -> C#
  • C# -> VB6
  • C# -> VB.NET

History

  1. Initial release to The Code Project
  2. Updated source and demos to include C# project.
  3. Updated source and demos to include VB6 to VB6App2.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here

About the Author

angus_grant
Web Developer
Australia Australia
Member
Located in sunny Brisbane Australia, was working for three years in Internet company (ASP website and Exchange 5.5 programming mainly), and have moved on to a bankruptcy and insolvency company.
 
Working on in-house VB6/SQL Server 2000 application, VB.NET/SQL 2000 apps, C#/SQL 2000 app's and company's ASP/SQL 2000 website (blah, ASP is so yucky now I've used ASP.NET).
 
Personally working on C# / GDI+ RPG game and some other websites soon to be released.

Sign Up to vote   Poor Excellent
Add a reason or comment to your vote: x
Votes of 3 or less require a comment

Comments and Discussions

 
You must Sign In to use this message board.
Search this forum  
    Spacing  Noise  Layout  Per page   
BugCritical bug in the code: Integer should be Long or VB6 app will crashmemberKenneth Choe20 Nov '12 - 5:37 
The code has a bug, and it is so annoying in a way that it doesn't happen all the time, and when it happens, it doesn't give you any details. It just crashes the VB6 app.
 
After spending some time, I came to the same finding as somebody noted below "Very Important Note". If I can reach the author, I would do so to make the good code to be correct and more beneficial to others... Wanted to leave a stronger comment so that other people read it before moving on.
GeneralMy vote of 5memberMember 43208444 May '11 - 16:45 
Good idea
NewsVery Important NotememberMember 267168810 Feb '09 - 23:02 
The example given has a slight error which only occurs under certain circumstances:
 
Private Function ProcessWindowMessages(ByVal hwnd As Long, ByVal wMsg As Long, _
ByVal wParam As Long, ByVal lParam As Long) As Long
 

The last parameter should be a long, not an int. If you lock your workstation with this set as an int, a negative value is shown which blows out the app with a win32 exception. Taken me months to track it down!
GeneralDownload book for Interprocess Communication and NetworkmemberAsirTim18 Jun '08 - 23:17 
You can download it from the following link:
Just type the link in the browser and download it
Download link: http://w17.easy-share.com/1700679295.html
Delete link: http://w17.easy-share.com/1700679295/del_6dkt4bvbdlua2o4n
GeneralIn Vistamemberjoemerchant6 Jun '07 - 3:43 
Does this work in Vista? I thought Vista dropped some stuff (of course later they added some stuff back in).
Generalpassing some data along with the windowMessagemembertheGhost_k820 Jun '06 - 23:35 
hay this is the best IPC thing i've ever read !!
But ,....
can i pass anything other than this with windows message . like a parameter..etc
wndproc does have parameters like wparam and lparam . how can i pass any string from first application to second using this IPC ?
is there any scope of using WM_COPYDATA or something simillar (please describe it.)
 
Kedar
GeneralRe: passing some data along with the windowMessage [modified]memberminja21 Jun '06 - 1:07 
Have a look at my reply to the "Passing parameters" post. You can pass through parameters with the wParam and lParam parameters.
 
http://www.codeproject.com/dotnet/VB6andVBNETWindowMessages.asp?msg=1541771#xx1541771xx
 
Angus
3 out of every 4 people make up 75% of the worlds' population
 
-- modified at 7:07 Wednesday 21st June, 2006
GeneralRe: passing some data along with the windowMessagemembertheGhost_k821 Jun '06 - 2:27 
thats really fine.
I'd like to send a structure{no,name} as a parameter .
how to do that using WM_COPYDATA ?

 
"You can do any thing you set to your mind" - theGhost_k8
GeneralRe: passing some data along with the windowMessage [modified]memberminja21 Jun '06 - 17:46 
I have not had to copy a structure through the method I used (so long ago), so have not used WM_COPYDATA method. This page seems to have a good code listing
http://www.codeguru.com/forum/printthread.php?t=231750
 
I also thought you could use a comma-delimited string, or something along those lines to transfer a heap of strings. I guess there would be a size limitation on the parameters of the SendMessage function though..
 
Hope that helps!!!
 
Angus
3 out of every 4 people make up 75% of the worlds' population
 
-- modified at 23:48 Wednesday 21st June, 2006
GeneralRe: Gr8 work but.....It's(problem) is differentmemberKedar V30 Jun '06 - 19:46 
hi ! glad to see your immediate reply to the post.
Now about the problem.
Yes ! The real reson of passing a structure is not to pass a string but an object. As u've suggested about comma-delimited string, thats of no use in that case. Check what i've tried here is:- pass a structure like

Structure PT
Dim x As Integer
Dim y As Char
Dim xy As String
End Structure
 
Dim MyVariable As PT
MyVariable.X = 125
MyVariable.y = "c"
MyVariable.xy = "String is here"
Dim MyPointer As IntPtr = Marshal.AllocHGlobal(Marshal.SizeOf(MyVariable))
Marshal.StructureToPtr(MyVariable, data.lpData, True)

and then used sendMessage to pass this structure. and on the other side at wndProc i listen to WM_COPYDATA unmarshall this and get this structure like this:
Dim MyPoint As PT
MyPoint = Marshal.PtrToStructure(CType(m.GetLParam(GetType(CopyData)), CopyData).lpData, GetType(PT))
MsgBox("->" & MyPoint.y & "<--->" & MyPoint.x & "<----->" & MyPoint.xy & "<-")

NOW THE PROBLEM !! :- i'm not able to get all values of the structure , only the first parameter. i.e. x. remaining vaules are "junk-values".. so i think this may be a marshalling problem. If u want i can mail you the code.
-------------------------------xxxxxx--------------------------------
-------:FYI:-----
WHAT I AM DOING ??
While i click on "Save" button of any office-document i want to save it using my application.. not using windows-save-dialogbox..
 
I HAVE FAILED TO DO:-
capture the event of "Save_Button" of "Word" AND call my_dialogbox to save. and pass this doc.'s object by applicationObject. But Cant get handle to that "save" button of "File" menu or track event of that click...
 
SO WHAT WAY I'M FOLLOWING??
I've created an addin to word. [VB.NET]
By clickin that addin-button i want to pass this word's object to my application (My_Save_dlgBox) to save this word.
 
HOPE:- ONLY YOU
- Thank You Very Much
[Kedar V]
Least I aspect:- any thoughts/Ideas from your side that i may have missed-out..
 
"You can do any thing you set to your mind" - theGhost_k8
GeneralRe: Gr8 work but.....It's(problem) is different [modified]memberminja30 Jun '06 - 21:33 
Hmm, after some googling I think the following link may help you, assuming add-in and receiving application are both in .Net. It deals with serialising objects and then passing them through WM_COPYDATA, but with a wrap-around class. It has some samples there so hope that helps.
 
http://www.vbaccelerator.com/home/NET/Code/Libraries/Windows_Messages/Simple_Interprocess_Communication/article.asp
 
I must admit I have not gone any further with inter-process communication than this article, and am not actually using that code any more. Everything has been converted across to VB.Net now.
 
Angus.
 
Angus
3 out of every 4 people make up 75% of the worlds' population
 
-- modified at 3:33 Saturday 1st July, 2006
GeneralPAssing parametersmemberbenchva13 Apr '06 - 4:59 
how do you pass paramters as part of the windows message?
ex.I want to send a message from a VB app to a VB.NEt app but LOAD_CUSTOMER with a customer number 12345
GeneralRe: PAssing parametersmemberminja13 Apr '06 - 12:05 
You can pass integers through window messaging using the wParam and lParam parameters. Note the new MessageBox in VB.Net to show the wParam value. Also note the new "12345" value in the VB6 code to pass through the value. This should help you on your way.
 
Example below:
VB.Net (clsWindowMessaging.vb)
    Protected Overrides Sub WndProc(ByRef m As System.Windows.Forms.Message)
        Select Case m.Msg
            Case VB6_TO_VBNET_MessageId().ToInt32
                MessageBox.Show("Windows Message from VB6 Environment", "VBNETMessaging")
                MessageBox.Show(m.WParam.ToString)
            Case CSHARP_TO_VBNET_MessageId().ToInt32
                MessageBox.Show("Windows Message from C# Environment", "VBNETMessaging")
                MessageBox.Show(m.WParam.ToString)
        End Select
        MyBase.WndProc(m)
    End Sub
 
VB6 (modMessaging.bas)
  Public Function SendMessageToVBNET()
    Dim hwndTarget As Long
    Dim MessageId As Long
 
    If WindowMessagingInitialised = False Then
      InitWindowMessaging
    End If
 
    'Get TargetWindow handle from global Window Name
    hwndTarget = VBNET_WindowHandle
 
    'Get MessageId from API call to RegisterMessage
    MessageId = VB6_TO_VBNET_MessageId
 
    'If Window target exists, then SendMessage to target
    If hwndTarget <> 0 Then
        Call PostMessage(hwndTarget, MessageId, 12345, 0)
    End If
  End Function 
Angus
3 out of every 4 people make up 75% of the worlds' population
GeneralHWND_BROADCASTmemberAndrewVos28 Mar '06 - 22:02 
HWND_BROADCAST not working for u?
 


All your source are belong to us!
GeneralVB6 to VB6messaging via LANmemberSalim Dallal22 Jan '06 - 21:16 
Hi Guys,
 
Great Tutorial. I like it and I like the code.
 
I have one question though. How can you make this work to pass messages from one VB6 application on a PC to another VB6 application on another PC. I have to place a small VB app on the server that triggers a refresh on the client side everytime the database is updated.
 
I appreciate any help on the subject.
 
Thanks Smile | :)
 
Salim
GeneralRe: VB6 to VB6messaging via LANmemberminja23 Jan '06 - 11:20 
What you need to be looking for is client-server communications in VB6. Having never done this (ewww), I can't really be much help besides googling some results for you.
 
-> http://www.vbip.com/winsock/winsock_control_ssahmed_01.asp
-> http://www.codeproject.com/Purgatory/winsock.asp
 
Book from Amazon: http://www.amazon.com/gp/product/1571691545/103-0165782-9268656?v=glance&n=283155
 
Hope they start you on a road to a solution.
 
Angus
3 out of every 4 people make up 75% of the worlds' population
GeneralRe: VB6 to VB6messaging via LANmemberSalim Dallal23 Jan '06 - 20:07 
Thanks minja. I have already looked up these pages myself and I am working on it. Hope this leads to where I want to be.
I will try to use winsock along with windows messaging for a better result but then again I might just stick to winsock.
 
Thanks again,
 
Salim
QuestionPassing string as messages?memberJosef Meile23 May '05 - 7:44 
Hi,
 
first at all thanks for this how-to. It is realy nice.
 
Going to the point, I'm trying to send a string through the LParam argument of the message. After doing some search in google, I found that you can even pass objects if you want; the trick is that instead of passing a string to the PostMessage function, you have to call it with a pointer to a memory address (IntPtr in other words) and change the declaration of PostMessage to reflect this.
 
I have tried several things, but it seams that either the contents of the memory address are deleted after doing the PostMessage or I'm doing something wrong in the other side of the comunication. Have you ever tried to do this?
 
Before posting my code here, I have to explain that I have two applications coded in C#: a server and a client which send messages each other (If you are interested, I could send the complete source to you). I modify your code a little bit to do a C# Server to C# Client communication and viceversa; messages are received, but the string in the memory address is lost. So here is the code:
 
[DllImport("user32.dll")]
//I replaced the lParameter type by an IntPtr
//in order to be able to pass a pointer to a
//string as a message. I found some messages
//in google, which says that you can do that
public static extern IntPtr PostMessage (IntPtr hwnd,
IntPtr wMsg, Int32 wParam, IntPtr lParam);
 

protected override void WndProc(ref Message m)
{
if (m.Msg == CSHARP_SERVER_TO_CLIENT_MessageId.ToInt32())
{
MessageBox.Show("Windows Message from Server environment", "C#ClientMessaging");
//Here I will try to get the contents of the memory
//address referenced by m.LParam. I checked this value
//in both: the client and the server and it has the same
//value, but the string returned by PtrToStringAuto is
//either empty or has garbage on it.
string test = Marshal.PtrToStringAuto(m.LParam);
MessageBox.Show(test);
}
base.WndProc(ref m);
}
 
public void SendMessageToServer()
{
IntPtr hwndTarget;
IntPtr MessageId;
//This is a pointer to a memory address I
//create to store a string
IntPtr messagePointer;
//The local variable to assign the string
string msgStr;
 
hwndTarget = this.SERVER_WindowHandle;
 
MessageId = CSHARP_CLIENT_TO_SERVER_MessageId;
 
//Assign some message to send
msgStr = "test";
//Allocate memory to the string and store it somewhere
//on the heap. messagePointer will get the memory
//address, where the string was stored
messagePointer = Marshal.StringToHGlobalAuto(msgStr);
//As a test, I read the string from the memory address
//on another variable and it works, I get "test"
string test = Marshal.PtrToStringAuto(messagePointer);
MessageBox.Show(test);
 
if (!System.IntPtr.Zero.Equals(hwndTarget))
{
//messagePointer is used as LParam for the PostMessage function
PostMessage(hwndTarget, MessageId, 0, messagePointer);
}
}
 
The client application is almost the same, the only thing that changes are the method and variable names.
 
I even tried to test it with your example by modifying the VB.Net and C# examples but it didn't work as well. Do you know how to solve it?
 
Thanks in advanced,
Josef Meile
AnswerRe: Passing string as messages?memberJosef Meile23 May '05 - 7:58 
Ah, by the way, before I found your code, I was trying to get it working with the WM_COPYDATA how to you mention in the "Confused... Smile | :) " thread, the problem here is that I'm working with Excel and Visio COM Objects and they can't be accessed from the WM_COPYDATA handler, I get:
 
An outgoing call cannot be made since the application is dispatching an input-synchronous call.
 
So, I search on google and found a delphi message, where the solution was to send a message with PostMessage and not *SendMessage*, which will throw the same exception as well. Unfortunatelly, all the messages I have found, not many, suggest diferent things, but none of them have a complete solution to the problem.
 
Regards,
Josef
GeneralRe: Passing string as messages?memberJosef Meile23 May '05 - 11:37 
Ok, I found the answer to my question:
 
It seems like I can't do what I want with PostMessage. As I have read, the difference with SendMessage is that the later sends a message to a window inmediately and waits for an answer; while PostMessage sends the message to the forms' message queue and exits. So, the memory location I stored in my SendMessageToServer method will be lost because it is local. To solve this problem I found that I could used something called FileMap, which creates a memory that can be shared between different processes. I guess it will work, but something more direct would have being better Frown | :-(
 
The other issue I had with the COM Objects is that they only can be used by the thread that originated then, so, there will be problems with the How-To you mentioned: WM_COPYDATA because it uses SendMessage, which creates a new thread whenever a new message arrives. Here I also tried the Invoke and BeginInvoke functions of DotNet, but I had the same error messages Frown | :-(
 
Thanks anyway,
Josef
GeneralConfused... :)sussIcingDeath4 May '05 - 10:31 
I am interested on VB.Net sample... let me get things straight....
You use CreateParams() to create some sort of fake form and then use FindWindow() from say VB6 to get this fake form's handle in order to finally use PostMessage() to send the message (which is actually a pointer)?
This is quite interesting but I cant seem to think of a way to use it Smile | :)
Its like having a pager in the age of cell phones... you just page the other app but cant actually send something usefull to it... let say a string or at least a pointer to a string (dont really know how to access the string from that pointer lol)... Will this baby work on a windowless application? Like an application that just sits on the tray? I figured out how to use a mutex to see if my app is already running and now i am trying to have the second 'copy' of the app deliver all the command line arguments it started with to the first app... that would be usefull Wink | ;) Any tips?
Thanks in advance
John
GeneralRe: Confused... :)memberIcingDeath4 May '05 - 12:04 
What I was searching is here. This article explains how to send simple datatypes between processes. I combined it with your 'fake' form code and I got the job done! Laugh | :laugh: Wink | ;)
GeneralRe: Confused... :)memberminja4 May '05 - 15:41 
cool, just as I was about to try to answer your question.
 
yep, that is a nice way to combine. The way we were using it was just as a notifier between VB.Net app and VB6 app when a certain process finished. We didn't need any data, just needed to know that something had finished.
 
maybe you should write up an extension article to mine!! Smile | :)
 
Angus
3 out of every 4 people make up 75% of the worlds' population
GeneralVB6 to VB6 communicationmemberminja18 May '04 - 13:27 
Due to a reader request, I have now updated source and demos to include VB6 to VB6 communication. There are plenty of demos of this around the web, but it was a 5 minute job because I have everything setup already.
 
Angus
3 out of every 4 people make up 75% of the Worlds' population
GeneralHelp!memberjagroff14 May '04 - 7:48 
I am experimenting with your code and seem to have gotten myself into the jam you describe below.
Now, when I run a VB6 project that initiates messaging, it blows out of VB as soon as I click on any other window. How can I undo the damage?
 
VB6: One thing to note that is VITALLY IMPORTANT is that you need to disconnect your custom Message Handler before the application closes, otherwise it starts to handle messages in the VB IDE. It was quite hard to track this problem down because it would work nicely when I ran the executable, but once inside the IDE, it would crash VB.
GeneralRe: Help!memberminja14 May '04 - 15:17 
Is it crashing when you start the VB app, and then click on another window? If so, then I am guessing you have a mistake in the modMessaging.ProcessWindowMessages function. Make sure that if the Window Message is not one of your custom messages, then it is handed back down to the default message handler via the CallWindowProc Win32 function.
 
If it is passing to this, then I guessing that the address of the original handler has been set incorrectly.
 
If it is crashing when you quit the VB app back to VB6 and then click on another app, then your custom Window handling is never being set back to the original handler. Call the modMessaging.StopWindowMessaging function to reset the handler back to the original (usually MainForm).
 
Try these and if it does not work, get back to me. Do the compiled app's work for you?
 
Angus
3 out of every 4 people make up 75% of the Worlds' population
GeneralRe: Help!memberjagroff17 May '04 - 1:58 
Thanks for getting back. Yes, that's exactly what happens. I start a VB app, then click on another window and the Vb app gets blown away. In fact, now when I start your VBMessaging VB app and click another window, it gets blown away. That's even after rebooting and having started no other applications. Some gremlin is living in my machine and has taken control.
GeneralRe: Help!memberminja17 May '04 - 14:49 
Hmm, maybe this is low level stuff like OS or vb runtime problems. What OS and runtime are you running under?
 
I haven't had any problems yet, but am running XP Pro on both machines. Not sure what vb runtime I have at home. Will check tonight.
 
I can also do up two VB apps which message each other to see if that helps at all. Are you running the prog's in debug mode. Can you tell me what lines it is crashing on? Any debug info?
 
Angus
3 out of every 4 people make up 75% of the Worlds' population
GeneralRe: Help!memberjagroff18 May '04 - 2:07 
Hey minja,
 
Thanks for your responsiveness to my problem, but as it so often turns out, this is one of my own creation. I dropped the "e" in the "else" in the ProcessWindowsMessages function somehow when I added the modmessaging module to my project. Any windows message then blew out my app because of a syntax error. So now I can resume testing. And thanks for the link to the tutorial. It explains things slightly differently than you do, and between the two demos I now have an understanding of a very useful function.
 
Joe
GeneralRe: Help!memberminja18 May '04 - 2:24 
You can say "explained it better than you did" if you like. Smile | :)
 
Because I was coding at work, and doing research at work/home I lost track of a few links. I am going to put that link the in the "referenced" links section.
 
Hopefully your problems are solved. Good luck!!
 
Angus
3 out of every 4 people make up 75% of the Worlds' population
GeneralRe: Help!memberminja16 May '04 - 19:24 
Also have a loook at this link for a simple tute on sub-classing in VB6.
 
http://www.vb-helper.com/tutorial_subclassing.html
 
Angus
3 out of every 4 people make up 75% of the Worlds' population

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Permalink | Advertise | Privacy | Mobile
Web02 | 2.6.130523.1 | Last Updated 17 May 2004
Article Copyright 2004 by angus_grant
Everything else Copyright © CodeProject, 1999-2013
Terms of Use
Layout: fixed | fluid