Results 1 to 26 of 26

Thread: [RESOLVED] Open several files using Command

  1. #1

    Thread Starter
    Lively Member
    Join Date
    Mar 2013
    Posts
    113

    Resolved [RESOLVED] Open several files using Command

    Goodmorning

    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.

  2. #2
    PowerPoster dilettante's Avatar
    Join Date
    Feb 2006
    Posts
    24,487

    Re: Open several files using Command

    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.

    Here is one page worth reading:

    DDE in Visual Basic, Web Browsers, the Windows Shell, Excel and other applications is very old info, but brings a lot together in one place.

  3. #3
    PowerPoster dilettante's Avatar
    Join Date
    Feb 2006
    Posts
    24,487

    Re: Open several files using Command

    You might also look at:

    Registering File Associations and Passing Command Line Parameters to an Existing App Instance

    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.

  4. #4

    Thread Starter
    Lively Member
    Join Date
    Mar 2013
    Posts
    113

    Re: Open several files using Command

    Let's see if I understood right.

    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

  5. #5
    Default Member Bonnie West's Avatar
    Join Date
    Jun 2012
    Location
    InIDE
    Posts
    4,060

    Re: Open several files using Command

    Try the attached Previous Instance Command Line Passing Demo. It uses a locked file in lieu of a Mutex object.

    Name:  Screenshot.png
Views: 2682
Size:  5.0 KB

    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.
    Attached Files Attached Files
    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
    Declare Sub CrashVB Lib "msvbvm60" (Optional DontPassMe As Any)

  6. #6

    Thread Starter
    Lively Member
    Join Date
    Mar 2013
    Posts
    113

    Re: Open several files using Command

    Thank you Bonnie. Your example will be surely useful.

    The only thing I don't have understood is why I should not use MUTEX to check if a previous instance is already running.

  7. #7
    Default Member Bonnie West's Avatar
    Join Date
    Jun 2012
    Location
    InIDE
    Posts
    4,060

    Re: Open several files using Command

    Quote Originally Posted by Fenrir View Post
    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:

      Quote Originally Posted by MSDN
      However, if the caller has limited access rights, the function will fail with ERROR_ACCESS_DENIED ...
    • 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. See Don't forget to pass the current directory along with the command line to your single-instance program.

    • 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
    Declare Sub CrashVB Lib "msvbvm60" (Optional DontPassMe As Any)

  8. #8

    Thread Starter
    Lively Member
    Join Date
    Mar 2013
    Posts
    113

    Re: Open several files using Command

    lpName:=StrPtr("72607477-1315-421F-A9D9-FBDABAC49155")
    What does this mean?

    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:
    1. Right click on a file
    2. Select 'Open As...'
    3. 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.

  9. #9
    Default Member Bonnie West's Avatar
    Join Date
    Jun 2012
    Location
    InIDE
    Posts
    4,060

    Re: Open several files using Command

    Quote Originally Posted by Fenrir View Post
    What does this mean?
    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.

    Quote Originally Posted by Fenrir View Post
    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.

    Quote Originally Posted by Fenrir View Post
    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.

    Quote Originally Posted by Fenrir View Post
    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
    Declare Sub CrashVB Lib "msvbvm60" (Optional DontPassMe As Any)

  10. #10

    Thread Starter
    Lively Member
    Join Date
    Mar 2013
    Posts
    113

    Re: Open several files using Command

    Quote Originally Posted by Bonnie West View Post
    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.

    Quote Originally Posted by Bonnie West View Post
    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?

    Quote Originally Posted by Bonnie West View Post
    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.

    Quote Originally Posted by Bonnie West View Post
    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.

    Quote Originally Posted by Bonnie West View Post
    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's not a problem

    Anyway, thank you.

  11. #11
    Default Member Bonnie West's Avatar
    Join Date
    Jun 2012
    Location
    InIDE
    Posts
    4,060

    Re: Open several files using Command

    Quote Originally Posted by Fenrir View Post
    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.

    Quote Originally Posted by Fenrir View Post
    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.

    Quote Originally Posted by Fenrir View Post
    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?

    Quote Originally Posted by Fenrir View Post
    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
    Declare Sub CrashVB Lib "msvbvm60" (Optional DontPassMe As Any)

  12. #12

    Thread Starter
    Lively Member
    Join Date
    Mar 2013
    Posts
    113

    Re: Open several files using Command

    [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?

    Quote Originally Posted by Bonnie West View Post
    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??

    Quote Originally Posted by Bonnie West View Post
    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?

    Quote Originally Posted by Bonnie West View Post
    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.

    Quote Originally Posted by Bonnie West View Post
    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?

  13. #13
    Default Member Bonnie West's Avatar
    Join Date
    Jun 2012
    Location
    InIDE
    Posts
    4,060

    Re: Open several files using Command

    Quote Originally Posted by Fenrir View Post
    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.

    Quote Originally Posted by Fenrir View Post
    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.

    Quote Originally Posted by Fenrir View Post
    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.

    Quote Originally Posted by Fenrir View Post
    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.

    Quote Originally Posted by Fenrir View Post
    I was reading that a ClassName is assigned for registered applications only (even though I don't know what it means).
    See About Window Classes.

    Quote Originally Posted by Fenrir View Post
    Are you sure that will it work?
    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
    Declare Sub CrashVB Lib "msvbvm60" (Optional DontPassMe As Any)

  14. #14

    Thread Starter
    Lively Member
    Join Date
    Mar 2013
    Posts
    113

    Re: Open several files using Command

    Quote Originally Posted by Bonnie West View Post
    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:
    Code:
    Const GUID = "72607477-1315-421F-A9D9-FBDABAC49155"
    might another application (that dinamically generates a new GUID or that already uses one) create/use the same of my own?

    Quote Originally Posted by Bonnie West View Post
    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 kind of message should I send?

    Quote Originally Posted by Bonnie West View Post
    As I use a MDI Form, my ClassName should be MDIClient, right?
    Last edited by Fenrir; Mar 24th, 2013 at 04:33 AM.

  15. #15
    Default Member Bonnie West's Avatar
    Join Date
    Jun 2012
    Location
    InIDE
    Posts
    4,060

    Re: Open several files using Command

    Quote Originally Posted by Fenrir View Post
    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:
    Code:
    Const GUID = "72607477-1315-421F-A9D9-FBDABAC49155"
    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:

    Quote 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.

    Quote Originally Posted by Fenrir View Post
    What kind of message should I send?
    In the Avoiding Multiple Instances of an Application article, the author declared a registered window message; in his example, it was "UWM_ARE_YOU_ME". BTW, I have tried to port the C++ example in that article to VB6, but so far, I've not been successful.

    Quote Originally Posted by Fenrir View Post
    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
    Declare Sub CrashVB Lib "msvbvm60" (Optional DontPassMe As Any)

  16. #16

    Thread Starter
    Lively Member
    Join Date
    Mar 2013
    Posts
    113

    Re: Open several files using Command

    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?

  17. #17
    Default Member Bonnie West's Avatar
    Join Date
    Jun 2012
    Location
    InIDE
    Posts
    4,060

    Re: Open several files using Command

    Quote Originally Posted by Fenrir View Post
    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:
    If you're a bit unsure about relative paths, then I suggest that you consult Fully Qualified vs. Relative Paths.

    Quote Originally Posted by Fenrir View Post
    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
    From Windows Coding Conventions:

    Boolean Type

    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.

    Quote Originally Posted by Fenrir View Post
    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
    Declare Sub CrashVB Lib "msvbvm60" (Optional DontPassMe As Any)

  18. #18

    Thread Starter
    Lively Member
    Join Date
    Mar 2013
    Posts
    113

    Re: Open several files using Command

    Quote Originally Posted by Bonnie West View Post
    The PathIsRelative(Command$) = True test will fail if PathIsRelative returns TRUE because 1& = -1% is False.
    I changed it like this:
    Code:
    Call SendData(hWndPrevInstance, IIf(PathIsRelative(Command$) = 1&, CurDir & "\", vbNullString) & Command$)
    that should work, right? Ok, it doesn't: see the image below and tell me where I mistake
    Name:  DOS SHELL.jpg
Views: 1066
Size:  76.4 KB
    Use a MsgBox to see what Command$ contains.

    Quote Originally Posted by Bonnie West View Post
    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.

    Quote Originally Posted by Bonnie West View Post
    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
    right?

  19. #19
    Default Member Bonnie West's Avatar
    Join Date
    Jun 2012
    Location
    InIDE
    Posts
    4,060

    Re: Open several files using Command

    Quote Originally Posted by Fenrir View Post
    I changed it like this:
    Code:
    Call SendData(hWndPrevInstance, IIf(PathIsRelative(Command$) = 1&, CurDir & "\", vbNullString) & Command$)
    that should work, right? Ok, it doesn't:
    From Windows Coding Conventions:


    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)
    {
        ...
    }


    Quote Originally Posted by Fenrir View Post
    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".

    Quote Originally Posted by Fenrir View Post
    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:

    Code:
    progetto1.exe "Path1\File1.txt" "Path2\File2.txt" "Path3\File3.txt" "Path4\File4.txt" ...
    How is your app going to react? You will need to consider cases like that.

    Quote Originally Posted by Fenrir View Post
    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:

    Quote Originally Posted by Bonnie West View Post
    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
    Declare Sub CrashVB Lib "msvbvm60" (Optional DontPassMe As Any)

  20. #20

    Thread Starter
    Lively Member
    Join Date
    Mar 2013
    Posts
    113

    Re: Open several files using Command

    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
    I solved a problem xD

  21. #21
    Default Member Bonnie West's Avatar
    Join Date
    Jun 2012
    Location
    InIDE
    Posts
    4,060

    Re: Open several files using Command

    Quote Originally Posted by Fenrir View Post
    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:

    Code:
    progetto1.exe "C:\Fully-qualified Path\Some File.txt" "..\Relative Path\Another File.txt"
    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.

    Quote Originally Posted by Fenrir View Post
    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.

    Quote Originally Posted by Fenrir View Post
    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):

    Code:
    'Assuming Path = "..\Relative Path\Another File.txt"
    
    ? Len(Path)
    33
    
    ? 33 = 0
     False
    
    ? Not False
    True
    
    ? PathIsRelative(Path)
    1
    
    ? 1 And True
    1
    To make that expression return a Boolean value, compare PathIsRelative(Path) against zero:

    Code:
    ? PathIsRelative(Path) <> 0&
     True
    
    IsRelative = (PathIsRelative(Path) <> 0&) And (Len(Path) <> 0&)
    Here's how I would do it:

    Code:
    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:

    Code:
    progetto1.exe "..\Relative Path\Another File.txt" "C:\Fully-qualified Path\Some File.txt"
    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
    Declare Sub CrashVB Lib "msvbvm60" (Optional DontPassMe As Any)

  22. #22

    Thread Starter
    Lively Member
    Join Date
    Mar 2013
    Posts
    113

    Re: Open several files using Command

    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.

    What do you suggest me to do?

  23. #23
    Default Member Bonnie West's Avatar
    Join Date
    Jun 2012
    Location
    InIDE
    Posts
    4,060

    Re: Open several files using Command

    Quote Originally Posted by Fenrir View Post
    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.

    Quote Originally Posted by Fenrir View Post
    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:
    1. Assuming that FindWindow finds your first instance, the second instance now sends its command line to the previous instance.
    2. 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.
    3. 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.
    4. The two instances are now waiting for each other and thus, they are deadlocked.

    From the SendMessage function:

    Quote Originally Posted by MSDN
    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.

    To send a message and return immediately, use the SendMessageCallback or SendNotifyMessage function. To post a message to a thread's message queue and return immediately, use the PostMessage or PostThreadMessage function.
    You should not use the WM_NULL message like that. The documentation states:

    Quote 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.
    Quote 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.

    Quote 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.

    Quote Originally Posted by Fenrir View Post
    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
    Declare Sub CrashVB Lib "msvbvm60" (Optional DontPassMe As Any)

  24. #24

    Thread Starter
    Lively Member
    Join Date
    Mar 2013
    Posts
    113

    Re: Open several files using Command

    Quote Originally Posted by Bonnie West View Post
    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?

    Quote Originally Posted by Bonnie West View Post
    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.

    Quote Originally Posted by Bonnie West View Post
    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).

    Quote Originally Posted by Bonnie West View Post
    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.

    Quote Originally Posted by Bonnie West View Post
    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.

    Quote Originally Posted by Bonnie West View Post
    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.

  25. #25

    Thread Starter
    Lively Member
    Join Date
    Mar 2013
    Posts
    113

    Re: Open several files using Command

    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
    This seems work fine. Thanks,

  26. #26
    Default Member Bonnie West's Avatar
    Join Date
    Jun 2012
    Location
    InIDE
    Posts
    4,060

    Re: Open several files using Command

    Quote Originally Posted by Fenrir View Post
    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.

    Quote Originally Posted by Fenrir View Post
    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.

    Quote Originally Posted by Fenrir View Post
    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&.

    Quote Originally Posted by Fenrir View Post
    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?

    Quote Originally Posted by Fenrir View Post
    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.

    Quote Originally Posted by Fenrir View Post
    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.

    Quote Originally Posted by Fenrir View Post
    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
    Declare Sub CrashVB Lib "msvbvm60" (Optional DontPassMe As Any)

Posting Permissions

  • You may not post new threads
  • You may not post replies
  • You may not post attachments
  • You may not edit your posts
  •  



Click Here to Expand Forum to Full Width