Jump to content
gioma

Intercept "WM_COPY" on Windows

Recommended Posts

Hi,
I'm making a program that sends files from PC A to PC B.
When I copy a file on one PC I would like it to be available on the other PC's cliboard.
What I can do is manipulate the clibboard by copying the paths of the files I intend to transfer, but then I have to intercept the "copy" command in order to start the transfer.
I tried with hooks but no way.
Do any of you know how I could do this? Or maybe you have a different better idea? Thank you.

Share this post


Link to post

I'm not an expert on Windows messages, but you would have to intercept the "WM_DRAWCLIPBOARD" message, to receive notification that MSExplorer intends to copy some object (file/folder/etc...).

But first your app must be registered with a "Clipboard Viewer" ( SetClipboardViewer() ), and when you finish the app, do the "Un-Register" ( ChangeClipboardChain() )

  • try verify this message in your app:
    • procedure WMDrawClipboard(var Msg: TMessage); message WM_DRAWCLIPBOARD;
    • procedure WMChangeCBChain(var Msg: TMessage); message WM_CHANGECBCHAIN;

 

NOTE: This does not guarantee that the file was actually copied, as you are only receiving the notification that something will be copied. The file may be large and the other application will need to check if the file "already exists on disk"

Edited by programmerdelphi2k
  • Like 1

Share this post


Link to post
27 minutes ago, programmerdelphi2k said:

I'm not an expert on Windows messages, but you would have to intercept the "WM_DRAWCLIPBOARD" message, to receive notification that MSExplorer intends to copy some object (file/folder/etc...).

But first your app must be registered with a "Clipboard Viewer" ( SetClipboardViewer() ), and when you finish the app, do the "Un-Register" ( ChangeClipboardChain() )

  • try verify this message in your app:
    • procedure WMDrawClipboard(var Msg: TMessage); message WM_DRAWCLIPBOARD;
    • procedure WMChangeCBChain(var Msg: TMessage); message WM_CHANGECBCHAIN;

 

NOTE: This does not guarantee that the file was actually copied, as you are only receiving the notification that something will be copied. The file may be large and the other application will need to check if the file "already exists on disk"

Thanks for the reply.
I already do this, in fact if I copy paste a text it works perfectly.
The problem instead I have with the files because they are not yet available on the file system because they come from the other PC.
I send the list of files but I would like it to start the transfer only when "paste" is pressed and then the "paste window" waits for the files to be completely transferred and therefore available.

Share this post


Link to post

@gioma

maybe exists some other msg about "file created on disk...", I dont know exactly because my knowledge it's not enough here!

 

try send the msg after the files was copyed from your app...

Edited by programmerdelphi2k
  • Like 1

Share this post


Link to post
22 hours ago, programmerdelphi2k said:

I'm not an expert on Windows messages, but you would have to intercept the "WM_DRAWCLIPBOARD" message, to receive notification that MSExplorer intends to copy some object (file/folder/etc...).

But first your app must be registered with a "Clipboard Viewer" ( SetClipboardViewer() ), and when you finish the app, do the "Un-Register" ( ChangeClipboardChain() )

That is the OLD way to monitor the clipboard.  Since Vista, the NEW way is using a Clipboard Format Listener instead, registered via AddClipboardFormatListener() and handling WM_CLIPBOARDUPDATE messages.

22 hours ago, gioma said:

Thanks for the reply.
I already do this, in fact if I copy paste a text it works perfectly.
The problem instead I have with the files because they are not yet available on the file system because they come from the other PC.
I send the list of files but I would like it to start the transfer only when "paste" is pressed and then the "paste window" waits for the files to be completely transferred and therefore available.

Have you read Microsoft's documentation on Transferring Shell Objects with Drag-and-Drop and the Clipboard and Handling Shell Data Transfer Scenarios yet?

 

Per Shell Clipboard Formats, you will likely want to implement the CFSTR_FILEDESCRIPTOR and CFSTR_FILECONTENTS formats.  That way, you can put descriptions of the files onto the clipboard, and when they are pasted then you can stream in the actual file data as needed.

Edited by Remy Lebeau
  • Like 2

Share this post


Link to post
10 hours ago, Remy Lebeau said:

That is the OLD way to monitor the clipboard.  Since Vista, the NEW way is using a Clipboard Format Listener instead, registered via AddClipboardFormatListener() and handling WM_CLIPBOARDUPDATE messages.

Hi, yes I'm using this way to monitor the clipboard, I didn't specify it because my problem arises later.

 

10 hours ago, Remy Lebeau said:

Have you read Transferring Shell Objects with Drag-and-Drop and the Clipboard yet? Per Shell Clipboard Formats, you will likely want to implement the CFSTR_FILEDESCRIPTOR and CFSTR_FILECONTENTS formats.  That way, you can put descriptions of the files onto the clipboard, and when they are pasted then you can stream in the actual file data as needed.

I've read this article, but I still can't get into it.
Time is never an ally, so I was looking for an example to start with, to avoid too many empty tests, and also because the testing phase is not easy, as we are dealing with two applications on two different PCs communicating via sockets.
My specific case is as follows:
I have two applications, A and B, connected by a socket connection.
Application A intercepts the modification of its clipboard and sends the event to application B.
Here we have two cases:
1 if it is a text, it copies it directly to the clipboard
2 if it is a file, it copies the list to the clipboard.
Up to point 1 there are, while I have difficulty understanding how to pass the list of files to PC B (or rather I can do this phase) and make sure that when I press "paste" (here is the real problem which I'm racking my brain over) the file list transfer begins and the system waits for the files to be transferred before completing the paste command.
I can't find half an example of this on the net, I can't believe no one has ever done this before, maybe even in C, C++.

 

 

Share this post


Link to post

@gioma, I think you should avoid using Clipboard, even if it works in your project, for the simple fact that it is a neutral layer and used by any other application in your system. In this way, nothing guarantees that the information obtained will be the desired information. You know it!

 

Now, as you are already communicating with the second (or any other) application on the network, then, I think the most sensible thing would be to approach this direction, that is, send the desired information through TCP/IP, UDP or similar communication ... etc...

 

In this way, you can guarantee that your transmissions will be directed exclusively to your interested applications, that is, to everyone who makes use of this communication, it can be one, two, or as many as you wish.

 

You could easily do this code without much impact on your current project, if you want, you could even create a small TCP/IP server using INDY or whatever to listen on a certain port, and thus receive all messages (information) desired. In this way, you can ensure that you receive the information sent by another application.

 

Delphi can easily generate a DataSnap Server (ie the code) for you to add to your applications. Delphi has Tethering components, which you can easily add to a form, so almost no code is needed. Communication is carried out by TCP/IP and UDP protocols, in this way, you just need to place a Tethering Server component in the applications that will receive the information, and another Tethering Client in the application that will send the information, the rest is just using one or two events to receive the information. Very easy and without much complexity.

 

here a great sample by Malcolm Groves : http://www.malcolmgroves.com/blog/?p=1891

  • Like 1

Share this post


Link to post

@gioma  based on @Remy Lebeau text, I have test this code and works... but not guaratee if it's good for all...ok?

 

// VCL tests
...
  private
    FClipboardListenerHandle: HWND;
    procedure ClipboardUpdateHandler(var Msg: TMessage); message WM_CLIPBOARDUPDATE;
...

implementation

{$R *.dfm}

uses
  Winapi.ShellAPI,
  Winapi.ShlObj,
  Vcl.Clipbrd;

{ TForm1 }

procedure TForm1.FormCreate(Sender: TObject);
begin
  if AddClipboardFormatListener(Handle) then; // ????
  //
  Memo1.Lines.Add('AddClipboardFormatListener: GetLastError = ' + GetLastError.ToString);
end;

procedure TForm1.FormDestroy(Sender: TObject);
begin
  if RemoveClipboardFormatListener(Handle) then; /// ???
  //
  Memo1.Lines.Add('RemoveClipboardFormatListener: GetLastError = ' + GetLastError.ToString);
end;

procedure TForm1.ClipboardUpdateHandler(var Msg: TMessage);
var
  LClipboardData : THandle;
  LGLobalLock    : Pointer;
  LFilename      : PWideChar;
  LFilenameBuffer: array [0 .. MAX_PATH] of WideChar;
  //
  cfShellIDList: UINT;
begin
  if not IsClipboardFormatAvailable(CF_HDROP) then
    exit;
  //
  cfShellIDList := RegisterClipboardFormat(CFSTR_SHELLIDLIST);
  //
  if OpenClipboard(Handle) then  // opening the Clipboard...
    begin
      try
        LClipboardData := GetClipboardData(CF_HDROP); // some data?
        //
        if (LClipboardData > 0) then
          try
            LGLobalLock := GlobalLock(LClipboardData); // lock it...
            try
              if (LGLobalLock <> nil) then
                begin
                  DragQueryFileW(HDROP(LGLobalLock), 0, LFilenameBuffer, MAX_PATH); // catch the file/folder name
                  //
                  Memo1.Lines.Add('File name: ' + LFilenameBuffer);
                end
              else
                Memo1.Lines.Add('LGLobalLock = nil');
            finally
              GlobalUnlock(LClipboardData); // unlock it...
            end;
          except
            on E: Exception do
              Memo1.Lines.Add('Exception: ' + E.Message);
          end
        else
          Memo1.Lines.Add('----- ClipboardData = 0 -----');
      finally
        CloseClipboard;
      end;
    end
  else
    Memo1.Lines.Add('OpenClipboard = false');
end;

 

Project1_mMQ8tZF0Zc.gif

Edited by programmerdelphi2k
  • Like 1

Share this post


Link to post
2 hours ago, programmerdelphi2k said:

@gioma, I think you should avoid using Clipboard, even if it works in your project, for the simple fact that it is a neutral layer and used by any other application in your system. In this way, nothing guarantees that the information obtained will be the desired information. You know it!

 

Now, as you are already communicating with the second (or any other) application on the network, then, I think the most sensible thing would be to approach this direction, that is, send the desired information through TCP/IP, UDP or similar communication ... etc...

 

In this way, you can guarantee that your transmissions will be directed exclusively to your interested applications, that is, to everyone who makes use of this communication, it can be one, two, or as many as you wish.

 

You could easily do this code without much impact on your current project, if you want, you could even create a small TCP/IP server using INDY or whatever to listen on a certain port, and thus receive all messages (information) desired. In this way, you can ensure that you receive the information sent by another application.

 

Delphi can easily generate a DataSnap Server (ie the code) for you to add to your applications. Delphi has Tethering components, which you can easily add to a form, so almost no code is needed. Communication is carried out by TCP/IP and UDP protocols, in this way, you just need to place a Tethering Server component in the applications that will receive the information, and another Tethering Client in the application that will send the information, the rest is just using one or two events to receive the information. Very easy and without much complexity.

 

here a great sample by Malcolm Groves : http://www.malcolmgroves.com/blog/?p=1891

Hi, thanks for the reply.
I already do, mine is a remote control software.
Currently I can transfer a file from one PC to another via a file browser (which opens on the parent site).

But now I need to implement the "copy-paste" so I need to interact with the system clipboard.

 

2 hours ago, programmerdelphi2k said:

@gioma  based on @Remy Lebeau text, I have test this code and works... but not guaratee if it's good for all...ok?

 


// VCL tests
...
  private
    FClipboardListenerHandle: HWND;
    procedure ClipboardUpdateHandler(var Msg: TMessage); message WM_CLIPBOARDUPDATE;
...

implementation

{$R *.dfm}

uses
  Winapi.ShellAPI,
  Winapi.ShlObj,
  Vcl.Clipbrd;

{ TForm1 }

procedure TForm1.FormCreate(Sender: TObject);
begin
  if AddClipboardFormatListener(Handle) then; // ????
  //
  Memo1.Lines.Add('AddClipboardFormatListener: GetLastError = ' + GetLastError.ToString);
end;

procedure TForm1.FormDestroy(Sender: TObject);
begin
  if RemoveClipboardFormatListener(Handle) then; /// ???
  //
  Memo1.Lines.Add('RemoveClipboardFormatListener: GetLastError = ' + GetLastError.ToString);
end;

procedure TForm1.ClipboardUpdateHandler(var Msg: TMessage);
var
  LClipboardData : THandle;
  LGLobalLock    : Pointer;
  LFilename      : PWideChar;
  LFilenameBuffer: array [0 .. MAX_PATH] of WideChar;
  //
  cfShellIDList: UINT;
begin
  if not IsClipboardFormatAvailable(CF_HDROP) then
    exit;
  //
  cfShellIDList := RegisterClipboardFormat(CFSTR_SHELLIDLIST);
  //
  if OpenClipboard(Handle) then  // opening the Clipboard...
    begin
      try
        LClipboardData := GetClipboardData(CF_HDROP); // some data?
        //
        if (LClipboardData > 0) then
          try
            LGLobalLock := GlobalLock(LClipboardData); // lock it...
            try
              if (LGLobalLock <> nil) then
                begin
                  DragQueryFileW(HDROP(LGLobalLock), 0, LFilenameBuffer, MAX_PATH); // catch the file/folder name
                  //
                  Memo1.Lines.Add('File name: ' + LFilenameBuffer);
                end
              else
                Memo1.Lines.Add('LGLobalLock = nil');
            finally
              GlobalUnlock(LClipboardData); // unlock it...
            end;
          except
            on E: Exception do
              Memo1.Lines.Add('Exception: ' + E.Message);
          end
        else
          Memo1.Lines.Add('----- ClipboardData = 0 -----');
      finally
        CloseClipboard;
      end;
    end
  else
    Memo1.Lines.Add('OpenClipboard = false');
end;

 

Project1_mMQ8tZF0Zc.gif

 

Thank you, it's an interesting example.

 

Now I'm trying to use the COleDataSource class, overriding the OnRenderFileData method. It's still theory at the moment, but I'm looking for a way to make it work.
Anyway, any ideas are always welcome!

 

PS
for example it would be interesting to know on which unit Delphi has the definition of these classes..

Edited by gioma

Share this post


Link to post
8 hours ago, gioma said:

My specific case is as follows:

I have two applications, A and B, connected by a socket connection.
Application A intercepts the modification of its clipboard and sends the event to application B.
Here we have two cases:
1 if it is a text, it copies it directly to the clipboard
2 if it is a file, it copies the list to the clipboard.
Up to point 1 there are, while I have difficulty understanding how to pass the list of files to PC B (or rather I can do this phase) and make sure that when I press "paste" (here is the real problem which I'm racking my brain over) the file list transfer begins and the system waits for the files to be transferred before completing the paste command.

All the more reason to try the route I suggested.  Application A could send its file list to Application B, then B can immediately put CFSTR_FILEDESCRIPTOR and CFSTR_FILECONTENTS items on its local clipboard for each file, having each CFSTR_FILECONTENTS hold an IStream to access its file data.  Then, when the user pastes the clipboard somewhere (or an app reads the clipboard directly), the individual CFSTR_FILECONTENTS streams can/will be read from, which can then transfer the actual file data from machine A to B as needed.  You could even go as far as having A transfer the file data to B in the background while waiting for a paste to happen, caching the data locally on B, and when CFSTR_FILECONTENTS is read from it can read from the local cache if available, otherwise initiate/wait-for the transfer to finish (even provide the data while it is begin transferred).  When the clipboard gets cleared, the IStream's will be released, and their destructors can cancel any transfers in progress and delete any local caches as needed.

Quote

I can't find half an example of this on the net

There are examples/tutorials online about working with CFSTR_FILEDESCRIPTOR/CFSTR_FILECONTENTS, for instance:

https://www.codeproject.com/Articles/15576/How-to-drag-a-virtual-file-from-your-app-into-Wind

https://www.codeproject.com/Articles/23139/Transferring-Virtual-Files-to-Windows-Explorer-in

Quote

I can't believe no one has ever done this before, maybe even in C, C++.

I'm sure they have, but it is very specialized work, so you are not likely to see it talked about publicly very much.

Edited by Remy Lebeau
  • Like 1

Share this post


Link to post
37 minutes ago, Remy Lebeau said:

Thanks for the reply.
I had seen the first example and in fact I would like to try to derive the COleDataSource class, I just need to understand in which Delphi unit it is present!

 

39 minutes ago, Remy Lebeau said:

I'm sure they have, but it is very specialized work, so you are not likely to see it talked about publicly very much.

Of course I imagined it, but a little something could also be shared ..

Share this post


Link to post

@gioma

dont forget: you need verify if the file really exists and then some way block another app to change/delete...

Quote

var
  LFileToWork: TFileStream;
begin
  // ...
  if FileExists('filename_received') then
    LFileToWork.Create('filename_received', fmOpenRead or fmShareExclusive { or another to avoid any other app changed } )
    // ... what to do?

 

  • Like 1

Share this post


Link to post
11 hours ago, programmerdelphi2k said:

@gioma

dont forget: you need verify if the file really exists and then some way block another app to change/delete...

 

the file does not exist on the filesystem at the time of the paste action because it has to be transferred from another PC.

Share this post


Link to post
17 hours ago, Remy Lebeau said:

All the more reason to try the route I suggested.  Application A could send its file list to Application B, then B can immediately put CFSTR_FILEDESCRIPTOR and CFSTR_FILECONTENTS items on its local clipboard for each file, having each CFSTR_FILECONTENTS hold an IStream to access its file data.  Then, when the user pastes the clipboard somewhere (or an app reads the clipboard directly), the individual CFSTR_FILECONTENTS streams can/will be read from, which can then transfer the actual file data from machine A to B as needed.  You could even go as far as having A transfer the file data to B in the background while waiting for a paste to happen, caching the data locally on B, and when CFSTR_FILECONTENTS is read from it can read from the local cache if available, otherwise initiate/wait-for the transfer to finish (even provide the data while it is begin transferred).  When the clipboard gets cleared, the IStream's will be released, and their destructors can cancel any transfers in progress and delete any local caches as needed.

it's not a walk in the park, if you were to do it in c++ it would be easier.
With Delphi I can't find the interface wrappers or the structures I should be using.

 

 

Ex In C++ 
 

// Create an OLE data source on the heap
   COleDataSource* pData = new COleDataSource;
   // Get the currently selected data
   HGLOBAL hGlob = GlobalAlloc(GMEM_FIXED, 64);
   strcpy_s((char*)hGlob, 64, "Current selection\r\n");
   // For the appropriate data formats...
   pData->CacheGlobalData( CF_TEXT, hGlob );
   // The Clipboard now owns the allocated memory
   // and will delete this data object
   // when new data is put on the Clipboard
   pData->SetClipboard();

 In Delphi not Exist class COleDataSource  ( Or I haven't found it yet ) 

 

COleDataSource has the virtual function OnRenderData that I might need.

 

 

Edited by gioma

Share this post


Link to post

@gioma

 

As I said just above, I would rule out using the Clipboard, and instead adopt a safer route like:

  1. PC1_aplicativoA sends the files to PC2 in a specific folder to receive the files (PC2_folderDownloadedFromPC1);
  2. PC2_aplicativoB monitors this folder from time to time, to check if a new file has been posted there (you could use a list to control the files already worked on, or even the date/time of the last jobs done, that is, if there are a new file, its creation date and time will be the latest in the folder);
  3. if there are new files in the folder (PC2_folderDownloadedFromPC1), then PC2_aplicativoB will be able to carry out the desired actions, and, clean the list or control date/time;
  4. as already mentioned, you can test whether the file has already completed its transfer from A to B, just by trying to open the file exclusively.


This way, you will always have greater control over what has been processed or not, and you will no longer depend on the data in the Clipboard's memory (which can be easily changed by other applications on the PC2);

  • Like 1

Share this post


Link to post
4 hours ago, gioma said:

 In Delphi not Exist class COleDataSource  ( Or I haven't found it yet ) 

I believe in Delphi you should use TDataObject.

Share this post


Link to post

@gioma

you could just check the data that "HDROP(LGlobalLock)" is pointed to, to get the objects this way ---> in message WM_CLIPBOARDUPDATE procedure

LGLobalLock := GlobalLock(LClipboardData);
try
  if (LGLobalLock <> nil) then
    begin
      LDQFindexFile         := 0;    // $FFFFFFFF = all;
      LDQFbufferSizeInChars := MAX_PATH;
      //
      LDQFresult := DragQueryFile(HDROP(LGLobalLock), LDQFindexFile, LFilenameBuffer, LDQFbufferSizeInChars);
      //
      if LDQFresult > 0 then
        begin
            // if is Directory, then use a "FOR" to process each object
          LItemIndex :=0; // "i" in "FOR" looping 
          LDQFresultProcessEachItem := DragQueryFile(HDROP(LGLobalLock), LItemIndex, LFilenameBuffer, 255); // reviewing objets...
          //
		  {$WARN SYMBOL_PLATFORM OFF} // remove this warning about platform specific!
          LIsDirectory := ((FileGetAttr(LFilenameBuffer) and faDirectory) = faDirectory);
		  {$WARN SYMBOL_PLATFORM ON}
		  //
          if LIsDirectory then
            Memo1.Lines.Add(string(LFilenameBuffer) + ' is a directory')
          else
            Memo1.Lines.Add(string(LFilenameBuffer) + ' is a file');
        end
      else
        Memo1.Lines.Add('DragQueryFile = 0 = failed!');
    end
  else
    Memo1.Lines.Add('LGLobalLock = nil');
finally
  GlobalUnlock(LClipboardData);
end;

 

 

Project1_C32Pngq9Qs.gif

 

Project1_GBwg0pk25A.gif

Edited by programmerdelphi2k
  • Like 1

Share this post


Link to post
22 hours ago, gioma said:

I had seen the first example and in fact I would like to try to derive the COleDataSource class, I just need to understand in which Delphi unit it is present!

 

6 hours ago, gioma said:

In Delphi not Exist class COleDataSource  ( Or I haven't found it yet ) 

 

COleDataSource has the virtual function OnRenderData that I might need.

COleDataSource is just a Microsoft C++ helper class that implements the IDataObject interface, which can be put on the clipboard using the OleSetClipboard() function.  In Delphi, you would have to implement the IDataObject interface manually (it is declared in the Winapi.Ole2 unit), or use a 3rd party implementation, such as from Melanders' Drag&Drop suite (probably via TDropEmptySource+TDataFormatAdapter).

Edited by Remy Lebeau
  • Thanks 1

Share this post


Link to post
15 hours ago, Remy Lebeau said:

 

COleDataSource is just a Microsoft C++ helper class that implements the IDataObject interface, which can be put on the clipboard using the OleSetClipboard() function.  In Delphi, you would have to implement the IDataObject interface manually (it is declared in the Winapi.Ole2 unit), or use a 3rd party implementation, such as from Melanders' Drag&Drop suite (probably via TDropEmptySource+TDataFormatAdapter).

Thank you, you gave me a great case study 😊

Share this post


Link to post

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now

×