14

I am handling from my Application associated extension files from Windows. So when you double click a file from Windows it will execute my program, and I handle the file from there, something like:

procedure TMainForm.FormCreate(Sender: TObject);
var
  i: Integer;
begin
  for i := 0 to ParamCount -1 do
  begin
    if SameText(ExtractFileExt(ParamStr(i)), '.ext1') then
    begin
      // handle my file..

      // break if needed
    end else
    if SameText(ExtractFileExt(ParamStr(i)), '.ext2') then
    begin
      // handle my file..

      // break if needed
    end else
  end;
end;

That works pretty much how I want it to, but when I was testing I realised it does not consider using only one instance of my program.

So for example, if I selected several Files from Windows and opened them all at the same time, this will create the same number of instances of my program with the number of Files being opened.

What would be a good way to approach this, so that instead of several instances of my program being opened, any additional Files from Windows being opened will simply focus back to the one and only instance, and I handle the Files as normal?

Thanks

UPDATE

I found a good article here: http://www.delphidabbler.com/articles?article=13&part=2 which I think is what I need, and shows how to work with the Windows API as mentioned by rhooligan. I am going to read through it now..

mjn
  • 36,362
  • 28
  • 176
  • 378
  • Related link: https://stackoverflow.com/questions/35516347/sendmessagewm-copydata-record-string/75451394#75451394 – Gabriel Feb 15 '23 at 09:32

6 Answers6

9

Here is some simple example code that gets the job done. I hope it is self-explanatory.

program StartupProject;

uses
  SysUtils,
  Messages,
  Windows,
  Forms,
  uMainForm in 'uMainForm.pas' {MainForm};

{$R *.res}

procedure Main;
var
  i: Integer;
  Arg: string;
  Window: HWND;
  CopyDataStruct: TCopyDataStruct;
begin
  Window := FindWindow(SWindowClassName, nil);
  if Window=0 then begin
    Application.Initialize;
    Application.MainFormOnTaskbar := True;
    Application.CreateForm(TMainForm, MainForm);
    Application.Run;
  end else begin
    FillChar(CopyDataStruct, Sizeof(CopyDataStruct), 0);
    for i := 1 to ParamCount do begin
      Arg := ParamStr(i);
      CopyDataStruct.cbData := (Length(Arg)+1)*SizeOf(Char);
      CopyDataStruct.lpData := PChar(Arg);
      SendMessage(Window, WM_COPYDATA, 0, NativeInt(@CopyDataStruct));
    end;
    SetForegroundWindow(Window);
  end;
end;

begin
  Main;
end.

 

unit uMainForm;

interface

uses
  Windows, Messages, SysUtils, Classes, Controls, Forms, StdCtrls;

type
  TMainForm = class(TForm)
    ListBox1: TListBox;
    procedure FormCreate(Sender: TObject);
  protected
    procedure CreateParams(var Params: TCreateParams); override;
    procedure WMCopyData(var Message: TWMCopyData); message WM_COPYDATA;
  public
    procedure ProcessArgument(const Arg: string);
  end;

var
  MainForm: TMainForm;

const
  SWindowClassName = 'VeryUniqueNameToAvoidUnexpectedCollisions';

implementation

{$R *.dfm}

{ TMainForm }

procedure TMainForm.CreateParams(var Params: TCreateParams);
begin
  inherited;
  Params.WinClassName := SWindowClassName;
end;

procedure TMainForm.FormCreate(Sender: TObject);
var
  i: Integer;
begin
  for i := 1 to ParamCount do begin
    ProcessArgument(ParamStr(i));
  end;
end;

procedure TMainForm.ProcessArgument(const Arg: string);
begin
  ListBox1.Items.Add(Arg);
end;

procedure TMainForm.WMCopyData(var Message: TWMCopyData);
var
  Arg: string;
begin
  SetString(Arg, PChar(Message.CopyDataStruct.lpData), (Message.CopyDataStruct.cbData div SizeOf(Char))-1);
  ProcessArgument(Arg);
  Application.Restore;
  Application.BringToFront;
end;

end.
David Heffernan
  • 601,492
  • 42
  • 1,072
  • 1,490
  • what do you mean by buffer overruns David? –  Dec 31 '11 at 13:47
  • 2
    @Craig A malicious program could send a WM_COPYDATA to this window and make it read beyond the end of `lpData`. That's what the `cbData` parameter is all about. The Delphi Dabbler article you have found uses an identical solution to mine but fails to check `cbData`. Would you like me to show how to do it correctly? – David Heffernan Dec 31 '11 at 13:55
  • 2
    OK, answer updated to protect against buffer overruns in the `WM_COPYDATA` handler. – David Heffernan Dec 31 '11 at 14:00
  • It would be very helpful if you could show how to do that. I wouldn't of even know some other bad programs could potentially do that, scary stuff! EDIT: thanks for updating it, I don't really understand some of it so I will read the article some more and research some of the code and things you posted. –  Dec 31 '11 at 14:01
  • 'Tis done. You just need to make use of `cbData`. The point is that you know for sure that you can read no more than that many bytes from `lpData`. It's worth using the watermark as described in the Delphi Dabbler article too, but that's actually less important than avoiding the buffer overruns in my view. – David Heffernan Dec 31 '11 at 14:04
  • Ok this works well, but for some reason the application just flashes on the taskbar when opening other files, it does not restore (bring to front) the window. I am looking at the WMCopyData procedure and I can see Application.BringToFront but it seems to have no effect. Does this happen with you, or could it be something with my code? –  Dec 31 '11 at 15:07
  • 1
    Yes, I can see that. I've addressed that in the latest update. The call to `SetForegroundWindow` from the .dpr file, and the call to `Application.Restore` in the .pas file. – David Heffernan Dec 31 '11 at 15:51
  • Thanks for updating I thought it may have been a problem of mine. I was trying to use ShowWindow and BringToFront with not much success. –  Dec 31 '11 at 16:10
  • 1
    Only certain windows can set the foreground window. You can't *steal* it. The trick here is that the current foreground process is allowed to nominate a new foreground window. – David Heffernan Dec 31 '11 at 16:17
  • Well, I didn't really explain that much. Here's Raymond Chen's take on the subject: http://blogs.msdn.com/b/oldnewthing/archive/2009/02/20/9435239.aspx – David Heffernan Dec 31 '11 at 19:33
  • let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/6288/discussion-between-david-heffernan-and-craig) – David Heffernan Jan 01 '12 at 14:03
  • I am using a similar piece of code and I have seen it failed starting with Win10 - I don't know what they changed in Win10. MAYBE the gap between the time when the app starts and the time when you set the unique ID via CreateParams is too big. Another instance has somehow time to run in this gap/interval. Imagine 2 instances started at only 1ms distance. Both of them will check for another instance, but none of them had the chance to set the flag because creating the form is slow. So, both of them will run. Maybe CreateSemaphore is safer? – Gabriel Jul 28 '17 at 15:41
  • Hi. What is the need for the +1 in: (Length(Arg)+1)*SizeOf(Char) ? With that +1, whenever you send the message, you send a 2 as size, even if the message is empty. – Gabriel Feb 15 '23 at 10:00
  • @ServerOverflow It's for the null terminator, which is present even when the original string is empty, because that's how you represent an empty string as a null-terminated character array – David Heffernan Feb 15 '23 at 10:33
  • @Dabid-Hi. I imagined that :) But since Arg is a Delphi string, and not a C "string" we don't care much about transmitting that null char. Right? In this specific case (sending pure Delph strings) it is totally safe to ignore it. – Gabriel Feb 16 '23 at 14:18
  • @ServerOverflow that's true enough because our call to SetString doesn't rely on the null terminator – David Heffernan Feb 16 '23 at 14:34
  • I like (and use) the second approach better (don't pass the Null) because: a) it is not obvious for a beginner what +1 is doing (could at least be a constant), and b) in the "receiver" part, I check cbData for zero. If we pass that darn null we have to check for 2, which looks very very very odd. (Just a personal opinion :) ) – Gabriel Feb 16 '23 at 14:40
  • I find it strange that today in 2023, C/++ still does not have proper support for strings :( – Gabriel Feb 16 '23 at 14:43
  • @server C++ has had proper support for strings for about 25 years – David Heffernan Feb 16 '23 at 14:48
  • then our definitions of "proper" have mismatch :) – Gabriel Feb 16 '23 at 15:49
  • @ServerOverflow What do you mean, what's wrong with `std::string`? – David Heffernan Feb 16 '23 at 17:04
  • This line tells it all: "A character array is simply an array of characters that can be terminated by a null character" https://www.geeksforgeeks.org/stdstring-class-in-c/ – Gabriel Feb 18 '23 at 10:58
  • And this one: Note that string objects handle bytes without knowledge of the encoding that may eventually be used to encode the characters it contains. Therefore, the value returned may not correspond to the actual number of encoded characters in sequences of multi-byte or variable-length characters (such as UTF-8). https://cplusplus.com/reference/string/string/length/ – Gabriel Feb 18 '23 at 10:59
  • and this one: http://www.delphigroups.info/2/ef/531997.html – Gabriel Feb 18 '23 at 10:59
  • And this: #0 ....... – Gabriel Feb 18 '23 at 10:59
  • And many others.... Admit it, there are many languages out there with better support for strings than C/++ – Gabriel Feb 18 '23 at 11:00
  • @ServerOverflow what do you think Length() does on a UTF8 encoded string? Number of bytes or something else? Also what is C/++. Whilst many languages may have better support, I think it's not right to say that it doesn't have proper support. – David Heffernan Feb 18 '23 at 12:01
  • @ServerOverflow also a post on a Delphi group from 20 years ago saying that Delphi strings are faster than those in VC++. I mean, you know that is not relevant today and probably wasn't true even then. – David Heffernan Feb 18 '23 at 12:06
1

The logic goes something like this. When you start your application, you iterate through the list of running processes and see if your application is already running. If it is running, you need to activate the window of that instance and then exit.

Everything you need to do this is in the Windows API. I found this sample code on CodeProject.com that deals with processes:

http://www.codeproject.com/KB/system/Win32Process.aspx

On finding and activating a window, the basic approach is to find the window of interest using the window class name then activate it.

http://www.vb6.us/tutorials/activate-window-api

Hopefully this gives you a good starting point.

Brian
  • 6,910
  • 8
  • 44
  • 82
0

There are many answers here that show how to implement this. I want to show why NOT to use the FindWindow approach.

I am using FindWindow (something similar with the one shown by David H) and I have seen it failed starting with Win10 - I don't know what they changed in Win10.
I think the gap between the time when the app starts and the time when we set the unique ID via CreateParams is too big so another instance has somehow time to run in this gap/interval.

Imagine two instances started at only 1ms distance (let's say that the user click the EXE file and then presses enter and keeps it pressed by accident for a short while). Both instances will check to see if a window with that unique ID exists, but none of them had the chance to set the flag/unique ID because creating the form is slow and the unique ID is set only when the form is constructed. So, both instances will run.

So, I would recommend the CreateSemaphore solution instead: https://stackoverflow.com/a/460480/46207
Marjan V already proposed this solution but didn't explained why it is better/safer.

Gabriel
  • 20,797
  • 27
  • 159
  • 293
-1

I used window/message approach by myself with addition of events for tracking if the other instance is running:

  1. Try to create event "Global\MyAppCode" (the "Global" namespace is used for handling various user sessions as I needed single instance system-wide; in your case you'll probably prefer "Local" namespace which is set by default)
  2. If CreateEvent returned error and GetLastError = ERROR_ALREADY_EXISTS then the instance is running already.
  3. FindWindow/WM_COPYDATA to transfer data to that instance.

But the drawbacks with messages/windows are more than significant:

  1. You must always keep your window's Caption constant. Otherwise you'll have to list all the windows in the system and loop through them for partial occurrence of some constant part. Moreover the window's caption could be easily changed by a user or 3rd part app so the search would fail.
  2. Method requires a window to be created so no console/service apps, or they must create a window and perform message loop especially for handling the single instance.
  3. I'm not sure FindWindow could find a window that is opened in another user session
  4. For me, WM_COPYDATA is rather awkward method.

So currently I'm a fan of named pipe approach (haven't implemented it yet though).

  1. On launch, app tries to connect to "Global\MyAppPipe". If successed, other instance is running. If failed, it creates this pipe and finishes instance check.
  2. 2nd instance writes the required data to pipe and exits.
  3. 1st instance receives data and does some stuff.

It works through all user sessions (with namespace "Global") or just a current session; it doesn't depend on strings used by UI (no localization and modification issues); it works with console and service apps (you'll need to implement pipe reading in a separate thread/message loop though).

Fr0sT
  • 2,959
  • 2
  • 25
  • 18
-1

I'd use mutexes. You create one when your program starts.

When the creation fails it means another instance is already running. You then send this instance a message with your command line parameters and close. When your app receives a message with a command line, it can parse the parameters like you are already doing, check to see whether it already has the file(s) open and proceed accordingly.

Processing this app specific message ia also the place to get your app to the front if it isn't already. Please do this politely (SetForegroundWindow) without trying to force your app in front of all others.

function CreateMutexes(const MutexName: String): boolean;
// Creates the two mutexes to see if the program is already running.
//  One of the mutexes is created in the global name space (which makes it
//  possible to access the mutex across user sessions in Windows XP); the other
//  is created in the session name space (because versions of Windows NT prior
//  to 4.0 TSE don't have a global name space and don't support the 'Global\'
//  prefix).
var
  SecurityDesc: TSecurityDescriptor;
  SecurityAttr: TSecurityAttributes;
begin
  // By default on Windows NT, created mutexes are accessible only by the user
  //  running the process. We need our mutexes to be accessible to all users, so
  //  that the mutex detection can work across user sessions in Windows XP. To
  //  do this we use a security descriptor with a null DACL. 
  InitializeSecurityDescriptor(@SecurityDesc, SECURITY_DESCRIPTOR_REVISION);
  SetSecurityDescriptorDacl(@SecurityDesc, True, nil, False);
  SecurityAttr.nLength := SizeOf(SecurityAttr);
  SecurityAttr.lpSecurityDescriptor := @SecurityDesc;
  SecurityAttr.bInheritHandle := False;
  if (CreateMutex(@SecurityAttr, False, PChar(MutexName)) <> 0 )
  and (CreateMutex(@SecurityAttr, False, PChar('Global\' + MutexName)) <> 0 ) then
    Result := True
  else
    Result := False;
end;

initialization
  if not CreateMutexes('MyAppNameIsRunningMutex') then
    //Find and SendMessage to running instance
    ;
end.

Note: above code is adapted from an example on the InnoSetup site. InnoSetup creates installer applications and uses this approach in the installer to check whether (a previous version of) the application being installed is already running.

Finding the other instance and sending it a message, I'll leave for another question (or you can use the WM_COPYDATA approach from David's answer). Actually, there is a StackOverflow question that deals exactly with this: How to get the process thread that owns a mutex Getting the process/thread that owns the mutex may be a bit of a challenge, but the answers to this question do address ways to get the information from one instance to the other.

Community
  • 1
  • 1
Marjan Venema
  • 19,136
  • 6
  • 65
  • 79
  • 1
    Problem with mutex approach is that it doesn't tackle the passing on of arguments. Since passing on arguments involves identifying an already running instance, the mutex becomes somewhat redundant. – David Heffernan Dec 31 '11 at 14:13
  • @DavidHeffernan You are absolutely right there. I prefer it though because process names can be changed. That would of course defeat the ability to find the other instance and send it a message, but if logic in the app is dependent upon it being the only instance that's a drawback I'll take. – Marjan Venema Dec 31 '11 at 14:36
  • I would opt for finding the other app by locating its main window based on class name. – David Heffernan Dec 31 '11 at 15:52
  • 1
    @DavidHeffernan: ah yes. That also avoids the problem of exe renaming and gives you the handle you need at the same time. D'oh. Been doing too much with installers lately... – Marjan Venema Dec 31 '11 at 17:28
-1

Windows has different ways to handle file associations to executable.

The "command line" approach is only the simplest one, but also the most limited one.

It also supports DDE (it still works although officially deprecated) and COM (see http://msdn.microsoft.com/en-us/library/windows/desktop/cc144171(v=vs.85).aspx).

If I recall correctly both DDE and COM will let your application receive the whole list of selected files.

David Heffernan
  • 601,492
  • 42
  • 1,072
  • 1,490