Cool Form ver2

Kintaro

Jest to inna wersja zrobienia ładnie wyglądającego formę.
Dla osób, które nie czytały poprzedniego słowo wprowadzenia.
Problem jest następujący:
chcemy, aby nasz program (forma), miała wygląd jakiegoś obrazka i żeby nie był to po prostu kwadrat, albo kółko, (bo to każdy umie zrobić), ale coś bardziej skomplikowanego, z wieloma kantami i różnego rodzaju dziurami.

Jak się do tego najlepiej zabrać? Jeden sposób przedstawiłem w poprzednim artykule, ale on nie rozwiązuje problemu, komu przecież chce się klikać z 1000 razy i nie daj boże, aby był jakiś błąd pod koniec, bo wtedy wszystko od nowa trzeba zaczynać L.
Najlepiej, więc aby program po prostu wyglądał jak obraz, a wszystkie, np. białe pola były ukryte. Jest to najlepsze rozwiązanie, którego sam długo szukałem i właśnie nim się tutaj zajmę.

W skrócie, co będziemy robić:

  1. Napiszemy funkcje, która analizuje nasz obraz zapisując, które obszary są do wyświetlenia a które należy ukryć.

  2. Napiszemy procedury, które będą zapisywać i odczytywać opisy obrazów na dysk.

  3. Funkcja analizująca obraz.

Ogólny pomysł opiera się na analizie linijka po linijce bitmapy. Znajdujemy obszary w linii, gdzie kolor jest różny od koloru tła. Następnie te obszary zapisujemy w postaci prostokątów, i na końcu dodajemy do siebie.

Skorzystamy z klasy TBitmap. Pozwala odczytać ona kolor poszczególnych pikseli.
Nazywamy nasza funkcje bitmaptoregion, zwracać będzie ona region HRGN, w liście parametrów będzie pobierać wskaźnik do bitmapy i wartość przezroczystego koloru
Najpierw tworzymy nowy obiekt TBitmap

HRGN bitmaptoregion(Graphics::TBitmap *bitmap,TColor tlo)
        {
        HRGN rgn_temp;
//tymczasowy region, do którego będziemy zapisywać poszczególne linijki
        HRGN region=CreateRectRgn(0,0,0,0);
//wynikowy region
        int x,pocz,kon;
// x czyli położenie poziome, pocz i kon będą zawierały początek i koniec poszczególnych pól w lini z 
// kolorem różnym od koloru tła
        for (int y=0;y<bitmap->Height;y++)
                {
                x=0;
                while (x< bitmap->Width)
                        {
                        while ((bitmap->Canvas->Pixels[x][y]==tlo) && (x<= bitmap->Width)) x++;
//szukamy pierwszego obszaru
                        pocz=x++;
                        while ((bitmap->Canvas->Pixels[x][y]!=tlo) && (x<=bitmap->Width)) x++;
                        kon=x;
// znaleźliśmy pierwszy obszar
                        if (pocz<bitmap->Width)
                                {
                                rgn_temp=CreateRectRgn(pocz+1,y,kon,y+1);
//tworzymy pierwszy prostokąt
                                if (rgn_temp!=0) CombineRgn(region, region,rgn_temp, RGN_OR);
//sprawdzamy czy udało się utworzyć obszar, a następnie dodajemy go do zmiennej region,
// czyli do rezultatu
                                DeleteObject(rgn_temp);
// czyścimy region
                                }
                        };
                };
        return region;
//zwracamy wynik naszej pracy
        };

Już praktycznie możemy tworzyć formy o kształcie obrazka, jednak jak się okazuje trochę to długo trwa, i wygodniej jest zapisać wynik naszej pracy na dysk.
Aby to zrobić potrzebne są dwie funkcje jedna do zapisy druga do odczytu regionu z dysku.

Procedura zapisująca:

void WriteRegion (HRGN region,char *nazwa_pliku) 
// procedura pobiera dwa elementy region i nazwę pliku
        {
        DWORD size;
// w tej zmiennej będzie przechowywana wielkość regionu
        RGNDATA *rgndata;
// struktura opisująca plik (wiecej na końcu)
        TFileStream *plik;
        plik= new TFileStream(nazwa_pliku, fmCreate | fmShareDenyWrite);
// tworzymy plik korzystając z klasy strumienia do pliku
// predefiniowanych wartość chyba nie trzeba tłumaczyć ;)
        size=GetRegionData (region, 0, NULL);
// pobieramy rozmiar struktury opisującej regiony
        rgndata = (RGNDATA*) malloc(size);
// rezerwujemy odpowiednią ilość pamięci
        GetRegionData(region, size, rgndata);
// przekształcamy region na RGNDATA
        plik->Write(rgndata, size);
// zapisujemy strukturę do pliku
        plik->Free();
        free((void*)rgndata);
// zwalniamy pamięć
        };

Teraz czas na procedurę odczytu

HRGN ReadRegion (char *nazwa)
        {
        TFileStream *plik;
        RGNDATA *rgndata;
        plik = new TFileStream(nazwa, fmOpenRead | fmShareDenyWrite);
        DWORD size= plik->Size;
        rgndata = (RGNDATA*) malloc(size);
        plik ->Read(rgndata, size);
        HRGN region=ExtCreateRegion(NULL,size,rgndata);
// konwertujemy to co zapisaliśmy do pliku, na odpowiedni region
        plik ->Free();
        free((void*)rgndata);
        return region;
//zwracamy rezultat i zwalniamy pamięć
        };

I to już wszystkie procedury do obsługi formularza.
Dobrze by było wpisać je do jakiegoś oddzielnego pliku np. regiony.cpp (kod na końcu)

Teraz należy przygotować nasza aplikację.

Najpierw tworzymy obrazek. Jeśli jest spakowany (jpg) to nie obejdzie się bez wyczyszczenia obszarów które chcemy usunąć – czyli musimy się upewnić, że jeśli usuwamy wszystko o kolorze białym, to wszystkie obszary do kasacji są czysto białe J

Jak mamy takowy plik, to najłatwiej będzie zapisać go do bitmapy.

Następnie tworzymy opis regionu.
Tworzymy przykładową aplikację.
W nagłówku umieszczamy dyrektywę (jeśli umieściliśmy procedury w osobnych plikach – jeśli nie to wpisujemy je do kodu programu)

#include "regiony.cpp"

W pliku nagłówkowym wpisujemy jeśli nie ma

#include <Graphics.hpp> //chyba jest potrzebny :P
#include <ExtCtrls.hpp>

Deklarujemy zmienna globalna
String obrazek (w public)

Właściwość formy BorderStyle ustawiamy na bsNone

Umieszczamy na niej komponenty:

  1. OpenDialogi SaveDialog
  2. przycisk wczytania bitmapy nazwijmy go „Load”
  3. przycisk zapisu „save”

Procedura Load

void __fastcall TForm1::LoadClick(TObject *Sender)
{
if (!OpenDialog1->Execute()) return;
Graphics::TBitmap *b = new Graphics::TBitmap();
// tworzymy nową bitmapę
b->LoadFromFile(OpenDialog1->FileName);
Form1->Canvas->Draw(0,0,b);
// ładujemy do pamięci obrazek i rysujemy na głównej formie
obrazek=OpenDialog1->FileName; //przypisujemy ścieżkę do obrazka
delete b;
};

Procedura save

void __fastcall TForm1::saveClick(TObject *Sender)
{
Graphics::TBitmap *b = new Graphics::TBitmap();
b->LoadFromFile(obrazek);
HRGN Region =bitmaptoregion (b,clWhite);
//używamy koloru białego jako tło
if (SaveDialog1->Execute()) WriteRegion(Region,SaveDialog1->FileName.c_str());
//zapisujemy region do pliku
Form1->Canvas->Draw(0,0,b);
//ponownie rysujemy; 
SetWindowRgn(Form1->Handle, Region, True); //zmieniamy wygląd formy
delete b;
};

Następnie kilkamy na przycisk load ładujemy bitmape, klikamy na save i zapisujemy plik regionu.
W ten sposób tworzymy plik regionu.

Teraz ostatnia część czyli wykorzystanie w programie.
W jednym folderze mam obrazek tła, plik regionu, oraz pliki regiony.h i regiony.cpp (jeśli utworzyliśmy oddzielne pliki, jeśli nie należy kody procedur umieścić w pliku gdzie edytujemy kod.

Ewentualnie dołączmy

#include "regiony.cpp"

W zdarzeniu OnPaint głównej formy umieszczamy kod rysujący na formie obrazek.

Graphics::TBitmap *b = new Graphics::TBitmap();
b->LoadFromFile("tlo.bmp");
Form1->Canvas->Draw(0,0,b);

Nastpępnie w zdarzniu OnCreate wpisujemy

SetWindowRgn (Form1->Handle, ReadRegion("plik_regionu.rgn"), True);

Właściwość formy BorderStyle ustawiamy na bsNone

I wszystko powinno chodzić. Jak widać w ten sposób łatwo napisać system skórek do naszego programu, wraz z ich generatorem, wystarczy jeszcze napisać jakiś program do zapisywania ustawień poszczególnych elementów i ładnie to pozapisywać na dysk.

Jeśli brakuje jakichś bibliotek to trzeba sprawdzić czy w plikach nagłówkowych mamy

#include <ExtCtrls.hpp>
#include <Graphics.hpp>

oraz czy wszystkie pliki są w odpowiednim katalogu

Kody źródłowe
-------- plik regiony.cpp -----------------

HRGN bitmaptoregion(Graphics::TBitmap *bitmap,TColor tlo)
        {
        HRGN rgn_temp;
        HRGN region=CreateRectRgn(0,0,0,0);
        int x,pocz,kon;
        for (int y=0;y<bitmap->Height;y++)
                {
                x=0;
                while (x< bitmap->Width)
                        {
                        while ((bitmap->Canvas->Pixels[x][y]==tlo) && (x<= bitmap->Width)) x++;
                        pocz=x++;
                        while ((bitmap->Canvas->Pixels[x][y]!=tlo) && (x<=bitmap->Width)) x++;
                        kon=x;
                        if (pocz<bitmap->Width)
                                {
                                rgn_temp=CreateRectRgn(pocz+1,y,kon,y+1);
                                if (rgn_temp!=0) CombineRgn(region, region,rgn_temp, RGN_OR);
                                DeleteObject(rgn_temp);
                                }
                        };
                };
        return region;
        };

void WriteRegion (HRGN region,char *nazwa_pliku) 
        {
        DWORD size;
        RGNDATA *rgndata;
        TFileStream *plik;
        plik= new TFileStream(nazwa_pliku, fmCreate | fmShareDenyWrite);
        size=GetRegionData (region, 0, NULL);
        rgndata = (RGNDATA*) malloc(size);
        GetRegionData(region, size, rgndata);
        plik->Write(rgndata, size);
        plik->Free();
        free((void*)rgndata);
        };

HRGN ReadRegion (char *nazwa)
        {
        TFileStream *plik;
        RGNDATA *rgndata;
        plik = new TFileStream(nazwa, fmOpenRead | fmShareDenyWrite);
        DWORD size= plik->Size;
        rgndata = (RGNDATA*) malloc(size);
        plik ->Read(rgndata, size);
        HRGN region=ExtCreateRegion(NULL,size,rgndata);
        plik ->Free();
        free((void*)rgndata);
        return region;
       };

Troche o użytych funkcjach
CombineRgn – funkcja składająca ze kilka regionów w jeden

int CombineRgn(
    HRGN hrgnDest,	// region do którego kopiujemy
    HRGN hrgnSrc1,	// składowe 
    HRGN hrgnSrc2,	//
    int fnCombineMode 	// typ składania regionów RGN_OR dodaje do siebie regiony
   );

RGNDATA struktura zawierająca m.in. tablice prostokątów tworzących region

GetRegionData funkcja ta przekształca region hRgn na strukturę RGNDATA, jeśli 3 parametr podamy jako NULL, zwraca wielkość jaka będzie miała struktura wynikowa

DWORD GetRegionData(
    HRGN hRgn,	// region 
    DWORD dwCount,	// rozmiar struktury RGNDATA
    LPRGNDATA lpRgnData* 	// adres do struktury RGNDATA, zawierającej opis regionu
   );

HRGN ExtCreateRegion

(CONST XFORM *lpXform, DWORD nCount,  CONST RGNDATA *lpRgnData);

Procedura odwrotna do GetRegionData
Pierwszy parametr to forma przekształcenia regionu, drugi rozmiar struktury, trzeci adres do struktury RGNDATA, zawierającej opis regionu.

Jakby było coś niejasne to mail w profilu

Wzorowałem się na wykładzie do delphi Bogdana Polaka

2 komentarzy

Sam sobie napisałem funkcję która zmienia kształt okna na podstawie bitmapy ale nie wiedziałem jak zapisać wynik bo też miałem problem z prędkością :). Dzięki za ten artukuł. Przydał się. Stawiam 6 chodź nie czytałem reszty :P

no właśnie mi się jakieś wydawało podobne... dokładnie przerobione z Delphi ;)