PDA

View Full Version : [c#/c++] Eseguire un'applicazione esterna dentro una form


mcaisco
15-12-2008, 10:57
Salve,

una premessa: non sto chiedendo come si esegue un programma esterno. Questo lo so già fare. Ho già guardato un sacco di post qui sul forum e tutti putroppo si riferiscono alla semplice esecuzione di programmi esterni.

Dunque io ho la necessità di eseguire un'applicazione esterna all'interno di una mia form, per la precisione dentro un suo pannello. In altre parole vorrei emulare una sorta di applicazione MDI in cui però le finestre child della principale (la mia applicazione) non sono form da me create, ma applicazioni esterne eterogenee, sul cui codice non ho alcun controllo quindi! Ho guardato un sacco di forum sul web e la cosa è fattibile, ma putroppo tutti gli esempi funzionanti eseguono applicazioni di windows come notepad o la calcolatrice. Queste applicazioni funzionano anche a me nella maniera voluta e questo fa ben sperare. Putroppo però non sempre le cose funzionano. Ad esempio con eseguibili come iexplore.exe (Internet Explorer) o write.exe (Wordpad) succede che l'applicazione esterna viene lanciata ma non viene "inglobata" nella mia form.

Tecnicamente quello che sul web indicano di fare è di usare alcune funzioni win-api dichiarandole esterne dentro il proprio codice "managed" in C# o C++ (o VB.NET).

Ecco un esempio del mio codice FUNZIONANTE (notate che eseguo appunto notepad.exe)

// dichiarazione delle funzioni esterne win-api
public:
[DllImport("USER32.DLL")]
static int SetParent(IntPtr hWndChild, IntPtr hWndNewParent);
static bool MoveWindow(IntPtr hwnd, int x, int y, int cx, int cy, bool repaint);

// setting del processo da lanciare
System::Diagnostics::ProcessStartInfo^ psi = gcnew System::Diagnostics::ProcessStartInfo();
psi->WindowStyle = System::Diagnostics::ProcessWindowStyle::Normal;
psi->FileName = "notepad.exe";
psi->Arguments = "";
psi->UseShellExecute = false;
System::Diagnostics::Process^ proc = System::Diagnostics::Process::Start(psi);

// il thread della mia applicazione attende INDEFINITAMENTE fino a quando l'applicazione
// esterna è stata inizializzata correttamente
bool wait = proc->WaitForInputIdle();

// imposto l'handle della finestra della nuova applicazione lanciata come child di
// un pannello della mia form
IntPtr gpsHandle = proc->MainWindowHandle;
SetParent(gpsHandle, panel3->Handle);
MoveWindow(gpsHandle, 0, 0, panel3->Width, panel3->Height, true);
this->panel3->ResumeLayout();
this->panel3->PerformLayout();
this->ResumeLayout();
this->PerformLayout();

Il codice sopra funziona correttamente. Viene lanciato il notepad e la finestra viene inglobata nel pannello della mia form. Ma se si prova a cambiare eseguibile le cose non sempre funzionano. E' come se la funzione WaitForInputIdle() non riesca sempre ad attendere il tempo necessario affinchè la nuova finestra venga inglobata. Con notepad.exe funziona. Con iexplore.exe ad esempio no. Con le applicazioni che devo utilizzare non funziona e queste semplicemente vengono eseguite ma non inglobate. Nei casi in cui le cose funzionano il valore assegnato a gpsHandle è un valore numerico positivo (suppongo un puntatore interno). Nei casi in cui non funziona, gpsHandle è 0 (ma pensa un po'...). Ho indagato anche sul ritorno di WaitForInputIdle e questa procedura ritorna true se la nuova finestra è pronta, false altrimenti. Ma nel mio caso l'attesa dovrebbe essere indefinita e quindi dle tutto bloccante. Invece la procedura ritorna sempre true, anche se poi la finestra non viene inglobata nella mi form.

Ma ecco un trucco strano. Basta sostituire la chiamata alla WaitForInputIdle() con una stupida sleep e tutto funziona. Il problema però è che io non posso sapere a priori quanto sia necessario attendere per una generica applicazione esterna da eseguire. Ho fatto delle prove e con una sleep di 300ms per ora funzionano tutte le applicazioni che ho provato. Con 100ms solo alcune... insomma è un casino!

In definitiva sembra quasi che il processo con la nuova applicazione esterna venga lanciato. Poi però il chiamante (la mia form) non rimane bloccato sulla WaitForInputIdle per un lasso di tempo sufficiente affinché l'handle della finestra associata all'applicazione esterna sia stato correttamente inizializzato. Questo sembra essere erroneo però, perchè la WaitForInputIdle, in mancanza di parametri espliciti, attende il processo chiamato per un tempo indefinito. Invece nel mio caso ritorna sempre immediatamente con valore true, come se avesse rilevato che la finestra dell'applicazione esterna sia stata inizializzata correttamente... anche se poi l'handle rimane a 0! Ovviamente ho anche provato a chiamare WaitForInputIdle passando come parametro il tempo di attesa, ma anche con valori molto alti la funzione ritorna immediatamente, fregandosene altamente insomma!

Potete aiutarmi? Qualcuno sa perchè questa procedura non fa il suo lavoro in alcune circostanze? Perchè con la sleep() le cose funzionano?

Grazie

mcaisco
15-12-2008, 17:40
Nessuno sa aiutarmi? Nessuno ha mai provato ad inglobare una finestra di un programma esterno in una propria form?

gugoXX
15-12-2008, 23:49
Nessuno sa aiutarmi? Nessuno ha mai provato ad inglobare una finestra di un programma esterno in una propria form?

Io Internet Explorer l'avevo usato piu' volte in un mio applicativo, cosi' come anche i vari Office.
Ma in maniera ortodossa, usando gli oggetti COM e i controlli relativi.

Per il tuo problema prova a passare delle API, ovvero da OpenProcess. Ti basta sapere il PID e l'handle dovresti prenderlo.
Proseguo. Poi dato il pid passi ad EnumWindows(), La tua e' una delle finestre... qual e' la principale?
Proverei a passare ogni finestra della enumwindows a GetWindowThreadProcessId(), e quando trovo il pid che coincide con quello di lancio, allora dovresti essere davanti alla main.

mcaisco
16-12-2008, 08:22
Io Internet Explorer l'avevo usato piu' volte in un mio applicativo, cosi' come anche i vari Office.
Ma in maniera ortodossa, usando gli oggetti COM e i controlli relativi.


Sinceramente non conosco gli oggetti COM per controllare applicazioni esterne. Puoi farmi qualche esempio o indicarmi qualche risorsa sul web? Comunque ti chiedo: questi oggetti COM possono essere utilizzati anche per la gestione di applicazioni grafiche esterne generiche o solo applicazioni windows predefinite (come quelle di Office)?

Per il tuo problema prova a passare delle API, ovvero da OpenProcess. Ti basta sapere il PID e l'handle dovresti prenderlo.
Proseguo. Poi dato il pid passi ad EnumWindows(), La tua e' una delle finestre... qual e' la principale?
Proverei a passare ogni finestra della enumwindows a GetWindowThreadProcessId(), e quando trovo il pid che coincide con quello di lancio, allora dovresti essere davanti alla main.

Mmm questa cose della EnumWindows() l'ho già vista da qualche parte mi pare. Effettivamente non ho provato. Comunque il mio dubbio è che la form principale, senza una sleep() o questa fantomatica WaitForInputIdle() fra la chiamata alla OpenProcess() e l'accesso al PID del processo o all'handle della finestra, non sempre dà il tempo all'applicazione esterna di essere effettivamente inizializzata, perchè comunque la OpenProcess() non è una chiamata bloccante. Comunque proverò anche questa via.

Grazie mille

banryu79
16-12-2008, 09:15
Secondo me dovresti seguire i consigli di GugoXX.
Io posto solo per "toglierti il prurito" riguardo la faccenda della WaitForInputIdle che torna subito.

E' bastata una ricerca di 5min. con Google per trovare questo:
LINK (http://mail.python.org/pipermail/python-list/2007-June/447514.html)
estratto:

...
Well, this is weird. I searched for doc on the internet and that description
differs:
The process you're waiting for needs to have a message queue (it does in my
case), not the process that calls WaitForInputIdle.
Hans

---
---
Waits until the specified process is waiting for user input with no input
pending, or until the time-out interval has elapsed.

DWORD WINAPI WaitForInputIdle(
HANDLE hProcess,
DWORD dwMilliseconds
);
Parameters
hProcess
[in] A handle to the process. If this process is a console application or
does not have a message queue, WaitForInputIdle returns immediately.
dwMilliseconds
[in] The time-out interval, in milliseconds. If dwMilliseconds is
INFINITE, the function does not return until the process is idle.

MarcoGG
16-12-2008, 10:30
...
Basta sostituire la chiamata alla WaitForInputIdle() con una stupida sleep e tutto funziona. Il problema però è che io non posso sapere a priori quanto sia necessario attendere per una generica applicazione esterna da eseguire. Ho fatto delle prove e con una sleep di 300ms per ora funzionano tutte le applicazioni che ho provato. Con 100ms solo alcune... insomma è un casino!
...
Potete aiutarmi? Qualcuno sa perchè questa procedura non fa il suo lavoro in alcune circostanze? Perchè con la sleep() le cose funzionano?


Secondo me la risolvi con la Sleep, usata in modo un po' più "furbo", e chiamando in causa anche un controllo periodico sul MainWindowHandle.
Ho fatto una prova veloce ( VB 2008 ) e sono riuscito ad aprire nella mia Form ( che non è manco MDI, come invece consigliato da molti suggerimenti online ) tutti gli applicativi Office, notepad, wordpad, Internet Explorer, ecc...

In soldoni :

Public Class Form1

Private Declare Auto Function SetParent Lib "user32" (ByVal hWndChild As IntPtr, ByVal hWndNewParent As IntPtr) As IntPtr

Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button1.Click

Dim PSI As New ProcessStartInfo("iexplore.exe")
Dim P As Process = Process.Start(PSI)
Dim i As Integer = 0
For i = 0 To 1000
System.Threading.Thread.Sleep(10)
P.Refresh()
If P.MainWindowHandle <> IntPtr.Zero Then Exit For
Next i
SetParent(P.MainWindowHandle, Me.Handle)

End Sub

End Class

MarcoGG
16-12-2008, 10:56
... E tradotto in C# :

public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
}

[System.Runtime.InteropServices.DllImport("user32.dll")]
static extern IntPtr SetParent(IntPtr hWndChild, IntPtr hWndNewParent);

private void button1_Click(object sender, EventArgs e)
{
ProcessStartInfo PSI = new ProcessStartInfo("iexplore.exe");
Process P = Process.Start (PSI);
for(int i = 0;i<1000;i++)
{
System.Threading.Thread.Sleep(10);
P.Refresh();
if(P.MainWindowHandle != IntPtr.Zero)
{
break;
}
}
SetParent(P.MainWindowHandle, this.Handle);
}
}

banryu79
16-12-2008, 11:24
... snip ...


Volpone! :mano:

mcaisco
16-12-2008, 11:29
Secondo me dovresti seguire i consigli di GugoXX.
Io posto solo per "toglierti il prurito" riguardo la faccenda della WaitForInputIdle che torna subito.

E' bastata una ricerca di 5min. con Google per trovare questo:
LINK (http://mail.python.org/pipermail/python-list/2007-June/447514.html)

Mmm, avevo già letto questa pagina ed in effetti non è che chiarisca molto, anzi alla fine aggiunge dubbi sul vero funzionamento di questa procedura. Però ho riflettuto di nuovo e le cose in effetti mi sembrano strane.

Allora...
The process you're waiting for needs to have a message queue (it does in my
case), not the process that calls WaitForInputIdle.
Il mio caso è identico, nel senso che avvio applicazioni grafiche che quindi hanno un message queue (altrimenti ritorna un preciso errore su questo fatto!). Mi fa pensare invece quel "not". Leggendo così sembra che il significato sia questo: il processo su cui sto attendendo deve avere un message queue, e non quello che chiama la WaitForInputIdle(). Ma a me pare che io nel codice faccia proprio questo con:

ProcessStartInfo PSI = new ProcessStartInfo("myApp.exe");
Process P = Process.Start (PSI);
P->WaitForInputIdle()

No?!

Waits until the specified process is waiting for user input with no input
pending, or until the time-out interval has elapsed.
...
hProcess
[in] A handle to the process. If this process is a console application or
does not have a message queue, WaitForInputIdle returns immediately.
...

Questo mi conferma le cose, anche se qui parla della procedura omonima winapi (ma alla fine la procedura che chiamo, all'interno usa proprio quella). Quel "Waits" iniziale si riferisce alla form principale no?! Quindi teoricamente il processo "padre" attende fino a quando il processo "figlio" (che chiama la WaitForInputIdle()) non termina la sua attesa.

Perchè quindi se chiamo sta benedetta WaitForInputIdle() sul nuovo processo creato per l'applicazione esterna, il chiamante, cioè la form principale, non sta lì fermo e buono ad aspettare?

gugoXX
16-12-2008, 12:10
Ogni processo potrebbe avere piu' di una finestra.
Il problema e' che per qualche applicazione il processo principale (spesso l'unico) coincide con una finestra, che coincide anche con la finestra principale (Notepad).
Per altre applicazione il processo principale, anche se unico, non coincide con la finestra principale. Per I.E. ho idea che il processo principale si tratti di un Loader, che si occupa di istanziare e lanciare i thread delle finestre, una delle quali sara' la principale per l'applicazione, ma verra' istanziata, aperta e sara' pronta solo in un secondo tempo.

La WaitForInputIdle, cosi' come la MainWindowHandle del C# fanno parecchie cose sotto la coperta, richiamando parecchie API, in un modo simile a quello che ho provato ad esporre sopra.

Sta di fatto che quando richiarmi WaitForInputIdle il processo principale non ha ancora avuto tempo di creare le finestre (almeno la principale), e quindi non si tratta ancora di un processo con una finestra principale. Per questo motivo ricade ancora nei "processi senza finestra", e ritorna subito.
D'altronde il C# non puo' sapere che prima o poi quel processo avra' una finestra principale (non lo sa nemmeno quel processo stesso...) e quindi non stara' ad aspettare senza sapere se mai dovra' ritornare.

Se invece aspetti un po' di tempo, il processo avra' avuto modo di creare tutto il necessario, e C# si trovera' aggiornate variabili come MainWindowHandle (che appunto sotto la coperta fa parecchie cose).
E anche WaitForInputHandle funzionera' come atteso, dato che i 2 campi sono fra loro in relazione.

banryu79
16-12-2008, 13:10
... snip ...


Grande GugoXX, grazie della spiegazione!
Ragazzi, siete dei pozzi di scienza infusa :ave:

mcaisco
16-12-2008, 14:23
Grazie mille GugoXX della spiegazione. Quindi a questo punto non c'è modo di essere sicuri che un'applicazione abbia creato la finestra principale. Dunque sembra che l'unico approccio certo è l'utilizzo di una sleep() prima della lettura dell'handle MainWindowHandle, magari ottimizzato come ha suggerito MarcoGG.
Concordate?

Ah però in effetti c'era anche il metodo sempre di GugoXX che suggeriva di rintracciare il processo lanciato con il suo PID e la EnumWindows()... però se non vado errato, anche in questo caso dovrei comunque attendere un po' prima di provare a rintracciare il processo, in modo da dargli tempo di costruire la finestra principale. Giusto?

gugoXX
16-12-2008, 14:33
Grazie mille GugoXX della spiegazione. Quindi a questo punto non c'è modo di essere sicuri che un'applicazione abbia creato la finestra principale. Dunque sembra che l'unico approccio certo è l'utilizzo di una sleep() prima della lettura dell'handle MainWindowHandle, magari ottimizzato come ha suggerito MarcoGG.
Concordate?

Il polling proposto da MarcoGG mi sembra una buona soluzione per il tuo problema.

Prova ancora a verificare che per le applicazioni che vorresti usare non esista un componente COM gia' pronto.
Se stai usando C#, prova ad aggiungere un riferimento di tipo COM alla tua applicazione. Dovresti vedere un lungo, lunghissimo elenco di tutti componenti COM disponibili sulla tua macchina.
Se le applicazioni di tuo interesse espongono un controllo COM dovresti vederlo li', e dovresti poterlo aggiungere al tuo progetto. A quel punto ti ritroveresti con il controllo disponibile sulla toolbar, pronto per essere usato sulla tua Form come un qualsiasi altro controllo (quasi, un po' piu' rognoso).

Questo e' il modo migliore.
Certo, se non hanno pensato di fare un controllo COM non puoi percorrere questa strada.
Come dicevo appunto InternetExplorer e' un Controllo Com utilizzato poi da una finestra (se non sbaglio il controllo di InternetExplorer si chiama axBrowser qualcosa)
E cosi' anche i vari applicativi Office.
Ma anche altri, es: VLC.

In C#, per lo stesso motivo, quando si progettano applicazioni con parti GUI, e' bene separare la parte grafica dalla parte logica, mettendo la parte grafica dentro un controllo in un progetto di tipo WindowsFormControlLibrary, la parte logica in un altro progetto di tipo Library a se stante (addirittura di tipo Windows Workflow se si vuole).
In questo modo sara' possibile utilizzare la WindowsFormControlLibrary allo stesso modo che i vecchi COM, dentro nuove applicazioni.
Il progetto principale risulterebbe essere solo un collante, la glue logic che mette insieme i vari pezzi.
Pezzi che possono essere addirittura cambiati in seguito con nuove versioni (retrocompatibili).