Kolejność WM_NCPAINT i WM_SIZE

0

Zauważyłem dosyć dziwną rzecz. W przypadku "niemanualnej" zmiany rozmiaru formy (np. poprzez Form1.Width: = 600) komunikat WM_NCPAINT odpowiedzialny za malowanie formy w części poza klientem (czyli np. ramki i przyciski systemowe) wysyłany jest jako pierwszy, a WM_SIZE odpowiedzialny za faktyczną zmianę rozmiaru formy i kontrolek, wysyłany jest później.

Moje pytanie - jaki sens ma namalowanie ramki dla okna o rozmiarze np. 350 x 200, skoro za chwilę okno zmieni rozmiar do powiedźmy 730 x 490 i ramka będzie niewiele warta, bo namalowana dla nieaktualnego już rozmiaru? Ja poradziłem sobie w ten sposób, że po WM_SIZE wysyłam jeszcze raz WM_NCPAINT i zmuszam program do ponownego narysowania ramki (co swoją drogą jest nieco nieekonomiczne, bo muszę rysowanie wykonać dwukrotnie). Działać działa, ale jakoś nie daje mi to spokoju. Ktoś wie, czemu Windows stosuje taką kolejność? Czyżby zmiana rozmiaru okna nie "szkodziła" jego domyślnym ramkom?

0

Kto wie - może przed zmianą rozmiaru np. trzeba wyłączyć przycisk od maksymalizacji i to pokazać:>? A może to po prostu niedopatrzenie... Czy naprawdę jest to takie ważne gdy aplikacja przyjmuje setki innych komunikatów w tym samym czasie? No i kto spierniczył MS czy Emba to już zagadka.

0

Już wiem. Po prostu to dziadostwo działa tak, że rysuje nową ramkę jeszcze przed zmianą rozmiaru całego klienta, przesyłając do WM_NCPAINT odpowiednie wymiary w wParam, zakodowane jako region (HRGN). Staram się odczytywać te liczby konwertując HRGN na TRect (przy pomocy GetRgnBox), ale czasem wychodzą mi jakieś krzaki (bo inny region zostaje wysłany przy konstruktorze formy, a inny przy np. zmianie rozmiaru okna). Czytałem na MSDN, że taki odebrany w komunikacie region, trzeba jeszcze rzutować na okno (clipping). Jak uda mi się coś sklecić, to nie omieszkam napisać :).

0

@Crow: ale nad czym Ty się zastanawiasz?

System działa tak a nie inaczej, VCL też jest zaimplementowany tak a nie inaczej, więc nie masz na to zbytnio wpływu; Redundancja to nieodzowna cecha wszelkich bibliotek komponentów - trzeba się z tym pogodzić, albo napisać swoją :]

0

Po mojemu to zwykły przypadek komunikat WM_NCPAINT został wywołany z zupełnie innego powodu, być może podczas testów zakrywałeś okno (przynajmniej częściowo) dlatego system wysłał komunikat nakazujący jego odmalowanie.Ogólnie nie jest zalecane aby user wnikał w komunikaty dotyczące tego obszaru okna choć wiadomo że czasem trzeba (np. gdy trzeba rysować własne przyciski na belce tytułowej) ale ogólnie nie ma co wnikać i lepiej pozostawić to systemowi i naprawdę nie ma co drążyć.

0

Moje pytanie - jaki sens ma namalowanie ramki dla okna o rozmiarze np. 350 x 200, skoro za chwilę okno zmieni rozmiar do powiedźmy 730 x 490 i ramka będzie niewiele warta, bo namalowana dla nieaktualnego już rozmiaru? Ja poradziłem sobie w ten sposób, że po WM_SIZE wysyłam jeszcze raz WM_NCPAINT i zmuszam program do ponownego narysowania ramki

Nie wiem jaki sens, ale skoro od xx lat tak jest i działa to znaczy że jest dobrze. Wysyłając ręcznie WM_NCPAINT ryzykujesz dziwnymi skutkami ubocznymi - jeśli nie dzisiaj to w przyszłości, albo przy innej konfiguracji (włączone/wyłączone Aero, zdalny pulpit itp.)

0

W tym rzecz, że niby działa, ale nie do końca optymalnie ;).

Apka rysuje ramki, następnie skaluje i przerysowuje klienta, a potem - tym razem już z mojej inicjatywy - jeszcze raz rysuje ramki. Trochę mi się to nie podoba, bo jednak wymusza dodatkowe, zupełnie niepotrzebne rysowanie. Samo rysowanie oczywiście wywołuję inaczej, niż poprzez wysłanie WM_NCPAINT. Czytałem na MSDN, żeby nigdy nie rozsyłać ani WM_NCPAINT ani WM_PAINT, bo to tylko sprawia problemy. Zamiast tego - za radą MSDN - stosuję RedrawWindow.

I wiem już na pewno, że WM_NCPAINT jest wysyłany nieprzypadkowo w takiej kolejności. Nie wiem czemu, ale Windows uznaje narysowanie ramek za priorytet i stawia go wyżej, niż np. zmianę rozmiaru klienta czy przerysowanie kontrolek w nim umiejscowionych. Staram się to prawidłowo obsłużyć, odczytując region wysłany w wParam, ale póki co idzie tak sobie. Pierwsze WM_NCPAINT, odebrane z automatu zaraz po WM_NCCALCSIZE liczącym rozmiary okna, zawiera jakieś bzdety (ramka w ogóle mi się nie rysuje), a późniejsze (np. po zmianie rozmiaru formy czy maksymalizacji), też bywają koślawe. Czytałem na MSDN, że ten region trzeba jeszcze rzutować jakoś na okno, ale jeszcze nie rozgryzłem odpowiednich funkcji ;d.

PS. Wszyscy tak straszyli tym WinAPI, a mi się tu bardziej podoba niż w VCL. Niby też niewiele rozumiem, ale przynajmniej fajna dokumentacja jest ;].

EDIT

Udało mi się prawidłowo obsłużyć region wysłany do WM_NCPAINT w wParam. Niestety, on też przesyła nieaktualne już wymiary okna, czyli jeszcze PRZED przeliczeniem na nowy rozmiar. Nie rozumiem tego. Skoro jednym z pierwszych komunikatów wywoływanych po manipulacji wartościami width i height jest WM_NCCALCSIZE (przeliczający wymiary całego okna), to na jaki ciul wysyłać do WM_NCPAINT szablon ze starymi danymi? Podejrzewam, że Windows ma jakieś dziwne sposoby rysowania swoich ramek (w nowszych wersjach Win'a, nawet nie za bardzo da się na nich coś nabazgrać, nawet po prawidłowym założeniu uchwytu na WindowDC).

Jakieś pomysły, jak z tego wybrnąć?

Ja myślę o wprowadzeniu globalnej zmiennej, która będę się aktualizować wraz z obsługą WM_NCCALCSIZE. Zmienna będzie trzymać wymiary okna, żeby WM_NCPAINT mógł z nich skorzystać, zamiast bazować na nieaktualnym parametrze otrzymywanym w wParam. To chyba bardziej wydajne niż ponowne inicjowanie WM_NCPAINT, zwłaszcza, że RedrawWindow aktywuje jeszcze masę innych operacji (w tym repaint całego klienta i wszystkich kontrolek), więc to strasznie niegospodarne.

EDIT 2

Działa!

0

Problem w ogóle niezwiązany z tematem, ale nie chcę zaśmiecać forum nowym.

Czy da się - bez tworzenia nowego komponentu - nadpisać wartość (property) komponentu? Mój moduł pracuje tylko z BorderStyle = bsNone i takie właśnie property chce narzucać każdej formie, modyfikując klasę TForm. Problem w tym, że zmiana tej właściwości nie może nastąpić w konstruktorze formy, bo to zbyt późno. Inne zdarzenia już wcześniej - bazując na tym ustawieniu - dokonują pewnych przekształceń i obliczeń w obrębie okna.

1

Tak możesz użyć do tego metod RTTI wystarczy dodać do uses RTTI lub ustawić helpera dla klasy TForm. Jest jeszcze jedna metoda ale jest to dość zawiane i myślę, że nie ma sensu o tym pisać ;)

0

To ja jednak poproszę o tę drugą metodę, bo mój kompilator nie obsługuje RTTI ani ClassHelperów ;d.

0
Crow napisał(a):

Zauważyłem dosyć dziwną rzecz. W przypadku "niemanualnej" zmiany rozmiaru formy (np. poprzez Form1.Width: = 600) komunikat WM_NCPAINT odpowiedzialny za malowanie formy w części poza klientem (czyli np. ramki i przyciski systemowe) wysyłany jest jako pierwszy, a WM_SIZE odpowiedzialny za faktyczną zmianę rozmiaru formy i kontrolek, wysyłany jest później.

Moje pytanie - jaki sens ma namalowanie ramki dla okna o rozmiarze np. 350 x 200, skoro za chwilę okno zmieni rozmiar do powiedźmy 730 x 490 i ramka będzie niewiele warta, bo namalowana dla nieaktualnego już rozmiaru? Ja poradziłem sobie w ten sposób, że po WM_SIZE wysyłam jeszcze raz WM_NCPAINT i zmuszam program do ponownego narysowania ramki (co swoją drogą jest nieco nieekonomiczne, bo muszę rysowanie wykonać dwukrotnie). Działać działa, ale jakoś nie daje mi to spokoju. Ktoś wie, czemu Windows stosuje taką kolejność? Czyżby zmiana rozmiaru okna nie "szkodziła" jego domyślnym ramkom?

Pewnie sam to psujesz i takie głupoty otrzymujesz.

NCPaint jaki i inne NC* należy obsługiwać prawidłowo, a wtedy nie ma problemów.

0
fol napisał(a):
Crow napisał(a):

Zauważyłem dosyć dziwną rzecz. W przypadku "niemanualnej" zmiany rozmiaru formy (np. poprzez Form1.Width: = 600) komunikat WM_NCPAINT odpowiedzialny za malowanie formy w części poza klientem (czyli np. ramki i przyciski systemowe) wysyłany jest jako pierwszy, a WM_SIZE odpowiedzialny za faktyczną zmianę rozmiaru formy i kontrolek, wysyłany jest później.

Moje pytanie - jaki sens ma namalowanie ramki dla okna o rozmiarze np. 350 x 200, skoro za chwilę okno zmieni rozmiar do powiedźmy 730 x 490 i ramka będzie niewiele warta, bo namalowana dla nieaktualnego już rozmiaru? Ja poradziłem sobie w ten sposób, że po WM_SIZE wysyłam jeszcze raz WM_NCPAINT i zmuszam program do ponownego narysowania ramki (co swoją drogą jest nieco nieekonomiczne, bo muszę rysowanie wykonać dwukrotnie). Działać działa, ale jakoś nie daje mi to spokoju. Ktoś wie, czemu Windows stosuje taką kolejność? Czyżby zmiana rozmiaru okna nie "szkodziła" jego domyślnym ramkom?

Pewnie sam to psujesz i takie głupoty otrzymujesz.

NCPaint jaki i inne NC* należy obsługiwać prawidłowo, a wtedy nie ma problemów.

Eeee nie? Dla testu zrobiłem coś takiego, że w obsłudze komunikatu miałem tylko:

procedure OBSŁUGA_KOMUNIKATU
begin
Log.Lines.Add('NAZWA KOMUNIKATU');
inherited;
end;

Po sprawdzeniu loga, widać jak na dłoni, jaka jest kolejność. WM_NCPAINT jest wysyłany przed WM_SIZE, bo Windows najpierw rysuje ramki i elementy interface'u a potem resztę. Nie wierzysz, to sprawdź u siebie :).

0

Może nawet tak być, w co i tak wątpię, ponieważ API było robione i doskonalone przez 20 lat z górką, więc tam aż takich bagów raczej nie powinno być!

Ale gdy faktycznie jest tak jak mówisz, to i tak nie powinno to wpływać na działanie aplikacji!

Bowiem, w systemach typu GUI, Ty zawsze masz obowiązek przerysować swoje okno - na żądanie systemu!

Mało: masz to robić w dowolnej żądanej rozdzielczości, bo user może zmienić rozdzielczość ekranu w dowolnej chwili,
albo chce to wydrukować - przesłać na drukarkę, która ma zwykle z 10 razy większą rozdzielczość od monitora!

Użyj niezależnego narzędzia do śledzenia komunikatów, np. winsight.

0
fol napisał(a):

Może nawet tak być, w co i tak wątpię, ponieważ API było robione i doskonalone przez 20 lat z górką, więc tam aż takich bagów raczej nie powinno być!

Ale gdy faktycznie jest tak jak mówisz, to i tak nie powinno to wpływać na działanie aplikacji!

Bowiem, w systemach typu GUI, Ty zawsze masz obowiązek przerysować swoje okno - na żądanie systemu!

Mało: masz to robić w dowolnej żądanej rozdzielczości, bo user może zmienić rozdzielczość ekranu w dowolnej chwili,
albo chce to wydrukować - przesłać na drukarkę, która ma zwykle z 10 razy większą rozdzielczość od monitora!

Ale czemu nazywasz to "bagiem"? Może tak właśnie miało być?

Jak nie wierzysz, napisz sobie na szybko obsługę WM_NCPAINT i odczytaj wParm, w którym zakodowany jest region (HRGN). Wtedy zobaczysz, że przesyłany jest tam rozmiar okna jeszcze przed zmianą formy, a nie już zaktualizowany. Testowałem to na czystej apce, bez obsługi jakichkolwiek komunikatów poza właśnie WM_NCPAINT, także wykluczam moją winę.

A tak w ogóle, to z tym już sobie poradziłem. Wrzuciłem do klasy zmienną, która po prostu zapisuje wymiary okna zaraz po WM_NCCALCSIZE (następującym na długo przed WM_SIZE) i potem WM_NCPAINT - gdy przyjdzie jego kolej - sobie z tego korzysta, zamiast z kulawego wParm.

Problem mam taki, że chcę odgórnie wymusić na formie BorderStyle = bsNone, jeszcze zanim apka zacznie wykonywać jakiekolwiek kalkulacje związane z formą. Próbowałem to zrobić w konstruktorze, w ReadState, w CreateParms, ale to ciągle zbyt późno. Po prostu źle mi przelicza rozmiar klienta, bo już wcześniej dokonuje przeliczeń w oparciu o BorderStyle pobrane z inspektora. I tak np. w inspektorze ClientWidth wynosi 1200, a po skompilowaniu zmienia się w 1186. Nie byłoby problemem wyrównać to ręcznie, gdyby nie fakt, że rozmiar klienta zmienia się w zależności o wartości BorderStyle w inspektorze. Inny pojawia się dla bsNone, inny dla bsSingle, inny dla bsSizeable... I to pomimo faktu, że w konstruktorze narzucam mu bsNone, który po odpaleniu aplikacji faktycznie zostaje wykonany (próbowałem na różne sposoby, także przez SetWindowLong i zmianę WindowStyle). tylko co z tego, skoro rozmiar klienta jest już wtedy skopany.

Wydaje mi się więc, że albo muszę odnaleźć pierwotny komunikat, który za to odpowiada, albo po prostu zmienić property BorderStyle jeszcze zanim apka cokolwiek przeliczy. Z tym ClassHelperem to była niezła sugestia, tylko mój kompilator (Delphi 7), tego nie ma :).

Ja tak to sprawdzam:

procedure TForm1.NonClientPaint(var MSG: TWMNCPAINT);
var
  Rec: TRect;
begin
  inherited;
  GetRgnBox(MSG.RGN, Rec);

  ShowMessage('Width ' + IntToStr(Rec.Right - Rec.Left));
  ShowMessage('Height ' + IntToStr(Rec.Bottom - Rec.Top));
end;

Potem w Buttonie ustawiam sobie coś takiego:

 Width:= 700

Okienko i tak wypluwa mi pierwotne Width = 1200.

0

Nie wiem czemu, ale Windows uznaje narysowanie ramek za priorytet i stawia go wyżej, niż np. zmianę rozmiaru klienta czy przerysowanie kontrolek w nim umiejscowionych.

To jest akurat zrozumiałe. Ramka okna (zwłaszcza domyślna, windowsowa) jest przez użytkownika postrzegana bardziej jako element systemu niż element programu. Jeśli błędne działanie programu miałoby oznaczać psucie się ramki, ludzie by krzyczeli „jaki głupi ten Windows, jak program zmuli to nawet ramki okna nie potrafi prawidłowo narysować”. Dlatego ramka psuje się jako ostatnia ;-) /no, chyba że akurat przy ramce coś celowo grzebiesz/

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