Wykrycie momentu zamykania aplikacji zewnętrznej

0

Z uprzejmością dostarczam ciekawych zagadnień na ten specjalny czas ...
Proszę o rozważenie takiego oto zagadnienia.

Mój program uruchamia zewnętrzną aplikację (Total Commander) i rezydując w pamięci oczekuje na jej zamknięcie (wykonując w tle kilka zadań).
Total Commander nie pozwala na automatyczny zapis ustawień/pozycji okna/rozmiaru przy wyjściu.

Niestety, można to zrobić tylko ręcznie używając menu (lub odpowiedniego przycisku) lub wysyłając odpowiedni komunikat.

cm_ConfigSavePos=493;Conf: Save position
cm_ConfigSaveSettings=580;Save current paths etc.

Chciałbym wykryć moment, w którym zewnętrzna aplikacja (w tym przypadku TC) zamyka się - tak, aby przed jej zamknięciem wysłać komunikat zapisu danych.
Czy istnieje jakiś komunikat, który aplikacja wysyła podczas zamykania się i który można przechwycić? Czy to jest w ogóle możliwe?

-Pawel

0

Według mnie nie wchodzi w grę rozwiązanie znane z Menadżera zadań bo zbyt mocno obciąża system. Tak samo sprawdzanie czy jest uchwyt co jakiś czas jest dość mocno zasobożerne.
Może by tak użyć Total Commandera jako childa w twojej aplikacji i uruchamiać go w ten sposób (w oknie). Zamknięcie polegało by na wyłączeniu twojej aplikacji w której bez problemu zapamiętasz swoje parametry.
Ale to tylko dywagacje.

0

W jaki sposób odpalasz tą aplikację zewnętrzną?

0

CreateProcess()
Dzięki temu wiem, że aplikacja zakończyła już działanie - ale potrzebuję tuż przed zakończeniem

0

wiesz co, to jest prawie pewne że w winapi jest funkcja coś na kształt before close tylko nie wiem czy łatwo będzie się do niej dostać ze względu na ochronę pamięci, myślę że są na forum koledzy którzy mogą coś o tym napisać.
Ale mam troszkę inny pomysł, może niech twoja aplikacja śledzi położenie kursora, gdy będzie nad X w total commander, możesz założyć że użytkownik będzie chciał zamknąć aplikację (nie wiem czy ktoś zamyka ten program z menu, no chyba że przez Alt + F4). Wtedy pobierzesz położenie i wymiar okna TC i zapiszesz w jego pliku wincmd.ini. Problem to oczywiście rozpoznanie że kursor jest nad X.

0

Heh, dzięki za pomysły. Niestety, nie da się wyśledzić kiedy użytkownik ma zamiar zamknąć aplikację i w jaki sposób. Może to zrobić przecież krzyżykiem na belce okna, poprzez menu, poprzez polecenie, poprzez skrypty, poprzez zewnętrzne polecenia obsługiwane przez TC, albo poprzez skróty klawiszowe...
Liczę na to, że istnieje jakiś komunikat, który jest wysyłany do systemu, tuż przed zamknięciem (zresztą nie wiem czy to w ogóle jest możliwe - bo ja chcę się wbić w ten moment, pomiędzy "chęcią zamknięcia" a zamknięciem aplikacji).

Na teraz kombinuje tak, że:

  1. Przy uruchomieniu programu i wyświetleniu okna TC pobieram niezbędne dane
  2. Co określony czas sprawdzam, czy się one nie zmieniły.
  3. Przed zamknięciem zapisuje ostatnie dane.
    Teoretycznie to działa, ale wymaga ode mnie (od aplikacji rezydującej w tle) do ciągłego pobierania danych, co ustalony czas. Muszę porobić testy, na ile wymagająca jest to operacja (podejrzewam że niewiele, bo wystarczy pobrać rozmiar okna, położenie na ekranie i stan okna).

Ale, najprostsze jest po prostu wysłać info do TC, żeby sam zapisał dane przy wyjściu (dzięki temu ja nie muszę kombinować ze strukturą zapisu danych TC ani wykrywaniem zmian).
-Pawel

0

A może spróbuj monitorować i reagować na WM_CLOSE - https://docs.microsoft.com/en-us/windows/win32/learnwin32/closing-the-window

The user can close an application window by clicking the Close button, or by using a keyboard shortcut such as ALT+F4. Any of these actions causes the window to receive a WM_CLOSE message. The WM_CLOSE message gives you an opportunity to prompt the user before closing the window. If you really do want to close the window, call the DestroyWindow function. Otherwise, simply return zero from the WM_CLOSE message, and the operating system will ignore the message and not destroy the window.

0

@cerrato WM_CLOSE normalnie nie da się, bo komunikatu przecież nie dostanie inna aplikacja. Jedynie jakiś HOOK tylko, że hookami ostatnio się nie bawiłem ale czytałem, że pod nowymi systemami są obostrzenia (chyba od wprowadzenia UAC) trzeba by się zorientować w temacie. Na pewno będzie potrzebna osobna biblioteka DLL a nie ma pewności czy apka do działania nie będzie potrzebowała praw admina ale tu niech się wypowie ktoś kto obecnie jest w temacie.

1

Total Commander obsługuje pluginy, więc pewnie w tym temacie da się znacznie więcej zdziałać. Z tego co pamiętam, @olesio zajmował się pluginami, więc być może będzie w stanie coś więcej na ten temat napisać.

2

Hej. Ja robiłem jedynie prosty plugin który pomagał poprawiać mi @kAzek ale i tak kaszaniły się właśnie modyfikowane kontrolki przez wycieki GDI. Nie będę się dużo powtarzał, że u mnie teraz pasja do klepania całymi dniami kodów i udzielania się regularnie też na tym forum przekuła się w mocną wkrętkę w amatorsko uprawiany sport więc gdyby nie oznaczenie mnie i mail który trafił do spamu o dziwo to bym się nie pokapował że coś potrzeba ode mnie :)

A zatem ja bym sprawdził czy któryś z pluginów ładuje się w momencie startu programu, bo tutaj może być problem. Plugin ładuje się pierwszy raz chyba tylko wtedy kiedy go potrzeba. Czyli mój odtwarzacz modułów ładował się po wybraniu Ctrl+Q i "najechaniu" kursorem na odpowiedni typ pliku (.mod/.xm/*.s3m itd) który mój plugin dodatkowo sprawdzał po rozszerzeniu. Plugin systemu plików ładuje się pewnie tylko kiedy wybierzemy "\" z listy dysków, natomiast plugin do archiwów w momencie obsługi archiwum. Mogę się mylić, ale chyba tak było i nadal jest, bo inaczej ubijalibyśmy sobie wiadome okienko na przykład z przyciskami 1, 2 i 3 ;) Jednak jeżeli się mylę to wtedy "podczepiamy" się naszą wtyczką po jej załadowaniu pod główne okno programów i obsługę komunikatów, przechwytując odpowiedni komunikat WM_QUIT czy podobne robimy co trzeba.

Oczywiście jeżeli plugin FileSystemowy ładuje się tylko kiedy pokaże się drzewko gdzie mamy też nasz czy inne komputery w sieci lokalnej, drukarki itd to jest gdzieś w opcjach (nigdy nie umiem tego znaleźć więc modyfikuje jak potrzeba wincmd.ini bo raz to znalazłem i teraz nie umiem) możliwość podania ścieżki dla lewego i prawego panelu po tym jak się uruchomi Total Commander.

Czyli na przykład w sekcji "left" tego pliku ini podajemy path=\\Task manager czy \\nasza_wtyczka i wtyczka ładuje się robiąc "podczepienie" w pętle obsługi komunikatów okna głównego. Co uda się nawet bez praw admina, bo to nie jest iniekcja dllki w proces zewnętrzny a ładowanie dllki pluginu przez program powoduje że jej kod ma też dostęp odpowiednimi metodami do jej okna, czytania oraz pisania po pamięci tego procesu itd.

Inne nieco "brzydkie" i toporne ale skuteczne wydaje się mi rozwiązanie to zrobienie sobie programu działającego w tle lub usługi która będzie sprawdzała czy aktualne okno na wierzchy ma klasę TTOTAL_CMD i jeśli tak sprawdzi uchwyt okna i odczyta z tego odpowiednimi funkcjami PID procesu. Następnie mając to ustalone sprawdza ten PID czy proces istnieje, ewentualnie jeżeli mamy inne okno z tą klasą to ponownie odczyta ten PID i jeśli nagle po na przykład 500 - 1000 milisekund pętli nie ma procesu o tym PID który program ustalił to znaczy że coś lub sam user program zamknął i należy wykonać czynności na których zależy autorowi tego wątku. Tak bym kombinował. Sorki że trochę zawile i rozpisane, jak to ja :D I jeśli coś nie jasne to proszę dopytać.

EDIT: aaa :) No teraz doczytałem że czynność ma być tuż przed zamknięciem, no to można zwrócić przy komunikacie wartość inną niż 1 i przerwać pętlę obsługi oryginalnych komunikatów, dokonać wywołania tych poleceń wewnętrznych do czego są odpowiednie metody komunikatów dla samego Total Commandera opisane zdaje się w dokumentacji czy przykładach wtyczek i na nowo wysłać komunikat do zamknięcia okna nie przechwytując go już i myślę że program się zamknie. Ale czy i jak to zadziała tego nie wiem. Nie mam teraz jak to sprawdzić. Ewentualnie może próba dywagacji na forum z Autorem i innymi userami sprawi, że doda on sam odpowiedni feature lub ktoś coś Ci doradzi co do zapisywania. Bo może się nagle okaże że ktoś kombinował już pluginami tak jak piszę i nie musisz sam rzeźbić tego od zera o ile taka osoba podzieliła się swoim kodem lub skompilowaną wtyczką.

0

Dziękuję za odpowiedzi!
Miałem nadzieję, że sprawa będzie łatwa i prosta w implementacji, takie lubię :P
Podsumowując, po zgłębieniu tematu nie zamierzam się bawić w przechwytywanie komunikatów (globalny hook, wymóg napisania dll, injekcja w proces - za dużo kombinacji i potencjalnych problemów dla tak błahej sprawy). Z wtyczkami to całkiem niegłupi pomysł. Ale, jako że nie mam doświadczenia i nie ma pewności jak to zrobić, nie będę szedł tą drogą.

Spróbuję zaimplementować to w sposób, który przedstawiłem wcześniej. Również, zasięgnę języka u autora TC, może po prostu doda taką funkcjonalność i wszyscy będą zadowoleni :)

-Pawel

1

a może po prostu napisz prośbę do autora na forum https://ghisler.ch/board/viewforum.php?f=3&sid=be64892cc110efe3198b8e905d9f54b0
z tego co widzę temat był już kilka razy podejmowany i raczej widać jakieś niezadowolenie że program nie ma tej bardzo prostej funkcjonalności.
Jak tam coś nasmarujesz to daj znać, też wyrzucę swoje żale a mam 4 licencje więc troszkę kasy na to wydałem.

0

OT; widzę, że tylko @olesio obronił się przed wirusem AVATAR ;-)
A przepraszam, nie ogarniam "z rana"...
To jednak @cerrato zaiwanił awatara.
Jak zwykle coś @cerrato namieszał :P

1

Ja też się mogę podłączyć pod ten wątek na Forum TC. Ale nie udzielałem się tam dawno i nie wiem jak jest z uwzględnieniem sugestii. Ja kiedyś sugerowałem poprawkę Alt+A jako skrót do "wpisz do okna" w wyszukiwaniu Alt+F7. No chyba kiedyś takie combo - dość łatwe do szybkiego wciśnięcia - było, a później dano Alt+K to sobie sam zmieniam plik językowy, ale po update to zawsze chwila zbędnej roboty, gdyby nie zmieniano tego co kiedyś było przez narzucenie (chyba jakiegoś tłumacza plików *.lng).

Natomiast co do problemu i rozwiązania go nim się doczekamy reakcji autora programu. Wtyczka jest tutaj najprostszym zabiegiem jak się mi wydaje, ponieważ podpięcie się pod pętlę komunikatów nie powinno stanowić żadnego problemu. Przykładowe pluginy w Delphi dla filesystemu (a taki uważam tutaj za najlepszy do użycia) znajdziesz na podstronie z wtyczkami o ile kojarzę. Mnie tam w dawnych czasach tylko przykład pluginów listera i szybkiego podglągu interesował. Ale zrobienie na szybko file systemu też się Ci powinno udać. Ewentualnie jeśli nie podoba się konieczność takie zmiany wincmd.ini jak pokazałem by plugin załadował się Tobie np. w lewym panelu plików, możesz do niego dodać opcję do jakiej ścieżki fizycznej ma przechodzić i po wykonaniu czynności typu podpięcie pod pętlę komunikatów przechodzi do żądanej ścieżki. Ewentualnie jakiś mutex lub jeśli jest w pluginie file systemowym funkcja ładowania pierwszy raz i ładowania ponownego. Na pewno taka jest dla pluginów contentu i listera. Może na zachętę to do tego wywodu dołączę bardzo stare i wieki nie ruszane źródła moich pluginów. Były nawet publikowane oficjalnie zdaje się. Ale styl kodowania jest jaki jest jak na lamera przystało ;) I na pewno są jakieś wycieki GDI w pluginie odtwarzającym moduły muzyczne ;/

Plus rozwiązania tego to raczej udane podczepienie się pod pętlę komunikatów (bo nie będzie to iniekcja w proces sensu stricte), wada to raczej wymuszenie otwarcia pluginu na starcie tak jak to opisałem. Opcja druga z programem w tle ma minusy działania optymalnego sprawdzając pewne rzeczy co jakiś czas i trzeba to ręcznie odpalić lub zrobić taki nie wiem jak to nazwać "odpalacz", który uruchomi w tle ten kod sprawdzający a później Total Commander przy okazji zyskując jego pid. A plus niewielki wizualny, bo nie ładujemy tak lamersko pluginu. Chyba że jest opcja ładowania pluginów na starcie, to póki autor nie zrobi tego feature na nasze żądanie jest to w sumie najlepsza opcja. Sorki za rozpisanie, wieki się nie udzielałem to poleciałem hurtem :P Jak coś to podpytaj :)

0

Podsumowując. Autor TC nie wprowadzi tej funkcjonalności i zaleca zrobić to ręcznie poprzez wbudowane mechanizmy.
Napisał:
"...Why? When you shutdown your PC while Total Commander is minimized
or in an unwanted size, it would be saved like that. This has happened
to me in other programs, and it's very annoying..."

Zatem, rozwiążę ten problem w inny sposób, który wymaga nieco pracy, ale powinno działać.
Dzięki za podpowiedzi.
-Pawel

1

No a takie rozwiązanie, może trochę dookoła i prowizoryczne, ale powinno się sprawdzić: sprawdzać cyklicznie (np. co 3 sekundy) pozycję i rozmiar okna. Raczej nikt nie będzie przesuwać czy skalować okna specjalnie sekundę przed jego zamknięciem, więc można przyjąć, że jak ustalisz, że TC został zamknięty, to ostatni odczyt pozycji okna przed jego zamknięciem będzie tym, który powinieneś zapamiętać.

P.S. jakoś tak niezgrabnie mi się napisało, ale chory jestem, więc mam prawo ;)

0

Tak, właśnie taki jest plan (i działa, muszę doszlifować szczegóły i zrobić interfejs dla opcji).

0

@Pepe: a zależy Ci tylko na rozmiarach i położeniu okien, czy na czymś jeszcze? Przypuszczam, że TC trzyma takie dane albo w rejestrze, albo w zwykłych plikach INI, które przecież sam możesz w prosty sposób edytować nawet po zamknięciu komandera.

0

Jak już pisałem, sprawa jest załatwiona. Tak, TC opiera się na plikach INI. Problemem nie był zapis danych tylko moment zapisu.
Tworzę projekt UFM, którego sercem jest Total Commander (w mojej opinii najlepszy menedżer plików). Zatem, mam obcykany program :P

Oto, jak to działa teraz:

  1. Przy uruchomieniu programu, po pojawieniu się okna TC pobieram dane
  2. Co określony czas sprawdzam, czy dane się nie zmieniły (jak rozmiar okna, tudzież położenie na ekranie).
  3. Jeśli się zmieniły, uaktualniam dane.
  4. Na koniec (to znaczy jak użytkownik zamknie okno TC) zapisuje dane do pliku konfiguracyjnego TC.

Stosuje tutaj pewne uproszczenia (być może to się zmieni w przyszłości, choć wątpię). Zapisuję tylko rozmiar i położenie okna oraz stan okna (zmaksymalizowany czy normalny). Postanowiłem również dezaktywować auto-zapis dla wielu monitorów (co znacznie upraszcza sprawę, być może to się zmieni w przyszłości).
Oczywiście, użytkownik sam może sobie te dane zapisać, poprzez interfejs TC - ale dzięki tej opcji, będzie to działało automatycznie (póki autor TC nie zmieni zdania i nie zaimplementuje tej de facto nieskomplikowanej operacji).

Tutaj krótki filmik, który pokazuje, że to działa :P
https://www.dropbox.com/s/omyvv3vpkpkhatt/2020-04-02-16-39-40.mp4?dl=0

Ps: Filmik usunę za parę dni...
-Pawel

2

Możesz użyć funkcji Windowsa do wykrycia zmiany położenia/rozmiaru okna. Gdy zmieni się rozmiar/położenie to idzie event i można go złapać i zareagować - np. zapisać zmianę. Jeśli user zamknie program to dane już będą zapisane.

program Test;

{$mode objfpc}{$H+}
{$calling stdcall}

uses
  Classes, Windows;

type
  HINSTANCE = HANDLE;
  HWINEVENTHOOK = HANDLE;
  WINEVENTPROC = procedure(
    EventHook: HWINEVENTHOOK;
    Event: DWORD;
    HWnd: HWND;
    IDObject, IDChild: LONG;
    EventThread, EventTime: DWORD);
  TInstallProcedure = procedure;
  TUninstallProcedure = procedure;

const
  WINEVENT_OUTOFCONTEXT = $0000;
  EVENT_OBJECT_CREATE   = $8000;
  EVENT_OBJECT_DESTROY  = $8001;
  EVENT_OBJECT_LOCATIONCHANGE = $800B;
  OBJID_WINDOW          = $0000;
  INDEXID_CONTAINER     = $0000;


function SetWinEventHook(
  EventMin, EventMax: UINT;
  HMod: HMODULE;
  EventProc: WINEVENTPROC;
  IDProcess, IDThread: DWORD;
  Flags: UINT): HWINEVENTHOOK; external 'User32.dll';

function UnhookWinEvent(
  EventHook: HWINEVENTHOOK): LongBool; external 'User32.dll';

function {%H-}EnumWindowsCallback(
  {%H-}HWnd: HWND;
  {%H-}LParam: LPARAM): LongBool;
begin
  Result := True;
  WriteLn('.');
end;

procedure WinEventCallback(
  {%H-}EventHook: HWINEVENTHOOK;
  {%H-}Event: DWORD;
  {%H-}HWnd: HWND;
  {%H-}IDObject, {%H-}IDChild: LONG;
  {%H-}EventThread, {%H-}EventTime: DWORD);
var
  Len: LongInt;
  Title: array of WideChar;
  LStyle: LONG;
begin

  if Event = EVENT_OBJECT_LOCATIONCHANGE then
  begin
    Len := GetWindowTextLengthW(HWnd) + 1;
    if Len - 1 = 0 then
      exit;

    SetLength(Title, Len);
    GetWindowTextW(HWnd, @Title[0], Len);

    LStyle := GetWindowLong(HWnd, GWL_STYLE);

    if LStyle = 0 then
      exit;

    WriteLn(WideCharToString(@Title[0]));

  end

end;

var
  Message: TMsg;
  EventHook: HWINEVENTHOOK;
begin
  Message := Default(TMsg);
  EventHook := SetWinEventHook(EVENT_OBJECT_CREATE, EVENT_OBJECT_LOCATIONCHANGE, 0,
    @WinEventCallback, 0, 0, WINEVENT_OUTOFCONTEXT);

  while GetMessage(Message, 0, 0, 0) do
    DispatchMessage(Message);

  if EventHook <> 0 then
    UnhookWinEvent(EventHook);
end.        

1 użytkowników online, w tym zalogowanych: 0, gości: 1