Reakcja aplikacji na zmianę schematu kolorów systemu

0

Tworzę program, w którym wszystkie kontrolki dziedziczące po TCustomListBox, TComboBox czy wszelkie TMenu/TPopupMenu itd. malowane są ręcznie; W każdej z metod malujących korzystam z rekordu przechowującego potrzebne kolory:

type
  TApplicationColors = packed record
    Font: packed record
      Normal,
      Hot,
      Disabled: TColor;
    end;
    Background: packed record
      NormalOrDisabled,
      Hot: TAppBackColors;
    end;
    Misc: packed record
      MenuBar,
      Line: TColor;
    end;
  end;

Uzupełnienie tego rekordu realizuję przy starcie aplikacji (po prostu pobieram kolory takie jak clMenuHighlight, clHighlight, clWindowText itd. i wpisuję je do odpowiednich pól w rekordzie);

I teraz potrzebuję zareagować na zmianę schematu systemowych kolorów, czyli na nowo uzupełnić ww. rekord nowymi kolorami, jakie od chwili zmiany obowiązują w systemie; Wszystkie kontrolki, które są automatycznie malowane (czyli ręcznie ich nie maluję), TEdit czy TSpinEdit od razu po zmianie kolorów obsługują już nowe kolory; Te które maluję ręcznie (w zdarzeniach OnDrawItem), niestety nie; Dlatego wychwytuję komunikat WM_SYSCOLORCHANGE i aktualizuję rekord kolorów; Wykorzystuję następującą procedurę do obsługi komunikatów:

private
  procedure MyOnMessage(var AMsg: TMsg; var AHandled: Boolean);

{...}

procedure TEngine.MyOnMessage(var AMsg: TMsg; var AHandled: Boolean);
const
  WM_SYSTEM_COLORS_CHANGED = Cardinal($31B);
begin
  case AMsg.message of
    WM_SYSTEM_COLORS_CHANGED: { aktualizacja rekordu kolorów };
  end;
end;

{...}

constructor TEngine.Create();
begin
  inherited Create();
  Application.OnMessage := MyOnMessage;

  {...}
end;

Sprawdzałem jaki komunikat ląduje w metodzie MyOnMessage po zmianie kolorów w systemie - dostaję cztery komunikaty, kolejno (kody szesnastkowe):

$31B
$F
$31B
$F

Komunikat o kodzie $F to pewnie WM_PAINT, bo trafia do obsługi non-stop, nawet podczas przesuwania kursorem po formularzu (ten jest nieistotny); Za to $31B pokazuje się tylko po zmianie kolorów w systemie; Normalnie komunikat WM_SYSCOLORCHANGE ma wartość 21 (hex $15), jednak po zmianie systemowych kolorów takowy nie dociera do metody MyOnMessage, więc tak jak sugerował @olesio - możliwe, że został po drodze przetłumaczony;

W każdym razie kominikat o kodzie $31B dociera do tej metody bez problemu; Dlaczego trafia dwa razy - nie mam pojęcia, jednak na MSDN jest napisane, że ten komunikat wędruje po kontrolkach aż wróci z powrotem i wtedy dalej już nie jest wysyłany, tak więc AHandled nie można ustawiń na True, bo pół aplikacji nie zostanie przemalowane, no i tego nie robię;

Podczas, gdy komunikat ten trafia do MyOnMessage - wykonuję jedynie aktualizację rekordu kolorów; Jednak aktualizacja nie działa prawidłowo, ponieważ najwidoczniej podczas otrzymania tego komunikatu nowe kolory jeszcze nie zostały w systemie zaktualizowane i zostają bez zmian; Kolejne ustawienie kolorów i obsłużenie komunikatu ustawia mi poprzednie kolory w rekordzie; Opiszę krok po kroku jak to wygląda (pod WinXP):

* Uruchomienie aplikacji i pobranie aktualnych kolorów (Oliwkowy)

Schemat: Oliwkowy
W rekordzie: Oliwkowy

* Zmiana na Niebieski

Schemat: Niebieski
W rekordzie: Oliwkowy

* Zmiana na Srebrny

Schemat: Srebrny
W rekordzie: Niebieski

* Zmiana na Oliwkowy

Schemat: Oliwkowy
W rekordzie: Srebrny

itd.

Więc zawsze po zmianie do rekordu wpisywane są poprzednie kolory, a nie bieżące; Myślałem, że coś namieszałem w swoim programie, więc napisałem mały dla testów - wynik ten sam - takie samo zachowanie; Wygląda to tak, jakby najpierw system wysyłał komunikat, a później zmieniał kolory w systemie;

Nie chcę kombinować i opóźniać np. Sleep'em czy tym bardziej pobierać kolory z systemu przy każdym wywołaniu każdej metody OnDrawItem; Myślę, że jest jakieś normalne rozwiązanie bez kombinowania;


Moje pytanie brzmi: W jaki sposób poprawnie zareagować na zmianę kolorów w systemie tak, by kontrolki ręcznie malowane także pomalowały się nowymi kolorami?

Na MSDN napisane jest, że system wysyła dodatkowo komunikat WM_PAINT, jednak ręczne wysłanie tego komunikatu nie pomaga, bo kontrolki do malowania (ręcznego) dalej wykorzystują stary schemat; Na to wychodzi, że po odebraniu komunikatu o zmianie kolorów w systemie do rekordu wpisywane są nadal stare, a po obsłudze tego komunikatu dopiero zmiany zostawały faktycznie wprowadzone;

Co ciekawe, jeśli wpiszę takie coś:

procedure TEngine.MyOnMessage(var AMsg: TMsg; var AHandled: Boolean);
const
  WM_SYSTEM_COLORS_CHANGED = Cardinal($31B);
begin
  case AMsg.message of
    WM_SYSTEM_COLORS_CHANGED:
      begin
        Application.MessageBox( {treść dowolna} );
        { aktualizacja rekordu kolorów }
      end;;
  end;
end;

Wtedy zadziała, jakby tym MessageBox'em opóźniło się pobranie nowych kolorów do rekordu i system zdążył je wprowadzić; Dziwne, ale prawdziwe; Nie chcę wykorzystywać MessageBox'a bo myślę, że można to zrobić po ludzku; Poza tym mam własne okno powiadomień;

W załączniku podaję źródło tego małego testowego programu, który ma identyczną metodę obsługującą komunikaty; Jeśli znacie rozwiązanie to przedstawcie je na kodzie z tego programu - jak zadziała to odpowiednio przerobię głowny projekt;

To w sumie tyle, proszę o jakieś wskazówki, z góry Bóg zapłać...


EDIT: Sleep nie pomaga, widać po deaktywacji okna przez MessageBox zmiany zostają wprowadzone, ale dlaczego - nie wiem...

OwnAppColors.zip

1

@furious programming: nie wiem czy o taki efekt Tobie chodziło, ale z tego co widzę mając otwartą Twoją aplikację oraz a swoją - to widzę, że raczej tak. Dlaczego teraz działa, a u Ciebie nie? Tego nie wiem tak na prawdę. Komponent był żywcem skopiowany z formatki w kodzie od Ciebie i wklejony na "moją" formatkę. Także wszelkie properties poza Allign i rozmiarami powinny się zgadzać. Oczywiście OnDrawItem zostało przekopiowane żywcem od Ciebie. Jak widać OnMessage, którego tak usilnie używasz to zło ;) Ponieważ chyba przekłamuje komunikaty albo działa w ogóle jakoś inaczej i się Ciebie nie chce słuchać :) Przykład z naszej korespondencji na PW. Komunikat "a'la" WM_KEYDOWN też jest przekłamany. Jednak ja po prostu zrobiłem odwrotnie niż Ty u siebie. To co usilnie pakujesz do OnMessage wrzuciłem do bliższej chyba WinAPI metodzie. Ja takim kodem podpinałem się w injectowanych dllkach pisanych z injectorami przez siebie, w wiele funkcji obsługi komunikatów, w obcych procesach. I do tej pory tylko Total Commander głupiał, nie pozwalając na przechwycenie zdarzenia LB_ADDSTRING i zwrócenie prawidłowego tekstu w PChar(ALParam). Ale może dlatego, że tamtejsze kontrolki tylko dziedziczą po TListBox i są tak na prawdę bardziej zbliżone do zaawansowanego TListView. Anyway, działający source masz dołączony do tego posta wraz z exekiem. Nie dojdę dlaczego tak działa chyba ok, a u Ciebie nie. Być może wina jest z przekazywaniem tego inaczej przez VCLowe OnMessage. Wiadomo, że pure WinAPI rulez i tyle :P Tylko ciężko się pod nim mi osobiście ogarnąć w samodzielne rysowanie kontrolek. Może ktoś tutaj jeszcze naprowadzi Ciebie na inne, również działające rozwiązanie.

1

Wg mnie wystarczy:

procedure TEngine.MyOnMessage(var AMsg: TMsg; var AHandled: Boolean);
const
  WM_SYSTEM_COLORS_CHANGED = Cardinal($31B);
begin
  case AMsg.message of
    WM_SYSTEM_COLORS_CHANGED: { aktualizacja rekordu kolorów };
    begin
       inherited;
       // tu sprawdzamy.
    end;
  end;
end;
0
olesio napisał(a)

nie wiem czy o taki efekt Tobie chodziło, ale z tego co widzę mając otwartą Twoją aplikację, a swoją to widzę, że tak.

Dokładnie o taki efekt - kontrolki ręcznie malowane przyjmują bez problemu nowe kolory;

olesio napisał(a)

Jak widać OnMessage, którego tak usilnie używasz to zło, które przekłamuje komunikaty albo działa w ogóle jakoś inaczej

Najwidoczniej przekłamuje; Z chęcią wykorzystałbym Twoją metodę już wcześniej, jednak miałem problem z przetłumaczeniem kodu obsługującego kombinację klawiszy Ctrl + BkSp:

case AMsg.message of
  WM_KEYDOWN:
    if (AMsg.wParam = VK_BACK) and (GetKeyState(VK_CONTROL) and 128 = 128) then
    begin
      ctrlActive := Screen.ActiveControl;

      if Assigned(ctrlActive) and (ctrlActive is TCustomEdit) then
      begin
        AHandled := True;

        if ((ctrlActive is TEdit) and TEdit(ctrlActive).ReadOnly) or
           ((ctrlActive is TMemo) and TMemo(ctrlActive).ReadOnly) or
           (ctrlActive is TRichEdit) then
          Exit;

        iTextLen := GetWindowTextLength(AMsg.hWnd);
        iCaretPos := LoWord(SendMessage(AMsg.hWnd, EM_GETSEL, 0, 0));
        SetLength(sLeft, iCaretPos);
        GetWindowText(AMsg.hWnd, PChar(sLeft), iCaretPos);
        iSelStart := iCaretPos;

        while (iSelStart > 0) and (sLeft[iSelStart] in STOP_CHARS_ARR) do
          Dec(iSelStart);

        while (iSelStart > 0) and (not (sLeft[iSelStart] in STOP_CHARS_ARR)) do
          Dec(iSelStart);

        while (iSelStart > 0) and (sLeft[iSelStart] in [#10, #13, #32]) do
          Dec(iSelStart);

        if iSelStart < iCaretPos then
        begin
          SendMessage(AMsg.hWnd, EM_SETSEL, iSelStart, iCaretPos);
          SendMessage(AMsg.hWnd, EM_REPLACESEL, Integer(True), Integer(PChar('')));

          if (GetWindowTextLength(AMsg.hWnd) <> iTextLen) and (ctrlActive.Name = 'memNote') then
          begin
            MainForm.UpdateToolbar();
            MainForm.UpdateNoteControls();
          end;
        end;
      end;
    end;
end;

Owszem, przetłumaczyłem ją, jednak kod wywala się na iTextLen := GetWindowTextLength(AMsg.hWnd) (sprawdzałem pod debugerem) czyli na pierwszej funkcji, która zapewne wysyła komunikat; No i nie mam zielonego pojęcia co z nią nie tak i jak temu zaradzić;

EDIT: tzn. program się nie wywala, tylko debugując linijka po linijce dochodzimy do GetWindowTextLength, następnie nie sa wykonywane kolejne instrukcje tylko przeskakuje na początek procedury i od razu na koniec (widać, że ta funkcja wysłała komunikat, ale nie zostaje obsłużony); Mi to wygląda tak, jakby funkcja GetWindowTextLength wysłała komunikat, który nie został obsłużony i na tym koniec, więc następny SendMessage już się nie wykona;

Pod tą moją metodą obsługa komunikatu WM_KEYDOWN jest prosta - wszystko działa jak należy;

0

@_13th_Dragon: sprawdziłem i zapomniane przez nas słowko inherited tutaj nie pomogło, chyba że coś źle robię. Sprawdziałem na kodzie jaki wstawił @furious programming w załączonym archiwum.

EDIT:
@furious programming: przecież mój kod zawiera przykład obsługi tej "nieszczęsnej" kombinacji Ctrl+BaskSpace. No chyba, że mimo tego coś nie działa wtedy dla aktywnej kontrolki, z którą później po swojemu kombinujesz. Jednak w takim przypadku nic pomocniejszego raczej sam nie wymyślę :/

1

Troszkę sie wtrącę ale przekombinowaliście z tą obsługą zmiany koloru wystarczy dadeklarować sobie:

procedure WMSysColorChange(var AMsg: TMsg); message WM_SYSCOLORCHANGE;

No i później tylko obsługa:

procedure TMainForm.WMSysColorChange(var AMsg: TMsg);
begin
  SetAppColors();
  inherited;
end;

Teraz wyjaśnię (jak przypuszczam to tylko moja teoria spiskowa) dlaczego nie działa w Application.OnMessage otóż Delphi jak wiadomo posiada niewidoczne okno Application które jest tak naprawdę głównym oknem programu. Prawdopodobnie dlatego że ono jest niewidoczne tego komunikatu nie dostaje a więc nie prześle go do okien potomnych. Oczywiście to tylko moje niczym nie potwierdzone przypuszczenia.

0

@kAzek: co do traktowania komunikatów przez TApplication, może być tak jak piszesz. Jednak czy sprawdziłeś swój kod z zadeklarowaniem komunikatu po message? Bo u mnie to działa znowu w inny sposób. Wiersze są pokolorowane, ale dolna część (pusta) ListBoxa jest w poprzednim kolorze. Taże Twój sposób, o ile nic nie pomyliłem, przynajmniej u mnie nie działa jak powinien.

@furious programming: z tego co widzę na http://msdn.microsoft.com/en-us/library/windows/desktop/ms633521(v=vs.85).aspx jeżeli problem w Twoim kodzie z obsługą Ctrl+Backspace może sprawiać GetWindowTextLengthto może należało by óbsłużyć ręcznie albo po prostu spróbować wysłać taki komunikat pod handle kontorlki i odczytać jej rezultat. Znowu nie patrzyłeś uważnie w mój moduł useful_winapi. Jest tam funkcja GetTextLen, która mi osobiście nie była potrzebna, bo zawsze sobie w swoich programach pod WinAPI z użyciem tego modułu - jeśli musiałem - używałem GetText. Dlatego GetTextLen nie ma w sekcji interface. Może tak pokombinuj. Poza tym niemal cały kod obsługi tego naciśnięcia "Furiousowego combo" to czyste WinAPI. Także albo pozostaje Ci spróbować obsługi WM_GETTEXTLENGTH albo w funkcji dla Application.Handle albo spróbować subclassingu i zrobić to dla tego Memo, zwracając po prostu Length(TamtoMemo.TexT);, jeśli to ma tak działać. Ja już nie przetestuje, bo przed pracą pora iśc spróbować trochę pospać. Do wątku zajrze dopiero najwcześniej w niedziele po południu.

0

No i jak zwykle @kAzek musi mieć racje :]

Sprawdziłem ten sposób - działa bez zarzutów; Zastanawiałem się, czy jak ten komunikat będzie odbierany przez główny formularz to czy jak mam otwarte okno dialogowe (własne) to czy także zostanie przemalowane - i zostaje także przemalowane; Fakt, było to przekombinowane, jednak sądziłem, że tworząc własną procedurkę przetwarzającą komunikaty będą tam trafiać nie dość, że wszystkie komunikaty, to jeszcze w niezmienionej postaci (wartości będą jedne, zawsze); Widać czemuś to ma służyć, ale nie za bardzo wiem czemu; Najważniejsze, że działa - nad teorią jeszcze się zastanowię;


olesio napisał(a)

Znowu nie patrzyłeś uważnie w mój moduł useful_winapi.

Ależ Ty nudzisz... Napisałem Ci, że nie zdążyłem przeanalizować tego modułu bo spieszyłem się, także nie zdążyłem się na spokojnie zastanowić jak to ugryźć, skoro oczywiste metody zawiodły; Ale nie martw się, jeszcze go przeglądnę :]

olesio napisał(a)

Poza tym niemal cały kod obsługi tego naciśnięcia "Furiousowego combo" to czyste WinAPI.

W sumie tak, jednak jak przystosowałem kod pod Twoją funkcję (m.in. pod inne parametry) to nie reagował na tą kombinację - rozpoznawał oczywiście komunikat poprawnie, ale tak jak wspomniałem wcześniej funkcja GetWindowTextLen musiała wysyłać komunikat i liczyła na to, że także jego się przetworzy, a skoro nie to dalesze instrukcje już nie wykonywały się; Zastanawiałem się czy może tak "rekurencyjnie" zostanie obsłużony, ale jednak nie; Choć nie rozumiem, przecież wszystkie komunikaty, których nie obsłużę sam nie są jakoś odtrącane, nie przepadają bez wieści, tylko obsługiwane są z automatu (domyślnie) - bo tak to trzeba by wszelkie komunikaty obsługiwać, czyli wracamy do WinAPI i pętli komunikatów;

olesio napisał(a)

albo w funkcji dla Application.Handle albo spróbować subclassingu i zrobić to dla tego Memo, zwracając po prostu Length(TamtoMemo.TexT);, jeśli to ma tak działać.

Dokładnie tak ma działać, jednak rozpoznawanie i reagowanie na tą kombinację obejmuje wszystkie kontrolki z całej aplikacji, wszystkie z klas TMemo i TEdit (łącznie z 10), więc żeby bez WinAPI pobrać tekst czy jego długość z aktywnej kontrolki trzeba by najpierw sprawdzić która to kontrolka, a później jeśli to ta właściwa dopiero pobrać interesujące informacje; A tak to wystarczy sprawdzić czy klasa aktywnej kontrolki jest właściwa - resztę za pomocą funkcji WinAPI można szybko załatwić;


Reasumując - dziękuję wszystkim za zainteresowanie i pomoc, pozdrawiam;

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