Rozdział 9. Multimedia

Adam Boduch

Na samym początku wypadałoby wyjaśnić, co kryje się pod pojęciem multimedia. Najprościej rzecz ujmując, jest to nazwa określająca techniki komputerowe służące do przekazywania informacji, w tym różne środki przekazu. Mam na myśli dźwięk, obraz, film, animację itp.

1 Z czego będziemy korzystać?
     1.1 OpenGL
     1.2 DirectX
2 Tworzenie bitmap
     2.3 Korzystanie z komponentu TImage
3 Klasa TBitmap
     3.4 Odczytywanie obrazka ze schowka
          3.4.1 Odczyt danych
          3.4.2 Zapisywanie danych
4 Pliki JPEG
     4.5 Klasa TJPEGImage
     4.6 Wyświetlanie obrazków w komponencie TImage
     4.7 Przykład działania ? kompresja plików
5 Pliki GIF
6 Zasoby
     6.8 Tworzenie zasobów
          6.8.3 Dołączanie bitmapy
          6.8.4 Pozostałe zasoby
     6.9 Wykorzystanie zasobów
          6.9.5 Ładowanie bitmapy
          6.9.6 Ładowanie ikony
          6.9.7 Ładowanie kursora
     6.10 Ręczne tworzenie zasobów
          6.10.8 Dodawanie plików JPEG
          6.10.9 Ładowanie pliku JPG
     6.11 Doklejanie plików EXE
7 Klasa TCanvas
8 Pióra i pędzle
     8.12 Klasa TPen
          8.12.10 Właściwość Mode
          8.12.11 Właściwość Style
     8.13 Klasa TBrush
          8.13.12 Właściwość Style
9 Czcionki
     9.14 Właściwości klasy TFont
10 Metody klasy TCanvas
     10.15 Draw
     10.16 FillRect
     10.17 StretchDraw
     10.18 TextOut
     10.19 TextRect
     10.20 TextWidth, TextHeight
     10.21 TextExtent
     10.22 MoveTo
     10.23 LineTo
     10.24 Inne funkcje służące do rysowania kształtów
     10.25 Przykładowy program
11 Proste animacje tekstowe
     11.26 Tekst trójwymiarowy (3D)
     11.27 Efekt maszyny do pisania
     11.28 Animacja na belce tytułowej
     11.29 Inne płynne animacje
12 Odtwarzanie dźwięków
     12.30 Funkcja PlaySound
     12.31 Użycie komponentu TMediaPlayer
13 Odtwarzanie filmów
          13.31.13 Odtwarzanie filmu
          13.31.14 Pozycja odtwarzanego filmu
          13.31.15 Ustawianie głośności
14 Kontrolka Flash
     14.32 Instalacja kontrolki
     14.33 Wykorzystanie komponentu
15 Podsumowanie

Rozdział 9. będzie właśnie poświęcony tworzeniu grafiki, odtwarzaniu dźwięków, wyświetlaniu filmów oraz tworzeniu animacji. Nie oczekuj, że od razu będziesz projektował świetne animacje ? do stworzenia takiej grafiki, jaką możemy zobaczyć w grach, potrzebna jest znajomość programowania OpenGL lub DirectX, a to wykracza poza ramy tej książki.

W tym rozdziale:
*dowiesz się, na czym polega malowanie w Delphi;
*napiszemy program umożliwiający odtwarzanie dźwięków;
*pokażę, w jaki sposób za pomocą komponentu TMediaPlayer można wyświetlić film;
8nauczysz się tworzyć proste animacje.

Z czego będziemy korzystać?

W Delphi nie mamy zbyt dużego wyboru narzędzi, jakimi moglibyśmy się posługiwać. Malowanie po formularzu ogólnie polega na zastosowaniu różnych funkcji WinAPI, lecz zostało ono nieco uproszczone dzięki klasie TCanvas (płótno). Moim skromnym zdaniem Delphi nie nadaje się do tworzenia zaawansowanej grafiki, mimo że posiada wspomaganie narzędzi typu OpenGL czy DirectX. Jednak jest to tylko moje zdanie; wiele osób uważa, że jest wręcz przeciwnie ? w Delphi można tworzyć wspaniałe programy z grafiką.

OpenGL

OpenGL, czyli Open Graphics Library, to biblioteka służąca do tworzenia aplikacji graficznych. W rzeczywistości jest to mały plik DLL, który ułatwia tworzenie różnych aplikacji wykorzystujących skomplikowane efekty graficzne (3D, 2D, obroty, światła itp.). Według niektórych obsługa tej biblioteki jest ?czarną magią?, a dla innych z kolei jest bardzo prosta. Jedno jest pewne: aby programować przy użyciu OpenGL, potrzebna jest dobra znajomość matematyki oraz fizyki. Listing 9.1 przedstawia bardzo prosty program napisany przy użyciu biblioteki OpenGL, wyświetlający prosty trójkąt.

Listing 9.1. Prosty program napisany przy użyciu OpenGL

unit MainFrm;

interface

uses
  Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs, OpenGL;

type
  TMainForm = class(TForm)
    procedure FormCreate(Sender: TObject);
    procedure FormDestroy(Sender: TObject);
    procedure FormResize(Sender: TObject);
  private
    RC  : HGLRC;     // rendering
    DC  : HDC;       // moduł
    procedure glDraw;
    procedure Idle(Sender: TObject; var Done: Boolean);
  end;

var
  MainForm: TMainForm;

implementation

{$R *.DFM}

procedure TMainForm.glDraw();
begin
  glClear(GL_COLOR_BUFFER_BIT or GL_DEPTH_BUFFER_BIT);    // czyszczenie
  glLoadIdentity();   // także czyszczenie
// procedura tworząca wierzchołki w przestrzeni (tutaj trójkąty)
  glTranslate(0, -1, -2);   // nasza pozycja lub raczej przesunięcie o wektor wierzchołków
// przypominam, że w OpenGL występuje system x,y,z
  glRotatef(10, 20, -10, 0);   // obrócenie widoku o dany kąt
// procedura rysująca trójkąt
  glBegin(GL_TRIANGLES);
  glVertex3f(0, 0, 0);
  glVertex3f(0.5, 0.5, 0);
  glVertex3f(1, 0, 0);
  glEnd();
// procedura rysująca zwroty x,y,z, gdzie:
// x - czerwony,
// y - zielony,
// z - niebieski.
  glColor3f(1, 0, 0);
  glBegin(GL_LINES);
  glVertex3f(0, 0, 0);
  glVertex3f(2, 0, 0);
  glColor3f(0, 1, 0);
  glVertex3f(0, 0, 0);
  glVertex3f(0, 2, 0);
  glColor3f(0, 0, 1);
  glVertex3f(0, 0, 0);
  glVertex3f(0, 0, 2);
  glEnd();
  glColor3f(1, 1, 1);
end;

procedure glInit();
begin
  glClearColor(0.0, 0.0, 0.0, 0.0); 	   // kolor wypełnienia: r - czerwony, g - zielony, b - niebieski, a - przezroczystość
  glShadeModel(GL_SMOOTH);
  glClearDepth(1.0);
  glEnable(GL_DEPTH_TEST);
  glDepthFunc(GL_LESS);
  glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST);
end;

procedure TMainForm.FormCreate(Sender: TObject);
var
  pFd : TPIXELFORMATDESCRIPTOR;
  pF  : Integer;
begin
  DC := GetDC(Handle);   // uchwyt modułu OpenGL
  pFd.nSize := SizeOf(pFd);
  pFd.nVersion := 1;
  pFd.dwFlags := PFD_DRAW_TO_WINDOW or PFD_SUPPORT_OPENGL or PFD_DOUBLEBUFFER or 0;
  pFd.iPixelType := PFD_TYPE_RGBA;
  pFd.cColorBits := 32;
  pF := ChoosePixelFormat(DC, @pFd);

  SetPixelFormat(DC, pF, @pFd);
  RC := wglCreateContext(DC);    // nasza scenka
  wglMakeCurrent(DC, RC);
  
  Resize;
  Application.OnIdle := Idle;  //ustalenie wartości ciągłego renderowania
  glInit;
end;

procedure TMainForm.FormDestroy(Sender: TObject);
begin
{ Usuniecie uchwytów z pamięci }
  wglMakeCurrent(0,0);
  wglDeleteContext(rc);
end;

procedure TMainForm.Idle(Sender: TObject; var Done: Boolean);
begin
  glDraw();                 // renderowanie sceny
  SwapBuffers(DC);          // przeniesienie z bufora na ekran 
end;

procedure TMainForm.FormResize(Sender: TObject);
begin
  glViewport(0, 0, Width, Height); // okienko (rozmiary)
  glMatrixMode(GL_PROJECTION);
  glLoadIdentity();
  gluPerspective(110.0, Width / Height, 0.01, 100.0);
  {
    pierwsza wartość (110.0) to kąt widzenia, domyślnie 90,
    druga to kalkulacja perspektywy - zawsze szerokość/wysokość,
    trzecia - z jakiej najbliższej odległości nie rysować sceny,
    czwarta - z jakiej najdalszej odległości nie rysować sceny;
  }
  glMatrixMode(GL_MODELVIEW);
end;

end.

Jak mówiłem, z OpenGL wiąże się sporo działań matematycznych, lecz przed ich użyciem należy zainicjować bibliotekę. Delphi zawiera moduł OpenGL.pas, który importuje wszystkie potrzebne procedury z pliku OpenGL.dll i ułatwia korzystanie z dobrodziejstw oferowanych nam przez ową bibliotekę.

Więcej na temat biblioteki OpenGL możesz dowiedzieć się z książek Wydawnictwa Helion: OpenGL. Księga eksperta oraz Programowanie gier w OpenGL. Informacje na temat tej biblioteki możesz znaleźć także pod adresami:
http://www.opengl.org ? strona poświęcona OpenGL;
http://www.eecs.tulane.edu/www/Terry/OpenGL/Introduction.html ? kurs OpenGL.

DirectX

Odpowiedzią Microsoftu na powstanie biblioteki OpenGL była biblioteka DirectX. DirectX obejmuje większy zakres multimediów, jak np. obsługę dźwięku lub urządzeń typu dżojstik. Wadą DirectX jest to, że obsługuje go jedynie system Windows ? w odróżnieniu od OpenGL, który dostępny jest praktycznie dla każdej platformy.

Jeśli chcemy napisać program w oparciu o DirectX, sprawa jest nieco prostsza, a to dzięki komponentom z serii DelphiX. Należy je pobrać z Internetu i zainstalować (instalacją komponentów zajmiemy się w III części książki) ? wówczas na palecie komponentów znajdzie się kilka kontrolek, dzięki którym tworzenie aplikacji w oparciu o DirectX stanie się o wiele łatwiejsze.

Tworzenie bitmap

Na początek zajmiemy się rzeczą najprostszą ? ładowaniem, wyświetlaniem i zapisywaniem plików typu *.bmp, czyli inaczej mówiąc ? bitmap. VCL jak zawsze o wiele bardziej ułatwia sprawę ? dzięki tej bibliotece wyświetlenie obrazka staje się bardzo proste. Możemy wykorzystać chociażby komponent TImage.

Korzystanie z komponentu TImage

Komponent TImage jest bardzo przydatny, jeśli chodzi o wyświetlanie obrazków, a jednocześnie bardzo prosty w użyciu. Aby załadować obrazek do komponentu jeszcze w trakcie projektowania programu, wystarczy skorzystać z właściwości Picture (rysunek 9.1).

9.1.jpg
Rysunek 9.1. Zaznaczona właściwość Picture

Kliknięcie przycisku wielokropka spowoduje wyświetlenie edytora obrazków (Picture Editor) ? patrz rysunek 9.2.

9.2.jpg
Rysunek 9.2. Okno Picture Editor

Naciśnięcie przycisku Load spowoduje wyświetlenie okna służącego do wyboru pliku graficznego. Następnie taki plik graficzny zostanie umiejscowiony w komponencie TImage i skompilowany wraz z programem (włączony do wynikowego pliku EXE).

Usunięcie owego pliku nastąpi po naciśnięciu przycisku Clear.

Ładowanie obrazków w trakcie działania programu
#Umieść na formularzu komponent TImage
#Właściwości Align komponentu TImage nadaj wartość alClient.
#Zmień właściwość Stretch komponentu TImage na True.
#Wygeneruj zdarzenie OnCreate formularza i umieść w nim taki kod:

procedure TMainForm.FormCreate(Sender: TObject);
begin
  Image.Picture.Bitmap.LoadFromFile('helion.bmp');
end;

Na samym początku po umieszczeniu komponentu na formularzu i rozciągnięciu go na całą szerokość nadałeś właściwości Stretch wartość True. Spowoduje to, że każdy obrazek załadowany do komponentu zostanie rozciągnięty lub zwężony tak, aby dostosować się do rozmiarów komponentu (rysunek 9.3).

9.3.jpg
Rysunek 9.3. Program w trakcie działania

Powróćmy do kodu, który powoduje załadowanie bitmapy. Teraz właśnie masz okazję docenić zalety biblioteki VCL ? wystarczy jeden wiersz kodu! Najpierw za pomocą operatora kropki (.) odwołujemy się do obiektu TPicture, a następnie do obiektu TBitmap. Klasa TBitmap zawiera natomiast procedurę LoadFromFile, która umożliwia załadowanie obrazka z pliku.

Podczas ładowania obrazka należy zwrócić uwagę, czy jest on rzeczywiście zapisany w formacie BMP. W przeciwnym wypadku wykonany zostanie wyjątek EInvalidType.

Klasa TBitmap

Do operowania na samych plikach *.bmp służy klasa TBitmap. Dzięki niej możliwe jest dokonywanie operacji pamięciowych, tzn. operacji na bitmapie odbywających się w pamięci komputera i nie dających rezultatów dla użytkownika.

Przed wykorzystaniem metod klasy TBitmap należy ją utworzyć:

var
  Bitmap : TBitmap;
begin
  Bitmap := TBitmap.Create;
  try
    { operacje na bitmapie }
  finally
    Bitmap.Free; // zwolnienie klasy
  end;
end;

Tworzenie oraz niszczenie obiektu odbywa się identycznie jak w przypadku innych klas VCL. W tabelach 9.1 oraz 9.2 przedstawiam najważniejsze właściwości oraz metody klasy TBitmap.

Tabela 9.1. Najważniejsze właściwości klasy TBitmap

WłaściwośćOpis
CanvasWskazanie na klasę TCanvas ? zajmiemy się tym nieco później
EmptyWłaściwość przybiera wartość True, jeśli bitmapa nie jest załadowana
HeightWysokość bitmapy (w pikselach)
IgnorePaletteMożesz przydzielić tej właściwości wartość True, jeśli szybkość ładowania jest priorytetowa ? wówczas wyświetlana bitmapa będzie posiadała jedynie 255 kolorów
PixelFormatOkreśla format pikseli: pf1bit (1 bit na piksel ? bitmapa czarno-biała), pf4bit, pf8bit, pf15bit, pf16bit, pf24bit, pf32bit, pfCustom (nieokreślone).
TransparentColorZwraca kolor pierwszego piksela w bitmapie (jeżeli TransparentMode jest ustawione na tmAuto)
TransparentModeOkreśla rodzaj przezroczystości. Jeżeli właściwość ma ustawioną wartość tmAuto, to kolor przezroczystości jest określany na podstawie lewego dolnego piksela. Jeżeli właściwość ma ustawioną wartość tmFixed, oznacza to, że kolor przezroczystości ma być odczytany z obiektu
TransparentWłaściwość określa sposób malowania bitmapy. Po ustawieniu wartości True bitmapa będzie przezroczysta
WidthOkreśla szerokość bitmapy w pikselach
ModifiedWłaściwość określa, czy bitmapa została zmodyfikowana

Tabela 9.2. Najważniejsze metody klasy TBitmap

MetodaOpis
LoadFromClipboardFormatProcedura powoduje załadowanie obrazka, który znajduje się obecnie w schowku
LoadFromResourceIDŁaduje bitmapę z zasobów (o tym w dalszej części rozdziału) na podstawie ID
LoadFromResourceNameŁaduję bitmapę z zasobów na podstawie nazwy zasobu
LoadFromStreamŁaduje bitmapę, która jest zapisana w strumieniu (TStream)
LoadFromFileŁaduje bitmapę z pliku
SaveToFileZapisuje bitmapę do pliku
SaveToStreamZapisuje bitmapę do strumienia (TStream)
SaveToClipboardFormatKopiuję bitmapę do schowka
FreeImageZwalnia obrazek i jednocześnie także pamięć

Odczytywanie obrazka ze schowka

Może zboczymy w tym momencie trochę z tematu grafiki, albowiem zamierzam opisać sposoby wykorzystania schowka w operowaniu grafiką, a dokładnie zapisywanie do schowka oraz odczytywanie.

Schowek jest specjalnym mechanizmem systemu Windows, umożliwiającym zapisywanie i przechowywanie dowolnych informacji (tekst, grafika) na potrzeby jednego lub kilku programów.

Zapis oraz odczyt danych ze schowka umożliwiają dwie funkcje klasy TBitmap: LoadFromClipboardFormat oraz SaveToClipboardFormat.

procedure SaveToClipboardFormat(var AFormat: Word; var AData: THandle; var APalette: HPALETTE); override;
procedure LoadFromClipboardFormat(AFormat: Word; AData: THandle; APalette: HPALETTE); override;

Ogólnie do wykorzystania schowka służy moduł Clipbrd, więc na samym początku będziesz musiał dodać go do listy uses. Operowanie klasą TClipBoard (znajduje się ona w module Clipbrd) jest całkiem proste. Zapisywanie i odczytywanie danych może odbywać się za pomocą procedur GetAsHandle oraz SetAsHandle.

Ogólnie w schowku może być kilka rodzajów danych (patrz tabela 9.3), lecz my chcemy odczytać jedynie dane w postaci bitmapy (CF_BITMAP).

Tabela 9.3. Rodzaje danych mogących znaleźć się w schowku

FlagaRodzaj danych
CF_TEXTTekst
CF_BITMAPGrafika w postaci bitmapy
CF_METAFILEPICTPlik metafile
CF_PICTUREZdjęcie (obiekt typu TPicture)
CF_COMPONENTDowolny inny obiekt

Odczyt danych

Image.Picture.Bitmap.LoadFromClipboardFormat(CF_BITMAP, ClipBoard.GetAsHandle(CF_BITMAP), 0);

W pierwszym parametrze podajemy rodzaj danych do odczytania; drugi parametr to już wywołanie funkcji GetAsHandle z klasy TClipBoard. Funkcja ta ma za zadanie odczytać dane i zwraca do nich uchwyt (THandle).

Wcześniej wypadałoby sprawdzić, czy w schowku rzeczywiście znajdują się dane w postaci bitmapy ? inaczej Delphi wywoła wyjątek:

if ClipBoard.HasFormat(CF_BITMAP) then { tutaj kod }

Funkcja HasFormat zwraca True, jeżeli w schowku znajdują się dane określone w parametrze (w tym wypadku CF_BITMAP).

Zapisywanie danych

Zapisywanie danych jest nieco trudniejsze, gdyż w funkcji SaveToClipboardFormat parametry muszą być zmiennymi.

Image.Picture.Bitmap.SaveToClipboardFormat(wFormat, AHandle, APalette);
ClipBoard.SetAsHandle(wFormat, AHandle);

Funkcja SaveToClipboardFormat powoduje przypisanie do zmiennej wFormat nowej wartości ? tak samo jest w wypadku zmiennej AHandle. Dopiero teraz można wywołać polecenie SetAsHandle, zapisujące dane do schowka.

Program w trakcie działania przedstawiony został na rysunku 9.4., a jego kod źródłowy znajduje się w listingu 9.2.

9.4.jpg
Rysunek 9.4. Program w trakcie działania

Listing 9.2. Kod źródłowy programu

unit MainFrm;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, StdCtrls, ExtCtrls;

type
  TMainForm = class(TForm)
    gbImage: TGroupBox;
    Image: TImage;
    rgSelect: TRadioGroup;
    btnDoIt: TButton;
    btnClear: TButton;
    procedure btnDoItClick(Sender: TObject);
    procedure btnClearClick(Sender: TObject);
  private
    { Private declarations }
  public
    { Public declarations }
  end;

var
  MainForm: TMainForm;

implementation

{$R *.dfm}

uses Clipbrd;

procedure TMainForm.btnDoItClick(Sender: TObject);
var
  AHandle : THandle;
  wFormat : WORD;
  APalette : HPALETTE;
begin
  case rgSelect.ItemIndex of
    0: Image.Picture.Bitmap.LoadFromClipboardFormat(CF_BITMAP, ClipBoard.GetAsHandle(CF_BITMAP), 0);
    1: begin
         Image.Picture.Bitmap.SaveToClipboardFormat(wFormat, AHandle, APalette);
         ClipBoard.SetAsHandle(wFormat, AHandle);
       end;
    2: Image.Picture.Bitmap.LoadFromFile('helion.bmp');
  end;
end;

procedure TMainForm.btnClearClick(Sender: TObject);
begin
{ zwolnij zasoby }
  Image.Picture.Bitmap.FreeImage;
  Image.Picture.Assign(nil);
end;

end.

Pliki JPEG

Pliki JPEG (albo inaczej ? JPG) są bardzo popularnym formatem zapisu obrazków. Wszystko dzięki znakomitej kompresji, pozwalającej na duże zmniejszenie wielkości pliku bez dużej utraty jakości obrazu.

Klasa TJPEGImage

Do operowania na plikach JPG służy klasa TJPEGImage, znajdująca się w module JPEG. Ładowanie i zapisywanie plików wygląda praktycznie tak samo jak w przypadku bitmap. Jedyna różnica polega na tym, że klasa TJPEGImage przy zapisywaniu stosuje już kompresję. Obrazek helion.bmp, którym posługiwałem się jako przykładem, został zmniejszony z 44 KB do 8 KB.

Najważniejsze właściwości klasy TJPEGImage przedstawione zostały w tabeli 9.4, a metody ? w tabeli 9.5

Tabela 9.4. Najważniejsze właściwości klasy TJPEGImage

WłaściwośćOpis
CompressionQualityOkreśla jakość kompresji ? od 1 do 100
GrayscaleOkreśla, czy obrazek ma być czarno-biały czy kolorowy
PerformanceSposób kompresji: jpBestQuality (lepsza jakość, większy plik), jpBestSpeed (optymalizacja pod względem szybkości ? mniejsza jakość)
ScaleSposób wyświetlania obrazka: jsFullSize (pełny obrazek), jsHalf (połowa), jsQuarter (ćwiartka), jsEighth (1/8)

Klasa TJPEGImage posiada także te same metody i właściwości co TBitmap ? np. Width, Height i Empty.

Tabela 9.5. Najważniejsze metody klasy TJPEGImage

MetodaOpis
CompressKompresuje na podstawie ustawień właściwości takich jak CompressionQuality czy Performance
AssignPowoduje ?skopiowanie? danych z innej klasy
DIBNeededDekompresja pliku JPEG do postaci bitmapy

Tak samo, jak w przypadku właściwości klasa TJPEGImage posiada metody klasy TBitmap (aczkolwiek nie wszystkie), takie jak SaveToFile, LoadFromFile, SaveToStream itp.

Wyświetlanie obrazków w komponencie TImage

Warto wspomnieć o jeszcze jednej kwestii dotyczącej wyświetlenia obrazków w komponencie TImage. Jak dotąd używaliśmy tego komponentu jedynie do wyświetlania bitmap. Oczywiście nadaje się on również do wyświetlania takich plików, jak np. JPEG, lecz wcześniej konieczne jest dodanie do listy modułów (uses) pliku JPEG.

  Image.Picture.LoadFromFile('helion.jpg');

Pamiętaj, aby nie odwoływać się w przypadku bitmap do klasy TBitmap, ale bezpośrednio do TPicture

Przykład działania ? kompresja plików

Na płycie CD-ROM dołączonej do książki znajduje się katalog ..listingi/9/JPEG/, a w nim projekt JpegApp.dpr. Ów program umożliwia kompresowanie bitmap do postaci plików *.jpg. Sama kompresja opiera się wyłącznie na zastosowaniu jednej procedury:

uses jpeg;

procedure TMainForm.btnConvertClick(Sender: TObject);
var
  Bitmap : TBitmap;
  JPG : TJPEGImage;
begin
  Bitmap := TBitmap.Create;
  try
  { w przypadku, gdy użytkownik wybierze w oknie plik BMP }
    if OpenDialog.Execute then
    begin
    { załaduj do klasy }
      Bitmap.LoadFromFile(OpenDialog.FileName);
    { miniaturkę wyświetl również w komponencie TImage }
      Image.Picture.Bitmap.Assign(Bitmap);
     { utwórz klasę }
      JPG := TJPEGImage.Create;
      try
      { przypisz obrazek z klasy TBitmap }
        JPG.Assign(Bitmap);

        if SaveDialog.Execute then
        { zapisanie już skompresowanego obrazka }
          JPG.SaveToFile(SaveDialog.FileName);
      finally
        JPG.Free;
      end;
    end;
  finally
    Bitmap.Free;
  end;
end;

Oprócz przycisku TButton na formularzu znajdują się także komponenty TImage oraz TOpenDialog i TSaveDialog. Te dwa ostatnie służą do wyświetlania standardowych ?windowsowych? okien Otwórz plik oraz Zapisz plik.

Jak działa program? Najpierw, po naciśnięciu przycisku, użytkownik musi wskazać plik *.bmp, który ma zostać skompresowany. W tym momencie zostaje utworzona klasa TBitmap, a podgląd obrazka zostaje wyświetlony w TImage. Następnie użytkownik musi podać nową nazwę pliku, w którym zapisane zostaną skompresowane dane. W tym momencie wystarczy pobrać dane z klasy TBitmap (za pomocą metody Assign), a następnie wywołać metodę SaveToFile.

Pliki GIF

Niestety Delphi nie posiada obecnie żadnego modułu (komponentu) wspomagającego wyświetlanie plików (animacji) GIF. Jedynym rozwiązaniem jest pobranie z Internetu komponentu o nazwie TGIFImage i zainstalowanie go w Delphi. Instalacją komponentów zajmiemy się w części III tej książki, a sam komponent możesz znaleźć choćby na stronie http://4programmers.net.

Zasoby

Słowo zasoby (ang. resources) może mieć wiele znaczeń, lecz w tym wypadku oznacza dane dodawane i kompilowane wraz z plikiem EXE.

Chcąc wyświetlić w trakcie działania programu jakiś obrazek, musielibyśmy dołączać te wszystkie pliki wraz z programem. Może to się jednak wydać nieco niewygodne ? nie lepiej mieć wszystko w jednej paczuszce (pliku)?
Zasoby w rzeczywistości są plikiem o rozszerzeniu *.res lub *.rc, który może być włączany do programu za pomocą dyrektywy:

{$R ZASOBY.RES}

Powyższa instrukcja może wydawać się komentarzem, ale w rzeczywistości jest poleceniem włączenia pliku ZASOBY.RES do skompilowanej wersji programu (plik *.exe).

Tworzenie zasobów

Najprostszym środkiem umożliwiającym stworzenie zasobów jest skorzystanie z edytora (program Image Editor). Ów edytor jest standardowo dołączany do Delphi, lecz w rzeczywistości stanowi osobną aplikację; można go uruchomić, wybierając z menu Tools polecenie Image Editor (rysunek 9.5).

9.5.jpg
Rysunek 9.5. Program Image Editor

Program Image Editor jest w rzeczywistości prostym edytorem graficznym, lecz również nadaje się do tworzenia zasobów.

Aby utworzyć nowy zasób, z menu File należy wybrać New/Resource File. W tym momencie zostanie otwarte nowe okno (rysunek 9.6), które może zawierać różne gałęzie zasobów (bitmapy, ikony, kursory).

9.6.jpg
Rysunek 9.6. Okienko do tworzenia nowych zasobów

Dołączanie bitmapy

Nie zamierzamy tworzyć nowej bitmapy, lecz wstawić do zasobów już istniejącą. W tym celu z menu File wybierz Open i znajdź na dysku jakiś obrazek BMP.

Image Editor jest dość prostym programem i nie umożliwia wyświetlania obrazków o większej liczbie kolorów niż 255. Ominięciem tego problemu zajmiemy się nieco później.

Najpierw dotychczasowy obrazek musisz skopiować do schowka, naciskając kolejno Ctrl+A (zaznaczenie) i Ctrl+C (skopiowanie). Po tym zabiegu możesz już zamknąć okno z bitmapą. Przejdźmy do naszych zasobów ? kliknij w obszarze tego okna prawym przyciskiem myszy i z menu New wybierz Bitmap. Będziesz musiał wpisać rozmiary nowej bitmapy oraz podać liczbę kolorów (rysunek 9.7).

9.7.jpg
Rysunek 9.7. Tworzenie nowej bitmapy

Po tym zabiegu na liście pojawiła się nowa gałąź Bitmap, a w niej nasza bitmapa. Po jej otwarciu zobaczymy puste okno ? tutaj należy wkleić skopiowaną uprzednio bitmapę.

Pozostałe zasoby

Tworzenie kolejnych zasobów (kursory, ikony) jest bardzo podobne. W wyniku tych działań nasze okno z zasobami wzbogaci się o nowe gałęzie (rysunek 9.8). Teraz nie pozostaje nam już nic innego, jak zapisać plik (File/Save As) pod nazwą resource.res.

9.8.jpg
Rysunek 9.8. Bitmapa, kursor oraz ikona włączona do pliku zasobów

Wykorzystanie zasobów

W poprzednim punkcie utworzyłeś plik resource.res, do którego włączony był kursor, bitmapa i ikona. Plik ten możesz znaleźć na płycie CD-ROM w katalogu ..listingi/9/Rest. Pierwszym krokiem jest dołączenie tego pliku do projektu:

  {$R RESOURCE.RES}

Ładowanie bitmapy

Podczas omawiania klasy TBitmap wspominałem o funkcji LoadFromResourceName, dzięki której w dość prosty sposób można załadować obrazek bezpośrednio z dołączonych zasobów:

procedure TMainForm.btnLoadBitmapClick(Sender: TObject);
begin
  imgBitmap.Picture.Bitmap.LoadFromResourceName(hInstance, '1st_bitmap');
end;

W pierwszym parametrze tej procedury należy podać uchwyt do instancji programu. Tak się składa, że zasoby mogą być także ładowane z biblioteki DLL (o tym będzie mowa w kolejnym rozdziale) ? w takim wypadku należałoby podać w tym miejscu uchwyt do biblioteki. Możesz jednak przyjąć, że w większości sytuacji wystarczy słowo kluczowe hInstance. Natomiast drugi parametr procedury LoadFromResourceName to nazwa bitmapy umieszczonej w zasobach.

Ładowanie ikony

Ikony w Delphi są reprezentowane przez klasę TIcon. Obsługa tej klasy jest niezwykle podobna do TBitmap oraz TJPEGImage ? większość właściwości i metod powtarza się, stąd postanowiłem nie omawiać jej dokładniej.

Ładowanie i wyświetlanie ikony może wyglądać tak:

procedure TMainForm.btnLoadIconClick(Sender: TObject);
var
  Icon : TIcon;
begin
  Icon := TIcon.Create;
  Icon.Handle := LoadIcon(hInstance, '1st_ico');   // załaduj ikonę z zasobów
  imgIcon.Picture.Icon := Icon;
  Icon.Free;
end;

Niestety klasa TIcon pozbawiona jest funkcji LoadFromResourceName (dostępna jest tylko LoadFromFile), więc aby ?wyciągnąć? ikonę z zasobów, należy zastosować metody zastępcze ? w tym wypadku funkcję LoadIcon. Użycie tej funkcji jest podobne do funkcji VCL, lecz zwracany rezultat ma postać typu HICON (typ WinAPI).

Ładowanie kursora

W Delphi obowiązuje tzw. tablica kursorów. Oznacza to, że każdy kursor ma swój numer i użycie go wiąże się z przypisaniem do odpowiedniej właściwości odpowiedniego numeru ? np. tak:

Screen.Cursor := 1;

W takim wypadku cała aplikacja będzie korzystała z kursora o numerze 1, lecz wcześniej trzeba taki kursor załadować:

procedure TMainForm.FormCreate(Sender: TObject);
begin
{ załaduj kursor do tablicy zasobów }
  Screen.Cursors[1] := LoadCursor(hInstance, '1st_cur');
{ wyświetl kursor }
  Screen.Cursor := 1;
end;

Tutaj także korzystamy z funkcji API ? LoadCursor ? i przydzielamy kursor do tablicy Cursors pod numerem 1. W listingu 9.3. znajduje się przykład ładowania kursora, ikony oraz bitmapy.

Listing 9.3. Kod źródłowy programu

unit MainFrm;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, StdCtrls, ExtCtrls;

  {$R RESOURCE.RES}

type
  TMainForm = class(TForm)
    btnLoadIcon: TButton;
    imgIcon: TImage;
    imgBitmap: TImage;
    btnLoadBitmap: TButton;
    procedure FormCreate(Sender: TObject);
    procedure btnLoadIconClick(Sender: TObject);
    procedure btnLoadBitmapClick(Sender: TObject);
  private
    { Private declarations }
  public
    { Public declarations }
  end;

var
  MainForm: TMainForm;

implementation

{$R *.dfm}

procedure TMainForm.FormCreate(Sender: TObject);
begin
{ załaduj kursor do tablicy zasobów }
  Screen.Cursors[1] := LoadCursor(hInstance, '1st_cur');
{ wyświetl kursor }
  Screen.Cursor := 1;
end;

procedure TMainForm.btnLoadIconClick(Sender: TObject);
var
  Icon : TIcon;
begin
  Icon := TIcon.Create;
  Icon.Handle := LoadIcon(hInstance, '1st_ico');   // załaduj ikonę z zasobów
  imgIcon.Picture.Icon := Icon;
  Icon.Free;
end;

procedure TMainForm.btnLoadBitmapClick(Sender: TObject);
begin
  imgBitmap.Picture.Bitmap.LoadFromResourceName(hInstance, '1st_bitmap');
end;

end.

Ręczne tworzenie zasobów

Program Image Editor nie daje nam zbyt dużych możliwości tworzenia bardziej zaawansowanych zasobów, więc należy posłużyć się nieco bardziej skomplikowanymi metodami ? pisaniem kodu zasobów. Tworząc swoje zasoby ręcznie, mamy możliwość większego manipulowania danymi, które mają się tam znaleźć. Możemy umieścić tam dosłownie wszystko ? począwszy od plików JPG, a skończywszy na innych programach.

Całość opiera się na pisaniu skryptów o rozszerzeniu *.rc. Takie skrypty to w rzeczywistości zwykłe pliki tekstowe zawierające odpowiednie polecenia. Następnie ? za pomocą dołączonego do Delphi programu (brcc32.exe) ? są one kompilowane do postaci pliku *.res. Program jest uruchamiany w oknie MS-DOS, a jego rozmiary są bardzo małe. Od razu zalecam skopiowanie go do katalogu, w którym ma się odbyć tworzenie zasobów, gdyż w przeciwnym wypadku wykorzystywanie poleceń systemu MS-DOS będzie dość niewygodne.

Dodawanie plików JPEG

Pierwszym krokiem jest stworzenie w katalogu z programem pliku files.rc. Plik taki możesz otworzyć w każdym edytorze tekstowym i umieścić w nim następujący wiersz:

PIC JPEGFILE "sfp.jpg"

Pierwszy człon tej linii do identyfikator ? słowo, jakie będzie identyfikować właśnie ten obrazek. Kolejny człon to typ pliku. Podawana tu wartość nie jest specjalnie ważna; istotna jest jedynie podczas pisania programu ładującego zasoby. Wreszcie ostatni człon ? słowo umieszczone w cudzysłowach ? to nazwa pliku przeznaczonego do skompilowania do postaci zasobów.

Teraz, mając już plik przeznaczony do skompilowania, musisz uruchomić w oknie MS-DOS program brcc32.exe z parametrem określającym nazwę zasobów ? np.:

brcc32.exe files.rc

W wyniku tego zostanie stworzony plik files.res, który zawiera już obrazek JPG.

Ładowanie pliku JPG

Ponieważ klasa TJPEGImage nie zawiera procedury umożliwiającej ładowanie obrazków z zasobu, trzeba napisać własną klasę, opartą częściowo na strumieniach.

Pierwszym krokiem jest deklaracja nowej klasy opartej na TJPEGImage:

{ klasa dziedzicząca po TJPEGImage, która posiada jedną dodatkową
  funkcję ładowania z zasobów }
  TJPEGRes = class(TJPEGImage)
  public
    procedure LoadFromResource(const ResID: PChar); virtual;
  end;

Dzięki takiemu zabiegowi nasza nowa klasa ? TJPEGRes ? zawiera wszystkie metody z klasy TJPEGImage, a dodatkowo jeszcze procedurę LoadFromResource:

procedure TJPEGRes.LoadFromResource(const ResID: PChar);
var
  Res : TResourceStream; // utwórz zmienną
begin
{ ładuj obrazek z zasobów }
  Res := TResourceStream.Create(hInstance, ResID, 'JPEGFILE');
  try
    LoadFromStream(Res); // ładuj obrazek do strumienia ze zmiennej Res
  finally
    Res.Free;  // zwolnij pamięć
  end;
end;

Mimo że klasa TJPEGImage jest pozbawiona funkcji LoadFromResourceName, to znajduje się w niej polecenie LoadFromStream, które teraz okazało się dla nas bardzo przydatne.

Najpierw należało utworzyć klasę TResourceStream, która posiada bardzo przydatny konstruktor, umożliwiający załadowanie dowolnego pliku wprost z zasobów. Zwróć uwagę na ostatni parametr konstruktora ? jest to typ zasobów, który musi być zgodny z typem, który zadeklarowaliśmy w pliku files.rc. Pełny kod źródłowy tego programu znajduje się w listingu 9.4.

Listing 9.4. Ładowanie pliku JPG z zasobów

unit MainFrm;

interface

{ tutaj przechowywane są zasoby z JPEG }

  {$R FILES.RES}


uses
  Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs,
  StdCtrls, ExtCtrls, jpeg;

type
  TMainForm = class(TForm)
    btnLoadRes: TButton;
    Image: TImage;
    procedure btnLoadResClick(Sender: TObject);
  end;

{ klasa dziedzicząca po TJPEGImage, która posiada jedną dodatkową
  funkcję ładowania z zasobów }
  TJPEGRes = class(TJPEGImage)
  public
    procedure LoadFromResource(const ResID: PChar); virtual;
  end;

var
  MainForm: TMainForm;

implementation

{$R *.DFM}

{ TJPEGRes }

procedure TJPEGRes.LoadFromResource(const ResID: PChar);
var
  Res : TResourceStream; // utwórz zmienną
begin
{ ładuj obrazek z zasobów }
  Res := TResourceStream.Create(hInstance, ResID, 'JPEGFILE');
  try
    LoadFromStream(Res); // ładuj obrazek do strumienia ze zmiennej Res
  finally
    Res.Free;  // zwolnij pamięć
  end;
end;

procedure TMainForm.btnLoadResClick(Sender: TObject);
var
  JPG : TJPEGRes;
begin
  JPG := TJPEGRes.Create; // tworzenie nowej klasy
  try
    JPG.LoadFromResource('PIC'); // załadowanie odpowiedniego zasobu 
    Image.Picture.Assign(JPG);
  finally
    JPG.Free;
  end;
end;

end.

Doklejanie plików EXE

Powyższej przedstawiłem sposób na ładowanie obrazków z zasobu wykorzystując klasę TResourceStream. Chcąc ?wyciągnąć? z zasobów inne pliki ? np. aplikacje ? także należy skorzystać z tego sposobu.

Do pliku exe_file.rc dodałem taką linię:

ASCII EXEFILE "ascii.exe"

W tym wypadku identyfikatorem zasobu jest ASCII, a typ pliku to EXEFILE.

?Wyciągnięcie? zasobu i zapisanie go gdzieś na dysku może wyglądać tak:

procedure TForm1.btnExtractClick(Sender: TObject);
var
  Res : TResourceStream;
begin
  if SaveDialog.Execute then // jeżeli okno zostanie wyświetlone
  begin
  { wywołaj konstruktor klasy }
    Res := TResourceStream.Create(Hinstance, 'ASCII', 'EXEFILE');
    try
      Res.SaveToFile(SaveDialog.FileName); // zapisz zasób do pliku
    finally
      Res.Free;
    end;
  end;
end;

Klasa TCanvas

W Delphi dostępna jest niezwykle wygodna i prosta w użyciu klasa TCanvas, która uwalnia nas od nieco żmudnego wykorzystywania funkcji API, służących do wykonywania prostych czynności graficznych (wyświetlenie obrazku, namalowanie tekstu itp.). Klasy TCanvas nie trzeba inicjować ani zwalniać ? wszystko dzięki temu, że zadanie to wykonuje formularz, który posiada właściwość Canvas.

Prosty przykład:

#Wygeneruj zdarzenie OnPaint formularza.
#W procedurze zdarzeniowej wpisz taki kod:

  Canvas.TextOut(10, 10, 'Witaj!');

Instrukcja TextOut z klasy TCanvas powoduje wyświetlenie napisu określonego w trzecim parametrze tej procedury. Dwa pierwsze parametry określają pozycje X i Y wyświetlanego napisu (rysunek 9.9).

9.9.jpg
Rysunek 9.9. Zastosowanie procedury TextOut

Zadajmy sobie pytanie: dlaczego kod musi być umieszczony w zdarzeniu OnPaint? System Windows działa na zasadzie wielowątkowej. W danym momencie może być uruchomionych wiele programów; mogą one wykonywać swoje, ustalone czynności. Okna te jednak są minimalizowane, a ich pozycje są zmieniane ? system nie jest w stanie ?zapamiętać? obrazu każdego okna, gdyż pochłonęłoby to wiele, wiele pamięci. Stąd w momencie, gdy jakieś okno jest wyświetlane i znajduje się na pierwszym planie, system wysyła do niego komunikat WM_PAINT, stanowiący informację dla programu, że znajduje się on na pierwszym planie. Reakcja an ten komunikat zależy od aplikacji. Stąd obecność zdarzenia OnPaint, dzięki któremu jesteśmy w stanie narysować cokolwiek na formularzu.

Pióra i pędzle

Przed rysowaniem mamy możliwość ustawienia pewnych parametrów. Mam na myśli style określające grubość linii, styl wypełniania figur, wszelkie kolory itp. Ponadto klasa TCanvas udostępnia dwie właściwości ? Pen (klasa TPen) oraz Brush (TBrush).

Klasa TPen

Właściwość Pen jest używana do określenia rodzaju linii, koloru linii i innych kształtów w czasie korzystania z klasy TCanvas. Wszystkie właściwości zgromadziłem w tabeli 9.6.

Tabela 9.6. Właściwości klasy TPen

WłaściwościOpis właściwości
ColorOkreśla kolor rysowanych linii i kształtów (krawędzi)
ModeDefiniuje tryb rysowanych linii i krawędzi (patrz tabela 9.7)
StyleStyl rysowanych linii (patrz tabela 9.8)
WidthSzerokość rysowanej linii (wartość podawana w pikselach)

Właściwość Mode

Dzięki właściwości Mode masz dostęp do większej ilości kombinacji definiujących tryb rysowanych linii. Właściwość Mode wskazuje na typ TPenMode:

type TPenMode = (pmBlack, pmWhite, pmNop, pmNot, pmCopy, pmNotCopy, pmMergePenNot, pmMaskPenNot, pmMergeNotPen, pmMaskNotPen, pmMerge, pmNotMerge, pmMask, pmNotMask, pmXor, pmNotXor);

Sam widzisz, że istnieje stosunkowo sporo rodzajów rysowania ? patrz tabela 9.7.

Tabela 9.7. Wartości typu TPenMode

WartośćOpis
pmBlackZawsze czarny
pmWhiteZawsze biały
pmNopNiezmienny
pmNotOdwrotność koloru tła klasy TCanvas
pmCopyKolor pióra
pmNotCopyInwersja koloru pióra
pmMergePenNotKombinacja koloru pióra i odwrotności koloru tła
pmMergeNotPenKombinacja koloru tła i inwersja koloru pióra
pmMergeKombinacja koloru tła i pióra.
pmNotMergeOdwrotność flagi pmMerge

Więcej kombinacji możesz znaleźć w systemie pomocy Delphi.

Właściwość Style

Inna ciekawa właściwość ? Style ? określa sposób rysowania linii (ciągły, przerywany itp.). W rzeczywistości właściwość ta wskazuje na typ TPenStyle:

type  TPenStyle = (psSolid, psDash, psDot, psDashDot, psDashDotDot, psClear, psInsideFrame);

W tabeli 9.8 przedstawiłem tłumaczenia powyższych wartości.

Tabela 9.8. Wartości typu TPenStyle

WartośćOpis
psSolidLinia ciągła
psDashLinia przerywana
psDotLinia kropkowana
psDashDotNa przemian: kropki i kreski
psDashDotDotNa przemian: kreska i dwie kropki
psClearBrak obrysowania

Mały przykład:

Canvas.Pen.Color := clGreen;
Canvas.Pen.Style := psDashDotDot;
Canvas.Rectangle(80, 10, 250, 150); // narysowanie kwadratu

W powyższym przykładzie zmieniliśmy kolor linii na zielony, a styl rysowania na psDashDotDot. Następnie z zastosowaniem tych ustawień narysowaliśmy kwadrat (polecenie Rectangle).

Na płycie CD-ROM w katalogu ..listingi/9/TPenStyle Demo/Demo.dpr znajduje się przykład ilustrujący praktyczne zastosowanie wszystkich stylów.

Jak dotąd nic nie mówiłem na temat kolorów używanych w Delphi. Wszystkie kolory to w rzeczywistości typ TColor. Wszystkie wartości typu TColor posiadają przedrostek cl, tak więc jeśli chcesz skorzystać np. z koloru czarnego i znasz odpowiednik słowa ?czarny? w języku angielskim, możesz się łatwo domyśleć, że wartość odpowiadająca temu kolorowi to clBlack.

Klasa TBrush

Dzięki klasie TBrush mamy możliwość ustawienia opcji wypełniania figur (kolor, bitmapa mająca wypełnić figurę, styl). Właściwości owej klasy przedstawione zostały w tabeli 9.9.

Tabela 9.9. Właściwości klasy TBrush

WartośćOpis
BitmapWskazuje na klasę TBitmap; określa bitmapę, jaka ma wypełniać figury
ColorOkreśla kolor wypełnienia
StyleWłaściwość definiuje styl wypełnienia figur (tabela 9.10)

Właściwość Style

Podobnie jak w przypadku klasy TPen, tutaj także właściwość Style określa styl, tyle że dotyczy on wypełnienia figur, a nie obrysowywania.

type TBrushStyle = (bsSolid, bsClear, bsHorizontal, bsVertical, bsFDiagonal, bsBDiagonal, bsCross, bsDiagCross);

Możliwe do zastosowania style opisałem w tabeli 9.10.

Tabela 9.10. Możliwe wartości klasy TBrushStyle

WartośćOpis
bsSolidPełne wypełnienie
bsCrossTło będzie siateczką
bsClearTło przezroczyste
bsDiagCrossSiatka przecinająca się pod kątem prostym
bsHorizontalLinie poziome
bsVerticalLinie pionowe
bsBDiagonalUkośnie z lewego dolnego narożnika do górnego
bsFDiagonalUkośnie z lewego górnego narożnika do dolnego

Teraz ? znając właściwości klasy TBrush ? możemy wypróbowywać nie tylko różne ustawienia pióra, ale również pędzla:

Canvas.Pen.Color := clGreen;
Canvas.Pen.Style := psDashDotDot;
Canvas.Brush.Color := clYellow; // kolor tła ? żółty
Canvas.Brush.Style := bsVertical;
Canvas.Rectangle(80, 10, 250, 150); // narysowanie kwadratu

Czcionki

Czcionki w Delphi reprezentowane są przez klasę TFont, dającą możliwość nie tylko zmiany kroju, ale także zmiany stylu wyświetlanego tekstu. Klasa TCanvas również pozwala na dostęp do TFont:

Canvas.Font.Name := 'Courier New';
Canvas.TextOut(100, 10, 'Hello World! ');

W powyższym wypadku zmieniliśmy czcionkę na Courier New, a dopiero później narysowaliśmy tekst. Na rysunku 9.10 przedstawiony został przykładowy program, umożliwiający przetestowanie zmiany wyglądu czcionki.

9.10.jpg
Rysunek 9.10. Testowanie czcionek

Kod źródłowy tego programu jest oczywiście dostępny na płycie CD-ROM.

Właściwości klasy TFont

W tabeli 9.11 znajdują się właściwości klasy TFont wraz z opisami.
Tabela 9.11. Właściwości klasy TFont

WłaściwośćWartość
CharsetKodowanie znaków
ColorKolor używanej czcionki
HeightWysokość czcionki (w pikselach)
NameNazwa czcionki (musi być podawana w apostrofach)
PitchMoże przybrać trzy wartości określające czcionkę: fpDefault (domyślna wartość), fpFixed (wszystkie znaki mają równą szerokość), fpVariable (znaki mają różne szerokości)
SizeRozmiar czcionki (w punktach)
StyleStyl czcionki: fsBold (pogrubiony), fsItalic (pochylony), fsUnderline (pokreślenie), fsStrikeOut (przekreślenie)

Tak się składa, że prawie w każdym komponencie wizualnym znajduje się właściwość Font typu TFont ? wiedza na jej temat może Ci się zatem nieraz przydać.

Tu jednak muszę poczynić małe zastrzeżenie. Jeżeli chcesz, aby tekst został pogrubiony, możesz napisać tak:

Font.Style := [fsBold];

W takim jednak wypadku tekst zostanie tylko pogrubiony. Czyli jeżeli wcześniej był pisany kursywą, to teraz kursywa zostanie usunięta, a tekst będzie tylko pogrubiony. Nam nie o to chodzi, gdyż chcemy, aby tekst był pogrubiony, ale przy zachowaniu wcześniejszych styli. Trzeba więc napisać to tak:

Font.Style := Font.Style + [fsBold];

Tutaj zastosowałem operator +. Jeżeli jednak chciałbyś, aby od stylu tekstu odjęty został styl pogrubienia, możesz napisać tak:

Font.Style := Font.Style ? [fsBold];

Jeżeli chcesz zresetować wszystkie style (wszystko zostanie odznaczone), wpisz coś takiego:

Font.Style := [];

Metody klasy TCanvas

Do narysowania różnych kształtów, figur geometrycznych, linii oraz tekstu można skorzystać z metod klasy TCanvas, które są bardzo proste w użyciu. W większości przypadków należy podać jedynie parametry określające położenie danej figury i jej rozmiar.

W tym podpunkcie przedstawię najważniejsze metody klasy TCanvas. Część opiszę trochę dokładniej, a inne zaprezentuję w postaci jednego punktu, przedstawiając przykładowy program reprezentujący rysowanie różnych figur geometrycznych.

Draw

procedure Draw(X, Y: Integer; Graphic: TGraphic);

Dzięki funkcji możesz wyświetlić na formularzu jakąś grafikę (np. bitmapę) w miejscu oznaczonym przez parametry X oraz Y.

var
  Bitmap : TBitmap;
begin
  Bitmap := TBitmap.Create;
  try
    Bitmap.LoadFromFile(?helion.bmp?); // ładowanie pliku
    Canvas.Draw(10, 10, Bitmap); // wyświetlenie obrazka w punkcie 10, 10
  finally
    Bitmap.Free;
  end;
end;

Dzięki metodzie Draw można się obyć bez komponentów typu TImage. Pamiętaj jednak, aby powyższy kod umieścić w zdarzeniu OnPaint albo w jakiejś procedurze zdarzeniowej (np. OnClick komponentu TButton).

FillRect

procedure FillRect(const Rect: TRect);

Procedura FillRect może posłużyć do wypełniania jakiegoś obszaru (określonego parametrem Rect) z zastosowaniem dotychczasowych ustawień pióra i pędzla.

  Canvas.Brush.Color := clWhite;
  Canvas.FillRect(Rect(10, 10, 100, 200));

Powyższy kod spowoduje wypełnienie określonego obszaru białym kwadratem. Zwróć uwagę, że w parametrze procedury FillRect znajduje się nieznany dotąd typ ? TRect. W rzeczywistości jest to rekord, którego deklaracja znajduje się w pliku Windows.pas:

TRect = record
    case Integer of
      0: (Left, Top, Right, Bottom: Integer);
      1: (TopLeft, BottomRight: TPoint);
  end;

Nie wykonuje on nic nadzwyczajnego ? chcąc zastąpić ten typ, należałoby zadeklarować w procedurze cztery zmienne typu Integer, które określałyby położenie w pionie i w poziomie oraz szerokość i wysokość. Z zastosowaniem rekordu przekazujemy do procedury tylko jeden parametr.

Zwróć uwagę, że w tym przypadku wcale nie jest konieczna deklaracja nowej zmiennej ? wystarczy przekazać parametry w ten sposób:

Canvas.FillRect(Rect(10, 10, 100, 200));

StretchDraw

procedure StretchDraw(const Rect: TRect; Graphic: TGraphic);

Pamiętasz, jak na początku tego rozdziału zmieniałeś właściwość Stretch komponentu TImage? Jakie to wówczas dawało efekty? Wtedy osiągaliśmy dopasowanie całego obrazka do wielkości komponentu. Procedura StretchDraw jest jakby rozszerzeniem polecenia Draw. Oprócz zwykłego malowania grafiki umożliwia dopasowanie jej rozmiarów ? nie tylko położenia X i Y, ale także szerokości i wysokości.

var
  Bitmap : TBitmap;
begin
  Bitmap := TBitmap.Create;
  try
    Bitmap.LoadFromFile('helion.bmp'); // ładowanie pliku
    Canvas.StretchDraw(Rect(10, 10, 100, 100), Bitmap);
  finally
    Bitmap.Free;
  end;
end;

W tym wypadku narysowany obraz będzie miał rozmiary 100×100.

TextOut

procedure TextOut(X, Y: Integer; const Text: string);

Z procedurą TextOut zetknąłeś się już wcześniej. Realizuje ona proste rysowanie po formularzu. Pierwsze dwa parametry są pozycjami reprezentującymi położenie narysowanego tekstu, a ostatni ? typu String ? to tekst, który zostanie wyświetlony.

Oto przykład wyświetlenia bitmapy oraz ? dodatkowo ? rysowania na niej tekstu:

procedure TForm1.Button1Click(Sender: TObject);
var
  Bitmap : TBitmap;
begin
  Bitmap := TBitmap.Create;
  try
    Bitmap.LoadFromFile('C:\helion.bmp');
    Canvas.Draw(50, 50, Bitmap);
    Canvas.Brush.Style := bsClear; // tło rysowanego tekstu ? przezroczyste
    Canvas.TextOut(60, 60, 'http://helion.pl');
  finally
    Bitmap.Free;
  end;
end;

Przed narysowaniem tekstu styl pędzla zostaje ustawiony na bsClear, co gwarantuje, że tekst będzie miał przezroczyste tło (rysunek 9.11).

9.11.jpg
Rysunek 9.11. Efekt wyświetlania bitmapy i rysowania tekstu

TextRect

procedure TextRect(Rect: TRect; X, Y: Integer; const Text: string);

Oprócz rysowania zwykłego prostokąta procedura TextRect umożliwia zdefiniowanie prostokąta, wewnątrz którego zostanie narysowany tekst (parametr Rect). Kolejne dwa parametry X i Y określają poziome i pionowe położenie tego tekstu:

  Canvas.Brush.Color := clWhite;
  Canvas.TextRect(Rect(10, 10, 100, 50), 20, 20, 'Helion');

Aby bardziej uwidocznić istnienie tego prostokąta, na początku zmieniłem kolor pędzla na biały. Efekt zastosowania takiego kodu widać na rysunku 9.12.

9.12.jpg
Rysunek 9.12. Efekt zastosowania funkcji TextRect

TextWidth, TextHeight

function TextWidth(const Text: string): Integer;
function TextHeight(const Text: string): Integer;

Przypominam, że do obliczania długości łańcucha (w znakach) służy funkcja Length. W przypadku, gdy chcemy obliczyć wysokość i szerokość tekstu w pikselach, musimy skorzystać z funkcji TextWidth, TextHeight. W parametrach tych funkcji należy wpisać jedynie tekst, który ma zostać zmierzony.

Szerokość tekstu zależy również od czcionki, jaką zastosowano. Np. w czcionce Courier New zawarte są znaki o tej samej szerokości, w odróżnieniu od np. czcionki Arial.

TextExtent

function TextExtent(const Text: string): TSize;

Funkcja TextExtent podaje zarówno wysokość tekstu, jak i jego szerokość. Zwracana wartość ma postać rekordu TSize:

type 
  TSize = packed record
    cx: Longint;
    cy: Longint;
  end;

MoveTo

procedure MoveTo(X, Y: Integer);

Procedura MoveTo służy do przeniesienia punktu startowego przed rysowaniem np. linii. Przykładowo rysowana linia ma mieć swój początek w prawym, górnym rogu formularza. Wówczas aby ustawić punkt startowy, należy skorzystać z funkcji MoveTo:

Canvas.MoveTo(500, 1); // ustawienie punktu startowego

Teraz jeśli chcemy narysować linię, wystarczy wywołać funkcję LineTo.

LineTo

procedure LineTo(X, Y: Integer);

W owym poleceniu należy podać parametry X i Y rysowanej linii.

procedure TForm1.FormPaint(Sender: TObject);
begin
  Canvas.Pen.Width := 5;
  Canvas.MoveTo(500, 20);
  Canvas.LineTo(10, 200);
end;

Aby lepiej pokazać rysowaną linię, jej szerokość ustawiłem na 5 pikseli (rysunek 9.13).

9.13.jpg
Rysunek 9.13. Rysowanie linii

Inne funkcje służące do rysowania kształtów

Oprócz funkcji przedstawionych wcześniej istnieje szereg poleceń, dzięki którym możesz narysować wiele różnych figur geometrycznych (tabela 9.12).

Tabela 9.12. Nazwy funkcji służących do rysowania figur geometrycznych

FunkcjaOpis
ArcRysowanie łuku
ChordZamknięta figura (wielokąt)
EllipseRysowanie elipsy
PieWycinek koła
PolygonFigura rysowana podstawie tablicy punktów
PolylineLinia łamana
RectangleProstokąt

Przykładowy program

Na dołączonej do książki płycie CD-ROM znajduje się projekt (..listingi/9/PaintTest/PaintTest.dpr), który prezentuje sposób rysowania różnych figur geometrycznych (rysunek 9.14). Użytkownik ma możliwość wyboru rodzaju pędzla oraz pióra ? stylów i kolorów.

9.14.jpg
Rysunek 9.14. Prezentacja rysowania figur geometrycznych

W listingu 9.5 znajduje się kod źródłowy modułu MainFrm.pas, będącego częścią projektu PaintTest.dpr.

Listing 9.5. Kod źródłowy modułu MainFrm.pas

unit MainFrm;

interface

uses
  Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs,
  StdCtrls, ExtCtrls, Spin;

{ Stałe oznaczające odpowiednio style wypełnienia (TBrushStyle) oraz
  styl pędzla. }

const
  BrushStyle : array[0..6] of TBrushStyle =
  (bsSolid, bsCross, bsDiagCross, bsHorizontal, bsVertical, bsFDiagonal,
   bsBDiagonal);
  PenStyle : array[0..5] of TPenStyle =
  (psDash, psDashDot, psDashDotDot, psDot, psInsideFrame, psSolid);


type
  TMainForm = class(TForm)
    Panel1: TPanel;
    Shape: TRadioGroup;
    Setup: TGroupBox;
    Label1: TLabel;
    Label2: TLabel;
    Label3: TLabel;
    Label4: TLabel;
    LineSize: TSpinEdit;
    Style: TComboBox;
    PColorLine: TPanel;
    PBrushColor: TPanel;
    LineColor: TColorDialog;
    BrushColor: TColorDialog;
    Label5: TLabel;
    PStyle: TComboBox;
    procedure ShapeClick(Sender: TObject);
    procedure PColorLineClick(Sender: TObject);
    procedure PBrushColorClick(Sender: TObject);
  private
{ Procedura ustawiająca właściwości pędzla oraz wypełnienia }
     procedure Ustaw(Sender: TObject);
  end;

var
  MainForm: TMainForm;

implementation

{$R *.DFM}

procedure TMainForm.ShapeClick(Sender: TObject);
{ Stała ta zawiera współrzędne punktów, wymagane do wyświetlenia
  wycinka koła }
const
  Wielokat: array[0..3] of TPoint =
  ((X: 0; Y:0), (X: 150; Y:50), (X:230; Y:130), (X:40; Y:120));
var
  R : TRect;
begin
  R := ClientRect; //obszar formy
{ do obszaru odejmij szerokość Panelu }
  R := Rect(R.Left, R.Top, R.Right ? Panel1.Width, R.Bottom);

{ W zależności od wybranej opcji na komponencie "TRadioButton" wykonywane
  są odpowiednie funkcje. }

  case Shape.ItemIndex of
    0: begin
         Repaint; // odswież obraz
         Ustaw(Sender); // wykonaj procedurę
         Canvas.Rectangle(20, 20, 220, 220); // prostokąt
       end;
    1: begin
         Repaint;
         Ustaw(Sender);
         Canvas.Ellipse(R); // elipsa
       end;
    2: begin
         Repaint;
         Ustaw(Sender); // wycinek koła
         Canvas.Pie(0, 0, ClientWidth ? Panel1.Width, ClientHeight, 90, 0, 300, 10);
       end;
    3: begin
         Repaint;
         Ustaw(Sender);
        with ClientRect do  // łamana  [ działa tylko wtedy, gdy Pen.Style = psSolid ]
         Canvas.Arc(Left, Top, Right ? Panel1.Width, Bottom, Right, Top, Left, Top);
       end;
    4: begin
         Repaint;
         Ustaw(Sender);
         Canvas.RoundRect(20, 20, 220, 220, 30, 30); // prostokąt z zaokrąglonymi narożnikami
       end;
    5: begin
         Repaint;
         Ustaw(Sender);
         Canvas.Polygon(Wielokat); // wielokąt
       end;
    6: begin
         Repaint;
         Ustaw(Sender);
       with ClientRect do   // odcinek
         Canvas.Chord(Left, Top, Right ? Panel1.Width, Bottom, Right, Top, Left, Top);
       end;
  end;
end;

procedure TMainForm.Ustaw(Sender: TObject);
begin
// Ustaw styl wypełnienia w zależności od wybranego stylu w kontrolce
  Canvas.Brush.Style := BrushStyle[Style.ItemIndex];
// kolor wypełniania w zależności od koloru Panelu
  Canvas.Brush.Color := PBrushColor.Color;
// Ustaw styl pędzla w zależności od opcji wybranej w kontrolce
  Canvas.Pen.Style := PenStyle[PStyle.itemIndex];
// kolor pędzla w zależności od koloru panelu
  Canvas.Pen.Color := PColorLine.Color;
// Ustaw szerokość pędzla w zależności od wybranej opcji w kontrolce
  Canvas.Pen.Width := LineSize.Value;
end;

procedure TMainForm.PColorLineClick(Sender: TObject);
begin
// wyświetla komponent i ustawia kolor Panelu w zależności
// od wybranego koloru w komponencie
  if LineColor.Execute then
    PColorLine.Color := LineColor.Color;
end;

procedure TMainForm.PBrushColorClick(Sender: TObject);
begin  // j/w
  if BrushColor.Execute then
    PBrushColor.Color := BrushColor.Color;
end;

{ END }

end.

Właściwie wszystko odbywa się tutaj z pomocą instrukcji warunkowej case. Gdy użytkownik wybierze odpowiedni kształt, program pobiera ustawienia kolorów, style pędzla i pióra, a następnie wyświetla żądany kształt.

Proste animacje tekstowe

Pisząc animacje tekstowe, mam na myśli wizualizację tekstu ? tj. jego przemieszczenie i rysowanie 3D. Do napisania programu, który wykonywałby jakieś bardziej skomplikowane animacje (obroty figur 3D), najlepiej jest wykorzystać biblioteki OpenGL albo DirectX.

Tekst trójwymiarowy (3D)

W gruncie rzeczy narysowanie tekstu trójwymiarowego jest bardzo proste. Polega jedynie na podwójnym namalowaniu tego samego napisu, z minimalnym przesunięciem i innym kolorem. Oto przykład:

  Canvas.Font.Name := 'Courier New'; // czcionka
  Canvas.Font.Size := 20; // rozmiar czcionki
  Canvas.Font.Style := Font.Style + [fsBold]; // pogrubienie
  Canvas.Brush.Style := bsClear; // tło przezroczyste
  Canvas.Font.Color := clWhite; // kolor czcionki
  Canvas.TextOut(20, 20, 'WWW.4PROGRAMMERS.NET');
  Canvas.Brush.Style := bsClear; // tło przezroczyste
  Canvas.Font.Color := clBlack; // kolor czcionki
  Canvas.TextOut(19, 19, 'WWW.4PROGRAMMERS.NET');

Na samym początku należy ustawić odpowiednią czcionkę. Następnie należy określić tło tekstu jako przezroczyste (bsClear). Ustawiamy białą czcionkę i teraz następuje narysowanie białego tekstu w punkcie 20, 20. Później tekst zostanie narysowany czarną czcionką, tyle że z przesunięciem 1 piksela. Efektem takiego kodu będzie napis wyglądający tak, jak na rysunku 9.15.

9.15.jpg
Rysunek 9.15. Wyświetlenie napisu 3D

Efekt maszyny do pisania

Kolejnym efektem graficznym, jaki pragnę zaprezentować, jest efekt, który nazwałem maszyną do pisania. W tej animacji litery wyświetlane są jedna po drugiej (rysunek 9.16).

9.16.jpg
Rysunek 9.16. Wyświetlanie jednej litery po drugiej

Cały efekt opiera się na wyświetlaniu kolejnych liter w czasowych odstępach ? np. 100 milisekund. Na samym początku konieczne staje się pobranie długości napisu, który chcemy przedstawić:

  TextLength := Length(DText); // pobierz długość tekstu

Skorzystałem tutaj z funkcji Length, gdyż naszym celem jest pobranie ilości znaków. Ważne jest również to, aby użyta czcionka posiadała wszystkie znaki o tej samej długości:

const DText = 'Delphi...';

procedure TMainForm.KrokPoKroku(X, Y: Integer);
var
  TextLength, I: Integer;
begin
  TextLength := Length(DText); // pobierz długość tekstu

  with Canvas do
  begin
    for I := 1 to TextLength do // pętelka...
    begin
      Application.ProcessMessages;
      Sleep(100);  // czekaj 100 milisekund
      Brush.Style := bsClear; // styl na przezroczysty
      Font.Name := 'Courier New'; // czcionka
      Font.Color := clWhite; // kolor czcionki ? biały
      Font.Size := 16;  // rozmiar
{ Wyświetlaj tekst jedna litera po drugiej z przesunięciem }
      TextOut((X + i * 16), Y, DText[i]);
      Brush.Style := bsClear;
      Font.Color :=  clBlack;
      TextOut((X + i * 16) ?2, Y ?2, DText[i]); // wyświetl ten sam tekst w innym położeniu ? efekt cienia
   end;
  end;
end;

Chyba najtrudniejszą rzeczą w tej procedurze jest wytyczenie odstępów pomiędzy kolejnymi znakami. Ja wykorzystałem metodę prób i błędów i okazało się, że najoptymalniejszym rozwiązaniem jest mnożenie zmiennej I przez liczbę 16. Wtedy pierwszy znak zostanie umieszczony w punkcie 16, drugi w punkcie 32 itd.

Przed wywołaniem tej procedury dobrze jest odświeżyć obraz znajdujący się na formularzu (metoda Repaint):

procedure TMainForm.btnRunClick(Sender: TObject);
begin
  Repaint; // odśwież Canvas
  KrokPoKroku(100, 100); // wywołaj procedurę
end;

Animacja na belce tytułowej

Kolejny efekt, jaki chcę zaprezentować, to przemieszczanie się tekstu po pasku tytułowym programu (rysunek 9.17).

9.17.jpg
Rysunek 9.17. Tekst wyświetlany na pasku tytułowym

Na samym początku będziesz musiał zadeklarować zmienną globalną, która będzie określać, czy animacja jest wciąż uruchomiona:

var Running : Boolean = TRUE;

Kolejny krokiem jest deklaracja w sekcji private procedury Go:

procedure Go(const PText : TStrings);

Owa procedura jako parametr przyjmuje dane w postaci typu TStrings. Każdy wiersz tekstu znajdujący się w tym typie będzie osobno pokazywany na pasku tytułowym. Gdy zostaną już pokazane wszystkie wiersze, animacja rozpocznie swe działanie od początku:

procedure TMainForm.Go(const PText: TStrings);
var
  PTextLong : Integer; // długość tekstu
  I : Integer;
  LinesCount : Integer; // ilość wierszy
begin
  while (Running) do  // dopóki zmienna będzie miała wartość True
  begin
    Application.ProcessMessages;
    if not Running then Break;   // sprawdzaj, czy przypadkiem nic się nie zmieniło
    for LinesCount := 0 to PText.Count ?1 do  // zaczynaj od pierwszego wiersza
    begin
      if not Running then Break;  // znów sprawdź...
      PTextLong := Length(PText[LinesCount]);  // pobierz długość wiersza
      Sleep(500); // odczekaj pół sekundy
      Caption := '';  // wymaż Caption
      for I := 0 to PTextLong do   // wykonuj pętlę literka po literce
      begin
        Application.ProcessMessages;
        Sleep(100);  // z przerwą 100 milisekund
        if not Running then Break;  // znów sprawdź!
        Caption := Caption + PText.Strings[LinesCount][i]; // wyświetl po kolei wszystkie litery
      end;
    end;
  end;
end;

Kod tej procedury może się wydać nieco odstraszający, lecz warto zapoznać się z komentarzami ? wówczas wiele spraw okaże się łatwiejszymi. Aby wszystko dobrze działało, należało umieścić tu aż trzy pętle. Pierwsza ? while ? służy do kontrolowania, czy użytkownik nie chce przypadkiem zamknąć aplikacji. Jeżeli nie ? powtarza cały proces od początku.

Kolejna pętla pobiera kolejne wiersze ze zmiennej typu TStrings; zagnieżdżona w niej jest kolejna pętla for, wyświetlająca napis litera po literze. W tym wypadku sprawa jest znacznie prostsza, niż w poprzednim przykładzie, gdzie rysowanie kolejnych liter musiało być dokładnie mierzone. W listingu 9.6. znajduje się kod źródłowy modułu.

Listing 9.6. Kod źródłowy programu

unit MainFrm;

interface

uses
  Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, 
  StdCtrls;

type
  TMainForm = class(TForm)
    btnGo: TButton;
    btnStop: TButton;
    procedure btnGoClick(Sender: TObject);
    procedure FormClose(Sender: TObject; var Action: TCloseAction);
    procedure btnStopClick(Sender: TObject);
  private
    procedure Go(const PText : TStrings);
  public

  end;

var
  MainForm: TMainForm;

implementation

{$R *.DFM}

var Running : Boolean = TRUE;  // zmienna określa, czy animacja ma być uruchomiona

procedure TMainForm.Go(const PText: TStrings);
var
  PTextLong : Integer; // długość tekstu
  I : Integer;
  LinesCount : Integer; // ilość wierszy
begin
  while (Running) do  // dopóki zmienna będzie miała wartość True
  begin
    Application.ProcessMessages;
    if not Running then Break;   // sprawdzaj, czy przypadkiem nic się nie zmieniło
    for LinesCount := 0 to PText.Count ?1 do  // zaczynaj od pierwszego wiersza
    begin
      if not Running then Break;  // znów sprawdź...
      PTextLong := Length(PText[LinesCount]);  // pobierz dlugość wiersza
      Sleep(500); // odczekaj pół sekundy
      Caption := '';  // wymaż Caption
      for I := 0 to PTextLong do   // wykonuj pętlę literka po literce
      begin
        Application.ProcessMessages;
        Sleep(100);  // z przerwą 100 milisekund
        if not Running then Break;  // znów sprawdź!
        Caption := Caption + PText.Strings[LinesCount][i]; // wyświetl po kolei wszystkie litery
      end;
    end;
  end;
end;

procedure TMainForm.btnGoClick(Sender: TObject);
var
  sText : TStrings;
begin
  btnStop.Visible := True;  // pokazanie przycisku umożliwiającego zatrzymanie
  sText := TStringList.Create;
  try
    with sText do
    begin
    { dodanie kolejnych wierszy, które po kolei będą wyświetlane }
      Add('Delphi 7.0 ...');
      Add('...jest doskonałym narzędziem typu RAD do szybkiego tworzenia aplikacji...');
      Add('...możesz się o tym przekonać sam. Ten program zajmuje 92 linie.');
      Add('Adam Boduch');
    end;
    Go(sText); // wywołaj procedurę z parametrem
  finally
    sText.Free;
  end;
end;

procedure TMainForm.FormClose(Sender: TObject; var Action: TCloseAction);
begin
  Running := False; // przy próbie zamknięcia zmień wartość
end;

procedure TMainForm.btnStopClick(Sender: TObject);
begin
  Running := False; // zmień wartość, jeżeli chcesz zatrzymać
end;

end.

Inne płynne animacje

W Delphi istnieje oczywiście możliwość tworzenia innych, płynnych animacji ? niekoniecznie tekstowych. Na rysunku 9.18 przedstawiony jest program, którego działanie polega na przemieszczaniu rysunku po całym oknie programu. Cały problem tkwi w tym, aby rysunek ?odbijał się? od krawędzi okna i wędrował w inne, losowe kierunki okna.

9.18.jpg
Rysunek 9.18. Rysunek przemieszczający się po oknie programu

Kod źródłowy tego programu zamieszczam na płycie CD-ROM ? zachęcam do samodzielnego przetestowania go (katalog ../listingi/9/ImgPaint/ImgPaint.dpr).

Przemieszczanie obrazków po formularzu opiera się, ogólnie rzecz biorąc, na każdorazowym rysowaniu obrazka w innej pozycji. W przypadku programu przedstawionego na rysunku 9.18 za przemieszczanie obrazka odpowiada osobny wątek (o wątkach była mowa w poprzednim rozdziale). Jeżeli jednak chcemy za każdym razem przerysowywać obrazek, należy uprzednio odświeżyć obraz (metoda Repaint). W wielu przypadkach może to powodować migotanie okna, dające nieprzyjemny dla oka efekt.

W takich przypadkach dobrze jest stosować tzw. podwójne buforowanie.

DoubleBuffered := True;

W przypadku, gdy właściwość (nie jest to właściwość klasy TCanvas, ale klasy TWinControl ? patrz rozdział 14.) ma wartość False, rysowanie odbywa się bezpośrednio na formularzu, co może rzeczywiście powodować migotanie obrazu. Zmiana wartości na True może częściowo poprawić efekt wizualny.

W przykładowym programie z rysunku 9.18 rozwiązałem ten problem nieco inaczej, umieszczając na formularzu komponent TPaintBox i rysując bezpośrednio na nim.

Odtwarzanie dźwięków

Obecnie istnieje wiele formatów dźwiękowych ? od tych najprostszych (jak WAV) po najpopularniejsze, typu mp3. Na razie zajmiemy się odtwarzaniem tych najmniej skomplikowanych plików dźwiękowych ? WAV (fala ? ang. wave). Może nie jest to bardzo popularny format (pliki tego rodzaju często mają duże rozmiary), lecz znakomicie nadaje się do odtwarzania prostych dźwięków.

Funkcja PlaySound

W module MMSystem zadeklarowana jest funkcja API PlaySound, dzięki której odtwarzanie prostych dźwięków jest czynnością raczej nieskomplikowaną ? oto jej deklaracja:

function PlaySound(pszSound: PChar; hmod: HMODULE; fdwSound: DWORD): BOOL; stdcall;

Pierwszy parametr musi zawierać ścieżkę do odtwarzanego dźwięku; drugi to wskazanie modułu ? w naszym przypadku w tym miejscu wystarczy wstawić cyfrę 0. Ostatni parametr to flaga. Najprostsze odtworzenie dźwięku *.wav może wyglądać tak:

PlaySound('C:\plik.wav' 0, SND_FILENAME);

W tabeli 9.13 umieściłem możliwe do zastosowania flagi.

Tabela 9.13. Flagi polecenia PlaySound

FlagaOpis
SND_ALIASOdtwarzanie dźwięku systemowego
SND_FILENAMEOdtwarzany plik znajduje się na dysku
SND_NOWAITFlaga nakazuje wstrzymanie odtwarzania, jeżeli jest już odtwarzany jakiś inny dźwięk
SND_NOSTOPOdtwarzanie nastąpi w przypadku, gdy nie jest odtwarzany inny utwór
SND_RESOURCEPlik muzyczny będzie odtwarzany z zasobów
SND_LOOPPowoduje odtwarzanie utworu w pętli. Tylko w zastosowaniu z SND_ASYNC
SND_ASYNCOdtwarzanie będzie odbywać się w tle
SND_NODEFAULTJeżeli plik do odtworzenia nie istnieje, nie jest generowany dźwięk ostrzegawczy
SND_PURGEZatrzymanie odtwarzania

Istnieje możliwość połączenia kilku flag za pomocą operatora or.

Przy omawianiu parametru SND_ALIAS wspomniałem o możliwości odtwarzania pliku systemowego. Takie dźwięki są zadeklarowane w rejestrze Windows i generowane w wyniku zajścia jakiś sytuacji ? np. uruchomienia systemu, zamknięcia go, kliknięcia myszą itp.

PlaySound('MailBeep', 0, SND_ALIAS or SND_NODEFAULT);

W tym przypadku odegrany zostanie dźwięk MailBeep, zapisany w rejestrze.

Użycie komponentu TMediaPlayer

Zabawa funkcjami WinAPI nie ma zbytniego sensu, gdy mamy pod ręką komponent TMediaPlayer, którego użycie wiąże się tylko z wywoływaniem kolejnych metod. Komponent ten znajduje się w palecie komponentów na zakładce System ? umieść go na formularzu. Wygląd kontrolki nie przedstawia się zbyt zachęcająco ? ja zawsze ukrywam widoczną paletę sterowania, zmieniając właściwość Visible na False.

Oto przykład rozpoczęcia odtwarzania pliku mp3:

  with Player do
  begin
    FileName := 'C:\Live set at home.mp3'; // określenie ścieżki do pliku
    Open; // otwarcie pliku
    Play; // rozpoczęcie odtwarzania
  end;

Na samym początku konieczne staje się przydzielenie ścieżki pliku do właściwości FileName komponentu. Jeżeli wiemy już, jaki plik chcemy odtwarzać, wystarczy wybrać metodę Open, a następnie Play.

Ważniejsze metody komponentu TMediaPlayer przedstawione zostały w tabeli 9.14.

MetodaOpis
BackCofa odtwarzanie o określoną we właściwości Frames liczbę klatek
CloseWyłącza urządzenie, co wiąże się ze zatrzymaniem odtwarzania
OpenWłącza urządzenie i przygotowuje do odtwarzania
PauseWstrzymuje odtwarzanie
PlayRozpoczyna odtwarzanie
ResumeWznawia odtwarzanie
StopZatrzymuje odtwarzanie

Odtwarzanie filmów

Za pomocą komponentu TMediaPlayer można równie dobrze odtwarzać także filmy. Na dołączonej do książki płycie CD-ROM (../lisingi/9/Player/Player.dpr) znajduje się program (odtwarzacz), który napisałem dość dawno, bo 3 lata temu, lecz jest wciąż aktualny. Program w trakcie działania przedstawiony jest na rysunku 9.19, a jego kod znajduje się w listingu 9.7.

9.19.jpg
Rysunek 9.19. Odtwarzacz multimedialny

Listing 9.7. Kod źródłowy odtwarzacza

unit MainFrmU;

interface

uses
  Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs,
  MPlayer, StdCtrls, ExtCtrls, ComCtrls, Menus, Buttons, MMSystem, Registry,
  ToolWin, ImgList;

type
  TMainFrm = class(TForm)
    HomeMenu: TMainMenu;
    File1: TMenuItem;
    FileOpen: TMenuItem;
    N1: TMenuItem;
    StatusB: TStatusBar;
    Open: TOpenDialog;
    PP: TPanel;
    TextPanel: TPanel;
    Scroll: TScrollBar;
    Player: TMediaPlayer;
    Timer: TTimer;
    Sound: TTrackBar;
    Image1: TImage;
    Exit: TMenuItem;
    Close: TMenuItem;
    Widok1: TMenuItem;
    FullS: TMenuItem;
    N2: TMenuItem;
    OnTop: TMenuItem;
    Bar: TToolBar;
    Play: TToolButton;
    Pause: TToolButton;
    ImageList: TImageList;
    Stop: TToolButton;
    About1: TMenuItem;
    procedure FileOpenClick(Sender: TObject);
    procedure ScrollScroll(Sender: TObject; ScrollCode: TScrollCode;
      var ScrollPos: Integer);
    procedure TimerTimer(Sender: TObject);
    procedure PlayerNotify(Sender: TObject);
    procedure SoundChange(Sender: TObject);
    procedure FormCreate(Sender: TObject);
    procedure Image1Click(Sender: TObject);
    procedure PPClick(Sender: TObject);
    procedure CloseClick(Sender: TObject);
    procedure FullSClick(Sender: TObject);
    procedure OnTopClick(Sender: TObject);
    procedure FormDestroy(Sender: TObject);
    procedure PauseClick(Sender: TObject);
    procedure PlayClick(Sender: TObject);
    procedure StopClick(Sender: TObject);
    procedure About1Click(Sender: TObject);
  private
     Button: TMPBtnType;
    procedure ShowText(Text: String);

  public
    FName : String;
    Tx : String;
  end;

var
  MainFrm: TMainFrm;

implementation

uses BigFrmU;

{$R *.DFM}


procedure TMainFrm.ShowText(Text: String);
begin
{
Procedura, która wyświetla w Panelu tekst.
}
   TextPanel.Caption := Text;
end;

procedure TMainFrm.FileOpenClick(Sender: TObject);

begin
 try

  if Open.Execute then


    FName := Open.FileName;  //Przypisanie ścieżki zmiennej

    Player.FileName := FName;  //Przypisanie odtwarzaczowi zmiennej
    Player.Open; // Otwarcie filmu

    Player.Display := PP; { Przypisanie odtwarzaczowi obszaru
    wyświetlania jako Panel }
    Player.DisplayRect := PP.ClientRect; { Dopasowanie rozmiarów
    filmu do rozmiarów Panelu }

    Scroll.Position := 0; //Wskaźnik na 0

    Tx := ExtractFileName(Player.FileName); //Odłączenie nazwy pliku od ścieżki
    ShowText(Tx); // i wyświetlenie jej w Panelu "TextPanel"
    Caption := Tx;

 except
   on EMCIDeviceError do
    raise Exception.Create(Format(
    'Nie mogę otworzyć pliku o rozszerzeniu %s. Sprawdź, czy plik nie '+
    'jest uszkodzony lub czy prawidłowy jest jego format danych.',
    [ExtractFileExt(Player.FileName)]));
 end;

end;

procedure TMainFrm.ScrollScroll(Sender: TObject; ScrollCode: TScrollCode;
  var ScrollPos: Integer);
begin
{
Pozycja odtwarzacza równać się będzie pozycji Scrolla.
Jeżeli zmieni się pozycja Scrolla, film zostanie "przewinięty"
}
  Player.Position := Scroll.Position;
end;

procedure TMainFrm.TimerTimer(Sender: TObject);
begin
{ j/w }
 Scroll.Position := Player.Position;
end;

procedure TMainFrm.PlayerNotify(Sender: TObject);
begin  // jeżeli film się skończy
  if Player.NotifyValue = nvSuccessful then  // Jeżeli operacja się zakończy
  begin
    Timer.Enabled := False;  // wyłączenie Timera
    Scroll.Position := Scroll.Position;  //Scroll na tej samej pozycji
    Player.Stop;

    StopClick(Sender);
 end;

end;

procedure TMainFrm.SoundChange(Sender: TObject);
begin  //Ustawienie głośności...
  case Sound.Position of
    1:
    WaveOutSetVolume(0, $20002000); {2000}
  2:
    WaveOutSetVolume(0, $60006000); {6000}
  3: 
    WaveOutSetVolume(0, $80008000); {8000}
  4:
    WaveOutSetVolume(0, $90009000); {9000}
  5:
    WaveOutSetVolume(0, $FFFFFFFF); {maksymalna głośność}
  end;   
end;

procedure TMainFrm.FormCreate(Sender: TObject);
var
  Reg : TRegistry;
  Key : Boolean;
  I : Integer;
  S : String;
begin
// wczytaj wartości z rejestru

  Reg := TRegistry.Create;
    try
      Key := Reg.OpenKey(
             'Software\Player', False);
      if Key then
      begin
        Top := Reg.ReadInteger('Top');
        Left := Reg.ReadInteger('Left');
        OnTop.Checked := Reg.ReadBool('StayOnTop');
      end else
        OnTop.Checked := False;
    finally
      Reg.Free;
    end;     
end;

procedure TMainFrm.Image1Click(Sender: TObject);
begin
  Sound.Position := 1;  // Wyciszenie
end;

procedure TMainFrm.PPClick(Sender: TObject);
begin
  try
    Player.Pause;
    ShowText('Wciśnięto pauzę...');
  except
    ShowText('Brak filmu...');
  end;
end;

procedure TMainFrm.CloseClick(Sender: TObject);
begin
  Player.Close; // Zamknięcie filmu
  ShowText('Film został zamknięty.'); // Wyświetlenie tekstu
  Caption := 'Player';
end;

procedure TMainFrm.FullSClick(Sender: TObject);
begin

    FullForm.BorderStyle := bsNone; // ukryj pasek
    FullForm.WindowState := wsMaximized; // maksymalizacja okna
    Player.Display := FullForm.FullPanel;// obraz na Panelu
    with FullForm.FullPanel do // rozmiar panelu dopasowany do rozmiaru filmu
    Player.DisplayRect := Rect(0, 0, Width, Height);

  OnTop.Checked := False;
  FullForm.ShowModal; // Wyświetlenie okna 2
end;

procedure TMainFrm.OnTopClick(Sender: TObject);
begin
{ Zawrze na wierzchu }
 OnTop.Checked := not OnTop.Checked;
 if OnTop.Checked = True then
 FormStyle := fsStayOnTop;
end;

procedure TMainFrm.FormDestroy(Sender: TObject);
var
  Reg : TRegistry;
begin
  Reg := TRegistry.Create;

  try
    Reg.OpenKey(
    'Software\Player', True);
   // zapisz pozycję okna
      Reg.WriteInteger('Top', Top);
      Reg.WriteInteger('Left', Left);
  // zapisz pozycję "StayOnTop"
      Reg.WriteBool('StayOnTop',OnTop.Checked);
  finally
    Reg.Free;
  end;
end;

procedure TMainFrm.PauseClick(Sender: TObject);
begin  // pauza
 try
   Player.Pause;  // zastopuj odtwarzanie filmu
   Play.Down := False; // przyciski "odciśnięte"
   Stop.Down := False;
   Pause.Down := True;
   ShowText('Wciśnięto pauzę...'); // zmień tekst
 except   // wyjątek
   Pause.Down := False;
     raise Exception.Create(
     'Film nie jest odtwarzany!');
 end;
end;

procedure TMainFrm.PlayClick(Sender: TObject);
begin
 try
  Scroll.Max := Player.Length; //Przypisanie maksymalnej wartości do długości filmu
  Timer.Enabled := True; // Włączenie Timera
  ShowText('Trwa odtwarzanie...');
  Player.Play;
  Play.Down := True;
  Stop.Down := False;
  Pause.Down := False;
 except
   Play.Down := False;
   raise Exception.Create(
   'Najpierw musisz wybrać film do otwarcia!');
 end;
end;

procedure TMainFrm.StopClick(Sender: TObject);
begin
 try
  Player.Stop;
  Play.Down := False;
  Pause.Down := False;
  Stop.Down := True;
  ShowText('Zatrzymano film...');
 except
   Stop.Down := False;
     raise Exception.Create(
     'Nie można wyłączyć filmu, ponieważ nie jest on odtwarzany!');
 end;    
end;

procedure TMainFrm.About1Click(Sender: TObject);
begin
  MessageDlg(
  '   Player v. BETA '+#13+#13+
  'Autor: Adam Boduch '+#13+
  'E-mail: [email protected] '+ #13+
  '  http://4programmers.net',mtInformation,
  [mbOK], 0);
end;

end.

Przy okazji analizowania kodu możesz sobie przypomnieć funkcje związane z zapisem danych do rejestru ? program bowiem zapisuje w nim aktualne ustawienia oraz położenie okna.

Odtwarzanie filmu

Odtwarzanie filmu przebiega w podobny sposób, jak odtwarzanie dźwięku ? na początku należy przypisać wartość zmiennej FileName, później otworzyć film (Open), a na końcu rozpocząć odtwarzanie (Play). Jedyna różnica jest taka, że odtwarzany film będzie wyświetlany na komponencie TPanel ? odpowiada za to taki kod:

Player.Display := PP; 
Player.DisplayRect := PP.ClientRect; 

Oprócz określenia obszaru na którym zostanie wyświetlony film, następuje tu także dopasowanie rozmiarów wyświetlanego filmu do rozmiarów panelu (właściwość DisplayRect).

Pozycja odtwarzanego filmu

Jak pewnie zauważyłeś, oprócz zwykłego odtwarzania pasek (komponent TScrollBar) pokazuje pozycję odtwarzanego filmu. Można to zrealizować za pomocą komponentu TTimer. Zadaniem komponentu TTimer jest wykonywanie jakiejś czynności w pewnych odstępach czasu (w naszym wypadku ? co 1 sekundę).

procedure TMainFrm.TimerTimer(Sender: TObject);
begin
 Scroll.Position := Player.Position;
end;

Co 1 sekundę pozycja scrolla (paska przewijania) jest uaktualniania w stosunku do pozycji odtwarzanego filmu.
Próba przewinięcia tego paska spowoduje ustawienie nowej pozycji dla odtwarzania filmu:

procedure TMainFrm.ScrollScroll(Sender: TObject; ScrollCode: TScrollCode;
  var ScrollPos: Integer);
begin
  Player.Position := Scroll.Position;
end;

Ustawianie głośności

Program posiada także opcje ustawiania głośności filmu. Wszystko to dzięki funkcji API ? WaveOutSetVolume:

WaveOutSetVolume(0, $90009000);  

W drugim parametrze należy podać wartości dla prawego oraz lewego głośnika ? w tym wypadku wartość wynosi $9000.

Niektóre odtwarzacze filmów umożliwiają również wyświetlanie napisów w przypadku, gdy film nie jest w polskiej wersji językowej. Jak to zrobić? Pokażę to w 13. rozdziale książki.

Kontrolka Flash

Format plików Flash zyskał sobie dużą popularność dzięki prostocie tworzenia animacji oraz dużej efektywności, jaką zapewnia. W Delphi istnieje możliwość odtwarzania animacji *.swf tworzonych za pomocą programu Flash, lecz będzie do tego potrzebna kontrolka ActiveX dołączona do tego programu.

ActiveX jest to plik OCX, który można nazwać komponentem. Jest to standard opracowany przez Microsoft. O kontrolkach ActiveX będzie mowa w rozdziale 13. niniejszej książki.

Plik SWFLASH.OCX znajdziesz w katalogu C:\Windows\System\Macromed\Flash (przynajmniej tak jest w moim przypadku).

Instalacja kontrolki

Z menu Component wybierz polecenie Import ActiveX Control. Okno to przedstawiono na rysunku 9.20.

9.20.jpg
Rysunek 9.20. Importowanie nowej kontrolki

Naciśnij przycisk Add i znajdź wśród katalogów szukaną kontrolkę ActiveX. Później na liście kliknij pozycję Shockwave Flash. Naciśnij przycisk Install, aby zainstalować kontrolkę. Zostanie wyświetlone okno, w którym powinieneś jeszcze raz nacisnąć OK. Zobaczysz wówczas takie okno, jakie przedstawiono na rysunku 9.21.

9.21.jpg
Rysunek 9.21. Instalacja kontrolki ActiveX

Zostaniesz zapytany, czy kontynuować instalację kontrolki w palecie komponentów. Naciśnij Yes. Następnie powinieneś ujrzeć informację o prawidłowym zainstalowaniu kontrolki. Naciśnij Ctrl+S, aby zapisać wszystko. Teraz możesz już otworzyć nowy projekt. Nowy komponent do wyświetlania animacji Flash znajduje się na palecie ActiveX.

Wykorzystanie komponentu

Przede wszystkim musisz dysponować jakąś animacją w formacie Flash! Jeżeli już takową masz, to na formularzu umieść komponent TShockwaveFlash z palety ActiveX. Wygeneruj zdarzenie OnCreate formularza ? do wyświetlenia filmu będą potrzebne tylko dwa wiersze kodu.

procedure TMainForm.FormCreate(Sender: TObject);
begin
  Flash.Movie := ExtractFilePath(Application.ExeName) + 'banner.swf';
  Flash.Play;
end;

W komponencie tym wymagane jest podanie dokładnej ścieżki do pliku SWF. Musimy posłużyć się funkcją Aplication.ExeName, która zwraca pełną ścieżkę dostępu do naszej aplikacji. Zakładając, że film znajduje się w katalogu z programem, musimy jeszcze użyć funkcji ExtractFilePath. Funkcja ta z parametru wyodrębnia jedynie ścieżkę. Przykładowo jeśli wywołamy funkcję w ten sposób:

ExtractFilePath('C:\Moje dokumenty\app\mój.exe');

zostanie zwrócona wartość: C:\Moje dokumenty\app
Działanie programu zaprezentowano na rysunku 9.22.

9.22.jpg
Rysunek 9.22. Wykorzystanie kontrolki TShockwaveFlash

Podsumowanie

Być może nie wyczerpałem do końca tematu i coś pominąłem, ale starałem się zaprezentować główne aspekty wykorzystania grafiki oraz operowania dźwiękiem. Mam nadzieję, że przedstawione tu informacje przydadzą Ci się w przyszłości?! A może napiszesz odtwarzacz filmów lepszy niż ten, który tu zaprezentowałem?

Załączniki:

de25kp.jpg Więcej informacji

Delphi 2005. Kompendium programisty
Adam Boduch
Format: B5, stron: 1048
oprawa twarda
Zawiera CD-ROM
[[Delphi/Kompendium|Spis treści]]

[[Delphi/Kompendium/Prawa autorskie|©]] Helion 2003. Autor: Adam Boduch. Zabrania się rozpowszechniania tego tekstu bez zgody autora.

4 komentarzy

czy możecie dać do pobrania plików ten odtwarzacz filmów???

Spoko, dodałem formatowanie do tabelek (mam nadzieję, że żadnej nie pominąłem) :)

Smialo - edytuj tekst i dodaj tabelke.
Po prostu jest to najbardziej czasochlonne (montowanie tabelki ;)) a w tym rozdziale jest ich duzo wiec sobie dalem sopkoj. Ale fajnie gdybys Ty dodal.

hmm... Tabela 9.7. Wartości typu TPenMode - może dać tę tabelkę w i tak dalej?