Wyświetlenie okna modalnie po zakończeniu działania Application.Run

0

Taki durny pomysł – wyświetlić okno modalnie po tym, gdy główne okno aplikacji zostanie zamknięte. Niby prosta rzecz, ale zagwozdka polega na tym, że o ile to modalne okienko się pojawia, to program nie wstrzymuje działania – pojawia się i znika, program kończy działanie i się wyłącza, w ogóle nie czekając na reakcję na komunikat. Podany w konstruktorze modalnego formularza właściciel nie ma znaczenia – nieważne czy nil czy Application.

Do zobrazowania niech posłuży poniższy kawałek kodu:

// aplikacja startuje i działa aż do zamknięcia głównego okna
Application.Run();

// stworzenie i pokazanie okna modalnego
MyForm := TMyForm.Create();
MyForm.ShowModal();
MyForm.Free();

Da się coś z tym zrobić? Zmusić program, aby jednak poczekał na zamknięcie tego modalnego okna? Chodzi mi konkretnie o zatrzymanie działania programu oknem modalnym, po zakończeniu działania metody Application.Run – ukrywanie głównego formularza nie wchodzi w grę.

Nie żeby mi to było jakoś szczególnie potrzebne, ale z ciekawości spróbowałem coś takiego wykonać i nie znalazłem sposobu na pokonanie tego problemu. W każdym razie chciałbym wiedzieć czy jest jakieś rozwiązanie. ;)

2

Moim zdaniem nie da się ;) Po prostu taka jest konstrukcja całego VCL'u i już. Zobaczmy na Delphi http://docs.embarcadero.com/products/rad_studio/delphiAndcpp2009/HelpUpdate2/EN/html/delphivclwin32/Forms_TApplication_Run.html

Do not call Run. When creating a new project, The IDE automatically creates a main program block in the project file that calls the Run method. When the application is executed, the application's Run method is called.

Run contains the application's main message loop. The call to Run does not return until the application's message loop terminates.

Czyli po prostu zakończenie metody Run następuje po zakończeniu pętli komunikatów aplikacji. Więc nawet jakby się dało utworzyć jakieś okno to aplikacja nie obsłuży już żadnych komunikatów, co za tym idzie będzie zupełnie bez odpowiedzi na wszystkie komunikaty jakie system Windows wysyła do niej. Nie wiem czy tak samo ma Lazarus, ale podejrzewam, że z takich samych względów nie jest to możliwe.

1

Możesz też przechwycic zdarzenie zamykania aplikacji i tam wstawić to swoje okno modalne. Ja tak mam w swoich aplikacjach.

Inaczej nie zrobisz chyba że po application.run zainicjujesz ponownie application

0

Co prawda mogę zresetować Application, albo wręcz zwolnić ten obiekt i utworzyć ponownie, dodając inne okno jako główne, ale to by się nadawało tylko do okien pokazywanych bezpośrednio za pomocą ShowModal – bez żadnej logiki przygotowawczej. A tak to spaghetti wyjdzie, więc lepiej nie kombinować.

No nic, miałem niecny plan, ale chyba nici z tego. ;)

3

Zawsze można "oszukiwać" przechwycić komunikat WM_CLOSE formy głównej i zamiast ją zamykać po prostu ukrywać i pokazywać dialog. Dla lepszego efektu trzeba pozostawić aplikacje widoczną na pasku o to zadba zmodyfikowana procedura CreateParams i zamykać aplikację (w przykładzie zamykam wraz zamknięciem okna modalnego)

procedure TForm4.WMClose(var Message: TMessage);
begin
  Self.Hide; //ukryj okno główne
  Form5.ShowModal; //pokaz dialog
end;

procedure TForm5.WMClose(var Message: TMessage);
begin
   inherited;
   Application.MainForm.Close; //zamkniecie głownej formy spowoduje zamknięcie aplikacji
end;

procedure TForm5.CreateParams(var Params: TCreateParams);
begin
  inherited CreateParams(Params);
  Params.ExStyle:= Params.ExStyle or WS_EX_APPWINDOW;
  Params.WndParent:= GetDesktopWindow; //aby aplikacja pojawiła sie na pasku
end;
0

@kAzek: pisałem o tym, że sztuczka z ukrywaniem głównego okna nie wchodzi w grę. ;)

Głównie chodzi o to, aby dało się cokolwiek zrobić po Application.Run, gdy mamy zaprogramowany konkretny mechanizm w głównym pliku projektu, gdzie jeszcze masa różnych rzeczy dzieje się po zamknięciu głównego okna i wyjściu z ww. metody.

No ale skoro główna pętla komunikatów znajduje się w tej metodzie, to trzeba by ją jeszcze raz wywołać, tym razem ustawiając inny formularz za główny. I wszystko będzie ok, bo formularz zostanie stworzony i będzie widoczny – można ten fakt wykorzystać np. do miękkiego zresetowania programu, bez zamykania procesu. Choć póki co nie udało mi się zwolnić i utworzyć ponownie Application w taki sposób, aby nie wykrzaczyć programu.

Jeśli jednak nasz formularz modalny jest dialogiem, wywoływanym za pomocą metody pośredniej, realizującej kupkę logiki przygotowawczej (dynamicznie tworzenie okna, wypełnianie go danymi itd.), to nie da się tego łatwo połączyć z ustawianiem Application.MainForm oraz Application.Run. Trzeba by kombinować, przez co wyjdzie spaghetii, a takie rozwiązanie raczej nie jest warte uwagi – chyba że z absolutnej konieczności. :D

2

No dobra panowie, zdołałem okiełznać ten mechanizm. Spokojnie można zwolnić i utworzyć ponownie Application, ustawić co trzeba i wywołać Run jeszcze raz, tym razem dla innego formularza. Szkielet:

// podstawowe uruchomienie głównego okna
RequireDerivedFormResource := True;

Application.Initialize();
Application.CreateForm(TMainForm, MainForm);
Application.Run();

// tutaj dodatkowy kod i pokazanie innego okna jako główne
if SomethingHappened then
begin
  Application.Free();
  Application := TApplication.Create(nil);

  Application.Initialize();
  Application.CreateForm(TOtherForm, OtherForm);
  Application.Run();
end;

Wszystko działa prawidłowo. Jedyny minus jest taki, że odpalając program w IDE pod debuggerem, drugie wywołanie Run ładuje wyjątkiem typu SIGSEGV i chwilowo nie wiem co jest jego przyczyną:

sigsegv.png

To jednak nie zmienia faktu, że przepływ sterowania po uruchomieniu poza debuggerem jest prawidłowy i nowe okno faktycznie pojawia się na ekranie jako modalne, więc problem nie taki duży – da się z tym żyć. W każdym razie gdzieś w metodzie Run się coś krzaczy, więc pasuje to sprawdzić. Chyba że to problem mojego PoC-a.


Dlaczego o tym sposobie myślę? Z kilku powodów. Po pierwsze, w ten sposób można całkowicie uniezależnić formularze od logiki biznesowej – brak konieczności ukrywania głównego okna i wołania z jego poziomu metod z obiektów niezależnych od UI. Po drugie, skoro już dane są odseparowane od widoku, można główne okno utworzyć ponownie, bez konieczności ponownego ładowania danych z dysku – czyli bezpiecznie wstrzymać zamykanie programu, nawet po zamknięciu głównego okna.

Jeszcze sobie podłubię trochę w tym, może coś ciekawego mi wyjdzie. ;)

2

To całkiem ciekawe, ponieważ poczytałem trochę i niestety piszą, że lepiej nie bawić się w takie rzeczy. Jednak u mnie w C++ Builderze (mam leciwą wersję 2009) taki dok:

 Application->Initialize();
 Application->MainFormOnTaskBar = true;
 Application->CreateForm(__classid(TForm1), &Form1);
 Application->Run();

 delete Application;
 Application = new TApplication(NULL);
 Application->Initialize();
 Application->MainFormOnTaskBar = true;
 Application->CreateForm(__classid(TForm2), &Form2);
 Application->Run();

O dziwo działa bez problemów i żadnych wyjątków nawet w IDE. Zatem wynika z tego, że Lazarus zachowuje się inaczej niż Delphi. Jednak dalej nie umiem pomóc.

Co do uniezależnienia GUI od logiki to sam w 100% nie mam pomysłu jak to zrobić. Ale to też wynika z tego, że mam nieco inną architekturę całej aplikacji. Nie dość, że cała apka jest pisana jako MDI, to apka główna tylko tworzy obiekty wywoływane z wielu różnych dll'ek. A okna głównego nie tworzę nigdy ponownie, ponieważ jest ono tylko kontenerem na pozostałe właściwe okna.

2

Po pierwsze, dowiedziałem się, że zwalnianie Application i tworzenie go ponownie nie jest bezpieczne – tak twierdzą spece od Lazarusa. Ja natomiast uważam, że nie powinno to być problemem, czego Twój przykład @Mr.YaHooo poniekąd dowodzi. A wyjątki lecą najprawdopodobniej przez obecnie nieznany mi bug – zajmę się tym kiedyś indziej, bo aby go namierzyć, trzeba debugować LCL, a teraz mi się nie chce. ;)

Po drugie, zwalnianie obiektu Application nie jest konieczne, by wykorzystać jego główną kolejkę komunikatów ponownie (i wszelkie inne dobrodziejstwa). Można tego obiektu używać wielokrotnie, co już niebezpieczne nie będzie (i nie wali wyjątkami). Jednak aby dało się to zrobić, należy po metodzie Run zwolnić MainForm i wywołać jeszcze raz Initialize, aby wewnętrznie przechowywane referencje formularzy zostały wyczyszczone i by cały mechanizm powiązany z formularzami został zresetowany. A po tym już utworzyć kolejny formularz za pomocą CreateForm i znów wywołać Run – nowy formularz będzie tym głównym, działającym w ramach głównej kolejki komunikatów.

Wykonałem przykładową aplikację realizującą powyższe – prosty PoC umożliwiający zalogowanie się, utworzenie odpowiedniego formularza w zależności od uprawnień (admin lub user) i wyświetlenie go. Po zamknięciu głównego okna (admina lub usera), okno logowania pojawia się ponownie – i tak w kółko, aż do zamknięcia okna logowania krzyżykiem.

Kod głównego modułu wygląda bardzo prosto:

begin
  RequireDerivedFormResource := True;
  Application.Scaled := True;

  while UserID <> -1 do
  begin
    Application.Initialize();
    Application.CreateForm(TLoginForm, LoginForm);
    Application.Run();

    if UserID <> -1 then
    begin
      Application.MainForm.Free();
      Application.Initialize();

      case UserID of
        0: Application.CreateForm(TAdminForm, AdminForm);
        1: Application.CreateForm(TUserForm, UserForm);
      end;

      Application.Run();
      Application.MainForm.Free();
    end;
  end;
end.

I działa super. W ten sposób można bezpiecznie manipulować formularzami, utrzymać logikę i dane odseparowane od okien i móc wybierać okna do utworzenia – wszystko w ramach wbudowanego mechanizmu zarządzającego całym tym bajzlem. Źródła w załączniku.

1
furious programming napisał(a):

Po pierwsze, dowiedziałem się, że zwalnianie Application i tworzenie go ponownie nie jest bezpieczne – tak twierdzą spece od Lazarusa. Ja natomiast uważam, że nie powinno to być problemem, czego Twój przykład @Mr.YaHooo poniekąd dowodzi. A wyjątki lecą najprawdopodobniej przez obecnie nieznany mi bug – zajmę się tym kiedyś indziej, bo aby go namierzyć, trzeba debugować LCL, a teraz mi się nie chce. ;)

Nie tylko w Lazarusie. Z tego co wiem, to generalnie nie powinno się tworzyć oraz zwalniać obiektów systemowych. W prostych przypadkach może zadziałać, w przypadku bardziej skomplikowanych systemów nie wiadomo do końca co się wydarzy i mogą być różne dziwne błędy.

furious programming napisał(a):

Po drugie, zwalnianie obiektu Application nie jest konieczne, by wykorzystać jego główną kolejkę komunikatów ponownie (i wszelkie inne dobrodziejstwa). Można tego obiektu używać wielokrotnie, co już niebezpieczne nie będzie (i nie wali wyjątkami). Jednak aby dało się to zrobić, należy po metodzie Run zwolnić MainForm i wywołać jeszcze raz Initialize, aby wewnętrznie przechowywane referencje formularzy zostały wyczyszczone i by cały mechanizm powiązany z formularzami został zresetowany. A po tym już utworzyć kolejny formularz za pomocą CreateForm i znów wywołać Run – nowy formularz będzie tym głównym, działającym w ramach głównej kolejki komunikatów.

Zatem skoro wystarczy taki zabieg, to rzeczywiście nie ma co kombinować za mocno. Tylko iść tym tropem.

Wykonałem przykładową aplikację realizującą powyższe – prosty PoC umożliwiający zalogowanie się, utworzenie odpowiedniego formularza w zależności od uprawnień (admin lub user) i wyświetlenie go. Po zamknięciu głównego okna (admina lub usera), okno logowania pojawia się ponownie – i tak w kółko, aż do zamknięcia okna logowania krzyżykiem.
I działa super. W ten sposób można bezpiecznie manipulować formularzami, utrzymać logikę i dane odseparowane od okien i móc wybierać okna do utworzenia – wszystko w ramach wbudowanego mechanizmu zarządzającego całym tym bajzlem. Źródła w załączniku.

Wygląda na to, że wszystko gra. Jednak nie mam pod ręką Lazarusa, więc nie sprawdzę tego teraz.

1
Mr.YaHooo napisał(a):

Z tego co wiem, to generalnie nie powinno się tworzyć oraz zwalniać obiektów systemowych. W prostych przypadkach może zadziałać, w przypadku bardziej skomplikowanych systemów nie wiadomo do końca co się wydarzy i mogą być różne dziwne błędy.

Według mnie, jeśli dany zabieg jest poprawny i przewidywalny, biorąc pod uwagę strukturę danego kodu i jego funkcjonalność, to mało istotne jest to, czy ktoś uważa to za sensowne czy nie.

W tym przypadku problem polega na tym, że zwolnienie i ponowne utworzenie klasy TApplication nie jest poprawne i przewidywalne, bo generuje wyjątki, więc nie należy tego robić. Nawet pomimo tego, że poza debuggerem przepływ sterowania jest jak najbardziej prawidłowy.

Problemem też jest to, że inne (choćby własne) obiekty mogą przechowywać referencję pierwotnej instancji lub referencje/wskaźniki na którekolwiek z jej elementów, więc po jej zwolnieniu przestaną być prawidłowe, co znów spowoduje SIGSEGV-y i randomowe wykrzaczanie.

Zatem skoro wystarczy taki zabieg, to rzeczywiście nie ma co kombinować za mocno. Tylko iść tym tropem.

Wystarczy, bo działa to prawidłowo i przewidywalnie, nie generuje wyjątków z tyłka i nie ma prawa wykrzaczyć innych obiektów. Jest jedno ale – do tej pory nikt mi nie napisał, że jest to w 100% bezpieczne, choć z moich dochodzeń wygląda, że tak.

Czuję się jakbym zdziwiał, a potrzebuję jedynie głównej kolejki komunikatów w więcej niż jednym miejscu.

0
furious programming napisał(a):

Według mnie, jeśli dany zabieg jest poprawny i przewidywalny, biorąc pod uwagę strukturę danego kodu i jego funkcjonalność, to mało istotne jest to, czy ktoś uważa to za sensowne czy nie.

Jaką masz pewność, że ponowne utworzenie jakiegoś obiektu jest poprawne bez wnikliwej analizy całego VCL'a? Na podstawie paru uruchomień prostego demo niestety nie mamy takiej pewności. I dlatego osobiście w kodzie produkcyjnym takich rzeczy nie daję.

furious programming napisał(a):

Wystarczy, bo działa to prawidłowo i przewidywalnie, nie generuje wyjątków z tyłka i nie ma prawa wykrzaczyć innych obiektów. Jest jedno ale – do tej pory nikt mi nie napisał, że jest to w 100% bezpieczne, choć z moich dochodzeń wygląda, że tak.

Dokładnie, jednak też w 100% nie da się tego przewidzieć czy gdzieś bokiem nie wylezie jakiś wyjątek. Żeby być w 100% pewnym trzeba by zanalizować źródła VCL i już.

0
Mr.YaHooo napisał(a):

Jaką masz pewność, że ponowne utworzenie jakiegoś obiektu jest poprawne bez wnikliwej analizy całego VCL'a?

Nie chodziło mi w tym momencie o TApplication, ale odpowiadając na powyższe – jeśli się nie upewnię, że coś będzie zawsze działać prawidłowo i przewidywalnie (oczywiście analizując źródła, bo jak inaczej?), to nie używam.

Dokładnie, jednak też w 100% nie da się tego przewidzieć czy gdzieś bokiem nie wylezie jakiś wyjątek.

Dlatego analiza to podstawa. Choć sam poszedłem na skróty i po prostu zapytałem deweloperów. :P

1

Ja bym zrobił tak:

var
   Msg: TMsg;
begin
  Application.Initialize;
  Application.MainFormOnTaskbar := True;
  Application.CreateForm(TForm1, Form1);
  Application.Run;
  Form1.Free;

  Application.CreateForm(TForm2, Form2);
  Form2.Show;
  SetWindowPos(Form2.Handle, HWND_TOP, (Screen.Width - Form2.Width) div 2, (Screen.Height - Form2.Height) div 2, Form2.Width, Form2.Height, SWP_SHOWWINDOW); //wycentrowane

  while GetMessage(Msg, 0, 0, 0) do 
  begin
    TranslateMessage(msg);
    DispatchMessage(msg);
  end;
  Form2.Free;
end.
1

@marogo: interesuje mnie rozwiązanie cross-platformowe, dlatego WinAPI nie biorę pod uwagę. ;)

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