R15-03.DOC

(287 KB) Pobierz
BCB 5 Developer's Guide - rozdzial 24

1

 

Rozdział 15.
Techniki multimedialne

Damon Chandler

Siu-Fan Wu

Rob Allen

 

·         Interfejs GDI

·         Wyświetlanie grafiki rastrowej

·         Przetwarzanie obrazu

·         Odtwarzanie zapisów audio, wideo i płyt CD


Przekaz multimedialny jest coraz powszechniejszym elementem nowoczesnych aplikacji. Wykorzystanie multimediów może sprowadzać się do zwykłego „ubarwienia” interfejsu programu, jednak w wielu przypadkach stanowi podstawowe narzędzie komunikacji z użytkownikiem. Obszerny wachlarz aplikacji przeznaczonych do tworzenia, odtwarzania i zarządzania zapisami multimedialnymi poszerza się z dnia na dzień

użycie przez autorów określenia „some applications” to ogromny eufemizm.

.

W rozdziale tym omówimy kilka wybranych technik multimedialnych stosowanych w aplikacjach. Zakres realizowanych operacji zależy li tylko od zapotrzebowania użytkowników programu – jak się wkrótce przekonamy, system operacyjny dostarcza standardowe mechanizmy, umożliwiające aplikacji wymianę danych ze sterownikami odpowiednich urządzeń odtwarzających.

Wyświetlanie „tradycyjnej” grafiki (nieruchomych obrazów dwuwymiarowych) jest domeną podsystemu GDI, czyli systemowego interfejsu urządzeń graficznych (Graphics Device Interface). Funkcje GDI tworzą warstwę pośrednią, położoną pomiędzy wywołującą aplikacją a sterownikami urządzeń graficznych. Interfejs GDI oraz udostępniające go klasy VCL (TCanvas, TBrush, TPen i TFont) omówimy w pierwszej części rozdziału, w której przedstawimy sposób wykorzystania klas VCL do wyświetlania obiektów graficznych na ekranie monitora

o drukowaniu w rozdziale nie ma mowy (wbrew oryginałowi)

. W następnej kolejności zajmiemy się klasami TBitmap i TJPEGImage, obsługującymi grafikę zapisaną w formacie zwykłych map bitowych oraz skompresowanym formacie JPEG. Wspomnimy także o innych formatach zapisu obrazów i metodach ich obsługi w programach bazujących na bibliotece VCL.

W drugiej części rozdziału przedstawimy wprowadzenie do przetwarzania danych obrazowych i zaprezentujemy kilka podstawowych metod ich obróbki, jak: odczytywanie i modyfikację pojedynczych punktów, dyskryminację, konwersję barw, korygowanie kontrastu i wyrównywanie histogramów, transformacje związane z powiększaniem fragmentów obrazu oraz filtry realizujące wygładzanie i detekcję krawędzi.

Odczytywanie i zapisywanie danych dźwiękowych i wizyjnych obsługuje w systemie Windows interfejs sterowania mediami (Media Control Interface, MCI). Udostępnia on aplikacjom zestaw funkcji, pozwalających pośrednio komunikować się z urządzeniami odtwarzającymi i zapisującymi obraz oraz dźwięk. Oprócz „ogólnego” interfejsu MCI Windows oferuje też inne, bardziej wyspecjalizowane mechanizmy związane z konkretnymi formatami zapisu danych multimedialnych. W ramach tego rozdziału zaprezentujemy wykorzystanie funkcji MCI do odtwarzania dźwięku i obrazu oraz użycie interfejsów systemowych do odtwarzania spróbkowanych danych dźwiękowych zapisanych w formacie WAV (Waveform Audio).

Interfejs GDI

Większość wyświetlanych na ekranie elementów interfejsu użytkownika tworzona jest w C++Builderze za pomocą formularzy i komponentów. Czasami okazuje się jednak konieczne wyświetlenie rysunku lub obrazu (np. mapy bitowej) – w takich sytuacjach do głosu dochodzą funkcje systemowego interfejsu urządzeń graficznych, w skrócie GDI. Podsystem GDI, będący jednym z filarów systemu Windows, zawarty jest w bibliotece dynamicznej GDI32.DLL

pliku GDI.DLL nie znalazłem ani w Windows 98, ani w NT (literówka?)

, zawierającej setki funkcji (w testowanej przez autorów wersji pochodzącej z systemu Windows NT zaimplementowano 401 wywołań). Wszelkie operacje rysowania czegokolwiek na ekranie (w tym oczywiście systemowe mechanizmy graficzne) wykorzystują funkcje GDI.

Podstawową zaletą zamknięcia obsługi grafiki w podsystemie GDI jest uniezależnienie operacji graficznych od fizycznych urządzeń wyjściowych. Wywołując funkcje GDI, nie trzeba zastanawiać się nad szczegółami programowania kart graficznych i drukarek, nie trzeba też martwić się o ewentualną niezgodność z przyszłymi modelami tych urządzeń. Innymi słowy, GDI zapewnia abstrakcyjną reprezentację urządzeń wyjściowych, izolując je od aplikacji.

Omówienie wszystkich funkcji GDI zdecydowanie przekracza skromne ramy tego rozdziału; zainteresowanych odsyłamy do dokumentacji biblioteki VCL (najlepiej rozpocząć od opisu klasy TCanvas) oraz elektronicznej dokumentacji Windows SDK.

Interfejs programowy Windows i konteksty urządzeń

Podobnie jak w przypadku innych funkcji udostępnianych przez interfejs programowy systemu Windows, komunikacja z obiektami GDI wykorzystuje specyficzny rodzaj uchwytu, zwany kontekstem urządzenia (ang. device context, DC). Udostępnia go systemowa funkcja GetDC():

HDC hDC = GetDC(hWindow);

Uzyskany w ten sposób kontekst urządzenia jest związany z danym oknem, co oznacza, że rysowanie poza obszarem okna jest niemożliwe (aby uzyskać dostęp do dowolnego miejsca pulpitu, należy wywołać funkcję GetDC() z parametrem hWindow równym NULL). Jak można się domyślać, uzyskana w ten sposób wartość kontekstu jest obowiązkowym parametrem każdego wywołania funkcji GDI. Po zakończeniu rysowania należy zwolnić kontekst urządzenia, do czego służy funkcja ReleaseDC():

ReleaseDC(hDC

oryginał: hD (literówka)

);

Kontekst urządzenia a VCL, czyli klasa TCanvas

Jak wiadomo, programując w systemie C++Builder, eliminuje się wywołania „czystych” funkcji API na korzyść użycia komponentów, co pozwala skrócić cykl rozwojowy oprogramowania. Rysowanie obiektów graficznych i manipulowanie nimi umożliwia klasa VCL TCanvas, „opakowująca” funkcje GDI i dostępna jako jedna z właściwości klas TForm i TPrinter, a także większości komponentów reprezentujących elementy interfejsu użytkownika. Odwołanie do właściwości Canvas[1] jest bardzo proste – oto przykład wykreślenia na formularzu prostokąta:

Canvas->Rectangle(10, 10, 100, 100);

Co dzieje się w środku?

Klasa TCanvas jest obiektową reprezentacją pewnego mechanizmu udostępnianego przez system operacyjny. Jej największą zaletą jest pełna automatyzacja zarządzania zasobami GDI, co w przypadku „ręcznego” wywoływania funkcji API jest zadaniem dość skomplikowanym (i podatnym na błędy – przyp. tłum.). Klasa TCanvas pozwala także na udostępnianie parametrów „stowarzyszonego” kontekstu urządzenia poprzez pojedynczą właściwość odpowiedniego komponentu, co ułatwia programowanie i poprawia czytelność kodu.

Elementy klasy TCanvas

Klasa TCanvas udostępnia kilka właściwości, o których należy tu wspomnieć. Są to:

·         TPen Pen – obiekt reprezentujący bieżące pióro (służące do kreślenia);

·         TBrush Brush – obiekt reprezentujący bieżący pędzel (służący do wypełniania konturów);

·         TFont Font – obiekt reprezentujący bieżącą czcionkę;

·         TPoint PenPos – obiekt reprezentujący bieżące położenie pióra.

Podkreślmy tutaj, że do rysowania obiektów na płótnie używane są zawsze bieżące (wybrane, ang. selected) pióro, pędzel, czcionka i pozycja pióra. Oznacza to, że w razie potrzeby należy zmienić ich ustawienia przed rozpoczęciem rysowania.

Czego brakuje?

Definicje TCanvas i związanych z nią klas zawarto w pliku graphics.pas. Jakkolwiek obszerne, nie obejmują one jednak wszystkich możliwości interfejsu GDI

oryginał: „definicje są zawarte w pliku... ale nie obejmują wszystkich funkcji GDI” – jednoz  drugim nie ma związku.

– projektanci firmy Borland zdecydowali się na zaimplementowanie tylko najczęściej używanych funkcji. Również niektóre klasy pomocnicze, jak np. TPoint i TRect, nie dodają wiele do reprezentowanych przez siebie obiektów API i mogłyby być nieco bardziej uniwersalne. Nie oznacza to bynajmniej, że programista stoi na straconej pozycji – wszak zawsze można odwołać się bezpośrednio do systemu. Klasa TCanvas udostępnia właściwość Handle, będącą niczym innym, jak tylko uchwytem kontekstu urządzenia wykorzystywanym w wywołaniach GDI. Tak więc alternatywna metoda wykreślenia prostokąta (zobacz wyżej) z użyciem funkcji GDI Rectangle() miałaby postać:

Rectangle(Canvas->Handle, 10, 10, 100, 100);

Uwaga
Trzeba uczciwie przyznać, że niektóre z VCL-owych odpowiedników obiektów GDI mogłyby nieść w sobie znacznie więcej „wartości dodanej”, aniżeli ma to miejsce obecnie. Przykładami mogą tu być klasy TRect, TPoint i TSize, tworzące zaledwie cienką otoczkę dla odpowiednich struktur GDI. Klasa TRect, reprezentująca prostokąt, zawiera np. tylko cztery użyteczne metody – operatory == i != oraz funkcje Width() i Height() – chociaż wystarczyłoby nieco inwencji, aby uczynić ją naprawdę przydatnym narzędziem. Można by np. pomyśleć o funkcji sprawdzającej, czy dany punkt znajduje się wewnątrz prostokąta, metodach jego przesuwania, skalowania itd. Co prawda można się przyłożyć i zaprogramować odpowiednie operacje samemu, jednak możliwość użycia np. konstrukcji

if

w oryginale „If” (literówka)

(myRect->Contains(myPoint){ // wykonaj operację... }

byłaby bardzo pożądana. Można mieć nadzieję, że projektanci firmy Borland postarają się wkrótce wyeliminować te niedociągnięcia.

Użycie klasy TCanvas

Procedury rysowania na płótnie umieszcza się najczęściej w funkcji obsługi zdarzenia OnPaint() klasy TForm, co pozwala na bieżąco aktualizować zawartość formularza. Powszechną praktyką jest również rysowanie „na drugim planie”, z wykorzystaniem oddzielnego, ukrytego obiektu klasy TCanvas, będącego kopią właściwego płótna. Po zakończeniu rysowania zawartość takiego obiektu kopiuje się w funkcji OnPaint() do obiektu Canvas, reprezentującego formularz ekranowy, co pozwala przyspieszyć wyświetlanie.

Jako przykład rozważmy wyświetlanie tarczy zegara analogowego. Jeśli zrezygnujemy z sekundnika, będzie można aktualizować tarczę co minutę. Ponieważ jednak wykreślenie obu wskazówek wymaga obliczenia kątów, czyli zabiera trochę czasu, nie ma sensu wykonywać go w funkcji OnPaint(), która wywoływana jest każdorazowo w chwili przesunięcia okna, zmiany jego rozmiarów, zasłonięcia itd. Użycie „ukrytego” obiektu klasy TCanvas pozwala w takiej sytuacji zoptymalizować wyświetlanie grafiki. Oto stosowny kod:

void __fastcall TForm1::FormPaint(TObject *Sender)

{

  // Skopiuj obraz z mapy drugoplanowej na ekran.

  Canvas->CopyRect(ClientRect, HiddenImage->Canvas,

                   HiddenImage->ClientRect);

}

Więcej na ten temat powiemy w dalszej części rozdziału.

Kreślenie linii

Rysowanie odcinków i krzywych z użyciem klasy TCanvas jest proste – wystarczy zdefiniować ustawienia pióra i wywołać odpowiednią funkcję. Zestaw funkcji służących do kreślenia linii opisano szczegółowo w dokumentacji C++Buildera; w charakterze przykładu przedstawimy dwie instrukcje kreślące odcinek:

Canvas->PenPos = TPoint(1, 1);

Canvas->LineTo(9, 1);

Warto zauważyć, że linie kreślone przez wszystkie funkcje z grupy XxxTo() rozpoczynają się zawsze od bieżącej pozycji pióra i nie zawierają punktu końcowego. Widać to na rysunku 15.1, przedstawiającym wynik wykonania powyższego przykładu – odcinek rozpoczyna się w punkcie (1, 1), ale kończy w punkcie o współrzędnych (9, 1). To samo dotyczy innych figur, np. prostokątów (funkcja Rectangle(1, 1, 10, 10) utworzy prostokąt nie obejmujący punktu o współrzędnych (10, 10)) czy elips (opisywanych poprzez podanie współrzędnych prostokąta ograniczającego).

Rysunek 15.1. Odcinek kreślony przez funkcję LineTo(1, 9) nie zawiera ostatniego piksela

Kreślenie figur geometrycznych

Do kreślenia figur geometrycznych i wypełniania ich konturów używa się dwóch narzędzi – pióra oraz pędzla. Pierwsze z nich wykreśla sam kontur, drugie zaś pozwala wypełnić jego wnętrze. Oto prosty przykład wykreślenia prostokąta:

TRect myRect(10, 10, 100, 00);

Canvas->Rectangle

oryginał „Rectange” (literówka)

(myRect)

W przypadku kreślenia elipsy współrzędne podaje się poprzez zdefiniowanie opisanego na niej prostokąta (tzw. prostokąta ograniczającego, ang. bounding rectangle). Funkcja Ellipse() kreśli elipsę „stykającą się” z bokami takiego prostokąta.

Wyprowadzanie tekstu

Do wyprowadzania tekstu można użyć kilku metod. Najprostszą jest wywołanie funkcji TextOut(), rysującej zadany tekst począwszy od punktu o określonych współrzędnych (określa on położenie lewego górnego wierzchołka prostokąta ograniczającego tekst – przyp. tłum.). Po zakończeniu rysowania pióro przenoszone jest do punktu położonego w prawym górnym rogu prostokąta ograniczającego, co ułatwia wyprowadzanie kolejnych napisów. Alternatywą jest użycie funkcji TextRect(), wyprowadzającej tekst zamknięty w zadanym prostokącie (i obcinającej nie mieszczące się w nim fragmenty).

Rozmiar wyprowadzanego tekstu zależy od kroju i wielkości użytej czcionki. Dla czcionek o stałej szerokości (ang. fixed-pitch), jak np. Courier, każda litera zajmuje dokładnie tyle samo miejsca w poziomie (czyli „m” jest tak samo „szerokie”, jak „i”). W przypadku czcionek proporcjonalnych (ang. variable-pitch) szerokość poszczególnych znaków jest zmienna. Do ustalenia całkowitej szerokości łańcucha można wykorzystać metodę TextExtent() klasy TCanvas. Zwraca ona obiekt klasy TSize, zawierający szerokość i wysokość napisu reprezentującego na płótnie zadany tekst, co pozwala na odpowiednie dobranie współrzędnych.

We wspomnianym już przykładzie zegara analogowego tekst wyprowadzany jest za pomocą instrukcji

AnsiString text = "Prawy przycisk -> menu";

TSize textSize = HiddenImage->Canvas->TextExtent(text);

int x = (HiddenImage->Width - textSize.cx) / 2;

int y = HiddenImage->Height - textSize.cy - 2;

HiddenImage->Canvas->TextOut(x, y, text);

Powoduje to wyświetlenie napisu pośrodku dolnej części płótna obiektu HiddenImage.

Zmiana ustawień rysowania

Rysunek zmontowany z kresek i tekstu jest raczej mało atrakcyjny, toteż warto powiedzieć nieco na temat możliwości zmiany parametrów rysowania. Najbardziej oczywiste wydają się ustawienia grubości i koloru linii oraz parametry czcionki używanej do wyprowadzania tekstu. C++Builder dostarcza cały zestaw klas pomocniczych, pozwalających „nadać szlif” tworzonym rysunkom.

Uwaga
Zmiany parametrów kreślenia (np. koloru linii) należy dokonać przed wywołaniem funkcji, której zmiana ta ma dotyczyć.

Klasa TColor

Manipulowanie kolorem obiektów graficznych umożliwia klasa VCL TColor, będąca odpowiednikiem znanego z Windows API typu COLORREF. Ten ostatni oznacza wartość 32-bitową, w której poszczególne bajty odpowiadają barwom składowym (najbardziej znaczący bajt jest równy zeru – przyp. tłum.). W modelu RGB, z którym mamy tu do czynienia, każdy kolor reprezentowany jest przez trzy barwy składowe: czerwoną (R), zieloną (G) i niebieską (B). Czystej czerwieni odpowiada np. kombinacja składowych RGB równa (255, 0, 0), zaś bieli – trójka wartości (255, 255, 255). Model RGB wykorzystywany jest m.in. do reprezentacji barw w monitorach komputerowych: jeśli przyjrzeć się monitorowi przez szkło powiększające, można zauważyć, że poszczególne piksele składają się z punktów (lub pionowych kresek) o wspomnianych wyżej barwach składowych. Przy oglądaniu z większej odległości poszczególne punkty zlewają się w całość.

Jak nietrudno policzyć, liczba możliwych kombinacji wszystkich składowych wynosi w opisywanym systemie 256×256×256 = 16 777 216

w oryginale 255x255x255

, czyli mniej więcej 16 milionów wartości. Sprzętowe ograniczenia niektórych kart graficznych powodują, że w określonych rozdzielczościach liczba wyświetlanych kolorów może być mniejsza i wynosić np. 16, 256 lub 65 536. Liczba dostępnych kolorów bywa też określana mianem głębi koloru (ang. color depth). Sposób reprezentacji kolorów przy różnych wartościach głębi koloru przedstawiono w tabeli 15.1.

Tabela 15.1. Reprezentacja barw dla różnych wartości głębi koloru

Liczba kolorów

Objaśnienie

16

Kolory dostępne w trybie 16-kolorowym są sztywno ustalone (zobacz opis klasy TColor)

256

Sterownik graficzny wykorzystuje paletę barw. Jednocześnie można wyświetlić co najwyżej 256 kolorów, jednak zawartość palety może być zmieniana

65 536

Wartości składowych czerwonej, zielonej i niebieskiej są zapisane w 16-bitowym słowie (oddzielnie dla każdego piksela); ze względu na niewystarczającą liczbę bitów (16 zamiast 24) niektóre kolory mogą być reprezentowane niedokładnie

16 milionów

Grafika wyświetlana jest z użyciem wszystkich możliwych barw

Zarządzanie paletami jest zagadnieniem dość obszernym i nie będziemy go tu rozwijać; zainteresowanym Czytelnikom polecamy sięgnięcie do literatury poświęconej programowaniu w Windows, jak np. Programming Windows Charlesa Petzolda (Microsoft Press)[2]. Określenia liczby dostępnych w danej konfiguracji kolorów można dokonać za pomocą funkcji Windows API GetDeviceCaps(), zwracającej m.in. liczbę tzw. płatów (ang. color plane) oraz bitów opisujących kolor pojedynczego piksela. Sposób ustalenia tych wartości przedstawiono poniżej.

int BitsPerPixel = GetDeviceCaps(Canvas->Handle, BITSPIXEL);

int NumberOfPlanes = GetDeviceCaps(Canvas->Handle, PLANES);

int NumberOfColors = 1<<(NumberOfPlanes * BitsPerPixel);

Klasa TPen

Wygląd linii używanej do wykreślania obiektów (np. kół czy prostokątów) określany jest przez bieżące ustawienia pióra (ang. pen) – kolor, grubość i styl linii. Ten ostatni parametr determinuje wygląd linii (ciągła, przerywana, kropkowa itd.). Warto pamiętać, że użycie szerokości większej niż 1 automatycznie wymusza styl linii ciągłej, toteż aby narysować grubą linię kropkową, trzeba wykreślić obok siebie kilka takich linii o grubości 1.

Klasa TPen pozwala także określić tryb kreślenia, tj. sposób, w jaki wartości pikseli kreślonej linii będą składane z pikselami tła. Na uwagę zasługuje tu stała pmNotXor, nakazująca złożenie typu „nie-albo” (zanegowana alternatywa wyłączająca XOR). Ułatwia to kreślenie linii „tymczasowych”, które mają zostać za chwilę usunięte – linię taką wystarczy wykreślić ponownie, by jej piksele zostały w wyniku złożenia usunięte bez wpływu na zawartość tła. Technikę tę można wykorzystać np. do rysowania prostokąta wyznaczającego obszar powiększenia (przesuwanie prostokąta myszą wymaga jego szybkiego przerysowywania bez wpływu na zawartość rysunku znajdującą się „pod spodem”).

Klasa TBrush

Bieżące ustawienia pędzla...

Zgłoś jeśli naruszono regulamin