Wariacje z tekstem

0

Hej,
Eksperymentuję z GDI+ w Delphi. Docelowo, chciałbym rysować wykres kołowy z antyaliasingiem (jest to dosyć proste do zrobienia, używając funkcji DrawArc).
Ale dzisiaj mam inny problem, wydawałoby się ten łatwiejszy... pozory jednak mylą.

Chciałbym narysować tekst na kontrolce TPaintBox, który jest w ustalonej pozycji (domyślnie wyśrodkowany).
Poniżej znajduje się prosty (pseudo)kod, który rysuje tekst na kontrole TPaintBox (o nazwie PB). Jak widać, tekst rysowany jest od lewego górnego rogu - celem jest wyrysowanie go NA ŚRODKU.

Jak ustawić tekst na środku (względem osi x i y)? Jak zmierzyć tekst (szerokość i wysokość)? Jak obliczyć położenie tekstu?

Eksperymentowałem też z kontrolką TLabel położoną na TPaintBox (wycentrowanie jest proste i działa - ale to bez sensu, skoro mogę to narysować w GDI+, skoro już tego używam).

uses
  Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics,
  Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.ComCtrls, Vcl.StdCtrls, Vcl.ExtCtrls, GDIPAPI, GDIPOBJ;


procedure DRAW_TEXT(AControl: TPaintBox; ASize: Integer; ATextColor: TColor; AProgress: Integer);
var
   LRect       : TRect;

   Graphics    : TGPGraphics;

   sText       : string;
   iColor      : Longint;
   R, G, B     : Byte;

   FontFamily  : TGPFontFamily;
   Font        : TGPFont;
   PointF      : TGPPointF;
   SolidBrush  : TGPSolidBrush;
   xPos        : Single;
   yPos        : Single;
begin
   // Control Size
   AControl.Width  := ASize;
   AControl.Height := ASize;

   // Control Background (Only for test!)
   LRect := AControl.ClientRect;
   AControl.Canvas.Brush.Color:= $002C1705;
   AControl.Canvas.Brush.Style:= bsSolid;
   AControl.Canvas.Pen.Style:= psClear;
   AControl.Canvas.FillRect(LRect);

   // GDI+
   Graphics := TGPGraphics.Create(AControl.Canvas.Handle);
   try
      // TEXT
      sText       := Inttostr(AProgress) + '%'; // 23%

      FontFamily  := TGPFontFamily.Create('Tahoma');
      Font        := TGPFont.Create(FontFamily, 96, FontStyleRegular, UnitPixel);
      iColor      := ColorToRGB(ATextColor);
      R := iColor;
      G := iColor shr 8;
      B := iColor shr 16;
      SolidBrush  := TGPSolidBrush.Create(MakeColor(255, R,G,B));

      // SET TEXT POSITION
      xPos := 0.0; // !!! Round((AControl.Width div 2) - TEXT_WIDTH div 2)
      yPos := 0.0; // !!! Round((AControl.Hight div 2) - TEXT_HEIGHT div 2)

      // TEXT_WIDTH ????
      // TEXT_HEIGHT ???
      
      PointF := MakePoint(xPos,yPos);



      Graphics.SetTextRenderingHint(TextRenderingHintAntiAlias);
      Graphics.DrawString(sText, Length(sText), Font, PointF, SolidBrush);

      FontFamily.Free;
      Font.Free;
      SolidBrush.Free;

   finally
      Graphics.Free;
   end;

end;


procedure TForm1.PBPaint(Sender: TObject);
begin
   DRAW_TEXT(PB, 340, $00C0C0C0, 23);
end;

Proszę o wskazówki...
-Pawel

1

GDI posiada funkcję DrawText, która pozwala renderować tekst oraz go tylko mierzyć. Jeśli o renderowanie chodzi, to możliwe jest wyrównanie w obu osiach, a także taki ficzery jak zawijanie tekstu, skracanie go poprzez jego ucięcie i dodanie wielokropka itd. To co ta funkcja wykona oraz w jaki sposób to wykona, określa się flagami (a tych jest od cholery).

Korzystam z tej funkcji w aplikacjach okienkowych, do renderowania najróżniejszego tekstu — jest wszechmocna. ;)


Natomiast jeśli chodzi o obliczenia potrzebne do centrowania, jeśli korzystamy z płótna komponentu i VCL/LCL, to są to bardzo proste rzeczy. Aby poznać pozycję docelową tekstu, należy go po prostu najpierw zmierzyć — są do tego metody TCanvas.TextWidth i TCanvas.TextHeight. Reszta to już prosta arytmetyka:

var
  TextToRender: String = 'IS THE TEXT CENTERED?';
begin
  Canvas.Brush.Color := clWhite;
  Canvas.FillRect(ClientRect);

  Canvas.TextOut(
    ClientRect.Left + (ClientRect.Width  - Canvas.TextWidth(TextToRender))  div 2,
    ClientRect.Top  + (ClientRect.Height - Canvas.TextHeight(TextToRender)) div 2,
    TextToRender
  );
end;

Jeśli lewy górny róg obszaru do narysowania tekstu zawsze ma współrzędne 0,0, to obliczenia mogą być prostsze:

var
  TextToRender: String = 'IS THE TEXT CENTERED?';
begin
  Canvas.Brush.Color := clWhite;
  Canvas.FillRect(ClientRect);

  Canvas.TextOut(
    (ClientRect.Width  - Canvas.TextWidth(TextToRender))  div 2,
    (ClientRect.Height - Canvas.TextHeight(TextToRender)) div 2,
    TextToRender
  );
end;

Efekt:

screenshot-20220216220603.png

Trochę ciulowo widać, bo tło jest tego samego koloru co belka okna, ale obliczenia są prawidłowe — zapewniam. :D

0

@furious programming: Dzięki. Już sprawdzam :)
W międzyczasie, użyłem zwykłego Canvas (czyli GDI, jeśli się nie mylę).

sText       := Inttostr(AProgress) + '%';
AControl.Canvas.Font.Size := 75;
AControl.Canvas.Font.Name := 'Tahoma';
AControl.Canvas.Font.Color := ATextColor;
AControl.Canvas.Brush.Style := bsClear;
lPos := (AControl.Height div 2) - (AControl.Canvas.TextWidth(sText) div 2);
tPos := (AControl.Width div 2)  - (AControl.Canvas.TextHeight(sText) div 2);
AControl.Canvas.TextOut(lPos, tPos, sText);

Czy mieszanie kodu GDI+ z GDI (zwykłe mazanie po Canvasie) ma sens?
Czy to świętokradztwo?
-Pawel

A tak wygląda wykres (prototyp) z wycentrowanym napisem.
Jak widać, wykres jest elegancko wygładzany i wygląda ładnie (wciąż walczę z "mignięciami" (flickering) przy odrysowywaniu wykresu co ustalony przedział czasu, np .1s... czasem widać jak wykres miga, co mnie wkurza maksymalnie :P

Test_GDI+.png

0
Pepe napisał(a):

@furious programming: Dzięki. Już sprawdzam :)
W międzyczasie, użyłem zwykłego Canvas (czyli GDI, jeśli się nie mylę).

Nie wiem czego używa VCL, czy GDI czy GDI+, ale to nie ma akurat większego znaczenia. Jeśli jesteś ciekaw i masz źródła VCL, to zawsze możesz sobie sprawdzić co konkretnie jest wykorzystywane wewnątrz metody TCanvas.TextOut.

Najświeższy LCL używa funkcji ExtTextOutW z biblioteki GDI.

lPos := (AControl.Height div 2) - (AControl.Canvas.TextWidth(sText) div 2);
tPos := (AControl.Width div 2)  - (AControl.Canvas.TextHeight(sText) div 2);

Te obliczenia nie pozwalają wyrenderować tekstu w dowolnym obszarze o dowolnych współrzędnych i rozmiarach, a to sprawia, że nie mogą być zastosowane wszędzie gdzie się zapragnie. Taki algorytm powinien być uniwersalny.

Jeśli chcesz mieć uniwersalną funkcję renderowania wycentrowanego tekstu, to skorzystaj z obliczeń które podałem w poprzednim poście — one uwzględniają pozycję obszaru oraz poprawnie wyznaczają lewy górny róg tekstu dla TextOut. Ewentualnie użyj wspomnianego DrawText z odpowiednimi flagami — niech centrowaniem zajmie się system.

Czy mieszanie kodu GDI+ z GDI (zwykłe mazanie po Canvasie) ma sens?

Nad takimi rzeczami się nie zastanawiaj. Jeśli VCL nie zapewnia interesującego Cię algorytmu, to zaimplementuj go sobie sam — nieistotne czy w WinAPI, czy w GDI, czy z wykorzystaniem innych metod VCL. Jedyne nad czym powinieneś się zastanowić to nad formą tego algorytmu, czyli nad tym czy ma to być goła funkcja, czy metoda jakiejś klasy, czy np. rozszerzenie klasy TCanvas (jako helper czy subclassing).

Jak widać, wykres jest elegancko wygładzany i wygląda ładnie (wciąż walczę z "mignięciami" (flickering) przy odrysowywaniu wykresu co ustalony przedział czasu, np .1s... czasem widać jak wykres miga, co mnie wkurza maksymalnie :P

Obszar miga, bo nie jest buforowany — mignięcie oznacza, że ekran odświeżył swoją zawartość akurat w trakcie renderowania zawartości komponentu, po zamalowaniu tła kolorem, ale przed namalowaniem jego elementów.

Rozwiązaniem zawsze jest tylny bufor — zwykła bitmapa typu TBitmap o rozmiarze takim jak klient komponentu (lub w rozmiarze interesującego nas obszaru), na niej renderujesz wszystko co potrzebujesz, a na koniec renderujesz bitmapę w komponencie. Ale bez wcześniejszego wypełniania go jednolitym kolorem, bo znów będzie migać. W końcu tylny bufor i tak przykryje wszystko co wcześniej było w kontrolce, dlatego nic więcej prócz jego namalowania nie należy robić.

0

Rzeczywiście, pomyliłem się w centrowaniu (pomieszałem width z height - u siebie nie widziałem problemu, bo są one tej samej długości).
Powinno być tak:

lPos := (AControl.Width div 2) - (AControl.Canvas.TextWidth(sText) div 2);
tPos := (AControl.Height div 2) - (AControl.Canvas.TextHeight(sText) div 2);

Powinno działać. Przykładowo, dla lPos -> Biorę szerokość kontrolki i dzielę na 2 (czyli jestem w środku (lewy górny róg tekstu)). Następnie odejmuję jeszcze połowę szerokości tekstu (przesuwam się w lewo). Analogicznie dla tPos (operuje na wysokości). Na logikę musi zawsze wycentrować...

Co do migania... tak, czasem się zdarza, że widzę "przemalowanie" kontrolki. Mówisz, że rozwiązaniem jest "tylny bufor".
Czyli, jeśli dobrze rozumiem, tworzę Bitmapę, rysuję na jej Canvasie, a potem przepisuję na Canvas TPaintBox (jak)?
Mógłbym prosić o przykład jak się to robi? najprostszy, ale pokazujący ideę...
-Pawel

0
Pepe napisał(a):

Rzeczywiście, pomyliłem się w centrowaniu (pomieszałem width z height - u siebie nie widziałem problemu, bo są one tej samej długości).
Powinno być tak:

lPos := (AControl.Width div 2) - (AControl.Canvas.TextWidth(sText) div 2);
tPos := (AControl.Height div 2) - (AControl.Canvas.TextHeight(sText) div 2);

Powinno działać.

Tak, będzie działać, choć masz dwa razy więcej dzielenia niż potrzeba. Niby nie wpływa to na wydajność, ale zawsze to trochę więcej kodu niż faktycznie jest potrzebne. Przy czym takie obliczenia są prawidłowe tylko wtedy, gdy lewy górny róg obszaru ma współrzędne 0,0 — Ty malujesz na środku kontrolki, więc działa. Jednak ich nie będzie się dało użyć dla innych obszarów, bo te obliczenia tego nie uwzględniają. Dlatego w moim przykładzie jest dodawanie Left i Top obszaru.

Co do migania... tak, czasem się zdarza, że widzę "przemalowanie" kontrolki. Mówisz, że rozwiązaniem jest "tylny bufor".
Czyli, jeśli dobrze rozumiem, tworzę Bitmapę, rysuję na jej Canvasie, a potem przepisuję na Canvas TPaintBox (jak)?

No dokładnie — to takie proste.

Mógłbym prosić o przykład jak się to robi? najprostszy, ale pokazujący ideę...

Serio potrzebujesz takiego przykładu? Ty obecnie malujesz na AControl.Canvas, więc jedyne co potrzebujesz to stworzyć tymczasową bitmapę, nadać jej rozmiar taki jaki ma kontrolka (czyli AControl.ClientRect.Width i AControl.ClientRect.Height), wszystko namalować na płótnie tej bitmapy (czyli na Bitmap.Canvas), a na koniec renderujesz bitmapę w całości na płótnie komponentu, czyli robisz AControl.Draw(0, 0, Bitmap). To wszystko. ;)

Jeśli będziesz potrzebował jeszcze bardziej wydajnego buforowania, to tymczasową bitmapę stwórz raz i pilnuj, aby zawsze miała rozmiar taki jaki obszar docelowy. Jeśli chodzi o tło komponentu, to skorzystaj z jego zdarzenia OnResize i w nim aktualizuj rozmiar bitmapy. A jeśli chcesz sobie opakować ten wykres w komponent, to poszukaj wirtualnej metody odpowiedzialnej za zmianę rozmiaru komponentu, nadpisz ją, w niej wywołaj inherited, a po nim dodaj aktualizację rozmiaru bitmapy.



Jeszcze jedna rada — po Twoim kodzie widzę, że korzystasz z VCL. Jeśli chcesz tę kontrolkę zrobić dobrze, to zrób z niej kontrolkę — po prostu opakuj ją w komponent, dodaj potrzebne właściwości, tak aby były dostępne w oknie Inspektora Obiektów i tę kontrolkę doinstaluj do IDE.

Nie dość, że pełną jej funkcjonalność będziesz miał w jednym, sensownym miejscu, to w dodatku łatwo i wygodnie będzie ją wykorzystywać w wielu osobnych instancjach, w wielu oknach czy nawet w wielu projektach. Poza tym, bardzo ważną zaletą takiego podejścia jest podgląd na żywo jego zawartości w designerze — dzięki temu efekty jego działania widać na bieżąco, bez konieczności ciągłego rekompilowania projektu i jego uruchamiania.

To jest jedyne sensowne podejście, dlatego warto zwrócić na to uwagę.

0

Dziękuję za wskazówki. Jutro pobawię się tą bitmapą...
Może rzeczywiście to proste :P Grafiką się nigdy nie zajmowałem, stąd tak, być może, naiwne i proste problemy do rozwiązania.
No, ale liczy się efekt końcowy. Mam nadzieję, że coś tam w końcu wyskrobię.
Pozdrawiam.
-Pawel

1

BTW Jeśli chcemy mieć okrągły ProgressBar, to w Lazarusie można użyć TBCRadialProgressBar z BGRAControls
radialprogressbar.png

2
Pepe napisał(a):

@furious programming: Dzięki. Już sprawdzam :)
W międzyczasie, użyłem zwykłego Canvas (czyli GDI, jeśli się nie mylę).

Co do zasady tak, to GDI.
Ale zwykłego Canvasa można obrabiać przy pomocy GDI+ (tworzysz IGPGraphics ze zwykłego TCanvas i jedziesz).
Problem polega jednak na czym innym, a mianowicie na samym Windowsie... :/
Jak oni namieszali w Windows z GDI/GDI+ to głowa mała; ale TLDR - GDI jest szybkie, bo jest akcelerowane (aczkolwiek to zależy, od wersji Windows...).
GDI+ jest ładne, ale wolne (ale niektóre operacje w niektórych wersjach Windows są również akcelerowane), ponieważ nie jest akcelerowane.
Generalnie horror szoł i ja się na GDI+ okrutnie przejechałem.

Ale jeśli już GDI+ to sam używam biblioteki Erika Van Bilsena:
https://bilsen.com/gdiplus/index.shtml

Do twoich celów olałbym całkowicie GDI i zrobiłbym to na Graphics32; szybciej działa od GDI+ (o rząd wielkości), prościej się robi i ma większe możliwości.

Jest jeszcze i oczywiście FMX, ;-)
Albo i Skia:
https://github.com/skia4delphi/skia4delphi
Zresztą, to dodaje renderer do FMX oparty o Skia, a więc ciekawe.

sText       := Inttostr(AProgress) + '%';
AControl.Canvas.Font.Size := 75;
AControl.Canvas.Font.Name := 'Tahoma';
AControl.Canvas.Font.Color := ATextColor;
AControl.Canvas.Brush.Style := bsClear;
lPos := (AControl.Height div 2) - (AControl.Canvas.TextWidth(sText) div 2);
tPos := (AControl.Width div 2)  - (AControl.Canvas.TextHeight(sText) div 2);
AControl.Canvas.TextOut(lPos, tPos, sText);

Czy mieszanie kodu GDI+ z GDI (zwykłe mazanie po Canvasie) ma sens?

Żaden problem.

Czy to świętokradztwo?

Nie.

A tak wygląda wykres (prototyp) z wycentrowanym napisem.
Jak widać, wykres jest elegancko wygładzany i wygląda ładnie (wciąż walczę z "mignięciami" (flickering) przy odrysowywaniu wykresu co ustalony przedział czasu, np .1s... czasem widać jak wykres miga, co mnie wkurza maksymalnie :P

Zamiast się wkurzać, lepiej zastosuj podwójne buforowanie ;-)

0

@wloochacz Dzięki!
Na moje szczęście, chcę użyć tylko kilku funkcji. Nie ma potrzeby porywać się na super rozwiązania, bo ja rysuję tylko wykres kołowy z tekstem. Żadnych transformacji ani innych cudnych technik.
Biblioteka Erika Van Bilsena wymaga osobnego pliku dll, więc odpada (tak, oglądałem wcześniej jego stronę, chcąc zobaczyć jak korzysta się z GDI+).

Sprawdzę sobie pozostałe rozwiązania (skia i Graphics32). Ogólnie preferuję w tym przypadku kod, który wkompiluję w exe (nie chcę żadnych zewnętrznych bibliotek).
Podwójne buforowanie oczywiście mam (ustawione dla rodzica, czyli tutaj głównej formy). Nie wiedzieć czemu, TPaintBox nie ma takiej właściwości, ale pewnie dziedziczy po rodzicu.

Na dzień dzisiejszy pozostaje przy funkcji DrawArc z biblioteki GDI+ oraz zwykłego Canvasa z funkcją TextOut (łatwo pobrać wysokość/szerokość tekstu i ustawić tekst jak się chce) + stosowanie triku z bitmapą jako bufor. Chciałem użyć funkcji DrawString() z GDI+, ale o wiele trudniej pobrać informacje o rozmiarze tekstu i na tej podstawie go umiejscowić odpowiednio (choć ze zwykłym Canvasem też są cuda w ustawianiu tekstu, bo tekst pozycjonowany jest różnie w zależności jak odpalam kod (PaintBox.OnPaint(sender); lub PaintBox.Repaint;, albo od tego czy ustawione jest takie cudo dla TPaintBox - PB.Controlstyle := PB.Controlstyle + [csopaque]; i pewnie innych czynników, na które szkoda zdrowia :) ).

-Pawel

0
Pepe napisał(a):

Ogólnie preferuję w tym przypadku kod, który wkompiluję w exe (nie chcę żadnych zewnętrznych bibliotek).

Też takie rozwiązanie preferuję — jak najmniej zewnętrznych bibliotek. Dlatego też nie czuję żadnych oporów jeśli chodzi o korzystanie z systemowego API, szczególnie, że zwykle nie potrzebuję wsparcia platform innych niż Windows. No i właśnie GDI jest jednym z tych zestawów funkcji, z których często korzystam do renderowania komponentów wizualnych, bo jest funkcjonalne i efektywne (za sprawą akceleracji sprzętowej).

Podwójne buforowanie oczywiście mam (ustawione dla rodzica, czyli tutaj głównej formy). Nie wiedzieć czemu, TPaintBox nie ma takiej właściwości, ale pewnie dziedziczy po rodzicu.

Trochę to dziwne, bo wszyscy piszemy o podwójnym buforowaniu jako o zwykłej, własnoręcznie stworzonej bitmapie lub teksturze (czy tam ”decalu”), podczas gdy podwójne i potrójne buforowanie to sposób, w jaki karta graficzna zmienia bufory do renderowania obrazu oraz do jego przesyłu do urządzenia wyświetlającego.

Ten termin chyba się wypaczył i stosowany jest również do opisu zwykłego tylnego buforowania. ;)

Na dzień dzisiejszy pozostaje przy funkcji DrawArc z biblioteki GDI+ oraz zwykłego Canvasa z funkcją TextOut (łatwo pobrać wysokość/szerokość tekstu i ustawić tekst jak się chce) + stosowanie triku z bitmapą jako bufor.

Nie wiem czy te wszystkie zabawy z doborem API mają sens w Twoim przypadku — chyba że po prostu chcesz się rozeznać w temacie i nauczyć czegoś nowego. GDI jest pod ręką i zapewnia sporo możliwości oraz efektywne renderowanie, więc śmiało można z niego korzystać. Tym bardziej, że ono i tak jest używane wewnętrznie przez VCL/LCL. Nie musisz też używać funkcji GDI bezpośrednio — wiele metod z klasy TCanvas opakowuje te funkcje w wygodne do użycia metody, więc po co się męczyć (przy okazji, kod będzie niezależny od platformy).

Natomiast tylne buforowanie zawsze jest rozwiązaniem problemu z miganiem.

[…] bo tekst pozycjonowany jest różnie w zależności jak odpalam kod (PaintBox.OnPaint(sender); […]

Jeśli chcesz zaktualizować stan komponentu, w tym wymusić jego przemalowanie, to korzystaj z metody Invalidate.

0
wloochacz napisał(a):

Jak oni namieszali w Windows z GDI/GDI+ to głowa mała; ale TLDR - GDI jest szybkie, bo jest akcelerowane (aczkolwiek to zależy, od wersji Windows...).
GDI+ jest ładne, ale wolne (ale niektóre operacje w niektórych wersjach Windows są również akcelerowane), ponieważ nie jest akcelerowane.
Generalnie horror szoł i ja się na GDI+ okrutnie przejechałem.

Jednak GDI+ jest mega przyjemniejsze w kodzeniu od GDI rodem z Windows 1.x.
Teraz chyba zalecane jest Direct2D, ale nigdy w nim nic nie robiłem.
Robiłem natomiast takie kółko progresu jako fragment shader w OpenGL...

0

@PrzemysławWiśniewski: Dzięki, temat jest mi znany. Przed opublikowaniem postu z zapytaniem/rozważeniem danego tematu robię research w sieci...
Tak, do rysowania wykresu używam funkcji DrawArc z biblioteki GDI+ (jest antyaliasing).

0
Pepe napisał(a):

@wloochacz Dzięki!
Na moje szczęście, chcę użyć tylko kilku funkcji. Nie ma potrzeby porywać się na super rozwiązania, bo ja rysuję tylko wykres kołowy z tekstem. Żadnych transformacji ani innych cudnych technik.
Biblioteka Erika Van Bilsena wymaga osobnego pliku dll, więc odpada (tak, oglądałem wcześniej jego stronę, chcąc zobaczyć jak korzysta się z GDI+).

Jakiej biblioteki?
Wymaga standardowej biblioteki GDI+ - po prostu.
Bo w stareńkich WindowsXP bywało różnie z tym czy ona była czy nie była w systemie...

Ale w takim razie to Graphics32 - zdecydowanie.

PS.
Sorki za zwłokę ;-)

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