The new vbAccelerator Site - more VB and .NET Code and Controls
Source Code
3 Code Libraries &nbsp


NOTE: this code has been superceded by the version at the new site.



&nbsp

Passing Command Line Parameters to an Existing Instance of your App

Associations in Explorer

Download the Startup files (50kb)

&nbsp UpdatedUpdated! 21 March 2000 &nbsp
&nbsp Fixed a problem with the document icon support. The previous version of this article stated that to create an icon for a document, you should refer to the resource ID of the icon within the application's .RES file. This was incorrect: in fact you should use the 0-based index of the icon in the resource script (noting that if your app has a standard VB icon, this automatically gets compiled in as the first icon within the app).

Also the updated cRegistry class is incorporated to ensure the app works with the HKEY_LOCAL_MACHINE\SOFTWARE\Classes registry hive as well as the standard HKEY_CLASSES_ROOT hive.

&nbsp
&nbsp Before you Begin &nbsp
&nbsp This project requires the SSubTmr.DLL component. Make sure you have loaded and registered this before trying the project.

&nbsp

Many windows applications register file associations. When you double click on an associated file, it is nice to have the flexibility to decide what happens. The built-in support for this in VB is like the Notepad SDI model - if you double click on a .TXT file you get a new instance. But other apps, for example Microsoft Word and WinZip, detect if a window is already open to handle the file, and if it is that window is used to open the file.

To do this you need to be able to achieve the following:

  1. Register a file association.
  2. Detect whether an instance of your app is running or not.
  3. Send the command line of one instance of your app cross-instance to the existing one.
TopBack to top

1. Registering a File Association
Associating a file type is achieved through the registry. For example, say you want to associate files of type *.GCF with your app, calling them 'Goldfish Clipboard Files'. Then you need to set up the following in the registry:


  HKEY_CLASSES_ROOT
     .GCF (default) = "Goldfish.ClipboardFile"
         ...
     Goldfish.ClipboardFile (default) = "Goldfish Clipboard File"
        shell
           open
              command (default) = "[Executable Path] "%1""

The simplest way to do this is to use the CreateEXEAssociation method of the vbAccelerator cRegistry class (note - this has been bug fixed to ensure that the EXE Association is still created when you do not specify a default document icon, and is also provided with the demonstration project).

Once you have done this, then when a user double clicks on the file with the extension specified Windows will shell your application, passing the filename on the command line. You can get this from VB's Command function.

The above registry structure creates a default Open association. You can also add other associations so when a user right clicks on a file or selects it and chooses the File menu in explorer additional menu items apply. For example, VB5 creates the following structure to provide the Open, Make and Run commands for .VBP files:


  HKEY_CLASSES_ROOT
     .VBP (default) = "VisualBasic.Project"
         ...
     VisualBasic.Project (default) = "Visual Basic Project"
        shell
           Make
              command (default) = C:\Program Files\DevStudio\VB\vb5.exe "%1" /make
           open
              command (default) = C:\Program Files\DevStudio\VB\vb5.exe "%1"
           Run Project
              command (default) = C:\Program Files\DevStudio\VB\vb5.exe "%1" /run

This functionality is available throught the cRegistry CreateAdditionalEXEAssociations function. Check the demo project code to see how it works.

The final thing you can do for a fully professional effect is to associate a particular icon with documents associated with your application. This is achieved by setting a registry key like this:


  HKEY_CLASSES_ROOT
     Goldfish.ClipboardFile (default) = "Goldfish Clipboard File"
        DefaultIcon (default) = "[Executable Path],[0 Based Index of Resource Icon]"

The [0 Based Index of Resource Icon] should be an index to an icon resource within your project. Resource identifier 0 is automatically created for VB EXEs and is the executable's icon. However, you can add further icon resources to your application through a resource file. Note: you cannot use the Resource Editor VB Add-in (provided with VB6, and available for VB5 from the MS VB Programmer's area to do this because all resources it creates are private and not exposed to the outside applications. You must instead use the external resource compiler RC.EXE to do this instead.

The default icon setting can also be set through the cRegistry CreateEXEAssociation method. The demonstration provides a resource script and the code used to do this.

If you only want to create an SDI app which has multiple instances, that is all you need to do. But if you want to control what happens next, then read on...

TopBack to top

2. Detect whether an instance of your app is running or not
This could be the easy bit, but I decided to make it harder. vbAccelerator isn't about advanced source code for nothing you know!

The easy way of checking whether your app is running is to use the PrevInstance property of VB's App object. For 99% of cases this will work perfectly well. However, if your app has a long start-up time, it is possible for App.PrevInstance to return False even when there is an existing instance running. If you relied on App.PrevInstance there is small but nightmarish (in support terms, at least!) possibility that you get two instances of your app.

To be sure only one instance runs, you can take advantage of the Mutex functions provided in Win32, which are normally used for thread synchronisation. You can create a virtually unlimited number of Mutex handles in Win32. Each one has it's own name and handle value. The only disadvantage of this method occurs in the VB IDE. A Mutex applies to an entire process, and when you are debugging an application in the VB IDE it runs in the VB IDE's process. So if you create a Mutex in the IDE, but fail to destroy it for some reason (say for example you do a nasty and press the stop button) then the Mutex continues to exist until the IDE is closed. Clearly this makes things difficult to debug, so my code works around this by using App.PrevInstance unless the app is running as compiled code. To detect whether you are in the VB IDE or not is easy but a hack - check it out!

Private Declare Function CreateMutex Lib "kernel32" Alias "CreateMutexA" _
(ByVal lpMutexAttributes As Long, ByVal bInitialOwner As Long, ByVal lpName As String) As Long
Private Declare Function CloseHandle Lib "kernel32" (ByVal hObject As Long) As Long
Private Const ERROR_ALREADY_EXISTS = 183&
Private m_hMutex As Long
Private m_bInDevelopment As Boolean
' Change this line to match your app:
Private Const mcTHISAPPID = "vbAcceleratorGOLDFISH"

Public Sub Main()

' Check if this is the first instance:
If (WeAreAlone(mcTHISAPPID & "_APPLICATION_MUTEX")) Then
' Do startup code here:
Else
' Pass command line if not empty, or
' activate existing app:
Endif
End Sub

Private Function WeAreAlone(ByVal sMutex As String) As Boolean
' Don't call Mutex when in VBIDE because it will apply
' for the entire VB IDE session, not just the app's
' session.
If InDevelopment Then
WeAreAlone = Not (App.PrevInstance)
Else
' Ensures we don't run a second instance even
' if the first instance is in the start-up phase
m_hMutex = CreateMutex(ByVal 0&, 1, sMutex)
If (Err.LastDllError = ERROR_ALREADY_EXISTS) Then
CloseHandle m_hMutex
Else
WeAreAlone = True
End If
End If
End Function

Public Function InDevelopment() As Boolean
' Debug.Assert code not run in an EXE. Therefore
' m_bInDevelopment variable is never set.
Debug.Assert InDevelopmentHack() = True
InDevelopment = m_bInDevelopment
End Function

Private Function InDevelopmentHack() As Boolean
' .... '
m_bInDevelopment = True
InDevelopmentHack = m_bInDevelopment
End Function

Public Function EndApp()
' Call this to remove the Mutex. It will be cleared
' anyway by windows, but this ensures it works.
If (m_hMutex <> 0) Then
CloseHandle m_hMutex
End If
m_hMutex = 0
End Function

The CreateMutex calls probably have many other uses in VB I haven't thought of yet, given a bit of imagination!

TopBack to top

3. Send the command line of one instance of your app cross-instance to the existing one.
The code so far has been tty trivial. Now we get on to why you need subclassing to achieve this task and also delve into the Windows API a bit more.

This task can be broken down into two parts:

  • Firstly, how do you find a window?
  • Secondly, how do you pass data across instances?
So, how do you find a window? There are various techniques to achieve this. But I warn you that a large number of the published versions are based on Win16 code which can't really be guaranteed to work under Win32. Without exception all the rest I have seen rely on some part of the window's caption or class name to find a window. Now I don't know about you but I find the whole idea of finding a window given that the caption is something like "My Cool App -*" and that its class is "ThunderWindowClass" somewhat less than desirable.

The worst thing these dubious methods is that you can do it properly with very few lines of code. For further reference to the methods I describe here, see my articles Using Enumeration API methods in VB and Subclassing without the Crashes - Use Window's built-in database to store information against hWnds.

The first thing you should do if you want to locate a window is to give it a property that guarantees you can find it again. To make sure you can find it, just choose a string value for the property that no-one else is going to use. (BTW: If you're really serious about this, use the appropriate OLE function to create a new GUID!) Once you have a string, use the SetProp method to associate the string and a long value with the window.

Having done, this you need to loop through all top-level windows to find the one which has the unique string value you set. To loop through top-level windows in Win32, you must use the EnumWindows function (using any other method could result in continuous loops or a failure to identify all top-level windows because Win32's pre-emptive Multi-Threading could modify the window list before you get them. Particularly in multi-processor NT systems). Here is the code I use to set the properties and get the top-level windows:

Private Declare Function EnumWindows Lib "user32" _
(ByVal lpEnumFunc As Long, ByVal lparam As Long) As Long
Private Declare Function GetProp Lib "user32" Alias "GetPropA" _
(ByVal hWnd As Long, ByVal lpString As String) As Long
Private Declare Function SetProp Lib "user32" Alias "SetPropA" _
(ByVal hWnd As Long, ByVal lpString As String, ByVal hData As Long) As Long
Private m_hWndPrevious As Long
' Change this line:
Private Const mcTHISAPPID = "vbAcceleratorGOLDFISH"


'... Sub Main frament:

' We have an existing instance.
' First try to find it:
EnumerateWindows

' If we get it:
If (m_hWndPrevious <> 0) Then
' Send information:
End If

' ... End

Public Sub TagWindow(ByVal hWnd As Long)
' Applies a window property to allow the window to
' be clearly identified.
SetProp hWnd, mcTHISAPPID & "_APPLICATION", 1
End Sub

Private Function IsThisApp(ByVal hWnd As Long) As Boolean
' Check if the windows property is set for this
' window handle:
If GetProp(hWnd, mcTHISAPPID & "_APPLICATION") = 1 Then
IsThisApp = True
End If
End Function

Public Function EnumWindowsProc( _
ByVal hWnd As Long, _
ByVal lparam As Long _
) As Long
Dim bStop As Boolean
' Customised windows enumeration procedure. Stops
' when it finds another application with the Window
' property set, or when all windows are exhausted.
bStop = False
If IsThisApp(hWnd) Then
EnumWindowsProc = 0
m_hWndPrevious = hWnd
Else
EnumWindowsProc = 1
End If
End Function

Public Function EnumerateWindows() As Boolean
' Enumerate top-level windows:
EnumWindows AddressOf EnumWindowsProc, 0
End Function

TopBack to top

The final stage is how to send information across processes. Sending information can be achieved in many ways, but one of the easiest is to use Window's WM_COPYDATA message. This message is used with the SendMessage function and is a wrapper around the more complicated File Mapping interprocess communication method. It is ideal when you are sending a small amount of information, say less than 4Kb.

You call the WM_COPYDATA message with SendMessage like this:


Public Const WM_COPYDATA = &H4A
Public Type COPYDATASTRUCT
dwData As Long ' A long value to pass to other application
cbData As Long ' The size of the data pointed to by lpData
lpData As Long ' A pointer to data
End Type
Private Declare Function SendMessage Lib "user32" Alias "SendMessageA" _
(ByVal hWnd As Long, ByVal wMsg As Long, ByVal wParam As Long, lParam As Any) As Long

Public Sub SendData(ByVal hWndTo As Long, ByVal sString As String, ByVal lData As Long)
Dim b() As Byte
Dim tCDS As COPYDATASTRUCT

If (sString <> "") Then
b = StrConv(Command, vbFromUnicode)
tCDS.dwData = lData
' Add Null Char:
tCDS.cbData = UBound(b) + 1
' Set lpData to point to the byte array
tCDS.lpData = VarPtr(b(0))
Else
ReDim b(0 To 0) As Byte
tCDS.dwData = lData
tCDS.cbData = 1
' Set lpData to point to byte array of Null Char:
tCDS.lpData = VarPtr(b(0))
End If

SendMessage hWndTo, WM_COPYDATA, 0, tCDS

End Sub

This sends the message to the window hWndTo. Now you need to receive it and process it in the main window of the receiving application. This is achieved by subclassing the window for the WM_COPYDATA message:


Option Explicit

' Implement the subclassing interface:
Implements ISubclass

Private Sub Form_Load()
' Start subclassing:
AttachMessage Me, Me.hWnd, WM_COPYDATA
End Sub

Private Sub Form_QueryUnload(Cancel As Integer, UnloadMode As Integer)
' stop subclassing:
DetachMessage Me, Me.hWnd, WM_COPYDATA
End Sub

Private Property Let ISubclass_MsgResponse(ByVal RHS As SSubTimer.EMsgResponse)
' Not needed.
End Property

Private Property Get ISubclass_MsgResponse() As SSubTimer.EMsgResponse
' This will tell you which message you are responding to:
' WM_COPYDATA, send response after we've done with it:
ISubclass_MsgResponse = emrPostProcess

End Property

Private Function ISubclass_WindowProc( _
ByVal hWnd As Long, ByVal iMsg As Long, _
ByVal wParam As Long, ByVal lParam As Long _
) As Long
Dim tCDS As COPYDATASTRUCT
Dim b() As Byte
Dim sCommand As String

Select Case iMsg
Case WM_COPYDATA
' Copy for processing:
CopyMemory tCDS, ByVal lParam, Len(tCDS)
If (tCDS.cbData > 0) Then
ReDim b(0 To tCDS.cbData - 1) As Byte
CopyMemory b(0), ByVal tCDS.lpData, tCDS.cbData
sCommand = StrConv(b, vbUnicode)

' We've got the info, now do it:
ParseCommand sCommand
End If

End Select

End Function

Public Sub ParseCommand(ByVal sCommand As String)
' Here you do with the command line whatever
' you need for the application.
End Sub

With this in place you can now fully achieve files associations and pass information between instances. The only things remaining to consider are:

  • If the window you are passing the command to is iconized, you probably want to restore it. Send a WM_COMMAND message with the wParam set to SC_RESTORE to the window handle.
  • If the window is hidden (say the application resides in the SysTray) then you will want to make it visible even when there is no command line. I do this by passing an empty command line to the application.
The full source for the main module is provided in the demonstration download, and this is the same source which is used in the Goldfish demonstration application.



TopBack to top
Source Code - What We're About!Back to Source Code

&nbsp

AboutContributeSend FeedbackPrivacy

Copyright 1998-2000, Steve McMahon ( steve@vbaccelerator.com). All Rights Reserved.
Last updated: 21 March 2000