I have built a program of which I wish only one instance opened at time. To do this, I used Mutex and related API calls:
Code:
Function Is2ndInstance() As Boolean
Dim hMutex As Long
hMutex = CreateMutex(ByVal 0&, 1, App.Title)
If (Err.LastDllError = ERROR_ALREADY_EXISTS) Then
Is2ndInstance = True
ReleaseMutex hMutex
CloseHandle hMutex
End If
End Function
Private Sub Form_Load()
If Is2ndInstance Then End
If Command <> "" Then
'Code to open file
End If
End Sub
Now there's my problem...
I'm trying to give user the possibility to open files simply clicking on them and use the path stored into Command function to load them into my application. The first time user tries to open a file, it is successfully opened. But if he tries again, the Mutex doesn't allow and closes the second instance of the program without opening the file chosen.
What can I do to open all files into the same instance of my application (avoiding my program is opened twice or more) ? Moreover I succeeded to use Command only if I select what program I want to open the file with (using the Open As... option in the contex Menu). In fact, the double click on the file didn't make anything useful. Why?
Thank you all.
Last edited by Fenrir; Mar 21st, 2013 at 05:08 AM.
While a lot of people think using a Mutex is a clever solution it isn't really how this sort of thing is meant to be done. As you are finding this actually introduces a few new hedaches of its own in addition to providing none of the features you actually need.
The proper way to do this is to make use of DDE instead.
DDE is not fully implemented in VB, there a number of things in a VB program that can interfere with it, and Microsoft tries to spread the word that it is deprecated. However the Windows Shell (once Program Manager, now for 18 years or so Windows Explorer), Office applications, and many other things make use of it even today.
What you are asking to do is more complex than simply making sure only one copy of your program runs, which VB6 DDE can handle reasonably well.
But the full-blown Shell DDE conversations involved in file associations are another topic and few people try to deal with this in VB6 much anymore.
This example does things in a bizarre manner and very much incorrectly, but it might be easier to implement in VB6 programs and should be reasonably reliable. It uses a Mutex hack so you might find it simpler to understand.
I read your links (above all the second one) and I have some doubts about what are the steps to do. Can you post me a walkthrough about how to proceed? (i.e. what is the first step, the second one, the third, ect...)
Because until now I understood I must follow this kind of reasoning:
Code:
'In a module
Dim myCommand As String
Public Sub Main()
'I check if the current is the second instance of my app
If Is2ndInstance() Then
'If it is, I begin to search the hWnd of the first one
Do While hPrevWnd = 0
hPrevWnd = FindWindow()
Loop
'When I found it, I send a message to that window containing the Command line of the current instance
SendMessage hPrevWnd, Command
'and at the end, I close the second instance
End
Else
'I start to hook the main form of my program
Hook Form1
End If
End If
End Sub
Private Sub Hook (Form As VB.Form)
'Begin Hook
End Sub
Private Sub WindowProc (wParam As Long, uParam As Long)
'I try to intercept the data passed through SendMessage function
If wParam = WM_COPYDATA Then
'and I assing the path of the file which is was stored into command line into a my var
myCommand = uParam
End If
End Sub
'In a form
Private Sub Form_Load()
'In the case Command is not empty, I open the file it specifies
If Command <> "" Then
'Open File
'else I open the file specified by my var if it is not empty
ElseIf myCommand <> "" Then
'Open file
End If
End Sub
The code is obviously not complete, but I hope it is enough to understand what I'm going to do (and in case there's something wrong, to help me to adjust
Try the attached Previous Instance Command Line Passing Demo. It uses a locked file in lieu of a Mutex object.
To run the demo, compile the project and then open the first instance. Afterwards, you can minimize the form and see what happens when you drag and drop any number of files onto the exe's icon.
If you would like an example that utilizes DDE instead, then see Karl E. Peterson's PrevInst.
Last edited by Bonnie West; Aug 27th, 2013 at 11:58 AM.
Reason: Added usage instructions
On Local Error Resume Next: If Not Empty Is Nothing Then Do While Null: ReDim i(True To False) As Currency: Loop: Else Debug.Assert CCur(CLng(CInt(CBool(False Imp True Xor False Eqv True)))): Stop: On Local Error GoTo 0
The only thing I don't have understood is why I should not use MUTEX to check if a previous instance is already running.
I believe there's nothing wrong with using a Mutex for checking previous instances. In fact, someone else thinks it's the most reliable solution. However, you should read first the Mutex Objects article in order to know what it's really for.
If you insist on using a Mutex, then you may alter the code in your OP like this:
Code:
Option Explicit
Private Declare Function CreateMutexW Lib "kernel32.dll" (Optional ByVal lpMutexAttributes As Long, Optional ByVal bInitialOwner As Long, Optional ByVal lpName As Long) As Long
Private Sub Form_Initialize()
Const ERROR_ALREADY_EXISTS = &HB7&, ERROR_ACCESS_DENIED = &H5&
If CreateMutexW(lpName:=StrPtr("72607477-1315-421F-A9D9-FBDABAC49155")) Then
Select Case Err.LastDllError
Case ERROR_ALREADY_EXISTS, ERROR_ACCESS_DENIED
'Pass this instance's command line arguments to the first
'Don't forget to include this instance's current directory
Set Form1 = Nothing
End Select
End If
End Sub
Private Sub Form_Load()
If LenB(Command$) Then
'Code to open file
End If
End Sub
Some notes about that code:
Check for the previous instance as early as possible. The best event would be in Sub Main, followed by Form_Initialize.
There's no reason to set the first instance as the initial owner of the Mutex object. Creating the Mutex is enough for previous instance checking purposes. Consequently, ReleaseMutex is no longer necessary.
Using a GUID as the Mutex' name decreases the chances that another app used the same Mutex name as your app did. Generate a different GUID for every single-instance app via GuidGen.exe.
You also need to check for ERROR_ACCESS_DENIED. From the Return value section of the CreateMutex function:
Originally Posted by MSDN
However, if the caller has limited access rights, the function will fail with ERROR_ACCESS_DENIED ...
The End statement can be avoided in Sub Main by substituting Exit Sub and in Form_Initialize by Set FormName = Nothing.
Since "the system closes the (Mutex) handle automatically when the process terminates", the call to CloseHandle can be omitted as well.
On Local Error Resume Next: If Not Empty Is Nothing Then Do While Null: ReDim i(True To False) As Currency: Loop: Else Debug.Assert CCur(CLng(CInt(CBool(False Imp True Xor False Eqv True)))): Stop: On Local Error GoTo 0
When passing command line arguments to the first instance, don't forget to include the current directory, in case the command line contains relative paths. [Cut] Don't forget to pass the current directory along with the command line to your single-instance program[/URL].
Can you be cleaner?
The End statement can be avoided in Sub Main by substituting Exit Sub and in Form_Initialize by Set FormName = Nothing.
Thank you for the suggestions
Since "the system closes the (Mutex) handle automatically when the process terminates", the call to CloseHandle can be omitted as well.
I didn't know that
However, till now I have wrote this (NOTE: Change the start object into Sub Main!)
(ps: I wrote it yesterday, I didn't read your last post yet. As soons as is possible, I will change the part concerning Mutex with your function)
Code:
'In a module
Option Explicit
Private Const WM_COPYDATA As Long = &H4A
Private Const WM_DESTROY As Long = &H2
Private Type COPYDATASTRUCT
dwData As Long
cbData As Long
lpData As Long
End Type
Private Declare Function SendMessage Lib "user32.dll" _
Alias "SendMessageW" _
( _
ByVal hWnd As Long, _
ByVal uMsg As Long, _
ByVal wParam As Long, _
ByRef lParam As Any _
) _
As Long
Private Declare Function SysReAllocString Lib "oleaut32.dll" _
( _
ByVal pBSTR As Long, _
Optional ByVal pszStrPtr As Long _
) _
As Long
Private Declare Sub CopyBytes Lib "msvbvm60.dll" _
Alias "__vbaCopyBytes" _
( _
ByVal ByteLen As Long, _
ByRef Destination As Any, _
ByVal Source As Long _
)
Private Declare Function DefSubclassProc Lib "comctl32.dll" _
Alias "#413" _
( _
ByVal hWnd As Long, _
ByVal uMsg As Long, _
ByVal wParam As Long, _
ByVal lParam As Long _
) _
As Long
Private Declare Function SetWindowSubclass Lib "comctl32.dll" _
Alias "#410" _
( _
ByVal hWnd As Long, _
ByVal pfnSubclass As Long, _
ByVal uIdSubclass As Long, _
ByVal dwRefData As Long _
) _
As Long
Private Declare Function RemoveWindowSubclass Lib "comctl32.dll" _
Alias "#412" _
( _
ByVal hWnd As Long, _
ByVal pfnSubclass As Long, _
ByVal uIdSubclass As Long _
) _
As Long
Private Declare Function FindWindow Lib "user32" _
Alias "FindWindowA" _
( _
ByVal lpClassName As String, _
ByVal lpWindowName As String _
) _
As Long
Private Declare Function CreateMutex Lib "kernel32.dll" _
Alias "CreateMutexA" _
( _
lpMutexAttributes As Any, _
ByVal bInitialOwner As Long, _
ByVal lpName As String _
) _
As Long
Private Declare Function ReleaseMutex Lib "kernel32.dll" _
( _
ByVal hMutex As Long _
) _
As Long
Private Declare Function CloseHandle Lib "kernel32.dll" _
( _
ByVal hObject As Long _
) _
As Long
Private Function Is2ndInstance() As Boolean
Const MutexExists = 183&
Dim hMutex As Long
hMutex = CreateMutex(ByVal 0&, 1, App.Title)
If (Err.LastDllError = MutexExists) Then
Call ReleaseMutex(hMutex)
Call CloseHandle(hMutex)
Is2ndInstance = True
End If
End Function
'Subclass is intended only for the first instance
Public Function Subclass(ByRef Frm As Form) As Boolean
Subclass = SetWindowSubclass(Frm.hWnd, AddressOf SubclassProc, ObjPtr(Frm), AddressOf SubclassProc)
Debug.Assert Subclass 'If code stops here, the Form wasn't subclassed
End Function
'SendData is intended for subsequent instances
Public Sub SendData(ByVal hWndPrevInst As Long, ByRef sMsg As String)
Dim CDS As COPYDATASTRUCT
CDS.cbData = LenB(sMsg) + 2& 'Include room for null terminator so SysReAllocString
CDS.lpData = StrPtr(sMsg) 'can determine the string's length
'SendMessage won't return until the data has been sent and the recipient has responded.
SendMessage hWndPrevInst, WM_COPYDATA, CDS.dwData, CDS
End Sub
'GetData is a function so it can be used in an expression (to reduce code lines)
'The Frm parameter can be typed as any specific Form name
'CDS_Ptr is a pointer to the received COPYDATASTRUCT structure
Private Function GetData(ByRef Frm As Form, ByVal CDS_Ptr As Long) As Long
Dim CDS As COPYDATASTRUCT, sCmdLine As String
CopyBytes LenB(CDS), CDS, CDS_Ptr 'Retrieve the CDS UDT
SysReAllocString VarPtr(sCmdLine), CDS.lpData 'Copy the string from the given pointer
MsgBox sCmdLine
End Function
'Do not try to debug this subclassing procedure!
Private Function SubclassProc(ByVal hWnd As Long, ByVal uMsg As Long, ByVal wParam As Long, ByVal lParam As Long, _
ByVal Frm As Form, ByVal dwRefData As Long) As Long
Select Case uMsg 'Return TRUE (1&) to signal that the message was processed (Skip DefSubclassProc)
Case WM_COPYDATA: SubclassProc = GetData(Frm, lParam) + 1&: Exit Function
Case WM_DESTROY: dwRefData = RemoveWindowSubclass(hWnd, dwRefData, ObjPtr(Frm)): Debug.Assert dwRefData
End Select 'The Form will be automatically unsubclassed when it unloads (unless code stops here ^^)
SubclassProc = DefSubclassProc(hWnd, uMsg, wParam, lParam)
End Function
Public Sub Main()
If Is2ndInstance() Then
Dim hPrevWnd As Long
hPrevWnd = FindWindow(vbNullString, Form1.Caption)
If hPrevWnd <> 0 Then
Call SendData(hPrevWnd, Command$)
End If
End
Else
Call Form1.Show
End If
End Sub
'In a form
Option Explicit
Private Sub Form_Load()
Call Subclass(Me)
MsgBox Command$
End Sub
Once you compiled the exe, and run it, follow these steps:
Right click on a file
Select 'Open As...'
Choose 'Project1'
The first time nothing happens. But if you try again, the second time you'll get TWO MsgBox that display the Command$ line, and when you close the last MsgBox, a system error message will appear (even though the program will be still running). Why? And how to solve this trouble?
PS: I wanted to attach a RAR file, I clicked on the Attachments icon button and then to 'Add files'. I selected the RAR file and when I was trying to click on 'Upload' an error raised (invalid file error). Have I been wrong within the procedure? What should I do in lieu?
Last edited by Fenrir; Mar 23rd, 2013 at 06:02 AM.
Since CreateMutexW's arguments were all declared Optional, the Named Arguments syntax was used to skip the unused parameters. Unicode APIs require that a string parameter must be passed as a pointer to the string, thus the need for the hidden StrPtr function. The string literal is a GUID expressed as a string. See earlier note on GUID and the reason for why it's better than something like App.Title.
BTW, if your app isn't going to run on a Win9x system anymore, then MS encourages you to always use Unicode versions for new app development. See Working with Strings for the reasons why.
Originally Posted by Fenrir
Can you be clearer?
I've already given you the link to Raymond Chen's article that clearly explains why you need to do that.
Originally Posted by Fenrir
The first time nothing happens. But if you try again, the second time you'll get TWO MsgBox that display the Command$ line, and when you close the last MsgBox, a system error message will appear (even though the program will be still running). Why? And how to solve this trouble?
I suspect it may have to do with the hPrevWnd = FindWindow(vbNullString, Form1.Caption) line. Accessing any of the Form's intrinsic property or method implicitly loads it if it isn't loaded yet. That behavior is described in the Life Cycle of Visual Basic Forms. Because you have started subclassing the Form in the Load event, something must have gone wrong after you dismissed the MsgBox.
If you want your app to be associated with a certain file extension(s), then check out the previously given links on PrevInst and Registering File Associations.... Those articles will tell you how to accomplish that goal.
Have you tried the "locked file" demo? It presents a much easier approach to single-instance apps and its accompanying problem of finding that first instance. Most implementations try to search for the previous instance by using FindWindow. However, as the author of the "most reliable solution" linked above pointed out, FindWindow is far from being a truly robust method.
Indeed, in the latest code you posted, FindWindow would likely fail if a slow system was configured to open files by single-clicking and a user accustomed to double-clicking mistakenly double-clicks on your exe. The problem would be exacerbated if your Form took too long to load up. I suggest that you read the previously given link on the "most reliable solution" if you really want to make your app as fool-proof as possible.
Originally Posted by Fenrir
PS: I wanted to attach a RAR file, I clicked on the Attachments icon button and then to 'Add files'. I selected the RAR file and when I was trying to click on 'Upload' an error raised (invalid file error). Have I been wrong within the procedure? What should I do in lieu?
Sometimes, the Attachment Manager fails on me too. Just try it again.
On Local Error Resume Next: If Not Empty Is Nothing Then Do While Null: ReDim i(True To False) As Currency: Loop: Else Debug.Assert CCur(CLng(CInt(CBool(False Imp True Xor False Eqv True)))): Stop: On Local Error GoTo 0
Since CreateMutexW's arguments were all declared Optional, the Named Arguments syntax was used to skip the unused parameters. Unicode APIs require that a string parameter must be passed as a pointer to the string, thus the need for the hidden StrPtr function. The string literal is a GUID expressed as a string. See earlier note on GUID and the reason for why it's better than something like App.Title.
That program (GuiGen.exe), where can it be downloaded from? Cause I searched into my PC and I don't have it.
Originally Posted by Bonnie West
I've already given you the link to Raymond Chen's article that clearly explains why you need to do that.
Excuse me, but if I open a file calling the 'START' statement of a DOS shell (within relative path), what will contain the Command$ VBA function?
Originally Posted by Bonnie West
I suspect it may have to do with the hPrevWnd = FindWindow(vbNullString, Form1.Caption) line.
Yeah, I forgot that calling a property of a Form that's not loaded yet, it will load the Form itself before to return the property value.
Originally Posted by Bonnie West
Have you tried the "locked file" demo? It presents a much easier approach to single-instance apps and its accompanying problem of finding that first instance. Most implementations try to search for the previous instance by using FindWindow. However, as the author of the "most reliable solution" linked above pointed out, FindWindow is far from being a truly robust method.
Yes, I have. But it seemed it requires more code than the version I provided. However, insted of using a file, I might save the first instance form hWnd into the Registry.
Originally Posted by Bonnie West
Indeed, in the latest code you posted, FindWindow would likely fail if a slow system was configured to open files by single-clicking and a user accustomed to double-clicking mistakenly double-clicks on your exe. The problem would be exacerbated if your Form took too long to load up. I suggest that you read the previously given link on the "most reliable solution" if you really want to make your app as fool-proof as possible.
I think these options can be reasonably discarded because nowadays PC are really powerful. However, there's a solution:since I'm sure that THERE MUST BE a second instance (because Mutex had tell me), I can modify the part that use the FindWindow call like this:
Code:
Dim hPrevWnd As Long
Do While hPrevWnd = 0
hPrevWnd = FindWindow(vbNullString, "Form1")
Loop
That program (GuiGen.exe), where can it be downloaded from? Cause I searched into my PC and I don't have it.
It's included in the Windows SDK. But if you don't want to download it, you can generate GUIDs instead via code.
Originally Posted by Fenrir
Excuse me, but if I open a file calling the 'START' statement of a DOS shell (within relative path), what will contain the Command$ VBA function?
You can simply pass """" & CurDir$ & """ " & Command$ to the previous instance. The first instance should then prepend the current directory of the second instance to any relative path in the command line.
Originally Posted by Fenrir
I think these options can be reasonably discarded because nowadays PC are really powerful.
Well, that's true. That is why most modern computers can run a lot more applications than ever before. But, what happens if a particular user had too many processes going on at the same time, especially with an app that constantly reads/writes to disk (video capture perhaps)? What if that user happened to multiple-clicked or double-tapped on the Enter key while opening your single-instance app in the middle of a video-recording session? Suppose that user wasn't using a SSD but instead was recording to a regular HDD. What do you think would happen to the FindWindow call?
Originally Posted by Fenrir
However, there's a solution:since I'm sure that THERE MUST BE a second instance (because Mutex had tell me), I can modify the part that use the FindWindow call like this:
Don't get me wrong, but I think you ought to know what you're getting yourself into with that code. I suggest that you should at least read Why FindWindow doesn't work in order to understand why looping with FindWindow like that is a bad idea. The following is a scenario presented in that article that highlights one of the pitfalls of FindWindow:
But things get worse. FindWindow really does an EnumWindows call, and for each top-level window handle found, it performs a GetWindowText operation. This is done by sending a WM_GETTEXT message to the window handle. But if the thread that owns the handle is blocked, on a Semaphore, a Mutex, an Event, an I/O operation, or some other manner, the SendMessage will block until that thread frees up and runs. But this may never happen. So FindWindow will block forever, and your application will never come up.
Of course, if your app doesn't need to be very robust, then FindWindow is good enough most of the time.
PS
Pass "ThunderRT6FormDC" rather than vbNullString to FindWindow to make the search more specific.
Last edited by Bonnie West; May 21st, 2019 at 09:51 PM.
On Local Error Resume Next: If Not Empty Is Nothing Then Do While Null: ReDim i(True To False) As Currency: Loop: Else Debug.Assert CCur(CLng(CInt(CBool(False Imp True Xor False Eqv True)))): Stop: On Local Error GoTo 0
[QUOTE=Bonnie West;4375555]It's included in the Windows SDK. But if you don't want to download it, you can generate GUIDs instead via code.
I've never used GUID, so excuse me if I ask for stupid questions. I'd like to create a new one via code, using the CreateGUID from your link like this:
Code:
If CreateMutexW(lpName:=StrPtr(CreateGUID()) Then
won't each new instance create a new univocal GUID for itself?
If it's so, I think the condition will always fail. I should always use the same GUID (declaring it as Const), but doing like this, there's the riks another app uses my same GUID? Well, what are the advantages of this practice if all my assumptions were true?
Originally Posted by Bonnie West
You can simply pass """" & CurDir$ & """ " & Command$ to the previous instance. The first instance should then prepend the current directory of the second instance to any relative path in the command line.
And what if user doesn't use relative paths? I'd have (for example) a variable that contains this: newCommand$ = """C:\Program files\my_exe_dir"" ""C:\documents and settings\user\desktop\file.ext"""
Now, if I try to execute the Open statment:
Code:
Open newCommand$ For Input As #1
won't I should receive an error??
Originally Posted by Bonnie West
Well, that's true. That is why most modern computers can run a lot more applications than ever before. But, what happens if a particular user had too many processes going on at the same time, especially with an app that constantly reads/writes to disk (video capture perhaps)? What if that user happened to multiple-clicked or double-tapped on the Enter key while opening your single-instance app in the middle of a video-recording session? Suppose that user wasn't using a SSD but instead was recording to a regular HDD. What do you think would happen to the FindWindow call?
Don't you think that if also my application has to keep always open a file to read/write the hWnd of the first instance, this may slow or damage the HDD?
Originally Posted by Bonnie West
Don't get me wrong, but I think you ought to know what you're getting yourself into with that code. I suggest that you should at least read Why FindWindow doesn't work in order to understand why looping with FindWindow like that is a bad idea. The following is a scenario presented in that article that highlights one of the pitfalls of FindWindow. Of course, if your app doesn't need to be very robust, then FindWindow is good enough most of the time.
These are cases at the borders of reality O_O A more concrete case maybe you find another program that uses the same Caption of your own. But, reading the article, I discovered a possible solution: the loop goes on until (hPrevWnd = 0) Or ((hPrevWnd <> 0) And (DataRecived = False)), where DataReceived is a variable that changes to True when the SendMessage will successfully processed by the first instance.
Originally Posted by Bonnie West
Pass "ThunderRT6FormDC" rather than vbNullString to FindWindow to make the search more specific.
I was reading that a ClassName is assigned for registered applications only (even though I don't know what it means). Are you sure that will it work?
I've never used GUID, so excuse me if I ask for stupid questions. I'd like to create a new one via code, using the CreateGUID from your link like this:
Code:
If CreateMutexW(lpName:=StrPtr(CreateGUID()) Then
won't each new instance create a new univocal GUID for itself?
If it's so, I think the condition will always fail. I should always use the same GUID (declaring it as Const), but doing like this, there's the riks another app uses my same GUID? Well, what are the advantages of this practice if all my assumptions were true?
You are mostly correct. The GUID should indeed be declared as a constant. You will need to generate a new GUID for every single-instance app that you create. That way, when any of your apps create a named Mutex, they won't clash with each other or with any other apps. GUIDs are designed to be very unique so there's virtually no chance that another app generated the same GUID as yours.
Originally Posted by Fenrir
And what if user doesn't use relative paths? I'd have (for example) a variable that contains this: newCommand$ = """C:\Program files\my_exe_dir"" ""C:\documents and settings\user\desktop\file.ext"""
Now, if I try to execute the Open statment:
Code:
Open newCommand$ For Input As #1
won't I should receive an error??
Here is Raymond's argument for including the current directory:
If you make a single-instance program, and somebody runs a second copy of the program and passes a command line, the most common way of handling this is to hand the command line to the first copy of the program and let the first copy deal with it. When you do this, don't forget about the current directory.
If somebody passes a relative path to the second copy of the program, that relative path needs to be resolved against the current directory of the second program. I've seen programs that fail to take this into account. Instead, they pass the command line to the first copy of the program, and the first copy resolves the relatives paths against its current directory.
Allow me to give a concrete example.
C:\Directory1> start LitWare file1.lit
... runs LitWare with the file C:\Directory1\file1.lit ...
C:\Directory1> cd ..\Directory2
C:\Directory2> start LitWare file2.lit
What you expect to happen is that LitWare opens the file C:\Directory2\file2.lit, since the relative path file2.lit should be resolved against the current directory, C:\Directory2. Unfortunately, I see some programs try to open the file C:\Directory1\file2.lit since they passed the command line to the first copy, and the first copy then resolved the relative path file2.lit against the current directory of the first copy, namely C:\Directory1.
Result: "File not found error."
Moral of the story: Be mindful of the current directory when parsing the command line. You can either have the second copy parse the command line (and resolve the relative paths against its own current directory), or you can pass the current directory to the first copy (and resolve the relative paths against that directory). Either works. What doesn't work is passing the relative paths to the first copy and having the first copy resolve it against its own current directory.
I will leave the command line parsing implementation up to you. You can probably find examples of that here.
Originally Posted by Fenrir
Don't you think that if also my application has to keep always open a file to read/write the hWnd of the first instance, this may slow or damage the HDD?
The locked file that will be created is only about 10 Bytes or so. Since your app reads from the disk while starting up, creating a file at the same time poses no problem for the HDD. Similarly, when your app shuts down, you probably persist some settings to disk. Deleting the locked file at around the same time should not have any detrimental effects to the HDD. Note that the Registry is composed of several files on the disk.
Originally Posted by Fenrir
These are cases at the borders of reality O_O A more concrete case maybe you find another program that uses the same Caption of your own. But, reading the article, I discovered a possible solution: the loop goes on until (hPrevWnd = 0) Or ((hPrevWnd <> 0) And (DataRecived = False)), where DataReceived is a variable that changes to True when the SendMessage will successfully processed by the first instance.
You might think that such cases are rare, but they do happen and when it does, your app had better be prepared.
But the point here is that FindWindow is unreliable. You'll probably be better off implementing your own window searching algorithm. You could use SendMessageTimeout for instance, so your app can avoid being blocked by a hung thread.
Originally Posted by Fenrir
I was reading that a ClassName is assigned for registered applications only (even though I don't know what it means).
You can always revert to vbNullString if it fails. However, if you want to reduce the chances of FindWindow finding another window that has the same caption as yours, then you should include the classname.
On Local Error Resume Next: If Not Empty Is Nothing Then Do While Null: ReDim i(True To False) As Currency: Loop: Else Debug.Assert CCur(CLng(CInt(CBool(False Imp True Xor False Eqv True)))): Stop: On Local Error GoTo 0
You are mostly correct. The GUID should indeed be declared as a constant. You will need to generate a new GUID for every single-instance app that you create. That way, when any of your apps create a named Mutex, they won't clash with each other or with any other apps. GUIDs are designed to be very unique so there's virtually no chance that another app generated the same GUID as yours.
What I haven't understood yet it's if there is a database where GUIDs are saved to guarantee the uniqueness. If they are kept in RAM from the moment they're created to that one they're deleted, if I do:
might another application (that dinamically generates a new GUID or that already uses one) create/use the same of my own?
Originally Posted by Bonnie West
But the point here is that FindWindow is unreliable. You'll probably be better off implementing your own window searching algorithm. You could use SendMessageTimeout for instance, so your app can avoid being blocked by a hung thread.
What I haven't understood yet it's if there is a database where GUIDs are saved to guarantee the uniqueness. If they are kept in RAM from the moment they're created to that one they're deleted, if I do:
might another application (that dinamically generates a new GUID or that already uses one) create/use the same of my own?
When you create a GUID, you can be pretty sure that no other app before, at the moment, or in the future have/will generate the same GUID. Of course, you can't prevent any app, that is yet to be created, from copying your GUID and using it for the same purpose. Such apps, however, are probably some type of malware. The only likely reason that another app would have the same GUID as yours is if they intentionally duplicated it. Microsoft acknowledges that possibility in the documentation for CreateMutex:
Originally Posted by MSDN
If you are using a named mutex to limit your application to a single instance, a malicious user can create this mutex before you do and prevent your application from starting. To prevent this situation, create a randomly named mutex and store the name so that it can only be obtained by an authorized user. Alternatively, you can use a file for this purpose. To limit your application to one instance per user, create a locked file in the user's profile directory.
So that is why I've chosen the "locked file" approach instead.
In case you have more questions about GUIDs, The Quick Guide to GUIDs should be able to address all of your concerns.
As I use a MDI Form, my ClassName should be MDIClient, right?
No. MDIClient, unsurprisingly, is the class name of the window within the client area of the MDI Form. You'll have to specify "ThunderRT6MDIForm" instead. You can find out the class name of any window by using a window spy tool such as WinID.
On Local Error Resume Next: If Not Empty Is Nothing Then Do While Null: ReDim i(True To False) As Currency: Loop: Else Debug.Assert CCur(CLng(CInt(CBool(False Imp True Xor False Eqv True)))): Stop: On Local Error GoTo 0
I was trying to check out whether a path is relative or not to can pass the CurDir$ correctly. I saw some time ago, the PathIsRelative API function, so I have written this code:
Code:
'Even though the function should return a Boolean value, I don't know why I found the declaration As Long
Private Declare Function PathIsRelative Lib "shlwapi.dll" Alias "PathIsRelativeA" (ByVal pszPath As String) As Long
Public Sub Main()
If Is2ndInstance() Then
'Here I get the handle of the previous instance
Call SendData(hWndPrevInst, IIf(PathIsRelative(Command$) = True, CurDir$ & "\", vbNullString) & Command$)
Exit Sub
End If
End Sub
But it still not work: sometimes I get back only the base file name, or both the current directory of the application and the absolute path of the file. What's wrong with that code?
PS: I see you can color all the key words in blue and the comments in green, such as VB does. I had to select all these words and change the font color manually. There is a way to do it automatically?
I was trying to check out whether a path is relative or not to can pass the CurDir$ correctly. I saw some time ago, the PathIsRelative API function, so I have written this code:
'Even though the function should return a Boolean value, I don't know why I found the declaration As Long
Private Declare Function PathIsRelative Lib "shlwapi.dll" Alias "PathIsRelativeA" (ByVal pszPath As String) As Long
Public Sub Main()
If Is2ndInstance() Then
'Here I get the handle of the previous instance
Call SendData(hWndPrevInst, IIf(PathIsRelative(Command$) = True, CurDir$ & "\", vbNullString) & Command$)
Exit Sub
End If
End Sub
BOOL is a typedef for an integer value that is used in a Boolean context. The header file WinDef.h also defines two values for use with BOOL.
Code:
#define FALSE 0
#define TRUE 1
BOOL is C++'s Boolean type. It is internally different from VB's Boolean type. A table might help you understand them:
C++
VB
FALSE/False
&H00000000 (0&)
&H0000 (0%)
TRUE/True
&H00000001 (1&)
&HFFFF (-1%)
C++'s BOOL is 32 bits (Long) wide while VB's Boolean is 16 bits (Integer) wide. The & and % are type-declaration characters for Long and Integer, respectively. Note also the different capitalization of both Boolean values.
The PathIsRelative(Command$) = True test will fail if PathIsRelative returns TRUE because 1& = -1% is False.
Originally Posted by Fenrir
But it still not work: sometimes I get back only the base file name, or both the current directory of the application and the absolute path of the file. What's wrong with that code?
Parsing the command line is no easy task. If your program is an MDI application, then I assume that your app accepts multiple input files at once through the command line. If that's the case, then you will have to deal with fully-qualified or relative paths with or without spaces in them and/or malformed or invalid paths typed by users at the command line. Note too that PathIsRelative expects a single path only. If the passed Command$ contained multiple paths, PathIsRelative will surely fail.
The code you showed provides no means for the first instance to know whether the passed command line contains the current directory of the second instance or not. You could either, always include the current directory as the first path in the command line, or you could define a command line switch that indicates the presence of the current directory.
On Local Error Resume Next: If Not Empty Is Nothing Then Do While Null: ReDim i(True To False) As Currency: Loop: Else Debug.Assert CCur(CLng(CInt(CBool(False Imp True Xor False Eqv True)))): Stop: On Local Error GoTo 0
that should work, right? Ok, it doesn't: see the image below and tell me where I mistake
Use a MsgBox to see what Command$ contains.
Originally Posted by Bonnie West
Parsing the command line is no easy task. If your program is an MDI application, then I assume that your app accepts multiple input files at once through the command line.
The project started within this purpose, but (fortunately at this time xD) for tecnical reasons I have forsaken this feature, even though I still need a MDI application.
Originally Posted by Bonnie West
The code you showed provides no means for the first instance to know whether the passed command line contains the current directory of the second instance or not. You could either, always include the current directory as the first path in the command line, or you could define a command line switch that indicates the presence of the current directory.
That is, I should do this:
Code:
Public Sub Main()
If Is2ndInstance() Then
Call SendData(hWndPrevInst, Command$)
Exit Sub
Else
Call Form1.Show
End If
End Sub
Private Sub Form_Initialize()
Call Subclass(Me)
End Sub
Private Sub Form_Load()
If PathIsRelative(Command$) = 1& Then
myCommand = CurDir$ & "\" & Command$
End If
Open myCommand For Input As #1
'etc etc
End Sub
Despite this definition of TRUE, however, most functions that return a BOOL type can return any non-zero value to indicate Boolean truth. Therefore, you should always write this:
Code:
// Right way.
BOOL result = SomeFunctionThatReturnsBoolean();
if (result)
{
...
}
and not this:
Code:
// Wrong!
if (result == TRUE)
{
...
}
Originally Posted by Fenrir
see the image below and tell me where I mistake
. . .
Use a MsgBox to see what Command$ contains.
start isn't necessary; just run the exe directly. Command$ will return "desktop\temp.txt".
Originally Posted by Fenrir
The project started within this purpose, but (fortunately at this time xD) for tecnical reasons I have forsaken this feature, even though I still need a MDI application.
Even if you're app is no longer supporting multiple files as command line input, that doesn't mean that the user can't mess with your app and call your program with something like:
How is your app going to react? You will need to consider cases like that.
Originally Posted by Fenrir
That is, I should do this:
Code:
Public Sub Main()
If Is2ndInstance() Then
Call SendData(hWndPrevInst, Command$)
Exit Sub
Else
Call Form1.Show
End If
End Sub
Private Sub Form_Initialize()
Call Subclass(Me)
End Sub
Private Sub Form_Load()
If PathIsRelative(Command$) = 1& Then
myCommand = CurDir$ & "\" & Command$
End If
Open myCommand For Input As #1
'etc etc
End Sub
right?
If your Main subroutine isn't called anywhere, then it makes no sense for it to be Public.
Your Exit Sub in Sub Main serves no real purpose because there are no more statements after the If..Then..Else block; that code will exit naturally.
Do not start subclassing in Form_Initialize because when the Form's hWnd is retrieved in the Subclass function, Form_Load will fire. Have you forgotten about that? I've also commented about it in the attached demo. My exact words were "Do this last in Form_Load". The reason for that is, you will want to defer subclassing for as long as you can because when it starts, it makes debugging using the IDE trickier.
As mentioned by MSDN, do not compare BOOL return values to TRUE (1&). That's because some functions may return a positive value other than TRUE (1&). Rather, you should either compare it to FALSE (0&) or get rid of the = 1&. VB treats all non-zero values in Boolean expressions as True. Thus, your code could be rewritten as:
Code:
If PathIsRelative(Command$) Then
BTW, I've already mentioned this in post #17. I'll say it again:
Originally Posted by Bonnie West
Note too that PathIsRelative expects a single path only. If the passed Command$ contained multiple paths, PathIsRelative will surely fail.
Do not assume that your users won't try to give your program invalid input. If they selected multiple files in Windows Explorer and dropped them onto your app, what should your program do then?
Have you tried to view the result of the concatenation of CurDir$ & "\" & Command$? Did the Open statement throw any error when you specified that result? It seems you first need to have a solid grasp on working with paths and filenames etc. Naming Files, Paths, and Namespaces should help you get started.
On Local Error Resume Next: If Not Empty Is Nothing Then Do While Null: ReDim i(True To False) As Currency: Loop: Else Debug.Assert CCur(CLng(CInt(CBool(False Imp True Xor False Eqv True)))): Stop: On Local Error GoTo 0
Well, I tried and tried again, and I've understood where the problem was: when the path is absolute, the Command$ is quoted, otherwise it isn't. The PathIsRelative checks correctly the path only if it isn't quoted.
So within this function:
Code:
Function IsRelative(Path As String) As Boolean
Path = Replace(Path, Chr(34), vbNullString)
IsRelative = (PathIsRelative(Path) And Not Len(Path) = 0))
End Function
when the path is absolute, the Command$ is quoted, otherwise it isn't.
That isn't always the case. Quotation marks are used to enclose a path with spaces in it so that one can determine where the start and end of a path is in a given command line (that might contain other paths). Consider this:
The command line arguments are delimited by spaces. If the Relative Path argument was unquoted, a command line parser would erroneously assume that "..\Relative", "Path\Another" and "File.txt" are all separate arguments. Quotation marks are necessary for both absolute and relative paths if they have spaces in them.
Originally Posted by Fenrir
The PathIsRelative checks correctly the path only if it isn't quoted.
That's right. Since it expects only a single path, there's no need for the double quotes to tell it where the path starts and ends.
Originally Posted by Fenrir
So within this function:
Code:
Function IsRelative(Path As String) As Boolean
Path = Replace(Path, Chr(34), vbNullString)
IsRelative = (PathIsRelative(Path) And Not Len(Path) = 0))
End Function
I solved a problem xD
The string passed to that function will be stripped of quotation marks in the Replace line because you implicitly passed it ByRef. That's probably unintended behavior. """" is better than Chr(34). The expression (PathIsRelative(Path) And Not Len(Path) = 0) evaluates to 1& if Path is a relative path, 0& otherwise. Assigning 1& to a Boolean variable coerces it to True, so that function still works as expected. Here's an example evaluation of that expression (in the Debug window):
Private Declare Function PathIsRelativeW Lib "shlwapi.dll" (ByVal lpszPath As Long) As Long
Public Function IsPathRelative(ByVal sPath As String) As Boolean
If InStr(sPath, """") Then sPath = Replace(sPath, """", vbNullString)
If LenB(sPath) Then IsPathRelative = PathIsRelativeW(StrPtr(sPath)) <> 0&
End Function
Keep in mind that if the user passed something like the following to the PathIsRelative API function:
It will return TRUE even if there are other (fully-qualified) paths on the command line. You will need to extract each path and pass that instead to PathIsRelative.
On Local Error Resume Next: If Not Empty Is Nothing Then Do While Null: ReDim i(True To False) As Currency: Loop: Else Debug.Assert CCur(CLng(CInt(CBool(False Imp True Xor False Eqv True)))): Stop: On Local Error GoTo 0
I'm sorry to post on Easter day (and I give you all my best wishes).
I'm at this point now:
using the FindWindow I get the handle of the first instance, and the with the SendMessage I pass to it the Command$ line. All should work unless there's another window with the same caption of my own. So, I would like to check if the COPYDATA message is successfully processed by my program (I thought to send back to the second instance a new message, such as WM_NULL to point out that I got the Command$).
Till now I wrote this:
Code:
'In a module
Dim MsgGot As Boolean
Public Function Subclass(ByRef Frm As Form) As Boolean
Subclass = SetWindowSubclass(Frm.hWnd, AddressOf SubclassProc, ObjPtr(Frm), AddressOf SubclassProc)
Debug.Assert Subclass 'If code stops here, the Form wasn't subclassed
End Function
Public Sub SendData(ByVal hWndPrevInst As Long, Optional ByVal sMsg As Variant)
Dim CDS As COPYDATASTRUCT
If Not IsMissing(sMsg) Then
CDS.cbData = LenB(sMsg) + 2& 'Include room for null terminator so SysReAllocString
CDS.lpData = StrPtr(sMsg) 'can determine the string's length
SendMessage hWndPrevInst, WM_COPYDATA, Form1.hWnd, CDS
Else
SendMessage hWndPrevInst, WM_NULL, ByVal 0&, ByVal 0&
End If
End Sub
Private Function GetData(ByVal CDS_Ptr As Long) As String
Dim CDS As COPYDATASTRUCT
CopyBytes LenB(CDS), CDS, CDS_Ptr 'Retrieve the CDS UDT
SysReAllocString VarPtr(GetData), CDS.lpData 'Copy the string from the given pointer
End Function
'Do not try to debug this subclassing procedure!
Private Function SubclassProc(ByVal hWnd As Long, ByVal uMsg As Long, ByVal wParam As Long, ByVal lParam As Long, _
ByVal Frm As Form, ByVal dwRefData As Long) As Long
Select Case uMsg 'Return TRUE (1&) to signal that the message was processed
Case WM_COPYDATA: SubclassProc = 1&
MsgBox GetData(lParam)
SendData wParam
Exit Function
Case WM_NULL: SubclassProc = 1&
MsgGot = True
Exit Function
Case WM_DESTROY: dwRefData = RemoveWindowSubclass(hWnd, dwRefData, ObjPtr(Frm)): Debug.Assert dwRefData
End Select 'The Form will be automatically unsubclassed when it unloads (unless code stops here ^^)
SubclassProc = DefSubclassProc(hWnd, uMsg, wParam, lParam)
End Function
Private Sub Main()
Load Form1
If Is2ndInstance() Then
Do While Not MsgGot
Dim hWndPrevInst As Long
hWndPrevInst = FindWindow("ThunderRT6FormDC", Form1.Caption)
If hWndPrevInst <> 0 Then
SendData hWndPrevInst, Command$
End If
Loop
Unload Form1
Else
Form1.Show
End If
End Sub
'In Form1
Private Sub Form_Load()
Subclass Me
End If
But this doesn't work and all stops into an endless loop.
All should work unless there's another window with the same caption of my own.
We've already discussed how GUIDs are useful in situations where you need a highly unique identifier. You can change a Form's caption to a GUID when it loads and revert it to the default caption when you're done with the GUID.
Originally Posted by Fenrir
So, I would like to check if the COPYDATA message is successfully processed by my program (I thought to send back to the second instance a new message, such as WM_NULL to point out that I got the Command$).
Till now I wrote this:
. . .
But this doesn't work and all stops into an endless loop.
What exactly doesn't work? "Doesn't work" is very vague; you need to be more specific.
I think this is what's happening with both instances of your program:
Assuming that FindWindow finds your first instance, the second instance now sends its command line to the previous instance.
The first instance manages to retrieve the passed command line. It then tries to send WM_NULL to the second instance as a confirmation of success.
But the problem is, the second instance is still waiting for SendMessage to return after having sent its command line via the WM_COPYDATA message and therefore, couldn't respond to the WM_NULL message.
The two instances are now waiting for each other and thus, they are deadlocked.
Sends the specified message to a window or windows. The SendMessage function calls the window procedure for the specified window and does not return until the window procedure has processed the message.
You should not use the WM_NULL message like that. The documentation states:
Originally Posted by MSDN
Performs no operation. An application sends the WM_NULL message if it wants to post a message that the recipient window will ignore.
Originally Posted by MSDN
Return value
An application returns zero if it processes this message.
If another program sent your app the WM_NULL message, your app's unexpected response will confuse that other program. You should instead send a registered window message.
But you don't even need to register a new window message just to send confirmation. The WM_COPYDATA message already does what you want to do.
Originally Posted by MSDN
Return value
If the receiving application processes this message, it should return TRUE; otherwise, it should return FALSE
Did you not understand the comments I wrote in the Previous Instance demo?
Code:
'SendMessage won't return until the data has been sent and the recipient has responded.
SendMessage hWndPrevInst, WM_COPYDATA, CDS.dwData, CDS
'Return TRUE (1&) to signal that the message was processed
SubclassProc = GetData(Frm, lParam) + 1&:
What do you need the confirmation for? Is it so that if FindWindow fails to find the right window, it will try again? I'm not sure but I think FindWindow will just find the same window again, unless that window was destroyed (or its caption was modified) in the meantime. This is where GUIDs become useful. However, things would be better if the first attempt to find the previous instance did not fail.
Originally Posted by Fenrir
What do you suggest me to do?
I've already showed you the easy way using a 'locked file'. Previously, you said "it seemed it requires more code than the version I provided", but now it seems that your current code is more complicated than my approach.
On Local Error Resume Next: If Not Empty Is Nothing Then Do While Null: ReDim i(True To False) As Currency: Loop: Else Debug.Assert CCur(CLng(CInt(CBool(False Imp True Xor False Eqv True)))): Stop: On Local Error GoTo 0
We've already discussed how GUIDs are useful in situations where you need a highly unique identifier. You can change a Form's caption to a GUID when it loads and revert it to the default caption when you're done with the GUID.
How this can be helpful? The result will always be that the second instance will try to find the Form's caption (because the GUID is already changed into the original title, unless first instance form's caption stays the GUID), or not?
Originally Posted by Bonnie West
What exactly doesn't work? "Doesn't work" is very vague; you need to be more specific.
If you compile first and then run the executable file, you'll see that what you get is not what you wanted: first of all, the second instance (even though any form is visible) is still kept into the running processes. Then, the first instance don't receive any Command line. These are only few aspects of what doesn't go, cause I changed and changed again the code within the hope it finally works.
Originally Posted by Bonnie West
If another program sent your app the WM_NULL message, your app's unexpected response will confuse that other program. You should instead send a registered window message.
But even if my app receives a COPYDATA message from another program it will confuse. Any message might be confused with I send to.
I need a confirmation to be sure that the first instance has received the Command line (that is it has successfully managed the COPYDATA message), otherwise I can try to send the message to the next window that has the same caption (if the FindWindow fails because it stops to the first window's caption that match the title bar, I may try to write a my own function using the EnumWindow and the GetWindowText).
Originally Posted by Bonnie West
Did you not understand the comments I wrote in the Previous Instance demo?
Comments are very clear, but the fact Subclass function returns TRUE is insignificant: the return value is visible to the first instance that processes the message only, and not also the the second instance (that sends it). And the confirmation is required into the second one.
Originally Posted by Bonnie West
What do you need the confirmation for? Is it so that if FindWindow fails to find the right window, it will try again? I'm not sure but I think FindWindow will just find the same window again, unless that window was destroyed (or its caption was modified) in the meantime. This is where GUIDs become useful. However, things would be better if the first attempt to find the previous instance did not fail.
I've already replied you above.
Originally Posted by Bonnie West
I've already showed you the easy way using a 'locked file'. Previously, you said "it seemed it requires more code than the version I provided", but now it seems that your current code is more complicated than my approach.
Assuming that there is free HHD space enough to save the file that will check if another instance is running and its Window handle, however you need the SendMessage to pass the Command line through the apps. Even though you already now to what window send the message, there always may be unexpected that forces you to want to know if your window has processed the message.
I don't think there's a way best of all to solve problems.
This morning I've finally solved (thank you Bonnie West). Here's the code:
Code:
'[Cut]
Private Sub SendData(ByVal hWndPrevInst As Long, ByVal sMsg As String, Optional ByVal NotifyWnd As Variant)
Dim CDS As COPYDATASTRUCT
If Not IsMissing(NotifyWnd) Then
CDS.cbData = LenB(sMsg) + 2& 'Include room for null terminator so SysReAllocString
CDS.lpData = StrPtr(sMsg) 'can determine the string's length
SendMessage hWndPrevInst, WM_COPYDATA, NofityWnd, CDS
Else
SendNotifyMessage hWndPrevInst, WM_NULL, ByVal 0&, ByVal 0&
End If
End Sub
Private Sub Main()
Load Form1
If Is2ndInstance() Then
Do While Not MsgGot
Form1.Caption = Form1.hWnd
Dim hWndPrevInst As Long
hWndPrevInst = FindWindow("ThunderRT6FormDC", "Form1")
If hWndPrevInst <> 0 Then
SendData hWndPrevInst, Command$
End If
Loop
Unload Form1
Else
Form1.Show
End If
End Sub
How this can be helpful? The result will always be that the second instance will try to find the Form's caption (because the GUID is already changed into the original title, unless first instance form's caption stays the GUID), or not?
If you don't mind utilizing your App's Title property as the holder of the GUID string, then see the technique I demonstrated in this post.
Originally Posted by Fenrir
If you compile first and then run the executable file, you'll see that what you get is not what you wanted: first of all, the second instance (even though any form is visible) is still kept into the running processes. Then, the first instance don't receive any Command line. These are only few aspects of what doesn't go, cause I changed and changed again the code within the hope it finally works.
It appears from your latest post that what actually happened was that FindWindow keeps finding your second instance's invisible Form, hence the infinite loop.
Originally Posted by Fenrir
But even if my app receives a COPYDATA message from another program it will confuse. Any message might be confused with I send to.
That is another thing that you will need to take care of. In the GetData function, you have to verify that each member of the COPYDATASTRUCT structure is valid, e.g., check that the lpData member is a non-Null pointer by comparing it against 0&.
Originally Posted by Fenrir
I need a confirmation to be sure that the first instance has received the Command line (that is it has successfully managed the COPYDATA message), otherwise I can try to send the message to the next window that has the same caption (if the FindWindow fails because it stops to the first window's caption that match the title bar, I may try to write a my own function using the EnumWindow and the GetWindowText).
. . .
Comments are very clear, but the fact Subclass function returns TRUE is insignificant: the return value is visible to the first instance that processes the message only, and not also the the second instance (that sends it). And the confirmation is required into the second one.
It does seem that you did not fully comprehend my comments nor that of MSDN's documentation. OK, I'll try to explain it as clearly as I can.
Recall that SendMessage is a function and therefore it has a return value (a Long data type, to be exact). In the SubclassProc, after processing the WM_COPYDATA message, SubclassProc's return value was set to TRUE (1&) and the DefSubclassProc was bypassed (by exiting) so that the return value isn't overwritten. Do you now see where I'm going with this? In short, the value that SubclassProc returns will be the same value that SendMessage returns. In the Previous Instance demo, I had no use for the return value so I did not check SendMessage's return value. But since you need it, just check after SendMessage returns what value the first instance returned. Is everything clear now?
Originally Posted by Fenrir
Assuming that there is free HHD space enough to save the file that will check if another instance is running and its Window handle, however you need the SendMessage to pass the Command line through the apps. Even though you already now to what window send the message, there always may be unexpected that forces you to want to know if your window has processed the message.
I've already told you that the created file is only about 10 Bytes or so. That is way less than the typical cluster size of 4KB for NTFS volumes. If the file was in your boot volume and that volume had only 1 cluster left, you wouldn't even be able to load Windows. Checking for the previous instance and passing the command line to it are separate but related activities. In the Previous Instance demo, I could've checked SendMessage's return value when the second instance passed its command line to the first, but since it was only a demo, I didn't feel that there was a need to do it. Besides, I can see that the second instance succeeded because the first instance showed the passed command line.
Originally Posted by Fenrir
I don't think there's a way best of all to solve problems.
I've pointed out already that somebody already figured out the best way of checking the previous instance and relaying the command line to it. Only problem is, I haven't yet seen a VB6 translation of the C++ code that he used and I also couldn't port it myself either.
Originally Posted by Fenrir
This morning I've finally solved (thank you Bonnie West). Here's the code:
. . .
This seems work fine. Thanks,
Instead of sending WM_NULL from inside the SendData subroutine, send it directly in the SubclassProc. Or better yet, don't misuse WM_NULL and just register a new window message that only your apps will know how to deal with and respond to. But the best way, of course, is to make use of what is already available - the return value of the WM_COPYDATA message will tell you whether it failed or not.
By now, you can probably see that FindWindow isn't that easy to work with. It is far too unreliable and probabilistic. The locked file approach, on the other hand, kills two birds with one stone. A single tiny file determines as efficiently as a Mutex whether a previous instance created it first or not, and it also holds the hWnd of that first instance so that subsequent instances have an easy time of retrieving it.
BTW, whenever you feel like concluding our discussion, don't forget to mark this thread Resolved.
On Local Error Resume Next: If Not Empty Is Nothing Then Do While Null: ReDim i(True To False) As Currency: Loop: Else Debug.Assert CCur(CLng(CInt(CBool(False Imp True Xor False Eqv True)))): Stop: On Local Error GoTo 0