Wyświetlanie formy z obszaru powiadomień (tTrayIcon)

Wyświetlanie formy z obszaru powiadomień (tTrayIcon)
Pepe
  • Rejestracja: dni
  • Ostatnio: dni
  • Postów: 505
0

Staram się napisać prostą aplikację, której zadaniem jest obsługa schowka windows. W dużym uproszczeniu, aplikacja przechwytuje komunikat WM_CLIPBOARDUPDATE i odpowiednio reaguje, dając użytkownikowi możliwość zapisania zawartości schowka w odpowiednim miejscu i w odpowiednim formacie.

Aplikacja może pracować w normalnym oknie (być cały czas widoczna dla użytkownika) lub dzięki użyciu komponentu TTrayIcon - być schowana w obszarze powiadomień Windows.

Mam problem z napisaniem elementu informującego użytkownika o zmianie zawartości schowka. Jeśli użytkownik wciśnie przycisk Print Screen (lub skopiuje jakiś tekst) na klawiaturze aplikacja przechwyci komunikat i wywoła odpowiednią procedurę - ta ma wyświetlić komunikat w prawym dolnym rogu ekranu o nowych danych schowka.

Zdecydowałem się na implementację własnego okienka informacji (nie korzystam z wbudowanej funkcjonalności TTrayIcon - Ballon Hint oraz co za tym idzie systemowych powiadomień). Po pierwsze, systemowe okienko jest jakie jest (ubogie), po drugie zależne jest od ikonki w trayu (zmiana ikonki powoduje wywołanie kolejnego komunikatu - a ja zmieniam indeks ikonki wyświetlając stan schowka) i po trzecie - każdy komunikat sygnalizowany jest dźwiękiem!.

Mam zatem oprócz głównej formy (MainFrm) drugą formę (TrayMsgFrm), która realizuje wyświetlenie komunikatu dla użytkownika.
Przechodzimy do najważniejszego. Okienko komunikatu ma "wślizgnąć się" na ekran z prawej strony i po kilku sekundach "wyjechać" z powrotem (poza obszar roboczy ekranu) i zamknąć się. Okienko komunikatu tworzone jest przy uruchomieniu programu - zatem tylko wyświetlam je i zamykam - nie niszczę (próbowałem z dynamicznym tworzeniem okna i niszczeniem go za każdym użyciem - ale napotkałem tutaj jeszcze większe problemy).

Wszystko działa dobrze, gdy główne okno jest widoczne na ekranie.
Kolejność jest następująca. Użytkownik ma otwarty program, wciska przycisk Print Screen. Do schowka Windows kopiowana jest zawartość ekranu - program przechwytuje ten obraz i wyświetla komunikat użytkownikowi - forma komunikatu jest wyświetlana (TrayMsgFrm.Show) - następuje wywołanie zdarzenia onShow (tutaj ustawiam formę oraz wczytuję dane do wyświetlenia) formy oraz onActivate (tutaj zaś "animuje" ruch okna). Wszystko działa OK.

No i w końcu problem. Jeśli program działa w trybie ukrytego okna w obszarze powiadomień (widoczna tylko ikonka w tray) dzieją się cuda.
Otóż:

  1. Użytkownik wciska przycisk Print Screen
  2. Aplikacja przechwytuje zmianę zawartości schowka i wywołuje okno komunikatu TrayMsgFrm
  3. Odpalane jest zdarzenie onShow formy (pobieram dane)
  4. Odpalane jest zdarzenie onActivate formy (wyświetlam okno i zamykam je po ustalonym czasie)

ALE - tak się dzieje tylko za pierwszym razem! Kolejne wciśnięcie przycisku Print Screen (czy skopiowanie tekstu Ctrl+C) wywołuje okienko komunikatu (OnShow) ale NIE WYWOŁUJE zdarzenia onActivate (kluczowego dla mnie). Dlaczego?

W załączniku znajdziecie przykładową implementację.
Proszę o pomoc. Co jest nie tak. Jak to zrobić poprawnie?

Ps: Pytanie zadałem również na forum https://en.delphipraxis.net/topic/11580-ttrayicon-hidden-main-window-and-second-form/?tab=comments#comment-91819
[mam nadzieję, że angielskojęzyczne i Polskie forum pomoże mi w tym (być może błahym) problemie)

-PawelTest.zip

abrakadaber
  • Rejestracja: dni
  • Ostatnio: dni
  • Postów: 6610
3

a aktywujesz to okno?

BTW dlaczego nie tworzysz okna za każdym razem (co to za problemy)? Dlaczego polegasz na OnShow/OnActivate zamiast po prostu zawołać jakąś metodę na formie kiedy ją pokazujesz?

Pepe
  • Rejestracja: dni
  • Ostatnio: dni
  • Postów: 505
0

Prościej zrobić to raz i tylko pokazywać (problemy z focusem, bo okno musi być niemodalne). Z moich testów wynika, że animacja nie działa w samym zdarzeniu onShow (po prostu okno się pokazuje, a nie "wjeżdża" ). Może robię coś źle (może gdyby forma była tworzona dynamicznie).

abrakadaber
  • Rejestracja: dni
  • Ostatnio: dni
  • Postów: 6610
2
  1. odpowiadaj w postach, nie jesteś tu od wczoraj a odpowiadasz w komentarzu...
  2. tray nie zawsze jest w prawym dolnym rogu - pasek możesz sobie zadokować do dowolnej krawędzi
  3. prawa krawędź monitora nie musi być prawą krawędzią pulpitu
  4. monitor z indeksem 0 nie musi być skrajnym lewym
  5. nie bardzo wiem z czym masz problem - "wjeżdżanie" formy od prawej, 5 sekund pozostaje i zjeżdża w dół
Kopiuj
//równie dobrze może to być po wystąpieniu jakiegoś eventu
procedure TForm2.Button1Click(Sender: TObject);
var
  form: TForm3;
begin
  form := TForm3.Create(Nil);
  form.Show;
  form.Left := 2560; //szerokość ekranu;
  form.Top := 1440 - form.Height; //wysokość ekranu
  form.SlideIn(2560, 1440);
end;

//TForm3 ma na sobie 3 timery, wszystkie Enabled := False, 1 i 3 Interval na 10 a 2 na 5000 
type
  TForm3 = class(TForm)
    Panel1: TPanel;
    Timer1: TTimer;
    Timer2: TTimer;
    Timer3: TTimer;
    procedure Timer1Timer(Sender: TObject);
    procedure Timer2Timer(Sender: TObject);
    procedure Timer3Timer(Sender: TObject);
  private
    FW: Integer;
    FH: Integer;
  public
    procedure SlideIn(W: Integer; H: Integer);
  end;

var
  Form3: TForm3;

implementation

{$R *.dfm}

{ TForm3 }

procedure TForm3.SlideIn(W: Integer; H: Integer);
begin
    FW := W;
    FH := H;
    Timer1.Enabled := True;
end;

procedure TForm3.Timer1Timer(Sender: TObject);
begin
  Left := Left - 10;
  if Left + Width < FW then
  begin
    Timer1.Enabled := False;
    Timer2.Enabled := True;
  end;
end;

procedure TForm3.Timer2Timer(Sender: TObject);
begin
  Timer2.Enabled := False;
  Timer3.Enabled := True;
end;

procedure TForm3.Timer3Timer(Sender: TObject);
begin
  Top := Top + 10;
  if Top > FH then
  begin
    Timer3.Enabled := False;
    Free;
  end;
end;

W załączniku projekt + exe

Nowy folder.7z

Pepe
  • Rejestracja: dni
  • Ostatnio: dni
  • Postów: 505
0
abrakadaber napisał(a):
  1. odpowiadaj w postach, nie jesteś tu od wczoraj a odpowiadasz w komentarzu...

Masz rację. Dziękuję za zwrócenie uwagi.

  1. tray nie zawsze jest w prawym dolnym rogu - pasek możesz sobie zadokować do dowolnej krawędzi
  2. prawa krawędź monitora nie musi być prawą krawędzią pulpitu
  3. monitor z indeksem 0 nie musi być skrajnym lewym

To prawda. Nie to jednak było problemem i przedmiotem dyskusji.
To wszystko mam oprogramowane (pozycja i wysokość paska zadań, aktywny monitor, etc), ale żeby nie zaciemniać obrazu użyłem jak najprostszych konstrukcji.

  1. nie bardzo wiem z czym masz problem - "wjeżdżanie" formy od prawej, 5 sekund pozostaje i zjeżdża w dół

Problemem było to, że zdarzenie OnActivate nie było odpalana, jeśli program siedział w zasobniku systemowym. Czy ktoś potrafi odpowiedzieć na to pytanie? Ciekawym zagadnieniem jest też poprawne ukrywanie programu w zasobniku systemowym (żeby okno główne zniknęło, również z paska zadań).
Być może niepotrzebnie zafiksowałem się na używaniu OnActivate - zapewne można to zrobić prościej, w samym onShow formy (co zweryfikuje, sprawdzając Twój poniższy kod).

Kopiuj
//równie dobrze może to być po wystąpieniu jakiegoś eventu
procedure TForm2.Button1Click(Sender: TObject);
var
  form: TForm3;
begin
  form := TForm3.Create(Nil);
  form.Show;
  form.Left := 2560; //szerokość ekranu;
  form.Top := 1440 - form.Height; //wysokość ekranu
  form.SlideIn(2560, 1440);
end;

//TForm3 ma na sobie 3 timery, wszystkie Enabled := False, 1 i 3 Interval na 10 a 2 na 5000 
type
  TForm3 = class(TForm)
    Panel1: TPanel;
    Timer1: TTimer;
    Timer2: TTimer;
    Timer3: TTimer;
    procedure Timer1Timer(Sender: TObject);
    procedure Timer2Timer(Sender: TObject);
    procedure Timer3Timer(Sender: TObject);
  private
    FW: Integer;
    FH: Integer;
  public
    procedure SlideIn(W: Integer; H: Integer);
  end;

var
  Form3: TForm3;

implementation

{$R *.dfm}

{ TForm3 }

procedure TForm3.SlideIn(W: Integer; H: Integer);
begin
    FW := W;
    FH := H;
    Timer1.Enabled := True;
end;

procedure TForm3.Timer1Timer(Sender: TObject);
begin
  Left := Left - 10;
  if Left + Width < FW then
  begin
    Timer1.Enabled := False;
    Timer2.Enabled := True;
  end;
end;

procedure TForm3.Timer2Timer(Sender: TObject);
begin
  Timer2.Enabled := False;
  Timer3.Enabled := True;
end;

procedure TForm3.Timer3Timer(Sender: TObject);
begin
  Top := Top + 10;
  if Top > FH then
  begin
    Timer3.Enabled := False;
    Free;
  end;
end;

W załączniku projekt + exe

Nowy folder.7z

Dzięki za przykład. Przeanalizuję go i dostosuję. Być może to jest właśnie to.

flowCRANE
  • Rejestracja: dni
  • Ostatnio: dni
  • Lokalizacja: Tuchów
  • Postów: 12269
1
Pepe napisał(a):

Problemem było to, że zdarzenie OnActivate nie było odpalana, jeśli program siedział w zasobniku systemowym. Czy ktoś potrafi odpowiedzieć na to pytanie?

Przerób program do takiej postaci, aby okno było tworzone dynamicznie i nie bazuj na zdarzeniach aktywacji.

Ciekawym zagadnieniem jest też poprawne ukrywanie programu w zasobniku systemowym (żeby okno główne zniknęło, również z paska zadań).

Jeśli nie chcesz, aby na pasku zadań pokazywał się przycisk aplikacji, to zainteresuj się właściwością Application.ShowMainFormOnTaskbar. Kiedy ustawi się ją na False, przycisku na pasku zadań nie będzie. Zapewne VCL (tak jak LCL) pozwala tworzyć osobny przycisk dla każdego formularza z osobna, więc dodatkowo sprawdź jakie właściwości ma klasa TForm i czy zawiera coś związanego z paskiem zadań, a jeśli tak to je deaktywuj.

Pepe
  • Rejestracja: dni
  • Ostatnio: dni
  • Postów: 505
0
furious programming napisał(a):
Pepe napisał(a):

Problemem było to, że zdarzenie OnActivate nie było odpalana, jeśli program siedział w zasobniku systemowym. Czy ktoś potrafi odpowiedzieć na to pytanie?
Przerób program do takiej postaci, aby okno było tworzone dynamicznie i nie bazuj na zdarzeniach aktywacji.

Tak też zrobię, wieczorem to rozpykam :P

Ciekawym zagadnieniem jest też poprawne ukrywanie programu w zasobniku systemowym (żeby okno główne zniknęło, również z paska zadań).

Jeśli nie chcesz, aby na pasku zadań pokazywał się przycisk aplikacji, to zainteresuj się właściwością Application.ShowMainFormOnTaskbar. Kiedy ustawi się ją na False, przycisku na pasku zadań nie będzie. Zapewne VCL (tak jak LCL) pozwala tworzyć osobny przycisk dla każdego formularza z osobna, więc dodatkowo sprawdź jakie właściwości ma klasa TForm i czy zawiera coś związanego z paskiem zadań, a jeśli tak to je deaktywuj.

Wyrażenie Application.MainFormOnTaskbar := True; jest domyślnie włączone w nowoczesnych wersjach Delphi (ustawiane jest zaraz po inicjalizacji i przed jej utworzeniem i ma wpływ na zachowanie się programu na pasku zadań i jego obsługę). Poza tym, większość czasu program będzie widoczny - więc musi to działać na zasadzie pokaż-ukryj.

Używam konstrukcji:

Kopiuj
// Pokaż główne okno
procedure TMainFrm.TRAY_MENU_SHOW_WINDOW();
begin
   MainFrm.Show;
   Application.BringToFront();
end;

// Ukryj główne okno
procedure TMainFrm.TRAY_MENU_HIDE_WINDOW();
begin
   Application.ShowMainForm := False;
   MainFrm.Hide;
end;

To działa. Ale, niepokoi mnie wpis Remy Lebeau, który napisał:
"...FYI, Application.ShowMainForm only affects the startup of Application.Run(), so once the app is up and running then setting ShowMainForm has no effect..."

-Pawel

flowCRANE
  • Rejestracja: dni
  • Ostatnio: dni
  • Lokalizacja: Tuchów
  • Postów: 12269
2

To może inaczej. W Lazarusie, aby osiągnąć efekt jakiego oczekujesz, wystarczy oprogramować zdarzenie OnWindowStateChange i jeśli okno zostało zminimalizowane, ustawić mu normalny stan, ukryć i wyłączyć widoczność przycisku na pasku zadań. Głównego pliku projektu nie trzeba modyfikować, zadziała to również dla formularza tworzonego automatycznie (w trakcie bootowania aplikacji):

Kopiuj
procedure TFormMain.FormWindowStateChange(Sender: TObject);
begin
  if FormMain.WindowState = wsMinimized then
  begin
    FormMain.WindowState := wsNormal;
    FormMain.Hide();
    FormMain.ShowInTaskBar := stNever;
  end;
end;

Program domyślnie startuje z widocznym okienkiem i przyciskiem na pasku zadań. Po zminimalizowaniu okna, to zostaje ukryte i znika przycisk na pasku zadań. Aby pokazać okno z poziomu menu kontekstowego przycisku w zasobniku, wystarczy je pokazać i podbić na pierwszy plan:

Kopiuj
procedure TFormMain.MenuItemShowClick(Sender: TObject);
begin
  Application.Restore();

  FormMain.Show();
  FormMain.BringToFront();
end;

Powyższy kod pozwala przywrócić okno ze stanu niewidocznego, ale też wyświetlić je na pierwszym planie, jeśli już jest widoczne, ale znajduje się pod innymi oknami. Aby program startował bez pokazywania głównego okna, czyli domyślnie schowany w zasobniku, należy w głównym pliku projektu ustawić właściwość Application.ShowMainForm na False:

Kopiuj
Application.ShowMainForm := False;

Wywołanie zdarzenia OnClose w jakikolwiek sposób zamyka cały program — można to zrobić przyciskiem na belce okna lub z poziomu kodu, np. w zdarzeniu kliknięcia w dedykowaną temu pozycję w menu kontekstowym przycisku zasobnika:

Kopiuj
procedure TFormMain.MenuItemQuitClick(Sender: TObject);
begin
  FormMain.Close();
end;

W razie gdybyś chciał, aby zamknięcie formularza zawsze minimalizowało okno do zasobnika, wystarczy oprogramować zdarzenie OnCloseQuery, w nim ustawić argument CanClose na False i wywołać Application.Minimize.


Teraz tylko pytanie czego użyć w VCL, co pozwałoby ukryć/pokazać przycisk na pasku zadań. Tutaj musisz sam poszukać czegoś przydatnego w VCL, ewentualnie — jeśli masz zainstalowanego Lazarusa — sprawdzić jak w widgetsecie zaprogramowana jest zmiana stanu właściwości TForm.ShowInTaskBar.

Jest w dokumentacji Delphi przykład jak to zrobić:

TTrayIcon (Delphi):

This example uses a tray icon and an application events component on a form. When the application runs, it loads the tray icon, the icons displayed when it is animated, and it also sets up a hint balloon. When you minimize the window, the form is hidden, a hint balloon shows up, and the tray icon is displayed and animated. Double-clicking the system tray icon restores the window.

Pytanie tylko czy ten przykład powoduje ukrycie przycisku na pasku zadań. 😉

Pepe
  • Rejestracja: dni
  • Ostatnio: dni
  • Postów: 505
0

Dziękuję za pomoc i wskazówki. Udało się zrobić działające rozwiązanie...
Wciąż muszę dopracować szczegóły (między innymi dopracować ukrywanie/pokazywanie formy - to, że działa dobrze, nie znaczy, że nie można zrobić lepiej).
Bazowanie na zdarzeniach onShow/onActivate było złym rozwiązaniem, by nie powiedzieć głupim.

Pozostał jeden problem. Po zmianie zawartości schowka program tworzy formę, która zostanie zniszczona po określonym czasie. Ale, gdy jest ona jeszcze wyświetlana może nastąpić kolejna aktualizacja schowka co utworzy kolejną formę, itd... Powinna być wyświetlana tyko "najnowsza", a wszystkie starsze instancje powinny zostać zniszczone...

-Pawel

flowCRANE
  • Rejestracja: dni
  • Ostatnio: dni
  • Lokalizacja: Tuchów
  • Postów: 12269
1
Pepe napisał(a):

Po zmianie zawartości schowka program tworzy formę, która zostanie zniszczona po określonym czasie. Ale, gdy jest ona jeszcze wyświetlana może nastąpić kolejna aktualizacja schowka co utworzy kolejną formę, itd... Powinna być wyświetlana tyko "najnowsza", a wszystkie starsze instancje powinny zostać zniszczone...

Zawsze możesz najpierw sprawdzić czy formularz już istnieje i jeśli tak, to zaktualizować jego dane i zresetować licznik odliczania czasu. W ten sposób zawsze tylko jeden formularz będzie zaalokowany, co znacznie ułatwi zaprogramowanie całości.

Pepe
  • Rejestracja: dni
  • Ostatnio: dni
  • Postów: 505
0

Jeszcze jedna bardzo istotna sprawa. Otóż napotkałem dziwny problem.
Aby wykryć zmiany zawartości schowka Windows przechwytuję komunikat WM_CLIPBOARDUPDATE (wcześniej dodaję opcje nasłuchiwania - > AddClipboardFormatListener(Handle);, którą zwalniam przy wyjściu z programu -> RemoveClipboardFormatListener(Handle);).
I to działa. Reaguję, gdy schowek ma obrazek lub tekst Clipboard.HasFormat(CF_PICTURE) OR Clipboard.HasFormat(CF_TEXT)

ALE - najwyraźniej ten komunikat jest wysyłany, gdy odpalam usługi systemowe (mimo, że zawartość schowka NIE ZMIENIA SIĘ), np:
Zarządzanie komputerem (compmgmt.msc)
Podgląd zdarzeń (eventvwr.msc)
Monitor wydajności (perfmon.msc)
i pewnie inne programy, o których nie wiem.
Dlaczego!!!!?

Czyli, dzieje się to tylko wtedy, gdy schowek ma jakąś zawartość (jeśli jest pusty, nic się nie dzieje). Jeśli ma tekst czy obrazek, czy cokolwiek innego - moja aplikacja odbiera komunikat o zmianie zawartości schowka (który de facto jest taki sam).

Dlaczego te usługi wysyłają komunikat WM_CLIPBOARDUPDATE?
Albo inaczej - co zrobić, by zignorować tę konkretną sytuację (to jest odbieram komunikat, ale nie chcę reagować - bo nic się nie zmieniło w schowku).
Jakoś nie widzę w implementacji Clipboard w Delphi niczego co by mi pomogło...

EDIT: Rozwiązaniem problemu jest użycie GetClipboardSequenceNumber().

-Pawel

Zarejestruj się i dołącz do największej społeczności programistów w Polsce.

Otrzymaj wsparcie, dziel się wiedzą i rozwijaj swoje umiejętności z najlepszymi.