Es una pregunta que he visto repetida y que yo mismo me hecho e implementado. Se trata de pasar parámetros a una aplicación que ya se está ejecutando.
Ejecutar una Aplicación con sus parámetros y su manejo, es de sobra conocido. El problema aparece cuando queremos pasar otros distintos cuando la aplicación ya se está ejecutando. Quizás una forma mui apetecible para el usuario sea volver a ejecutar la aplicación con sus nuevos parámetros y que esta segunda instancia se encargue de enviarlos a la primera para autodestruirse después. Eso es lo que voy a plantear.
En alguna ocasión he comentado el asunto de los archivos de memoria compartida para comunicar aplicaciones. También lo podemos hacer vía sockets pero en esta ocasión voy a usar WM_COPYDATA de la que también se ha hablado. El planteamiento es simple, se envía la nueva linea de comandos y la aplicación la recibe, correcto, pero ahora hay que recibir y leer esa linea e interpretarla. Se me ha ocurrido usar justamente el mismo sistema que usa delphi con ParamCount y ParamStr y para ello he preparado una Unit que las sobrecarga y hace transparente al desarrollador todo este asunto. Sin embargo, siempre podremos recuperar los parámetros iniciales si usamos System.ParamCount y System.ParamStr. Esta técnica nos va a abstraer de como se obtienen los parámetros y simplemente usaremos para su manejo el mismo código que si fuesen los parámetros pasados en el arranque de nuestra aplicación (sin especificar el namespace System o usando Param).
Para terminar la introducción tengo que añadir que no usaremos unicode para el manejo de WM_COPYDATA, de esta forma el sistema será compatible con las versiones de delphi que no lo usan. Esto quiere decir que el mensaje WM_COPYDATA va a manejar cadenas estilo C AnsiChar. Esto también será transparente para el desarrollador, que podrá después usar el tipo de cadena que le plazca.
Esta es la Unit con el código necesario para cambiar y leer la línea de comandos de la Aplicación:
{******************************************************************************* Unidad para pasar parámetros en caliente a una aplicación © escafandra 2020 La linea de parámetros se envía con WM_COPYDATA. La funcion SendParamsLine sirve de apoyo. La Aplicación debe disponer de un sistema para recibir el mensaje WM_COPYDATA si se desea puede ayudarse de GetParamsLine. Los nuevos parámetros sustituyen a los originales y se leen como siempre con ParamCount y ParamStr Si se desean recuperar los parámetros que se pasaron originalmente a la APP deben usarse System.ParamCount System.ParamStr *******************************************************************************} unit Param; interface uses Windows, Messages, ShellAPI; procedure SetCommandLine(S: String); function ParamCount: Integer; function ParamStr(n: integer): String; function SendParamsLine(hWSrc, hWDest: HWND; CmdLine: AnsiString): LRESULT; function GetParamsLine(var Msg: TMessage): String; implementation type AWCHAR = Array [0..0] of PWCHAR; PAWCHAR = ^AWCHAR; var CmdLine_: String; {$WARN SYMBOL_PLATFORM OFF} // Establece una nueva línea de comandos para la aplicación procedure SetCommandLine(S: String); begin CmdLine_:= S; end; // Lee la Linea de comandos de la Aplicación function GetCommandLine: String; begin Result:= CmdLine_; if CmdLine_ = '' then Result:= String(CmdLine); end; // Devuelve el numero de parametros de la App // Sobrecarga System.ParamCount function ParamCount: Integer; begin CommandLineToArgvW(PWCHAR(WideString(GetCommandLine)), Result); Result:= Result - 1; end; // Devuelve un parámetro dado un índice // Sobrecarga System.ParamStr // Si el indice es 0, devuelve el Path de la App: System.ParamStr(0) function ParamStr(n: integer): String; var Count: integer; Argv: PAWCHAR; begin Result:= ''; Argv:= PAWCHAR(CommandLineToArgvW(PWCHAR(WideString(GetCommandLine)), Count)); if n = 0 then Result:= System.ParamStr(0) else if n < Count then Result:= Argv[n]; end; {$WARN SYMBOL_PLATFORM ON} // Envía un Mensaje WM_COPYDATA con la nueva línea de parámetros function SendParamsLine(hWSrc, hWDest: HWND; CmdLine: AnsiString): LRESULT; var Data: TCOPYDATASTRUCT; begin Data.dwData:= hWSrc; Data.lpData:= PAnsiChar(CmdLine); //PCHAR(windows.GetCommandLine); Data.cbData:= lstrlenA(PAnsiChar(Data.lpData))+1; Result:= SendMessage(hWDest, WM_COPYDATA, 0, Integer(@Data)); end; // Lee un Mensaje WM_COPYDATA con la nueva línea de parámetros function GetParamsLine(var Msg: TMessage): String; begin if Msg.msg = WM_COPYDATA then begin Result:= PAnsiChar(PCopyDataStruct(Msg.lParam).lpData); Msg.Result:= 1; end; end; end.
Como veis, uso una variable local CmdLine_ que se encarga de guardar la nueva linea de parámetros. ParamCount y ParamStr están sobrecargados por el namespace y usan la API CommandLineToArgvW para convertir la línea de comandos en una matriz o array de comandos individuales, que luego manejaremos con un índice.
Lo siguiente es cambiar el comportamiento de inicio nuestra aplicación, para ello tenemos que escribir código en el archivo de proyecto, cosa que ya se ha visto en el foro otras veces.
Un proyecto simple tipo puede ser esto:
program PasaParam; uses Forms, Unit1 in 'Unit1.pas' {Form1}; {$R *.res} begin Application.Initialize; Application.CreateForm(TForm1, Form1); Application.Run; end.
Sobre él vamos a introducir unos cambios y quedará como esto:
program PasaParam; uses Forms, Windows, Param, Unit1 in 'Unit1.pas' {Form1}; {$R *.res} var W : Integer; begin // Buscamos la instancia original Application.Title:= ''; W:= Windows.FindWindow('TApplication', 'Mi_Aplicacion'); Application.Title:= 'Mi_Aplicacion'; // Si existe le enviamos parametros, la colocamos en primer plano y nos vamos if(W<>0) then begin SendParamsLine(Application.Handle, W, windows.GetCommandLine); ShowWindow(W, SW_RESTORE); SetForegroundWindow(W); Application.Terminate; end else begin Application.Initialize; Application.CreateForm(TForm1, Form1); Application.Run; end; end.
Este proyecto trata de localizar un Handle perteneciente a otra instancia de TApplication y que tenga el nombre de nuestra aplicación. Es importante no dar a todas nuestras aplicaciones el mismo nombre y buscar para este caso un nombre inequívoco o encontrará la primera ventana que cumpla el requisito y puede no ser la que nos interesa.
Si encuentra otra instancia, enviará la línea de parámetros con SendParamsLine que usa WM_COPYDATA. Luego traerá a primer plano esa primera instancia de a APP y terminará su ejecución. Si no hemos encontrado otra instancia, nuestra App arranca normalmente y hace su tarea.
Queda un punto por cubrir, es la recepción de los parámetros que se manden. Para ello tenemos que husmear en los mensajes que reciba TApplication. Delphi lo tiene previsto con el método TApplication.HookMainWindow:
procedure TForm1.FormCreate(Sender: TObject); begin Application.HookMainWindow(HookAppMessages); end; function TForm1.HookAppMessages(var Msg: TMessage): Boolean; begin Result:= false; if Msg.msg = WM_COPYDATA then begin //........... // Código ara tratar el mensaje..... Result:= true; end; end;
Pues esto es todo lo necesario, ahora os muestro un ejemplo de una pequeña aplicación que recibe parámetros y simplemente los escribe en un Memo:
unit Unit1; interface uses Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms, StdCtrls, Param, Dialogs; type TForm1 = class(TForm) Memo1: TMemo; procedure FormCreate(Sender: TObject); private function HookAppMessages(var Msg: TMessage): Boolean; public procedure LoadParams; end; var Form1: TForm1; implementation {$R *.dfm} procedure TForm1.FormCreate(Sender: TObject); begin Application.HookMainWindow(HookAppMessages); LoadParams; end; function TForm1.HookAppMessages(var Msg: TMessage): Boolean; begin Result:= false; if Msg.msg = WM_COPYDATA then begin SetCommandLine(GetParamsLine(Msg)); Result:= true; LoadParams; end; end; procedure TForm1.LoadParams; var i: integer; begin Memo1.Lines.Add('Params:'); for i:= 0 to ParamCount do Memo1.Lines.Add(Format('Param(%d): %s',[i, ParamStr(i)])); Memo1.Lines.Add(''); end; end.
El código está probado en delphi7 y Berlin. La comunicación entre la APP compilada con delphi7 l la compilada con Berlin es posible gracias al uso de AsnsiString el el manejo del mensaje WM_COPYDATA. como ya os comentaba.
Bueno, hasta aquí hemos llegado y espero que sea de utilidad. Os dejo los enlaces de descarga del código y ejemplos.
Saludos.