furious programming

  1. senior wheel reinventer
  2. Tuchów
Klikam trochę – w Lazarusie.
furious programming
2019-07-22 17:06

Dawno nie pisałem o różnych ciekawych bazgrołach graficznych, związanych z programowaniem komponentów dla aplikacji okienkowych. Od ostatniego wpisu minęły prawie dwa lata, więc dziś kolejny w tej tematyce. Ale nie o komponentach będzie – tym razem pobawimy się oknem i jego właściwościami. ;)


Splash Screen z półprzezroczystą grafiką

Wiele programów podczas rozruchu wyświetla tzw. splash screen, czyli okienko wizytówki, najczęściej z logotypem programu, kilkoma informacjami i czasem z postępem ładowania danych. Z reguły są to prostokątne okienka bez obramowania, zawierające w sobie jedynie obraz ładowany z zasobów. W końcu służy głównie do tego, aby pokazać użytkownikowi że program startuje i musi poczekać.

Ale niektóre aplikacje mają bardziej skomplikowaną wizytówkę, o nieregularnym kształcie, z tłem w postaci obrazu wykorzystującego półprzezroczystość (jako kanał alpha w 32-bitowej grafice) i antialiasing. Dzięki temu możliwe jest wyświetlenie okna, które nie tylko charakteryzuje się gładkimi, rozmytymi krawędziami, ale też może rzucać cień na zawartość znajdującą się pod oknem. Efekt jest taki, jakby półprzezroczystą grafikę po prostu namalować na płótnie pulpitu.

Jak to zrobić? Jak sprawić, aby okno przyjęło kształt grafiki, w dodatku o nieregularnym kształcie? Jak zmusić je do wykorzystywania fragmentów płótna pulpitu znajdującego się pod oknem? I jak sprawić, aby można je było przesuwać, cały czas używając odpowiedniego obszaru tła w miejscach półprzezroczystych?


1. Przygotowanie obrazu tła

Żeby móc sprawdzić czy okno wizytówki spełnia wszystkie założenia, wykorzystamy grafikę charakteryzującą się fragmentami całkiem przezroczystymi, półprzezroczystymi i nieprzezroczystymi. Aby nakładanie półprzezroczystych pikseli na zawartość pod oknem było dobrze widoczne, użyjemy grafiki wykorzystującej antialiasing krawędzi, z dodatkowym cieniem całości.

Żeby długo nie szukać, wejźmy logo 4programmers i je trochę podrasujmy:

0.png

Obraz powinien wspierać kanał alpha, dlatego posłużę się zwykłą, 32-bitową grafiką PNG (choć BMP też by się nadał).


2. Przygotowanie formularza

Do stworzenia takiej wizytówki wystarczy zwykły formularz bez żadnej zawartości. Aby wyświetlał się zawsze na środku ekranu, należy ustawić Position na poScreenCenter (nie poDesktopCenter, bo w przypadku stanowiska wielomonitorowego może się wyświetlić w połowie na jednym i w połowie na drugim ekranie, i niestety sporo programów tak robi…). Aby nie miał obramowania, wystarczy ustawić BorderStyle na bsNone, a żeby wizówka podczas ładowania danych programu nie przepadła w natłoku okien, można ustawić FormStyle na fsSystemStayOnTop.

Goły formularz o dowolnych rozmiarach, nic więcej:

1.png

Rozmiar okna może być zgodny z rozmiarem obrazu tła wizytówki, ale można go dostosować po załadowaniu grafiki do pamięci – dzięki temu nie trzeba będzie dotykać kodu źródłowego, jeśli zechcemy użyć innego, większego lub mniejszego obrazu.

Warto ustalić treść dla tytułu okna, bo choć nie jest ono w nim wyświetlane (jego obramowanie jest wyłączone), to system wykorzystuje je np. podczas przełączania aplikacji za pomocą klawiatury (w popupie po wciśnięciu Alt+Tab). Zapewne też w wielu innych miejscach.


2. Ładowanie obrazu tła do pamięci

W tym przykładzie posłużę się sposobem najbardziej uniwersalnym i najłatwiejszym do testowania, czyli załadowaniem obrazu z pliku znajdującego się obok pliku wykonywalnego programu. Nic nie stoi na przeszkodzie, aby załadować go z zasobów pliku wykonywalnego czy biblioteki DLL, ale tu niech będzie prosto.

Przyda się więc pole do przechowywania grafiki w pamięci, także kontruktor i destruktor do zadbania o pamięć:

type
  TSplashForm = class(TForm) 
  private
    FLogo: TPortableNetworkGraphic;
  private
    procedure InitLogo();
    procedure DoneLogo();
  public
    constructor Create(AOwner: TComponent); override;
    destructor Destroy(); override;
  end;

{..}

constructor TSplashForm.Create(AOwner: TComponent);
begin
  inherited Create(AOwner);
  InitLogo();
end;

destructor TSplashForm.Destroy();
begin
  DoneLogo();
  inherited Destroy();
end;

procedure TSplashForm.InitLogo();
begin
  FLogo := TPortableNetworkGraphic.Create();
  FLogo.LoadFromFile('logo.png');
end;

procedure TSplashForm.DoneLogo();
begin
  FLogo.Free();
end;

3. Aktualizacja właściwości okna

Po załadowaniu obrazu do pamięci, najpierw należy dostosować rozmiar okna do wymiarów grafiki. Dodajmy nową metodę i dorzućmy jej wywołanie do kontruktora klasy okna:

type
  TSplashForm = class(TForm)
  private
    procedure InitForm();
  {..}
  end;

{..}

procedure TSplashForm.InitForm();
begin
  SetBounds(Left, Top, FLogo.Width, FLogo.Height);
end;

Teraz, skoro już obraz znajduje się w pamięci i formularz ma odpowiedni rozmiar, można się zabrać za aktualizację właściwości okna. Jednak zamiast samemu kombinować z ręcznym pobieraniem obszaru płótna pulpitu i renderowaniem zawartości gdzieś w metodzie Paint czy WMPaint, można nakazać systemowi, aby sam zajął się wszystkim – niech się w końcu do czegoś przyda. Dzięki temu okno nie tylko będzie posiadało odpowiedni kształt (zgodny z zawartością obrazu tła), ale też system zajmie się odpowiednim odmalowywaniem jego zawartości. Rozwiązanie krótkie i wygodne.

Aby było to możliwe, należy skorzystać z funkcjonalności wielowarstwowych okien, czyli ze stylu WS_EX_LAYERED. Do samej aktualizacji wielowarstwowego okna służy funkcja UpdateLayeredWindow, dostępna już w Windows 2000. Niestety Lazarus jak zwykle nie posiada importu tej funkcji w module Windows, więc trzeba ją sobie dociągnąć samemu:

function UpdateLayeredWindow(
  hWnd: HWND;
  hdcDst: HDC;
  pptDst: PPOINT;
  psize: PSIZE;
  hdcSrc: HDC;
  pptSrc: PPOINT;
  crKey: COLORREF;
  pblend: PBLENDFUNCTION;
  dwFlags: DWORD
): BOOL; stdcall; external 'user32.dll';

Teraz wystarczy dodać sobie kolejną metodę, która zaktualizuje styl i właściwości wielowarstwowego okna:

type
  TSplashForm = class(TForm)
  private
    procedure UpdateForm();
  {..}
  end;

{..}

procedure TSplashForm.UpdateForm();
var
  WindowExStyle: LONG;
var
  LogoBlend: BLENDFUNCTION;
  LogoLocation: POINT;
  LogoSize: SIZE;
begin
  WindowExStyle := GetWindowLong(Handle, GWL_EXSTYLE);

  if WindowExStyle and WS_EX_LAYERED = 0 then
    SetWindowLong(Handle, GWL_EXSTYLE, WindowExStyle or WS_EX_LAYERED);

  LogoBlend.BlendOp := AC_SRC_OVER;
  LogoBlend.BlendFlags := 0;
  LogoBlend.SourceConstantAlpha := 255;
  LogoBlend.AlphaFormat := AC_SRC_ALPHA;

  LogoLocation := Windows.POINT.Create(0, 0);
  LogoSize := Windows.SIZE.Create(FLogo.Width, FLogo.Height);

  UpdateLayeredWindow(Handle, 0, nil, @LogoSize, FLogo.Canvas.Handle, @LogoLocation, 0, @LogoBlend, ULW_ALPHA);
end;

Najpierw pobieramy styl okna i sprawdzamy czy jest wielowarstwowe – jeśli nie to ustawiamy mu odpowiedni styl za pomocą SetWindowLong, podając nowy zestaw flag. Następnie wypełniamy strukturę dotyczącą sposobu obsługi przezroczystości grafiki, określamy rozmiar i lokalizację grafiki i na koniec wołamy funkcję aktualizującą, podając wszystkie dane w parametrach.


4. Efekt działania kodu

Powyższy kod działa jak należy, tworzy okno zgodnie z zawartością obrazu tła (tu: o nieregularnym kształcie, z całkowicie przezroczystymi obszarami wewnątrz i na zewnątrz) i dobrze obsługuje przezroczystość:

2.png

Ale jest jeden problem – o ile rozmyte krawędzie okna są półprzezroczyste, to niektóre piksele źle łączą się z tłem pod oknem, tworząc brzydką, jasną obwódkę. Najlepiej jest to widoczne na kolorowym tle, bo na jasnym (albo wręcz białym) można nie zauważyć i klienci będą się turlać ze śmiechu:

3.png

Trzeba więc to naprawić.


5. Przemnożenie składowych pikseli

Aby pozbyć się niechcianego defektu, należy ręcznie przemnożyć składowe wszystkich pikseli przez odpowiadającą im wartość kanału alpha. Do tego celu jak zwykle można skorzystać z właściwości ScanLine i zaktualizować dane obrazu bezpośrednio modyfikując dane w bloku pamięci, w którym zapisane są wartości kanałów.

Pocukrujmy trochę składnię, implementując ten algorytm w postaci metody helpera dla klasy obrazu:

type
  TPortableNetworkGraphicHelper = class helper for Graphics.TPortableNetworkGraphic
  public
    procedure PremultiplyChannels();
  end;

{..}

procedure TPortableNetworkGraphicHelper.PremultiplyChannels();
type
  PPNGLine = ^TPNGLine;
  TPNGLine = packed array [UInt16] of packed record B, G, R, A: UInt8; end;
var
  Line: PPNGLine;
  LineIndex, PixelIndex: Integer;
begin
  BeginUpdate();
  try
    for LineIndex := 0 to Height - 1 do
    begin
      Line := ScanLine[LineIndex];

      for PixelIndex := 0 to Width - 1 do
        with Line^[PixelIndex] do
          if (A > 0) and (A < 255) then
          begin
            R := R * (A + 1) shr 8;
            G := G * (A + 1) shr 8;
            B := B * (A + 1) shr 8;
          end;
    end;
  finally
    EndUpdate();
  end;
end;

Samą aktualizację wartości składowych przeprowadza się tylko na półprzezroczystych pikselach. Tych całkowicie przezroczystych i całkowicie nieprzezroczystych ruszać nie trzeba. Metodę tę najlepiej jest wywołać tuż po załadowaniu obrazu do pamięci, aby mieć pewność, że faktycznie jest wywoływana:

procedure TSplashForm.InitLogo();
begin
  FLogo := TPortableNetworkGraphic.Create();
  FLogo.LoadFromFile('logo.png');
  FLogo.PremultiplyChannels();
end;

Po tych wszystkich poprawkach okno nareszcie wygląda tak jak powinno – nie ma żadnych glitchy:

4.png


6. Końcowy kod

Pełny kod modułu wrzucam na Pastebin. Dodałem do niego obsługę lewego przycisku myszy, tak aby można było złapać okno i je przesuwać po ekranie, co było bardzo przydatne podczas szukania najlepszego rozwiązania.

Zaletą tego rozwiązania jest fakt, iż nawet jeśli ładowanie danych nie będzie synchronizowane, system i tak zadba o odmalowywanie okna. Czyli ogólnie pisząc, jeśli splash będzie zamrożony, to przesunięcie nad nim innego okna nie spowoduje zglitchowania jego zawartości.


Podsumowanie

Po długim a ciężkim, wizytówka wygląda super. Użyte rozwiązanie sprawuje się świetnie, przy relatywnie niedużej liczbie linijek kodu do napisania. Co prawda można zrobić to inaczej, np. obsługując komunikat WM_ERASEBKGND, jednak sposób ten zmusza do ręcznego pobierania obszaru tła i jest wrażliwy na przemieszczanie okien pod oknem wizytówki. Poza tym komunikat ten nie jest wysyłany przez system podczas przesuwania okna, więc trzebaby taką sytuację wymuszać, co nie jest proste. Oczywiście nie w przypadku wizytówki, bo taka funkcja nie jest jej potrzebna, ale nie tylko do wizytówek wykorzystuje się półprzezroczystość okna.

Teraz każdy z Was może w prosty sposób zrobić w swoim programie wizytówkę wyglądają profesjonalnie. Pamiętam jak dawno temu urzekł mnie splash screen komunikatora AQQ, w postaci właśnie półprzezroczystego logotypu – robił wrażenie. Nie zajmowałem się wtedy programowaniem, więc długo zastanawiałem się jak oni to zrobili. No a teraz już nie muszę. :]

#free-pascal #lazarus

Silv

Zastanawiam się, czy w Twoich ostatnich wpisach nie bywa więcej niż 10 000 znaków?

furious programming

Limitem jest 12.000 i przekroczyłem go – dlatego wrzuciłem pełny kod modułu na Pastebin i przeszło. ;)

Silv

Ciekawe, a myślałem, że 10 000. Ech, pamięć...

czysteskarpety

i znowu fajne obrazki, zaczynam sie przyzwyczajać

Sunnydev

rzucany cień bardzo fajnie wygląda. Ile zajęło Ci mniej/więcej przemyślenie i napisanie tego splash screena? [offtop]: właśnie teraz odkryłem, że na Twoim awatarze jest kameleon!

furious programming

@Sunnydev: za długo mi to zajęło. Wszystko dlatego, że najpierw próbowałem z komunikatem WM_ERASEBKGND. Wszystko działało, ale później zauważyłem, że przesunięcie okna nie powoduje wysłania tego komunikatu, więc tło malowane w oknie nie jest pobierane na nowo. W rezultacie przesuwałem wycinek pulpitu.

Dopiero później zacząłem szukać informacji w sieci (łącznie z jakimiś niemieckimi forami Delphi) i po kilku godzinach poszukiwań i testów znalazłem przedstawione we wpisie rozwiązanie. A później je poprawiłem, tak aby kod wyglądał czytelnie. Łącznie ten splash pochłonął wczoraj ze sześć godzin (nie licząc przerw na odpoczynek) – siedziałem nad nim prawie do piątej rano. Bo nie chciałem mieć byle czego – musiałem znaleźć najlepsze rozwiązanie. :]

Natomiast oryginalnego kalemeona znajdziecie np. tutaj, gdyby ktoś potrzebował.

furious programming

Zapomniałem dodać – to że okno wygląda i zachowuje się ładnie, wcale nie oznacza, że nie będzie problemu z jego pokazaniem na ekranie przed ładowaniem danych i tworzeniem głównego okna. Bo będą problemy – w końcu to wspaniałe LCL nigdy nie może działać zadowalająco.

Musiałem posprzątać bajzel jaki mi się zrobił w kodzie, gdy próbowałem zmusić ten badziew do poprawnego działania. I zmusiłem, choć trwało to ze dwie godziny. Ale to już materiał na kolejny wpis z cyklu #lazabug, bo za dużo tego na komentarz.

no_solution_found

czy taki ekran jest Ci naprawdę potrzebny? Aż tak długo Ci apka startuje? :D

karpov

Jak zwykle klasa :) Dzięki za wpis!

furious programming

@no_solution_found: poniekąd tak. Aplikacja wymaga istnienia w pamięci ponad dwudziestu obrazów PNG w rozdzielczości 720p. Podczas działania program musi być gotowy natychmiast z nich skorzystać, więc dynamiczne doładowywanie nie wchodzi w grę, ze względu na opóźnienia odczytu z dysku.

Dlatego wszystkie obrazy ładowane są do pamięci podczas rozruchu – w pamięci zajmują około 100 megabajtów. A to powoduje, że aplikacja startuje około pięciu sekund z mojego peceta. Splash nie byłby konieczny gdyby nie to, że aplikacja jest przenośna, a jej rozruch z pendrive'a może zająć dużo, dużo dłużej. ;)

winuser

@furious programming: Z ciekawosci - uzywasz Windows XP na codzien, czy tylko na potrzeby projektow "for fun"?

furious programming

@winuser: używam WinXP na głównym stanowisku pracy (najwięcej czasu spędzam właśnie na nim) w połączeniu z dwoma innymi laptopami z Win7 i Win10. Tych ostatnich używam głównie do testowania rozwijanego oprogramowania i korzystania z programów, które nie są kompatybilne z dziadkiem XP lub działają na nim zbyt wolno.

Azarien

@furious programming: czy przyglądałeś się funkcji WinAPI AlphaBlend? bo wydaje mi się że mniej-więcej do tego służy.

furious programming

Tak, ale czytałem pobieżnie i szybko zainteresowałem się innymi. Jednak wybrałem to co wybrałem, dlatego że potrzebowałem szybkiego i sprawdzonego rozwiązania, bo i tak straciłem na to okienko za dużo czasu.

furious programming
2019-07-19 23:53

No to kolejny wpis, bo widzę że bugów i niedoróbek związanych z kontrolką TCheckListBox wyłania się coraz więcej. Im dłużej rozwijam program wykorzystujący te kontrolki, tym coraz to nowsze błędy mnie nękają… :/


W poprzednim wpisie pokazałem co mi wyszło z redefiniowania obsługi komunikatów dotyczących lewego przycisku myszy i ogólnie z łatania bugów i bubli w kodzie tego komponentu. Ucieszyłem się, że w końcu kontrolka zachowuje się przyzwoicie i przedstawia dane w odpowiedni sposób. Ale żeby nie było zbyt dobrze, niedługo po napisaniu tego wpisu zauważyłem, że w niektórych przypadkach komponent zachowuje się inaczej niż przed moimi modyfikacjami.

Ogólnie rzecz ujmując, subclassingiem nadpisałem całą obsługę pojedynczego wciśnięcia lewego przycisku myszy, czyli obsługę komunikatu LM_LBUTTONDOWN. Jak się okazało, przez ten zabieg kilka standardowych funkcji lewego przycisku przestało działać. Np. w zdarzeniu OnMouseDown komponentu miałem kod, który w momencie wciśnięcia przycisku myszy sprawdzał koordynaty kursora i jeśli pod kursorem nie było żadnego itema, ustawiał bieżący indeks na -1, co owocowało odznaczeniem wszystkich zaznaczonych pozycji (jeśli takowe istniały).

No i po moich poprawkach kod tego zdarzenia przestał działać – nagle metoda ItemAtIndex zaczęła zawsze zwracać wartość inną niż -1, czyli mój kod w rezultacie przestał spełniać swoje założenie. Klikając w puste miejsce komponentu, automatycznie zaznaczana jest ostatnia pozycja. Nie będę tego poprawiać – trudno, niech komponent działa tak jak standardowy.


Podpiąłem więc logikę do kontrolek i przetestowałem wszystkie funkcje – dodawanie danych, edycję, usuwanie zaznaczonych, blokowanie edycji i usuwania pozycji zablokowanych. Wszystko było w najlepszym porządku. Oczywiście do czasu – dziś sprawdziłem jak kontrolka się zachowa na większej liczbie danych. No i dostałem w twarz czymś takim:

0.png

Dlaczego tekst znów wyłazi poza obszar dla niego przeznaczony?!

Dodanie pozycji do listy wymusza automatyczne obliczenie jej rozmiaru, czyli pośrednio wołane jest zdarzenie OnMeasureItem, w którym wykonywany jest mój kod przeprowadzający obliczenia wysokości dla itema, na bazie danych które ma wyświetlać (pochodzą z zewnętrznej, generycznej listy obiektów). To tak w dużym skrócie.

W momencie wypełniania komponentu danymi, ten nie wie, że wszystkie pozycje nie zmieszczą się w jego obszarze, a więc nie wie, że jak trzeba będzie wyświetlić pionowy scrollbar, to szerokość itemów będzie mniejsza. Widać nie wie tego, bo podczas dodawania danych, jego funkcja ItemRect zwraca obszar itema bez uwzględnienia szerokości scrollbara. Mój kod liczy wysokość na pełnym obszarze, komponent wyświetlany jest na ekranie, przeliczana jest łączna wysokość itemów i dodawany jest pionowy scrollbar. Ponowna rekalkulacja wysokości itemów po wyświetleniu scrollbara nie jest wykonywana, więc wszystkie itemy są zgliczowane.

Zacząłem więc kombinować i za pomocą funkcji z WinAPI sprawdzać czy w momencie wykonywania mojego kodu obliczającego wysokość itema scrollbar jest widoczny – GetWindowLong się kłania. Okazuje się, że jest widoczny. Idę dalej, skoro jest widoczny, to pobieram jego szerokość funkcją GetSystemMetrix i zmniejszam offset prawej krawędzi obszaru itema – to powinno załatwić sprawę. No i załatwiło:

1.png

Jeszcze większa kupa niż wcześniej… Teraz item jest zbyt wąski, a tekst wyłazi jeszcze bardziej poza jego obszar…


Na całe szczęście można ten problem rozwiązać wykorzystując to co mam, czyli subclassing i metodę Changed, która woła metodę wymuszającą rekalkulację wysokości itemów. W poprzednim wpisie ten kod znajdował się jeszcze w metodzie ChangeBounds, ale sobie go wydzieliłem do osobnej metody i zrobiłem ją publiczną, bo używam jej obecnie w kilku miejscach. Ale nie można tej metody wywołać w konstruktorze formularza, bo niczego to nie zmieni. Trzeba to zrobić później – wybrałem zdarzenie OnShow, nareszcie wszystko gra:

2.png


Niestety aby komponenty TCheckListBox (z moimi poprawkami rzecz jasna) zachowywały się prawidłowo przez cały czas życia aplikacji, za każdym razem gdy zmienia się wyświetlane w nich dane, trzeba dodatkowo wywołać tę cholerną metodę Changed, bo inaczej itemy zaczną się glitchować. Podejrzewam, że w dalszych testach wylezą następne problemy z tymi komponentami. Im więcej tym lepiej – będzie o czym pisać.

#free-pascal #lazarus #lazabug

Silv

Możesz tak przyzwyczaić się do tych lorem ipsum, że nie zauważysz, jak aplikacja pójdzie na produkcję z nimi. ;)

furious programming

Nie ma szans – wszystkie listy domyślnie mają być puste. ;)

Silv

A testy do tej pustości masz?

furious programming

No ba, w dodatku wszystkie organoleptyczne – nic mi nie umknie.

Silv

A na Linuksie będzie dało się to uruchomić?

furious programming

Nie – targetem jest wyłącznie Windows. Obecnie aplikacja jest kompatybilna z WinXP, Vista, 7, 8 i 10. Ale jak już kiedyś wspomniałem, jest przeznaczona dla wąskiego grona odbiorców – nawet bardzo wąskiego, raptem kilkadziesiąt osób na świecie.

Silv

Ech, i nici z mojego szukania bugów.

furious programming

Miałbyś szansę co nieco znaleźć, bo aplikacja jest pisana w pośpiechu – bez projektowania. Może nie bugi, ale niezabezpieczone kawałki kodu, wykonujące jakieś egzotyczne operacje czy te wykonywane w awaryjnych sytuacjach.

Jeśli użytkownik nie będzie majstrował ręcznie przy plikach to się nic nie wysypie, bo wszystkie operacje do których daje użytkownikowi interfejs są odpowiednio zabezpieczone i nawet jeśliby bardzo chciał, to klikaniem niczego nie zepsuje.

furious programming
2019-07-18 17:22

Wczoraj pisałem o walce z poprawną obsługą dynamicznie mierzonych itemów w komponentcie TCheckListBox oraz z naprawieniem kodu odpowiedzialnego za obsługę lewego kliknięcia myszą w obrębie itema, w celu zmiany stanu checkboxa. Dzisiejsze testy ostatecznie potwierdziły poprawność działania moich łatek, a przy okazji sprawdziłem też czy mój kod odpowiedzialny za renderowanie itemów działa prawidłowo – działa elegancko.

Pomyślałem więc, że zamiast dodawać kolejne zrzuty do tamtego wpisu, który i tak już jest dość krowiasty, wzamian napiszę nowy. Ale nie tylko po to, aby pokazać co mi z tego wyszło – jest jeszcze jedna rzecz, która psuła interfejs mojego programu. Ale tym razem nie z winy błędów w LCL, a w związku ze specyfiką działania komponentów z rodziny ListBox.

Tak więc końcowy efekt przedstawia się następująco, na trzech głównych systemach Windows:

0.png

Kilka pierwszych itemów jest zablokowanych, co oznacza, że checkbox w nich nie może zmieniać zaznaczenia – nie da się tego zrobić ani za pomocą myszy, ani klawiatury. Samo blokowanie robi się za pomocą właściwości ItemEnabled. Założenie jest takie, że x wbudowanych pozycji nie może być usuniętych, a mechanizm usuwania itemów bazuje właśnie na stanie checkboxów.


Itemy w komponencie zachowują się poprawnie, zawartość itemów renderowana jest prawidłowo, zaznaczanie i odznaczanie checkboxów za pomocą myszy działa perfekcyjnie (co do piksela). Ale jest jeden haczyk – podczas rozciągania komponentu, system nie wysyła do niego komunikatów nakazujących przekalkulowanie wysokości itemów, gdy użyty jest styl lbOwnerDrawVariable. To powoduje, że rozciągnięty komponent posiada itemy o wysokość ustalonej podczas dodawania pozycji i albo tekst wyjeżdża pod obszar dla tekstu (gdy kontrolka została ”skurczona” w poziomie), albo pozostają brzydkie, puste pola (gdy została rozciągnięta w poziomie):

1.png

Aby rozwiązać ten problem, należy zmusić komponent do rekalkulacji wysokości itemów w trakcie rozciągania komponentu. I tutaj z pomocą przychodzi metoda ChangeBounds, która jest wywoływana za każdym razem, gdy rozmiar lub położenie kontrolki zmieni się choćby o piksel. Przykładowo, płynne rozciągnięcie komponentu np. o 20px w poziomie oznacza wywołanie tej metody dwadzieścia razy. Pozwala to wykonywać dany kod na bieżąco podczas modyfikacji obszaru komponentu – i tego właśnie potrzebujemy.

Wczoraj zmuszony zostałem do subclassowania TCheckListBox, aby połatać buble w jego kodzie, więc skorzystamy z tej klasy do nadpisania metody ChangeBounds i zdefiniowania kodu forsującego rekalkulację wysokości pozycji. Wiele kodu pisać nie trzeba:

type
  TCheckListBox = class(CheckLst.TCheckListBox)
  protected
    procedure ChangeBounds(ALeft, ATop, AWidth, AHeight: Integer; AKeepBase: Boolean); override;
  end;

{..}

procedure TCheckListBox.ChangeBounds(ALeft, ATop, AWidth, AHeight: Integer; AKeepBase: Boolean);
var
  ControlItemIndex: Integer;
  ControlItemHeight: Integer;
begin
  inherited ChangeBounds(ALeft, ATop, AWidth, AHeight, AKeepBase);

  if Style = lbOwnerDrawVariable then
    for ControlItemIndex := 0 to Items.Count - 1 do
    begin
      MeasureItem(ControlItemIndex, ControlItemHeight);
      Windows.SendMessage(Handle, LB_SETITEMHEIGHT, ControlItemIndex, ControlItemHeight);
    end;
end;

Najpierw wołamy bazową metodę za pomocą inherited, która wykona aktualizację położenia i rozmiaru komponentu. Następnie sprawdzamy czy komponent posiada ustawiony styl lbOwnerDrawVariable i jeśli tak, w pętli wykonujemy obliczenia. Klasa TCheckListBox posiada publiczną metodę MeasureItem, która pośrednio wywołuje zdarzenie OnMeasureItem, czyli wykonuje nasz kod przeprowadzający obliczenia. Po pobraniu nowej wysokości wysyłany jest komunikat do komponentu, w którym dostarczamy indeks pozycji oraz nową jego wysokość.

To wszystko – od tej pory komponent automatycznie dba o to, aby pozycje posiadały poprawnie dopasowaną wysokość do swojej zawartości:

2.png


Pozostaje jeszcze zabezpieczyć jedną rzecz. Zawartość wszystkich itemów komponentu może być modyfikowana. W przypadku tych zablokowanych, zmienić można niektóre dane (w tym tytuł i treść itema), ale pozycje te nie mogą być usunięte.

Aby komponent dopasował rozmiar pozycji po aktualizacji danych, wystarczy wywołać metodę ChangeBounds, podając w parametrach bieżącą pozycję i rozmiar komponentu. W ten sposób wymusi się rekalkulację wysokości itemów, bez zmiany obszaru zajmowanego przez komponent. Można też napisać sobie dedykowaną metodę do tego celu, tak aby utrzymać czytelność kodu.

Oczywiście należy też pamiętać, że modyfikacja treści wyświetlanej przez pozycje też musi wymuszać ponowne obliczenie obszaru itemów i dodatkowo jeszcze przemalowanie komponentu. W przeciwnym razie znów będziemy mieli do czynienia z pustymi przestrzeniami lub tekstem nie mieszczącym się w obrębie obszaru itema, a brak odmalowania kontrolki będzie powodował zglitchowanie interfejsu.

#free-pascal #lazarus

czysteskarpety

Podobają mi się obrazki, powinieneś częściej dodawać.

furious programming

Następnym razem wkleję same obrazki. ;)

Silv

@furious programming: może @czysteskarpety chodziło o zmianę proporcji zawartości tekst/obrazki na skalę np. 50/50. ;)

furious programming

@Silv: wizualnie by nawet pasowało do tej skali. :]

Silv

No tutaj tak, chyba tak.

cerrato

Ej, Panie Furious... Gonisz innych za pisanie te zamiast to (https://4programmers.net/Mikroblogi/View/53165#comment-53177), a jednocześnie piszesz o "chaczykach"... Ja rozumiem, że szewc bez butów chodzi i takie tam, ale jakieś granice przyzwoitości trzymajmy :P

furious programming

Dobra dobra – czym innym jest literówka popełniona z pośpiechu (bardzo często pisząc szybko robię właśnie takie buble), a czym innym podążanie za durną modą używania te zamiast to. Poprawione.

several

Szkoda, że takie rzeczowe i merytoryczne posty otrzymują mniej kciuków niż moje z #IJustHateMicrosoft bazujące na szyderze i taniej sensacji.

cerrato

@several: a i tak najwięcej łapek dostaje zdjęcie kota ze śmiesznym podpisem :(

karpov

Zdradzisz nad czym pracujesz?

furious programming

@several: podejrzewam, że gdyby to nie był Pascal, to zainteresowanie byłoby większe. ;)


@karpov: nad niedużym narzędziem do zarządzania niewielką ilością danych – raptem kilkadziesiąt ”rekordów”. Funkcjonalność nie jest imponująca, kilka okien z kontrolkami rozsianymi po kilku zakładkach, trochę wyświetlania obrazów i dynamicznego renderowania tekstu/sprajtów. Komponenty, o których napisałem kilka wpisów, służą do czytelnego przedstawiania danych, którymi operuje użytkownik. Przewiduję około 10.000 linijek kodu, nie więcej niż 30 modułów.

Być może napiszę o nim coś więcej za jakiś miesiąc, bo całkiem możliwe że trafi na GitHub. Ale na razie to tajemnica. :P

furious programming

Jeszcze napieszę co najmniej jeden wpis na temat komponentów użytych w tym projekcie (na temat renderowania).

winuser

Raptem kilkadziesiat rekordow i mowisz o 10 tys. linijek kodu oraz 30 modulow? Prychlem grubo. Sztuka dla sztuki. W C++ oraz QT mozna to naklepac w kilka minut.

Silv

@winuser: poczekajmy na odpowiedź @furious programming, ale od siebie zapytam: czy sztuka dla sztuki jest zła?

furious programming

@winuser: a kto powiedział, że ten program ma być prymitywnym edytorem kilkudziesięciu rekordów?

Numerologią się zainteresuj skoro tak dobrze idzie ci wróżenie z liczb – zrobisz większą karierę niż w programowaniu.

winuser

Widze kogos tu zablolalo, klasyczne spiecie goscia ktory potrafi tylko klepac w pascalu (lol). Sam o tym wspominales, ze pracujesz nad nieduzym narzedziem z niewielka iloscia danych raptem kilkadziesiat rekordow. Cos poszlo nie tak w Twojej sieci neuronowej i zapomniales?

furious programming

Może dla ciebie program na 10k linijek jest duży – dla mnie nie. I może dla ciebie liczba rekordów w bazie jest wyznacznikiem wachlarzu funkcjonalności i rozmiaru kodu źródłowego programu – dla mnie nie. Coś poszło nie ta w twojej sieci neuronowej i nie pomyślałeś? Pytanie retoryczne – idź trollować gdzieś indziej.

furious programming
2019-07-17 21:55

Ostatnio pisałem o trzech problemach i bugach związanych z biblioteką komponentów i ogólnie Lazarusem, i niestety czas na kolejny. Jak tak dalej pójdzie to wpisów poruszających takie tematy będzie dużo, więc korzystając z okazji wszystkie będę tagował na wzór wpisów kolegi @several (dotyczących środowiska Visual Studio), tak aby łatwo było je wyfiltrować. Wybrałem sobie tag #lazabug – był wolny i dobrze oddaje istotę sprawy.

No, to czas na właściwą treść – miłej lektury.


Dzisiejszy wpis dotyczyć będzie komponentu klasy TCheckListBox i można go traktować jako kontynuację poprzedniego wpisu na temat ręcznego renderowania zawartości itemów. Nowy problem też dotyczy checkboxów, ale nie ich renderowania – tym razem chodzi o obsługę myszy.

Komponent TCheckListBox to zwykły TListBox – posiada proste itemy w postaci prostokątów z tekstem, ale dodatkowo ma redefiniowany kod odpowiedzialny za renderowanie itemów oraz wyposażony jest w dodatkowe dane. Dane te służą do przechowywania stanu checkboxów (dalej będę je nazywał przyciskami, dla ułatwienia), a metody pozwalają manipulować nimi. Tak więc nie ma żadnych fizycznych kontrolek osadzonych w itemach – to tylko wizualny efekt. Można to poznać po tym, że przyciski te nie reagują na kursor (nie podświetlają się).

Wiemy już, że przycisków tam nie ma. Aby możliwe było ich zaznaczanie myszą, przyjęło się, że cała lewa strona itemu (obszar obok ramki tła zaznaczenia) do tego służy. Cały ten obszar, nie tylko ten na którym maluje się przycisk. Przy wysokich itemach (np. jeśli wysokość każdego z nich ustalamy samemu w zdarzeniu OnMeasureItem) może to być pomocne, bo można kliknąć byle gdzie i przycisk zmieni stan. Może być, bo równie dobrze taka obsługa myszy może mylić użytkownika – w końcu skoro nie klikamy w checkbox, to nie powinien zmieniać stanu. Dla mnie jest to mylące.

Teraz kluczowa sprawa. Kilka komponentów typu TCheckListBox wykorzystuję do przedstawiania różnych danych, z reguły dłuższego tekstu lub wieloliniowych tekstowych wartości, odpowiednio sformatowanych. Te z bardziej skomplikwaną zawartością posiadają ustawiony styl lbOwnerDrawVariable i ustalam itemom wysokość na podstawie zawartości. To pozwala stworzyć wygodny interfejs, dobrze przedstawiający dane użytkownikowi.


WTF na dzień dobry – komponent klasy TCheckListBox nie posiada zdarzenia OnMeasureItem w sekcji published, więc nie da się go wygenerować z poziomu inspektora obiektów, a więc informacje na temat tego zdarzenia nie są przechowywane w pliku formularza (czyli w pliku .lfm). Ktoś widać zapomniał go podbić z sekcji public przy pisaniu kodu końcowej klasy komponentu. :/

Skoro nie można tego zdarzenia wygenerować, trzeba napisać sobie metodę samemu i ją ręcznie podpiąć gdzieś w konstruktorze formularza. Paskudne rozwiązanie, tym bardziej że tych metod musiałbym pisać wiele, co mi zrobi bajzel w kodzie. Wolałem więc dodać brakującą właściwość w kodzie LCL, skompilować tę bibliotekę i przebudować środowisko. No i mam zdarzenie, oczywiście do czasu aktualizacji IDE, bo wtedy moje poprawki zostaną nadpisane i nie będzie się dało skompilować projektu. Ale to tylko chwilowe – byle bym mógł wypuścić release programu, potem się coś pomyśli. Albo odbiorę sobie życie – zastanowię się jeszcze.


Siadam więc do pisania kodu mojego zdarzenia OnMeasureItem, bo czas na renderowanie zawartości itemów. WTF numer dwa – pomimo wybrania stylu lbOwnerDrawVariable i wygenerowania zdarzeń, OnMeasureItem wywoływane jest tylko raz i tylko dla pierwszego itema, przez co wszystkie pozycje mają taką wysokość, jaka zostanie obliczona dla pierwszej pozycji listy. Krew się gotuje…

Zwęszyłem problem – flagi dotyczące stylu itemów są hardkodowane w metodzie CreateParams komponentu, i oczywiście wykorzystywana jest flaga LBS_OWNERDRAWFIXED, która oznacza jednakowy rozmiar wszystkich pozycji:

procedure TCustomCheckListBox.CreateParams(var Params: TCreateParams);
begin
  inherited CreateParams(Params);
  Params.Style := (Params.Style and not LBS_OWNERDRAWVARIABLE) or LBS_OWNERDRAWFIXED;
end;

Rozwiązać to można poprzez subclassing, nadpisanie tej metody i redefiniowanie zestawu flag:

type
  TCheckListBox = class(CheckLst.TCheckListBox)
  protected
    procedure CreateParams(var AParams: TCreateParams); override;
  end;

{..}

procedure TCheckListBox.CreateParams(var AParams: TCreateParams);
begin
  inherited CreateParams(AParams);

  if Style = lbOwnerDrawVariable then
    AParams.Style := (AParams.Style and not LBS_OWNERDRAWFIXED) or LBS_OWNERDRAWVARIABLE;
end;

Całe szczęście, że w momencie wywołania tej metody, właściwość Style komponentu jest już ustawiona na wartość wczytaną z zasobów, czyli w prostych słowach na tę zdefiniowaną w oknie inspektora obiektów. Działa – teraz kontrolka pozwala na ustalanie różnych wysokości itemów:

0.png

To co widać na zrzucie to efekt działania własnych metod renderujących zawartość. Póki co malowane jest tło pozycji (białe), przycisk (zawsze w lewym górnym rogu itema) oraz tło dla tekstu (białe lub w kolorze, według stanu zaznaczenia).

Już po problemie? Gdzie tam, zabawa dopiero się zaczyna…


Ucieszyłem się poprawną wysokością itemów, obliczaną w zdarzeniu OnMeasureItem, ale szybko się okazało, że jest kolejny błąd w kodzie tego komponentu. Jak wcześniej wspomniałem, obszar umożliwiający zmianę stanu zaznaczenia checkboxów za pomocą myszy znajduje się po lewej stronie (na zrzucie wyżej jest to obszar po lewej stronie niebieskiej ramki zaznaczenia). Klikając w obrębie obszaru przeznaczonego dla zawartości (głównie tekstu), stan przycisku nie powinien się zmieniać – to logiczne i tak było w VCL dla Delphi 7.

A jak jest w LCL? A no tak, że obszar ten bywa znacznie szerszy niż sam przycisk… Mało tego, im wyższy jest item, tym jest szerszy! W przypadku gdy item jest bardzo wysoki (lub wyższy niż szerszy), większość jego powierzchni przeznaczona jest do zmiany stanu przycisku za pomocą kliknięcia. Sobie wyobraź, że kliknięcie w tekst powoduje nie tylko podświetlenie pozycji, ale też odwrócenie zaznaczenia checkboxa – użytkownik na pewno doceni ten ficzer… fajna łamigłówka… :/

Kod odpowiedzialny za skopane obliczenia znajduje się w czeluściach widgetsetu, dokładniej w module Win32WSCheckLst, w którym to w sekcji implementation znajduje się funkcja CheckListBoxWndProc. Jest to dodatkowy handler komunikatów, rejestrowany podczas tworzenia uchwytu kontrolki. W nim reaguje się na komunikaty dotyczące lewego przycisku myszy (wciśnięcie oraz dwuklik) i odpalana jest lokalna dla tej funkcji procedura CheckListBoxLButtonDown, która waliduje pozycję kursora podczas wciśnięcia przycisku myszy i odwraca zaznaczenie checkboxa, jeśli kursor znajduje się w odpowiednim obszarze. A obszar odpowiedzialny za zmianę zaznaczenia przycisku obliczany jest takim kwiatuszkiem:

if TCheckListbox(WindowInfo^.WinControl).UseRightToLeftAlignment then
  ItemRect.Left := ItemRect.Right - ItemRect.Bottom + ItemRect.Top
else
  ItemRect.Right := ItemRect.Left + ItemRect.Bottom - ItemRect.Top;

Wygląda znajomo? Tak, wynikiem jest kwadrat. Zawsze. A w przypadku kwadratowego itema, obszar checkboxa przykrywa całą pozycję, więc nie da się podświetlić itema nie zmieniając automatycznie stanu jego checkboxa. Na zrzucie niżej zaznaczyłem je czerwonymi ramkami:

1.png

Genialne, w końcu kto by przypuszczał, że wysokość itemów może być inna niż standardowa…

Rozwiązanie jest proste – korzystając z już i tak używanego subclassingu, wystarczy nadpisać kolejną metodę, tym razem WndProc. Ona odpowiedzialna jest za obsługę wszystkich komunikatów komponentu, więc tam można samemu obsłużyć m.in. WM_LBUTTONDOWN, a dla pozostałych wywołać metodę bazową. Więc sobie taką na(d)pisałem – wykorzystałem oryginalny kod, ale użyłem własnych obliczeń obszaru przycisku (szukaj komentarza):

type
  TCheckListBox = class(CheckLst.TCheckListBox)
  protected
    {..}
    procedure WndProc(var AMessage: TLMessage); override;
  end;

{..}

procedure TCheckListBox.WndProc(var AMessage: TLMessage);
var
  WindowInfo: PWin32WindowInfo;
  ControlItemRect: Windows.Rect;
  ControlItemIndex: Integer;
var
  CursorPos: Windows.Point;
begin
  if (AMessage.msg = LM_LBUTTONDOWN) or (AMessage.msg = LM_LBUTTONDBLCLK) then
  begin
    WindowInfo := GetWin32WindowInfo(Handle);

    if WindowInfo^.WinControl <> nil then
    begin
      CursorPos.X := GET_X_LPARAM(AMessage.lParam);
      CursorPos.Y := GET_Y_LPARAM(AMessage.lParam);

      for ControlItemIndex := 0 to Windows.SendMessage(Handle, LB_GETCOUNT, 0, 0) - 1 do
      begin
        Windows.SendMessage(Handle, LB_GETITEMRECT, ControlItemIndex, PtrInt(@ControlItemRect));

        if Controller.UserInterface.ListBox.PointInCheckBoxRect(ControlItemRect, CursorPos) then // ło tu
        begin
          if ControlItemIndex < TCheckListBox(WindowInfo^.WinControl).Items.Count then
            if TCheckListBox(WindowInfo^.WinControl).ItemEnabled[ControlItemIndex] then
            begin
              TCheckListBox(WindowInfo^.WinControl).Toggle(ControlItemIndex);

              AMessage.msg := LM_CHANGED;
              AMessage.wParam := ControlItemIndex;

              DeliverMessage(WindowInfo^.WinControl, AMessage);
            end;

          Exit;
        end;
      end;
    end;
  end
  else
    inherited WndProc(AMessage);
end;

Uradowany kompiluję projekt, odpalam i… szlag mnie trafia – moja obsługa komunikatów jest wykonywana, ale i tak domyślny handler robi swoje (choć w ogóle nie powinien być używany, skoro go nadpisałem). Nie mam pojęcia o co chodzi. Z tego co można wywnioskować po kodzie LCL, procedura obsługi komunikatów sama w sobie jest subclassowana, przez co cuda się dzieją z tymi komunikatami. Nie dałem rady zablokować domyślnych mechanizmów…

Zamiast męczyć się z mechanizmami komponentu i dodatkowo z Windows API, zaremowałem fragment kodu domyślnego handlera, który odpowiedzialny jest za zmianę stanu checkboxów. Po prostu. Skoro nie można normalnie obsługiwać komunikatów, to walić kod widgetsetu.


Od tej pory komponent zachowuje się tak jak powinien. Mało tego, moje modyfikacje powodują, że myszą można zmienić stan zaznaczenia checkboxa tylko jeśli kliknie się w przycisk. Kliknięcie w puste pole wokół checkboxa tylko podświetla pozycję. Takie zachowanie komponentu nie jest zgodne z założeniami, ale za to jest zgodne z tym co widzi użytkownik na ekranie, więc w mojej ocenie jest lepsze, bo intuicyjne.

Gdybym miał na to czas, to dorzuciłbym sobie jeszcze obsługę hovera, tak aby checkboxy ładnie się podświetlały po najechaniu nań kursorem, czyli aby zachowywały się tak samo jak zwykłe checkboxy. Ale nie mam na to czasu – deadline się zbliża wielkimi krokami.

Całe szczęście, że kod biblioteki komponentów jest dostarczony wraz ze środowiskiem i każdy ma pełny dostęp do niego, a tym samym każdy może sobie poprawić czy dodać to co jest mu potrzebne. Albo połatać bugi… Gdybym używał Delphi, tym bardziej wersji CE, to byłbym w czarnej dupie.

#free-pascal #lazarus #lazabug

cerrato

to tylko wizualny efekt. Można to poznać po tym, że przyciski nie reagują na kursor – nie podświetlają się po najechaniu kursorem - ale to o niczym nie świadczy, przecież kontrolka-rodzic może reagować na ruchy myszą i jeśli wykryje, że kursor jest w określonym miejscu, może wykonać animację/podświetlenie/cokolwiek w odniesieniu do checkboxa (albo jakiegokolwiek innego elementu).

furious programming

Tak, o tym napisałem w końcowej części tego wpisu.

cerrato

No ale nie do końca napisałeś to samo. Pod koniec dodałeś informację, że jakbyś miał więcej czasu, to byś zrobił hover dla przycisku. A mi chodzi nie o to, że się nie podświetla, ale o to, że brak efektu podświetlenia wcale nie wskazuje jednoznacznie, czy checkbox jest realną kontrolką, czy jedynie kawałkiem obrazu

furious programming

Wszystkie systemowe przyciski obsługują stan hover – jeśli nie ma efektu to wiedz, że coś się dzieje.

cerrato

No w sumie słuszna uwaga. Aczkolwiek miałem na myśli coś innego. Bo można skorzystać z czysto systemowych elementów, albo zrobić coś własnego. W przypadku robienia czegoś własnego, można albo zrobić tak, jak to opisałeś, albo wsadzić tam jakąś inną kontrolkę, która będzie odpowiadać za obsługę i wyświetlanie checkboxa (coś na kształt TPanel chociażby). I miałem na myśli tą drugą opcję, czyli wsadzenie tego przykładowego Panela. Wtedy już nie masz efektu rysowania sobie gdzieś na formatce/komponencie, tylko masz dodatkowy obiekt, który odpowiada za obsługę checkboxa, ale przy tym wcale nie jest powiedziane, że taki obiekt musi reagować na ruchy myszą. Nie wiem, czy dobrze udało mi się oddać to, co chciałem przekazać i czy mnie rozumiesz ;)

furious programming

Gdybym miał samemu napisać odpowiednik TCheckListBox, to bym skorzystał z osadzonych przycisków, a nie bawił się w udawanie że tam są. Trochę więcej kodu trzebaby naklepać, ale końcowy efekt byłby elegancki. Albo po prostu prawidłowy z punktu widzenia użytkownika.

furious programming

Jutro (po testach) dorzucę zrzuty z konkretną zawartością, zrobione na trzech wersjach Windows.

furious programming

Nowy wpis (ze zrzutami) znajduje się tutaj – Wczoraj pisałem o walce z po.... ;)

furious programming
2019-07-14 02:07

Dziś nowy wpis na temat dostosowywania komponentu do systemowego wyglądu. Pierwszy wpis był na temat kontrolki TCheckListBox i ręcznego malowania itemów, a dziś co nieco na temat TTreeView.


Program operuje na zbiorze grafik, znajdującym się w podkatalogu graphics\grounds\. Oprócz wykorzystywania stałego zbioru ponad dwudziestu obrazów, możliwe jest dodawanie nowych i używanie ich w programie. W takim przypadku grafiki użytkownika muszą się znaleźć w tym podkatalogu, bezpośrednio lub zgrupowane w kolejnych podkatalogach (poziom zagłębienia dowolny). Muszą się znaleźć w tym podkatalogu, dlatego że program jest w wersji portable, stąd katalog użytkownika odpada. Drugie wymaganie jest takie, że biblioteka obrazów musi się składać wyłącznie z obrazów w formacie PNG o rozdzielczości 1280×720 pikseli, czyli zgodnych z rozdzielczością 720p. Kolejne założenia są dość niestandardowe.

Okno do wyboru pliku z dysku zawsze musi się pojawiać w przewidzianym miejscu na ekranie (najlepiej wycentrowane w stosunku do okna wywołującego). Musi i koniec. Dlatego też zmuszony byłem do stworzenia własnego okna dialogowego, dlatego że OpenDialog pojawia się tam gdzie system to uzna za stosowne – czyli raz tu, raz tam. Z tego co mi wiadomo, nie da się w 100% kontrolować jego pozycji, tak samo jak systemowego okna typu MessageBox (którego swoją drogą też zreprodukowałem).

Aby użytkownik mniej więcej orientował się w strukturze plików składających się na bibliotekę obrazów, skorzystamy z komponentu typu TTreeView. I tu jest drugie niestandardowe założenie. Drzewo plików musi zawierać wszystkie pliki PNG zawarte wewnątrz katalogu graphics\grounds\, ale jednocześnie nie pokazywać innych podkatalogów folderu graphics\ i ich zawartości. Dlatego też dwa pierwsze węzły drzewa będą stałe – korzeniem będzie węzeł graphics, a jego dzieckiem grounds. Wszystkie nazwy plików muszą więc być zawarte wewnątrz węzła grounds, bezpośrednio lub pośrednio.


Do wypełnienia komponentu danymi, czyli stworzenia drzewa zawartości ww. katalogu, wystarczy kilka prostych metod, w tym jedna rekurencyjna. Ale od początku – najpiew metoda tworząca ścieżkę węzłów:

function TGroundsListDialog.AddGroundNodes(const APath: String): TTreeNode;
var
  NodeNames: TStringList;
  NodeName: String;
  NodePath: String = '';
begin
  Result := nil;

  NodeNames := TStringList.Create();
  try
    if ExtractStrings(AllowDirectorySeparators, [' '], PChar(APath), NodeNames) > 0 then
      for NodeName in NodeNames do
      begin
        Result := AddGroundNode(Result, NodePath, NodeName, True);
        NodePath += NodeName + '\';
      end;
  finally
    NodeNames.Free();
  end;
end;

Nic trudnego. Podział łańcucha ścieżki według separatora, następnie iteracyjne stworzenie węzłów i nadpisywanie referencji bieżącego. Teraz metoda odpowiedzialna za stworzenie węzła, wypełnienie danymi wejściowymi i dodanie go do drzewa:

function TGroundsListDialog.AddGroundNode(AParentNode: TTreeNode; const APath, AName: String; AIsFolder: Boolean): TTreeNode;
begin
  if AParentNode = nil then
    Result := FForm.CGroundsTreeView.Items.AddFirst(nil, AName)
  else
    Result := FForm.CGroundsTreeView.Items.AddChild(AParentNode, AName);

  {..}
end;

Nie wszystkie parametry są użyte, ale napiszę o tym później. Teraz metoda rekurencyjna, przeszukująca zawartość dysku:

procedure TGroundsListDialog.ListGroundPictures(AParentNode: TTreeNode; const APath: String);
var
  FoundItem: TSearchRec;
  FolderNode: TTreeNode;
begin
  if FindFirst(APath + '*', faAnyFile, FoundItem) = 0 then
  try
    repeat
      if (FoundItem.Name = '.') or (FoundItem.Name = '..') then Continue;

      if FoundItem.Attr and faDirectory = faDirectory then
      begin
        FolderNode := AddGroundNode(AParentNode, APath, FoundItem.Name, True);
        ListGroundPictures(FolderNode, APath + FoundItem.Name + '\');
      end
      else
        if LowerCase(ExtractFileExt(FoundItem.Name)) = '.png' then
          AddGroundNode(AParentNode, APath, FoundItem.Name, False);
    until FindNext(FoundItem) <> 0;
  finally
    FindClose(FoundItem);
  end;
end;

Też nic skomplikowanego. Dla każdego znalezionego obiektu w danej lokalizacji sprawdzane jest czy znaleziono katalog i jeśli tak, dodawany jest do drzewa węzeł i następuje rekurencyjne jego uzupełnienie. A jeśli znaleziono plik to waliduje się rozszerzenie i jeśli jest to plik odpowiedniego formatu, zostaje dodany do bieżącego węzła. Ostatnia metoda posłuży do wygenerowania kompletnego drzewa, używając tych powyższych:

procedure TGroundsListDialog.FillControls();
var
  GroundNode: TTreeNode = nil;
begin
  FForm.CGroundsTreeView.Items.BeginUpdate();
  try
    GroundNode := AddGroundNodes('graphics\grounds\');
    ListGroundPictures(GroundNode, 'graphics\grounds\');
  finally
    FForm.CGroundsTreeView.Items.EndUpdate();
    FForm.CGroundsTreeView.FullExpand();
  end;
end;

I gotowe – teraz można się cieszyć wspaniałym drzewkiem reprezentującym strukturę biblioteki obrazów:

0.png

No dobra, o ile zawartość drzewa jest poprawna, to wygląda prymitywnie, więc trzeba dodać ikonki. Ale nie byle jakie ikonki – pozyskamy je z systemu. Aby to wykonać, możemy skorzystać z systemowej funkcji SHGetFileInfo, która służy do pobierania różnych danych na temat pliku, nie tylko ikonki. Rozwińmy więc kod metody AddGroundNode o pobieranie tych danych:

function TGroundsListDialog.AddGroundNode(AParentNode: TTreeNode; const APath, AName: String; AIsFolder: Boolean): TTreeNode;
var
  FilePath: WideString;
  FileInfo: SHFileInfoW;
  FileIcon: TIcon;
begin
  {..}

  Result.ImageIndex := Ord(not AIsFolder);
  Result.SelectedIndex := Result.ImageIndex;
  Result.StateIndex := Result.ImageIndex;

  if AIsFolder and (FForm.CGroundsListImageList.Count > 0) then Exit;
  if not AIsFolder and (FForm.CGroundsListImageList.Count > 1) then Exit;

  FilePath := UTF8ToUTF16(APath + AName);
  SHGetFileInfoW(PWideChar(FilePath), 0, FileInfo, SizeOf(FileInfo), SHGFI_ICON or SHGFI_SMALLICON);

  FileIcon := TIcon.Create();
  try
    FileIcon.Handle := FileInfo.hIcon;
    FForm.CGroundsListImageList.AddIcon(FileIcon);
  finally
    FileIcon.Free();
  end;
end;

Te dwa dziwne warunki z Exitami służą do tego, aby do ImageList nie były dodawane duplikaty. Sama lista nie posiada metod sprawdzających czy obraz już w niej się znajduje (zapewne ze względu na jej wewnętrzny postprocessing związany ze skalowaniem itp.), więc te instrukcje warunkowe służą do pomijania pobierania ikon z systemu, jeśli wcześniej dana ikonka została już pobrana.

No i gotowe, możemy się cieszyć systemowymi ikonkami:

1.png

O ile kogokolwiek cieszą takie z paskudnym, czarnym tłem, którego być nie powinno… :/

Problem leży gdzieś w systemie i można to naprawić używając systemowej funkcji ImageList_ReplaceIcon, wywołując ją zaraz po dodaniu ikony do listy. Tyle że ona w pierwszym parametrze przyjmuje uchwyt ImageList, którego klasa listy nie publikuje w postaci właściwości… Obejściem tego problemu jest namalowanie ikonki na bitmapie i dodanie bitmapy do ImageList – przezroczystość nie zostanie utracona:

function TGroundsListDialog.AddGroundNode(AParentNode: TTreeNode; const APath, AName: String; AIsFolder: Boolean): TTreeNode;
var
  FileBitmap: TBitmap;
begin
  {..}

  FileIcon := TIcon.Create();
  FileBitmap := TBitmap.Create();
  try
    FileIcon.Handle := FileInfo.hIcon;

    FileBitmap.SetSize(FileIcon.Width, FileIcon.Height);
    FileBitmap.Canvas.Brush.Style := bsSolid;
    FileBitmap.Canvas.Brush.Color := FForm.CGroundsListImageList.BkColor;
    FileBitmap.Canvas.FillRect(0, 0, FileBitmap.Width, FileBitmap.Height);
    FileBitmap.Canvas.Draw(0, 0, FileIcon);

    FForm.CGroundsListImageList.Add(FileBitmap, nil);
  finally
    FileIcon.Free();
    FileBitmap.Free();
  end;
end;

Teraz ikonki wyglądają prawidłowo, zachowując oryginalną przezroczystość:

2.png


Niestety Lazarus nie posiada komponentu drzewa plików/katalogów, który by wyglądał jak systemowy i obsługiwał systemowe menu kontekstowe, dlatego trzeba było zrobić swoją kontrolkę i napisać trochę kodu. Szkoda, bo takie Delphi 7 miało ich masę, każda nie tylko przedstawiała nazwy plików czy lokalizacji, ale też sama dbała o pobieranie ikon z systemu. Czasem tęsknię za starym dobrym VCL.

#free-pascal #lazarus #lazabug

stivens

Rozwaliles layout. A na telefonie to w ogole ten wpis jest 3x szerszy niz ekran :P
https://i.imgur.com/l8SstFo.png

cerrato

Rzeczywiście, u mnie (FF na android) taki sam efekt.

furious programming

@stivens, @cerrato: to stary błąd – zgłaszałem go już (https://4programmers.net/Foru[...]zanie_sie_wpisu_na_mikroblogu). Przy długim wpisie, jeśli ukryta jest jego dalsza część, treść nie jest zwijana prawidłowo. Rozwinięcie ukrytej części wpisu naprawia problem. Dotyczy to nie tylko przeglądarek mobilnych, ale też desktopowych.

Coś tam było poprawiane, ale nadal jest problem z łamaniem linii.

MasterOf

Co mnie wołają :D Wiem, że miałem coś robić przy forum ale teraz mam taki zapierdziel, że przełożyłem to trochę w czasie :P

MasterOf

Przeprowadzam się pod koniec sierpnia. Wczoraj wróciłem z Wrocławia, 10h w pociągu, potem spałem całą sobotę prawie. Do tego mam zlecenia, dzwonią do mnie rekruterzy, mam chyba z 3 zadania rekrutacyjne do zrobienia. No wymówki jak nic :P

Silv

@MasterOf: możesz poprawić to jako jedną z pierwszych rzeczy dot. wyglądu (chyba że to by zajęło dużo czasu, to może lepiej zrobić więcej mniejszych zadań na początek). Adam (i inni?) napracował się przy froncie, wygląda naprawdę sensownie, a tu taki bug prosto w oczy.

PrzemysławWiśniewski

TreeView... ile ja nocy przy tym nie przesałem... Dzięki @furious programming fajna robota !

furious programming

Dorzucę jeszcze zrzuty ekranu z innych wersji Windows, tak aby było widać jak drzewko wygląda przy różnych schematach. ;)

Silv

@furious programming: lubię zrzuty ekranu. :) Stoję na stanowisku, że jeden obraz mówi więcej niż tysiąc słów.

furious programming

Dobrze, więc przy okazji kolejnych testów na innych wersjach systemu porobię i dodam więcej zrzutów. :]

Silv

Choć nie wiem, czy jest sens robić to dla mnie. Kod mnie nie interesuje zbyt (mam za dużo własnego kodu JS do skończenia), jedynie zrzuty oglądam i porównuję.

furious programming

I tak miałem je zrobić na potrzeby artykułu, ale podczas poprzednich testów po prostu zapomniałem. A chcę, żeby było tak samo jak w poprzednim moim wpisie na temat interfejsu, bo to drzewko też ma wyglądać jak systemowe.

furious programming

Ok panowie, zrzuty zaktualizowane. ;)

Silv

I gitara. :) Z innej beczki: mnie tam się podobały te ikonki folderów z XP, wyraziste. Co w nich było do zmiany? PS. A może po prostu przyzwyczaiłem się przez 10 lat używania... PS2. Mówię oczywiście o zmianie przez Microsoft, a nie przez Ciebie, @furious programming.

furious programming

W WinXP ikonki były po prostu ładne.

Natomiast w WinVista zrobili jakiś zcudowane (ikonki otwartych folderów z dynamicznie renderowaną zawartością – paskudztwo). W późniejszych wersjach było tylko gorzej. Choć małe ikonki katalogów w Win10 mi się podobają (co innego te płaskie w rzucie izometrycznym – jeszcze większy syf).

Silv

A dla Win10 nie dałeś nazwy kompozycji.

furious programming

Bo to podstawowy schemat – nie wiem czy ma jakąś dodatkową nazwę.

furious programming

Oprócz klasycznego (rodem z Win98) chyba nie – ale nie używam na bieżąco tego systemu, więc musiałbym sprawdzić.

Silv

Tzn. w dokumentacji. :P

Silv

Wygląda na to, że za kompozycję uważa się zarówno kompozycję kolorów, jak i stylów okien... Trochę mało przejrzysty taki podział (tzn. jego brak).

furious programming
2019-07-09 23:24

Chwilowo pracuję nad aplikacją okienkową dla systemów Windows, której interfejs zbudowany jest ze standardowych komponentów, wyglądających tak jak reszta systemu. Ale posiada ona kilka komponentów klasy TCheckListBox, służących do przedstawiania pewnych informacji. Listy te mają być docelowo wyższe niż standardowe i zawierać tekst formatowany – co nieco pogrubione, co nieco wieloliniowe, niektóre z bitmapami. Aby to wykonać, wystarczy zmienić styl komponentu np. na lbOwnerDrawFixed i obsużyć zdarzenie OnDrawItem. Niby nic trudnego, jednak zmiana stylu komponentu powoduje, że checkboxy przestają być malowane:

before.png

Z jednej strony trochę mnie to zdziwiło – kiedyś coś podobnego robiłem w Delphi 7 (zrzuty powinny być gdzieś w starym wątku na forum) i tam checkboxy były i tak malowane przez wewnętrzne mechanizmy, a tylko tekst i tło pod nim trzeba było namalować samemu (przekazywany w parametrze obszar itemka nie uwzględniał checkboxa). Z drugiej strony, nawet to logiczne – w końcu skoro sami mamy malować pozycje to w całości, a nie tylko tekst.

Tylko jak tu namalować checkboxa, tak aby wyglądał tak jak systemowy i skąd wziąć jego rozmiary… hmm… ;)


Rozwiązanie nie jest skomplikowane – trzeba skorzystać z magicznego obiektu ThemeServices, który komunikuje się z systemem, pozwala na pobranie ”detali” systemowych kontrolek oraz na ich renderowanie. Zakładając że cały kod renderujący znajduje się w zdarzeniu OnDrawItem, najpierw trzeba wypełnić item kolorem tła komponentu – nic nadzwyczajnego:

var
  ListBox: TCheckListBox absolute AControl;

{..}

ListBox.Canvas.Brush.Color := Windows.GetSysColor(COLOR_WINDOW);
ListBox.Canvas.FillRect(ARect);

Następnie należy pobrać detale checkboxa za pomocą metod obiektu ThemeServices, następnie pobrać jego wymiary i wyznaczyć dla niego obszar, w którym będzie renderowany i go namalować. Tutaj jest nieco więcej roboty, ale też nic skomplikowanego:

const
  CHECKBOX_STATE: array [TCheckBoxState, Boolean] of TThemedButton = (
    (tbCheckBoxUncheckedDisabled, tbCheckBoxUncheckedNormal),
    (tbCheckBoxCheckedDisabled, tbCheckBoxCheckedNormal),
    (tbCheckBoxMixedDisabled, tbCheckBoxMixedNormal)
  );
var
  CheckBoxDetails: TThemedElementDetails;
  CheckBoxSize: TSize;
  CheckBoxEnabled: Boolean;
var
  CheckBoxRect: TRect;

{..}

CheckBoxEnabled := ListBox.Enabled and ListBox.ItemEnabled[AIndex];
CheckBoxDetails := ThemeServices.GetElementDetails(CHECKBOX_STATE[ListBox.State[AIndex], CheckBoxEnabled]);

CheckBoxSize := ThemeServices.GetDetailSize(CheckBoxDetails);
CheckBoxRect := Bounds(ARect.Left + 4, ARect.Top + (ARect.Height - CheckBoxSize.Height) div 2, CheckBoxSize.Width, CheckBoxSize.Height);

ThemeServices.DrawElement(ListBox.Canvas.Handle, CheckBoxDetails, CheckBoxRect);

Następnie należy wyznaczyć obszar dla tekstu, który nie będzie zawierał obszaru zajmowanego przez checkbox (plus dodatkowy odstęp):

var
  TextRect: TRect;

{..}

TextRect := ARect;
TextRect.Left := CheckBoxRect.Right + 4;

Skoro już wiadomo gdzie znajdować się będzie tekst, należy sprawdzić czy item jest podświetlony i jeśli tak, namalować tło w odpowiednim kolorze:

if odSelected in AState then
begin
  ListBox.Canvas.Brush.Color := Windows.GetSysColor(COLOR_HIGHLIGHT);
  ListBox.Canvas.FillRect(TextRect);
end;

Teraz czas na tekst. Ten przykład dotyczy prostego tekstu, jednoliniowego, wyśrodkowanego w pionie. Do tego celu trzeba pobrać właściwości tekstu i ustawić wyrównanie wertykalne. Kolorów i stylu fontu nie trzeba samemu ustawiać – tym zajmują się wewnętrzne mechanizmy komponentu, wystarczy go po prostu namalować w zadanym obszarze:

var
  TextStyle: TTextStyle;

{..}

TextStyle := ListBox.Canvas.TextStyle;
TextStyle.Layout := tlCenter;

ListBox.Canvas.Brush.Style := bsClear;
ListBox.Canvas.TextRect(TextRect, TextRect.Left + 4, TextRect.Top, ListBox.Items[AIndex], TextStyle);

I to wszystko. No prawie, bo jest jeszcze jedna rzecz – ramka fokusa…

System renderuje itemki w taki sposób, aby ta ramka obejmowała tylko obszar tekstu. Zmiana stylu komponentu i ręczne renderowanie itemków powoduje, że ramka obejmuje cały obszar pozycji, od lewej do prawej. Dziwne jest to, że pomimo własnego zdarzenia malowania pozycji, ramka fokusa i tak jest malowana (kolejna różnica w stosunku do VCL z Delphi 7). Dlatego trzeba się pozbyć tej rysowanej niezależnie od nas. Łatwo to zrobić – ramka ta renderowana jest metodą DrawFocusRect, a ona sama jest zwykłym xorem kolorów pikseli pod nią. Sam xor jest z natury przemienny, więc aby usunąć ramkę, wystarczy ją… namalować drugi raz. ;)

Tak więc aby namalować ramkę fokusa w odpowiednim miejscu, najpierw zamowywujemy tę domyślną, a następnie renderujemy własną, tylko w obszarze zajmowanym przez tekst (ten sam obszar co dla kolorowego tła, gdy item jest zaznaczony):

if odFocused in AState then
begin
  ListBox.Canvas.DrawFocusRect(ARect);
  ListBox.Canvas.DrawFocusRect(TextRect);
end;

I to wszystko – teraz mamy wygląd teł i checkboxów zgodny z bieżącym schematem systemu, ale własnoręcznie wyrenderowany tekst. Końcowy efekt dla kilku wersji Windowsa poniżej (pierwsza pozycja w moim programie zawsze ma być renderowana fontem pogrubionym):

after.png

Pełny kod zdarzenia renderującego pozycje wrzucam niżej dla zainteresowanych:

procedure TMyForm.CMyListBoxDrawItem(AControl: TWinControl; AIndex: Integer; ARect: TRect; AState: TOwnerDrawState);
const
  CHECKBOX_STATE: array [TCheckBoxState, Boolean] of TThemedButton = (
    (tbCheckBoxUncheckedDisabled, tbCheckBoxUncheckedNormal),
    (tbCheckBoxCheckedDisabled, tbCheckBoxCheckedNormal),
    (tbCheckBoxMixedDisabled, tbCheckBoxMixedNormal)
  );
var
  ListBox: TCheckListBox absolute AControl;
var
  CheckBoxDetails: TThemedElementDetails;
  CheckBoxSize: TSize;
  CheckBoxEnabled: Boolean;
var
  CheckBoxRect, TextRect: TRect;
  TextStyle: TTextStyle;
begin
  ListBox.Canvas.Brush.Color := Windows.GetSysColor(COLOR_WINDOW);
  ListBox.Canvas.FillRect(ARect);

  CheckBoxEnabled := ListBox.Enabled and ListBox.ItemEnabled[AIndex];
  CheckBoxDetails := ThemeServices.GetElementDetails(CHECKBOX_STATE[ListBox.State[AIndex], CheckBoxEnabled]);

  CheckBoxSize := ThemeServices.GetDetailSize(CheckBoxDetails);
  CheckBoxRect := Bounds(ARect.Left + 4, ARect.Top + (ARect.Height - CheckBoxSize.Height) div 2, CheckBoxSize.Width, CheckBoxSize.Height);

  ThemeServices.DrawElement(ListBox.Canvas.Handle, CheckBoxDetails, CheckBoxRect);

  TextRect := ARect;
  TextRect.Left := CheckBoxRect.Right + 4;

  if odSelected in AState then
  begin
    ListBox.Canvas.Brush.Color := Windows.GetSysColor(COLOR_HIGHLIGHT);
    ListBox.Canvas.FillRect(TextRect);
  end;

  TextStyle := ListBox.Canvas.TextStyle;
  TextStyle.Layout := tlCenter;

  if AIndex = 0 then
    ListBox.Canvas.Font.Style := [fsBold];

  ListBox.Canvas.Brush.Style := bsClear;
  ListBox.Canvas.TextRect(TextRect, TextRect.Left + 4, TextRect.Top, ListBox.Items[AIndex], TextStyle);

  if odFocused in AState then
  begin
    ListBox.Canvas.DrawFocusRect(ARect);
    ListBox.Canvas.DrawFocusRect(TextRect);
  end;
end;

#free-pascal #lazarus #lazabug

cerrato

Masz pomysł, jak to ugryźć?

furious programming

Ale co? Już to ugryzłem – rozwiń sobie wpis i przeczytaj do końca. ;)

cerrato

:D Tak się wpis zawinął, że akurat wszystko co jest po pierwszych grafikach zniknęło i nie zauważyłem, że było coś dalej :D :D :D

cerrato

ThemeSerives- nie powinno być SERVICES?

furious programming

Ach, zawsze się literówki trafiają – poprawione.

cerrato

A tak w celu czepiania się :D Jeśli chcesz mieć zgodność z systemem (pamiętam, jak w innym wątku zastanawiałeś się, jak pobrać systemowy kolor odnośników), to czy nie lepiej zmienić we fragmencie CheckBoxRect.Right + 4 liczbę 4 na coś zależnego od aktualnych ustawień systemu? Bo ludzie mogą mieć różne dpi, rozmiar czcionek itp. A Ty zawsze dajesz odstęp na 4.

Silv

4 to ładna liczba. Co dokładnie oznacza fragment CheckBoxRect.Right + 4?

furious programming

Nie wiem czy lepiej czy nie, ale kod mojego zdarzenia pisałem w oparciu o kod widgetsetu dla Win32, w którym odstęp pomiędzy obszarem checkboxa i tekstu zawsze wynosi 4 piksele. Sprawdź sobie – moduł Win32WSCheckLst, klasa TWin32WSCustomCheckListBox, metoda DefaultWndHandler, lokalna procedura DrawCheckListBoxItem.

cerrato

@silv - FP ładnie to wyjaśnił we wpisie - Następnie należy wyznaczyć obszar dla tekstu, który nie będzie zawierał obszaru zajmowanego przez checkbox (plus dodatkowy odstęp) :P Czwórka to właśnie ten odstęp

Silv

@cerrato: właśnie to przedtem przeczytałem i bez kontekstu całego wpisu (lub całego projektu) to zdanie nie jest jasne. ;)

cerrato

@furious programming: nie wnikałem tak głęboko, skoro tak jest w oryginale to w takim razie zwracam honor ;)

furious programming

Wygląda na to, że we wszystkich wersjach Windows odstęp ten jest stały i jednocześnie nie jest możliwy do pobrania za pomocą GetSystemMetrix. Dlatego twórcy LCL użyli stałych wartości, ale trochę innych obliczeń, dlatego że kod widgetsetu bazuje na danych dostarczonych przez systemowe komunikaty, a ja skorzystałem z parametrów zdarzenia i wysokopoziomowych klas/typów w swojej implementacji.

cerrato

we wszystkich wersjach Windows odstęp ten jest stały - co swoją drogą jest trochę dziwne, bo przy powiększeniu czcionek. odpaleniu jakiegoś trybu dla niepełnosprawnych czy w inny sposób zmieniając ustawienia wyświetlania, będziemy mieli czcionkę na 1/4 ekranu, a tuż obok niej, praktycznie nachodzącą na nią wielką ramkę checkboxa...

furious programming

@Silv: to odstęp pomiędzy checkboxem a obszarem tła dla tekstu. Przy zaznaczonej pozycji go widać – wtedy jest to odstęp od prawej krawędzi checkboxa do lewej krawędzi niebieskiego tła pod tekstem.


@cerrato: tutaj trzeba by się zagłębić w internalsy, bo nie wiem czy system maluje większe checkboxy w trybie skalowania interfejsu. W oryginalnym kodzie skalowanie jest uwzględnione, tak samo jak kierunek renderowania (left to right i right to left) ale tylko dla tekstu – checkbox i tak malowany jest przez funkcje z API systemu.

Ale i tak pamiętaj, że ten checkbox jest tylko wirtualny – jego fizycznie tam nie ma, to nie jest osadzony komponent, a tylko namalowana grafika. Dlatego te checkboxy nie reagują na kursor (brak obsługi stanu hover), a kliknięcie na itemku gdziekolwiek po lewej stronie (przy kierunku left to right rzecz jasna) od ramki tekstu powoduje zmianę stanu zaznaczenia checkboxa. Przy wyższych itemach (np. na 100px), cały pas po lewej (wysoki na 100 pikseli) słiuży do zaznaczania/odznaczania checkboxów. I w sumie dzięki temu można je malować gdziekolwiek, byle po lewej stronie.

cerrato

Zgadza się. Nie chodziło mi o analizę tego, czym ten kwadracik jest (jakimś obiektem, czy namalowaną ramką bez własnej osobowości ;) ). a jedynie o założenie, że on się może skalować, ale odstęp ramki od tekstu już nie.

furious programming

Jeśli może być skalowany to mój kod to uwzględni – w końcu po to używa się ThemeServices, aby pobrać jego rozmiar i namalować odpowiednią grafikę, a tym zajmuje się system. Tyle tylko że akurat w tej mojej aplikacji nie ma parcia na obsługę skalowania, ani też na kierunek renderowania, więc w swoim kodzie tego nie uwzględniałem.

Silv

@furious programming: A, już rozumiem. :) Dzięki. :) Więc dalej uważam, że 4 to ładna liczba.

cerrato

Ale odstęp się nie przeskaluje, bo jest na stałe wpisany na 4 :P

furious programming

@cerrato: sprawdź na msdn i pytaj twórców LCL dlaczego używają stałych offsetów.

Mój kod jest jedynie portem metody z widgetsetu. ;)

furious programming
2019-06-19 02:14

Przed chwilą znów Lazarus rzucił wyjątkiem o wszystkomówiącej treści Access Violation.


Tym razem moja ulubiona funkcja tego IDE (czyli randomowe wykrzaczenie) uruchomiła się w designerze, podczas cofania wprowadzonych zmian. Na formularzu miałem poustawiane komponenty, w tym etykietę z tekstem wieloliniowym (kilka paragrafów, wiersze zawijane, dopasowanie rozmiaru do treści). Pospinałem komponenty za pomocą Anchor Editor (świetne narzędzie swoją drogą), tak aby podczas rozciągania okna ”same” się odpowiednio przesuwały (przydatne, jeśli niektóre komponenty zmieniają swój rozmiar). Wszystko było cacy.

Aż tu nagle przez przypadek przesunąłem tę wieloparagrafową etykietę, która zaczęła wariować – włączony AutoSize w połączeniu z ustawionymi kotwicami spowodował, że komponent zaczął zmieniać rozmiar, totalnie wyjeżdżając poza obszar okna. Puściłem więc LPM i zacząłem cofać zmiany z poziomu klawiatury, licząc na to, że rozmiar i położenie komponentu wrócą do poprzednich wartości. Ale nie – designer rzucił wyjątkiem.

I w sumie bardzo dobrze że tak zrobił, bo pół godziny bym przywracał układ komponentów. A że zanim etykieta się rozjechała, projekt miałem zapisany, więc wystarczyło kliknąć przycisk Abort w oknie z informacją o wyjątku i IDE zostało ubite, następnie otworzyć je ponownie, załadować projekt i gotowe – układ kontrolek przywrócony, więc wyjątek w końcu się do czegoś przydał.


Dlaczego o tym piszę? Bo komponent TLabel to jakaś parodia… Jakiś geniusz wpadł na pomysł, aby dać możliwość tworzenia etykiet jednoliniowych i wieloliniowych (z zawijaniem wierszy lub nie), ale do dostosowywania rozmiaru komponentu dać cholerną jedną właściwość – AutoSize.

Do jasnej cholery! Taki problem jest rozbić ją na dwie – AutoWidth i AutoHeight – tak aby można było wybrać w jaki sposób ma się etykieta zachowywać? Czy lepiej cudować z automatycznymi mechanizmami bazującymi na kotwicach, które w zależności od wymagań mogą być różnie ustawione? A może kompatybilność z Delphi jest ważniejsza? Hmm?!


Już wam napiszę jak to wygląda. Domyślnie AutoSize jest włączony, WordWrap nie. Włączenie WordWrap nic nie daje – wpisując kolejne słowa w Caption, kontrolka rozciąga się w poziomie, cały czas będąc jednoliniową. Aby zmusić ją do zawijania wierszy, należy dodatkowo wyłączyć AutoSize – wtedy wiersze zaczną się zawijać, ale rozmiar nie będzie dostosowywany do zawartości (logiczne). Wpisujemy więc do Caption tekst (np. kilka paragrafów) i akceptujemy. Jak teraz zmusić komponent do tego, aby dopasował swoją wysokość do wieloliniowej zawartości? Logiczne, że trzeba włączyć AutoSize, robimy to i dup – etykieta znów jest jednoliniowa, szlag trafił ustaloną szerokość…

Jak więc tę cholerę dopasować? Nie zgadniecie… Najpierw trzeba wyłączyć AutoSize, ustalić wymaganą szerokość i wpisać swój wieloliniowy tekst, tak aby mieć podgląd na zawartość. Następnie trzeba włączyć obie horyzontalne kotwice i dopiero wtedy włączyć AutoSize – wtedy komponent nie zmieni swojej szerokości, ale dopasuje swoją wysokość – o to od początku chodziło. Teraz możemy dopisywać tekst do Caption i jedynie wysokość będzie się zmieniała.

Dobrze jest? Ta, to teraz zrób w ten sposób etykietę, która podczas rozciągania okna nie będzie zmieniać swojej szerokości. Wyłącz prawą kotwicę to etykieta znów będzie jednoliniowa i szlag trafi wszystko co do tej pory zrobiłeś… Tylko nie cofaj zmian – bo designer może walnąć wyjątkiem… Wyłącz AutoSize, wyłącz prawą kotwicę, ręcznie ustal rozmiar, włącz prawą kotwicę i z powrotem włącz AutoSize.

Masz jeszcze nerwy? To włącz Anchor Editor i spróbuj pospinać kontrolki, tak aby spełniały założenia… :/


Nie mam pojęcia jak, ale ”udało się” wyklikać wszystko. W jednym oknie – teraz morduje drugie… z dziesięciu… :|

#lazarus #wtf #lazabug

Paweł Dmitruk

Myślę, że kompatybilność z Delphi jest ważniejsza - takie odniosłem wrażenie przeglądając forum Lazarusa. Co do labela, to przy Label1.Autosize:=True; ustawiasz maksymalną szerokość kontrolki Label1.Constraints.MaxWidth:=200; i wtedy masz AutoHeight. Oczywiście musi być zaznaczony WordWrap.
Możesz też wpisując tekst do labela sam zawijać tekst dodając znaki końca linii ;-)

furious programming

Z tą kompatybilnością to i tak jest niezła hipokryzja, biorąc pod uwagę to jak bardzo przywiązują do niej wagę, jednocześnie broniąc się przed implementacją wybranych nowości tego języka (jak np. inline'owane deklaracje zmienych). Funkcjonalności implementują wybiórczo, więc nie ma tutaj mowy o jakiejś konkretnej kompatybilności.


Twoja propozycja co do Constraints – no niby tak, tyle że szerokość musiałbym znać już na początku i jej nigdy nie zmieniać. A w trakcie projektowania interfejsu testuję różne warianty, różne układy i rozmiary komponentów i za każdym razem gdy trzeba by poszerzyć etykiety, musiałbym im najpierw usuwać Constraints, aby to było możliwe. A jeśli bym zapomniał z powrotem ustawić tego ograniczenia, to nawet samo przesunięcie etykiety spowoduje zmianę jej rozmiaru, całkowicie ignorując WordWrap.

Co do ręcznego zawijania – nie mogę tak zrobić, bo na różnych systemach są różne fonty i różne ich ustawienia (rozmiary, skalowanie itd.). To powoduje, że u mnie tekst może mieć pięć linii, a na komputerze z innym systemem może być tych linii mniej lub więcej, a one same mogą być krótsze lub dłuższe. Poza tym w trakcie projektowania mogę testować różne kroje fontów i ich style, więc za każdym razem musiałbym poprawiać łamanie linii, co jest bez sensu. Już nie wspominając o wsparciu wielu języków interfejsu.

Dlatego komponent musi sam łamać linie i dopasowywać swoją wysokość, tak aby zawsze wyglądał prawidłowo, wyświetlał swoją pełną zawartość i nie komplikował mi pracy. Natomiast ustawienie kotwic za pomocą Anchor Editor pozwala zrobić stałe odstępy pomiędzy komponentami przy zmiennej ich wysokości, bez dotykania edytora kodu, co jest wygodne. Jedno mi pracę ułatwia, a drugie komplikuje – tak być nie powinno…

furious programming
2019-04-25 18:45

Kolejna dawka Tetrisa i znów kolejne rekordy! :]


Dziś osiągnąłem swój nowy personalny rekord jeśli chodzi o wersję NTSC, pobijając ten dotychczasowy w zaokrągleniu o półtora tetrisa (około 35.000 punktów). Po drugie, nareszcie pobiłem barierę 700.000 punktów, zaczynając od poziomu 18. A po trzecie, pierwszy raz osiągnąłem pułap 200 linii, również zaczynając rozgrywkę od poziomu 18. Tak więc trzy sukcesy jednocześnie.

Niestety nauka gry na poziomie 19 w wersji NTSC nie szła tak dobrze jak bym sobie tego życzył. Pierońsko trudno jest robić tetrisy, a jeden błąd praktycznie oznacza game over. Dlatego też wpadłem na pomysł, aby do treningu dodać etap pośredni, w postaci gry na poziomie 18 w wersji PAL. Prędkość opadania klocków w tej wersji jest szybsza niż na poziomie 18 w NTSC, ale wolniejsza niż na poziomie 19. Po dwóch tygodniach grania naprzemian po kilka gier wersji amerykańskiej i kilka europejskiej, widzę postępy – jest dużo łatwiej.

Tak więc jeśli chodzi o wersję NTSC, z wynikiem 700.547 wchodzę do pierwszej sześćdziesiątki w oficjalnym rankingu. A jakby tego było mało, w międzyczasie huknąłem niezły rekord w wersji PAL606.243 punktów – co plasuje mnie na 18 miejscu w światowym rankingu.

Oby tak dalej, bo ślinka cieknie solidnie na swój pierwszy max-out.


Jak sończę platformera to muszę sobie napisać wrapper na emulator, tak aby po pierwsze przykrywał pulpit (bo gram na małym okienku), obramowanie okna emulatora i nieinteresujące mnie fragmenty obrazu gry, a po drugie, aby móc prowadzić dodatkowe statystyki i widzieć je podczas rozgrywki (głównie TRT, BRN oraz liczbę brakujących punktów do własnego rekordu). Oprócz tego dorzucę sobie jeszcze zapamiętanie punktacji w momencie przejścia z poziomu 18 do 19. No i znając siebie, pewnie jakieś mniej znaczące bajery (np. miniaturka kontrolera z podświetlanymi przyciskami). Całość na kształt tego co widzą oglądający CTWC.

Wtedy też zacznę nagrywać swoje rozgrywki, tak aby w końcu mieć konkretne dowody, a nie tylko zrzuty.

#nes #tetris

cerrato

Jeszcze trochę i trzeba będzie koledze zmienić nick na furious tetrissing

winuser

Nie szkoda Ci zycia na takie bzdury...?

superdurszlak

a co według Ciebie powinien robić? udawać że jest robotem i nie poświęcać czasu na nic, co nie jest pracą lub potrzebą fizjologiczną? :)

Spine

To nie bzdury, to życie! :)

cerrato

No tak, bo granie to głupota, ale chodzenie 4x w tygodniu na siłownię albo do kina to porządne hobby ;)

czysteskarpety

Gra w tetrisa, a robota przy gierce leży :/

winuser

Granie w takie gry to marnowanie czasu - to moja opinia. Programowanie, rozwoj,czytanie ksiazek - to nie jest marnowanie czasu. Subiektywna opinia. :)

superdurszlak

Rozwijanie zręczności i refleksu to nie jest rozwój? :P

somedev

Rozwój i praca to też strata czasu. Żyjemy 70/80 lat i potem się to już nie przydaje, zatem powinniśmy korzystać z życia żeby być spełnionym, a nie marnować życie w pracy. Każdy ma swoją definicje, nie marnowania czasu, tak jak każdy ma swoją definicje pomagania - jedni będą pomagać w WOSP, inni ratują dzieci w Afryce, a jeszcze inni mają gdzieś ludzi i ratują konie. Ale może ktoś kiedyś pojedzie tym koniem zainspiruje sie pięknym widokiem, i będzie eksplorował las, gdzie odkryję roślinę leczącą raka, a gdyby nie ten koń nie zrobił by tego. Może @furious programming ewoluuje do wyższego stanu świadomości i będąc w transie wywołanym przekroczeniem 1kk naprawi wszystkie błędy w Lazarusie, ja zacznę go normalnie używać i stworzę w nim SkyNet, który zapewni pokój na świecie, więc @furious programming zasuwaj z tym wynikiem, bo mnie już świerzbią palce do pisania i bycia sławnym.

furious programming

@czysteskarpety: nie leży – cały czas coś się dzieje. W ostatnich tygodniach walczyłem z portowaniem kodu na inne platformy i na chwilę obecną projekt jest kompilowalny i działa na Windows i Linux, a w weekend będą testy na BSD – pozostał jeszcze Solaris. Kod jest prawie kompatybilny z macOS, ale z powodu braku implementacji funkcji testującej klawisze w widgetsecie, na razie nie będzie z nim zgodny (może kiedy indziej). Mogę napisać w wątku jak się sprawy mają.

@winuser: gra się po to, aby odpocząć i wyluzować. Jedni odpoczywają mordując się w GTA czy fortnite, inni oglądając głupoty na facebooku czy YouTube, a ja odpoczywam układając klocki. A do tego robiąc wyniki, jakimi pochwalić się może garstka ludzi na świecie. We wrześniu startuje CTEC w Kopenhadze, jakoś w grudniu CTWC w Portland, a reprezentanta z Polski jak nie było tak nima… :]

karpov

@furious programming: kusi Cię świat wielkiego e-sportu? :P

furious programming

@karpov: trochę kusi. :]

Z chęcią bym pojechał na jakieś zawody i zobaczył jak to wygląda od środka, a nie tylko na ekranie komputera. Tyle że jeszcze jest za wcześnie na to, bo jeszcze muszę się podszkolić. No ale zawody są każdego roku, więc kto wie.

nohtyp

Jak długo cieszy taki wynik? Czy następnego dnia jak wstajesz to jesteś tym podjarany?

furious programming

Dziwne pytanie… Wynik się robi, on jest i będzie już na zawsze, więc powód do ”dumy” nie zniknie. A potem siada się znów i gra dalej, licząc na więcej i więcej. Teraz jestem zadowolony ze świeżego rekordu, ale im dłużej gram, tym mam większą ochotę na jego pobicie.

superdurszlak

Jeśli Tetris nie różni się zbytnio pod tym względem od innych gier, to zapewne taki wynik cieszy przez chwilę, jak się trafi, dopóki nie stanie się rutyną. Wtedy już nie cieszy, za to jak się wypadnie mocno poniżej, to irytuje, że kiepsko poszło. Trzeba nabić jeszcze wyższy wynik, żeby zaczął cieszyć. Jak regularnie masz K:D ratio w okolicach 1.0 w jakichś strzelankach, to cieszysz się gdy trafi się dobry mecz i wyciągniesz 2.0. Jak regularnie nabijasz K:D ratio rzędu 3.0-4.0 i trafi się 2.0, to martwisz się, że wypadasz z formy :/

Michał Kuliński

O święty Izydorze, to Solarisa ktoś jeszcze używa?

furious programming

@Michał Kuliński: nie mam pojęcia, ale kod powinien być kompatybilny. :]

somedev

@furious programming: lepiej ten czas na testy solarisa byś poświecił na MacOS

furious programming

Na macOS nie będzie portu – przynajmniej ja nie będę go robił. Głównie dlatego że jest problem z klawiaturą (widgetset jest niekompletny) i z renderowaniem obrazu (tu akurat wystarczy znaleźć odpowiednią kolejność składowych), a nie mam żadnego urządzenia z tym systemem, aby testować różne rozwiązania.

Najbradziej mi zależy na Windows, Linux i BSDSolaris łapie się przypadkowo, bo posiada ten sam zestaw potrzebnych funkcji co Linux. Natomiast macOS to wrzód na dupie – wypychanie okna na ekran nie działało i musiałem robić obejścia, metoda GetKeyState dla tego systemu nie jest zaimplementowana więc musiałbym szukać innego rozwiązania, format pikseli się nie zgadza i musiałbym szukać odpowiedniego. Za dużo tych problemów. Poza tym ciągle muszę liczyć na cudzą pomoc (a to zawsze trwa i trwa…), a chcę już skończyć ten projekt.

somedev

@furious programming: a kod już jest open source? Gdzie go znajdę? No i legło w gruzach „napisz raz, kompiluj gdziekolwiek” ;)

Spine

@somedev: Gdyby używał technologii przeznaczonej do gier (chociażby coś prostego, jak SDL), to pewnie nie byłoby takich trudności. Okienka jednak różnią się pod różnymi systemami...

somedev

Nie tylko okienka. Dostęp np do portów również. Są komponenty w delphi które dają rade dać międzyplatformowych interface ale w lazarusue takie rs232 to kpina akurat. Niemniej jako strugania do kawy i ciastka w domu to nawet ciekawa rozrywa ;)

furious programming

@Spine: uparłem się że zrobię coś z niczego to teraz mam za swoje. ;)

Choć w sumie to kodu dotyczącego konkretnych platform jest niewiele, raptem kilkadziesiąt linii spośród kilkunastu tysięcy linii całości. Jedyne co robi się inaczej na różnych platformach to pobiera czas w postaci cykli procesora (na Windows) lub w postaci liczby nanosekund (na systemach Uniksowych). A drugą rzeczą jest ”konwersja” ładowanej z pliku bitmapy na natywną, 32-bitową dla Uniksów – to akurat kilka linijek.

Natomiast całą resztę załatwia widgetset, więc mogę spokojnie korzystać z LCL i wieloplatformowość mam zapewnioną.


@somedev: źródła są dostępne w moim wątku – Deep Platformer – technologiczne demo prostej platformówki. Najświeższe dodałem do załączników tego posta. Za niedługo coś w tym wątku napiszę i pewnie znów dorzucę kolejne źródła.

furious programming

Nowy rekord – 709.337 punktów, 203 linie. :]

tetris

furious programming
2019-03-12 17:17

Kolejna dawka Tetrisa – dziś szczególny dzień!

W końcu pokonałem barierę 600k punktów, zaczynając od poziomu 18. Nie jest to łatwe, bo taka efektywność wymaga TRT (tzw. tetris rate) na poziomie 80%, ale dałem radę. Co prawda już wczoraj zanosiło się na personalny rekord, bo kilka razy miałem grubo ponad 500k i kilka(naście) linii do zmiany poziomu, ale jakoś za każdym razem psułem i albo nie trafiłem klockiem tam gdzie chciałem i ładny stosik szlag trafiał, albo zaczęły spadać same identyczne lub niepasujące (taki tetrisowy shitstorm)… :/


Nowy wynik przeniesie mnie z obecnego, 81 miejsca na miejsce 61 w światowym rankingu najlepszych graczy. Teraz mogę przejść do ostatniego etapu, czyli trenowania poziomu 19 i atakowania szczytu, czyli zamknięcia licznika i dołączenia do zacnego grona max-outowców.

A minęło raptem trzy miesiące, odkąd zacząłem się bawić w te klocki. :]

#nes #tetris

czysteskarpety

To krzepiące, że ludzie w średnim wieku również mogą mieć refleks i charyzmę, odkładam kupno trumny i sam również spróbuję coś zrobić.

furious programming

no nie przesadzaj, jeszczem przed trzydziestką – całe życie przede mną. ;)

czysteskarpety

@furious programming: nie no oczywiście, John Glenn był w kosmosie w wieku 77 lat :)

furious programming

Wspomnę tylko, że większość najlepszych graczy świata jest starszych ode mnie, niektórzy po czterdziestce.

Niektórzy z nich grają w Tetrisa praktycznie od początku istnienia wersji na konsolę NES, czyli już 30 lat!

axelbest

@furious programming: pewnie Ci co grają w tetrisa od 30 lat zeszli by na zawał, jakby im odpalić Crysisa :)

czysteskarpety

@axelbest: wiesz, CS ma już 20 lat więc kto wie, można się zdziwić czasami jak trafisz na starego wyjadacza z kafejek internetowych :)

Pipes

Szacunek :) Jedyne klocki, jakie ja teraz mam to te, które są w ramach projektu "Migracja z pieluchy na nocnik".

WhiteLightning

A jest jakas wersja albo hack zeby moc cwiczyc od razu od wysokiego poziomu? a nie grac od poczatku za kazdym razem :)

furious programming

@WhiteLightning: możesz rozpocząć grę od dowolnego poziomu z przedziału od 0 do 19. Dla poziomów od 0 do 9 po prostu ustawia się migający kwadracik na odpowiednim numerku i wciska przycisk Start. Aby rozpocząć z poziomu od 10 do 19, wciska się kombinację A+Start – wtedy do wybranego numerka dodane zostanie 10.

Z tego właśnie powodu poziomy indeksowane są od 0. ;)

furious programming
2019-03-05 02:34

Summoning Salt – jeśli ktoś tak jak ja lubi sobie czasem pooglądać speedruny starych gier, to ten kanał powinien go zainteresować.

Znajduje się na nim obecnie kilkanaście filmów na temat historii pobijania rekordów świata, m.in. w takie klasyki jak Super Mario Bros., Metroid, The Legend of Zelda, Sonic czy Pokemon – każdy odcinek po kilkadziesiąt minut, ze szczegółowymi wyjaśnieniami i fragmentami z prób bicia rekordów.

Szkoda tylko, że odcinki pojawiają się dość rzadko, mniej więcej co 2-3 miesiące. :/

#nes #retro

Akihito

Gosc musi miec zycie a nie tylko speedruny xd