Grafické aplikace ve Visual C++ (5.)

Nyní je ten správný čas, abychom si vytvořily tzv. Off-screen surfaces neboli pomocné povrchy, do kterých si uložíme naše bitmapy. Tyto povrchy posléze vykreslujeme do zadního povrchu, který jsme vytvořili v minulé lekci. V dnešní lekci vytvoříme další dvě členské funkce třídy CControl. První z nich vytvoří Off-screen surface a vrátí ukazatel na něj a druhá do tohoto povrchu nahraje bitmapu ze souboru. Poté tento povrch můžete blittovat do zadního bufferu, jehož obsah se zobrazí na monitoru.

6.1 Off-screen surfaces

Off-screen surfaces jsou opět buffery, které ji6 nemusí být v grafické paměti (záleží na velikosti grafické paměti). Pokud vlastníte grafickou kartu, který na sobě nese méně než 8MB, je vhodné všechny pomocné povrchy vytvářet v systémové paměti.

Zpravidla vytváříme povrchy, které jsou velké jako bitmapa, kterou chceme do povrchu nahrát. Dále záleží na hloubce barev, se kterou právě pracujeme. Pochopitelně paměťové nároky se zvýší, pokud pracujete v 16-bitech (65 tis. barev) oproti 8-bitům (256 barev) (o 32-bitech nemluvě).

6.2 Vytvoření povrchu

Povrchy vytváříme stejně jako v minulých lekcích, s tím rozdílem, že strukturu DDSURFACEDESC naplníme jinými daty a navíc přesně určíme rozměry vytvářeného povrchu. Přidejte funkci CreateSurface() do třídy CControl. Hlavičku funkce vidíte níže. Tato funkce vám pouze alokuje paměť a vrátí ukazatel na prázdný povrch. Všimněte si, že funkce má jako první parametr ukazatel na ukazatel, to proto, že mění hodnotu ukazatele.

Jednoduchý kód jak by mohla vypadat vaše funkce pro vytvoření povrchu :

HRESULT CControl::CreateSurface(DWORD dwWidth, DWORD dwHeight, LPDIRECTDRAWSURFACE7 * lpSurface) 
{
	DDSURFACEDESC2 ddsd; // 1
	ZeroMemory( &ddsd, sizeof(ddsd));
	ddsd.dwSize = sizeof(ddsd); // 2
	DWORD dwRet;

	ddsd.dwFlags = DDSD_CAPS | DDSD_HEIGHT | DDSD_WIDTH; // 3
	ddsd.ddsCaps.dwCaps = DDSCAPS_OFFSCREENPLAIN; // 4
	ddsd.dwHeight = dwWidth; // 5
	ddsd.dwWidth = dwHeight;

	dwRet = m_lpDD->CreateSurface(&ddsd, lpSurface, NULL); // 6
	if(dwRet != DD_OK) {
		TRACE("Cannot create surface due %d\n", dwRet);
	}
	return dwRet;
}
Nyní si rozebereme každý řádek zvlášť:
  1. Definice objektu ddsd typu DDSURFACEDESC2.
  2. Inicializece atributu dwSize. Tento atribut musí být vždy inicializován dříve než začneme se strukturou pracovat (viz minulá lekce).
  3. Nyní určíme, které atributy struktury jsou platné, tedy mají význam. Chceme šířku,  výšku a že povrch je Off-screen.
  4. Atribut dwCaps určuje typ povrchu. V našem případě je to Off-screen surface.
  5. Dále následují rozměry povrchu : dwWidth (šířka) a dwHeight (výška). Jednotky jsou pochopitelně pixely.
  6. Jako poslední krok zavoláme funkci CreateSurface(), kterou již známe z minulé lekce, když jsme vytvářeli přední buffer.

Funkce vrací hodnotu DD_OK pokud je úspěšná.

6.3. Nahrání bitmapy do povrchu

Nyní máme v paměti alokovanou oblast paměti, do které si můžete nahrát bitmapu. Tuto bitmapu můžete nahrát ze zdrojů přímo ve VC++, ale i z naprosto nezávislého souboru bitmapy. Osobně doporučuji druhý způsob, protože je bitmapa uchována mimo spustitelná soubor, který pak nenarůstá do obrovských velikostí. Následující funkce nahrává bitmapu mimo soubor aplikace. Jako první parametr jí předáváte cestu (absolutní nebo relativní) na bitmap. Druhý parametr určuje povrch, do kterého se bitmapa bude nahrávat.

HRESULT CControl::CopyBitmap(CString csFile, LPDIRECTDRAWSURFACE7 Surface) 
{
	HDC hdcImage, hdc = NULL; 
	BITMAP bm;
	DDSURFACEDESC2 ddsd;
	HBITMAP hbm;
	DWORD dwRet;
	// 
	// Get handle to bitmap
	hbm = (HBITMAP)LoadImage(NULL, csFile, IMAGE_BITMAP, 0, 0, LR_LOADFROMFILE|LR_CREATEDIBSECTION);//1
	if(!hbm) {
		dwRet = GetLastError();
		TRACE("Cannot load image due %d\n", dwRet);
		return dwRet;
	}
	// 
	// Get size of the bitmap
	GetObject(hbm, sizeof(BITMAP), &bm); //2
	// 
	// Make sure this surface is restored. 
	Surface->Restore();//3
	// 
	// Create DC for our bitmap
	hdcImage = CreateCompatibleDC(NULL);//4
	ASSERT(hdcImage);
	// 
	// Select bitmap intoa a memoryDC so we can use it.
	SelectObject(hdcImage, hbm);//5
	// 
	// get size of surface. //6
	ddsd.dwSize = sizeof(ddsd);
	ddsd.dwFlags = DDSD_HEIGHT | DDSD_WIDTH;
	Surface->GetSurfaceDesc(&ddsd);

	//blit bitmap to GDI surface
	dwRet = Surface->GetDC(&hdc);//7
	if (dwRet != DD_OK) {
		TRACE("Cannot get DC of surface due %d\n", dwRet);
		return dwRet;
	}

	dwRet = StretchBlt( 	hdc, 0, 0, 
				ddsd.dwWidth, 
				ddsd.dwHeight, 
				hdcImage, 0, 0, 
				bm.bmWidth, 
				bm.bmHeight, 
				SRCCOPY);//8
	if(dwRet == 0) {
		dwRet = GetLastError();
		TRACE("Cannot blit bitmap to surface due %d\n", dwRet); 
	}
	else {
		dwRet = ERROR_SUCCESS;
	} 

	Surface->ReleaseDC(hdc);//9
	// 
	// Release handle to GDI object and handle to DC
	DeleteObject(hbm);//10
	DeleteDC(hdcImage);
	return dwRet;
}

Výše uvedená funkce je trošku složitější, takže si ji pěkně vysvětlíme. Využíváme zde GDI, které je popsáno v lekci č.2.

  1. Jako první získáme handle na naši bitmapu. To nám zajistí funkce Win32 LoadImage(), ve které určujeme jméno bitmapy (strBMPFile) a další parametry, které nám určí, že bitmapu nahráváme ze souboru atd. Detailně je tato funkce popsána v MSDN.
  2. Dále získáme rozměry naší bitmapy. Funkce GetObject() naplní strukturu BITMAP, která mimo jiné obsahuje šířku a výšku bitmapy.
  3. Funkce Restore() realokuje paměť určenou pro povrch. Může se stát, že se povrch ztratí, například pří přepnutí rozlišení, pak je třeba zavolat tuto funkci, aby nám realokovala paměť. My pak musíme nahrát znovu bitmapu do povrchu !!! Problematika ztrácení povrchů bude vysvětlena nejspíše v příští lekci.
  4. Vytvoříme paměťový kontext zařízení pro naší bitmapu. (viz lekce č. 2)
  5. Vybereme bitmapu do tohoto kontextu.
  6. Nyní získáme zpětně parametry povrchu, do kterého chceme bitmapu nahrát. Funkce GetSurfaceDesc() přijímá jako parametr ukazatel ne strukturu DDSURFACEDESC2.
  7. Dále získáme handle na kontext zařízení našeho povrchu. Po té můžeme překopírovat bitmapu jako v běžném GDI. Jediný problém, že tento handle nemůžeme udržovat dále a musíme ho ihned uvolnit.
  8. Funkce StretchBlt() kopíruje kontext zařízení bitmapy do kontextu povrchu.
  9. Uvolnění DC povrchu!!! Toto je velmi důležité, protože jinak byste nemohli dále pracovat s povrchem.
  10. Jako poslední krok uvolníme handle na bitmapu a DC bitmapy.

Problém se získáním DC našeho povrchu je v tom, že operace je časově náročná tzn. že pokud budete ve vašem programu stále přistupovat k DC, program se citelně zpomalí. 

Poznámka:
Pokud používáte 8-bitovou barevnou hloubku, pravděpodobně bitmapa nebude vypadat tak jak má, jelikož není nastavena žádná paleta. Pokud nepracujete na úplně pomalém počítači, můžete nastavit 16-bitů a vše bude v pořádku.

6.4 Nastavení a vykreslení pozadí

    Nejprve vytvoříme povrch pozadí, do kterého nahrajeme požadovanou bitmapu ve formátu .bmp. Nejlepší je, když bitmapa je stejně velká jako je nastavené rozlišení (viz minulá lekce), pak nedochází k deformaci bitmapy (i když teoreticky funkce CopyBitmap() umí bitmapu roztáhnout, zkuste si to a uvidíte, že bitmapa bude zkreslená). Ve třídě CControl deklarujte novou proměnnou typu ukazatel na povrch, který bude ukazovat na povrch našeho pozadí. Typ je stejný jako u zadního a předního bufferu tj. LPDIRECTDRAWSURFACE7 a proměnnou nazvěte třeba m_lpDDSBackground. Dále na konec funkce DDInit() přidejte  následující kód:

//
// Jako prvni vytvorime povrch pro nase pozadi, velikost je stejna jako rozliseni protoze pozadi pokryva celou obrazovku

dwResult =
CreateSurface(RES_X, RES_Y, &m_lpDDSBackground);
if(dwResult != DD_OK) {
        TRACE("Cannot create surface for background due %d\n", dwResult);
        return dwResult;
}
//
// Za druhe do vytvoreneho povrchu nakopirujeme zvolenou bitmapu

dwResult = CopyBitmap("background.bmp", m_lpDDSBackground);
if(dwResult != DD_OK) {
        TRACE("Cannot copy bitmap to surface due %d\n", dwResult);
        return dwResult;
}

 Nyní už můžeme vykreslit naší bitmapu do zadního bufferu. To provedeme ve funkci UpdateFrame(). Těsně před prohozením připište tento kód :

CRect rcBackground;
rcBackground.SetRect(0,0,RES_X, RES_Y);


m_
lpDDSBack->Blt(&rcBackground, m_lpDDSBackground, &rcBackground, DDBLT_WAIT , NULL);

Funkce Blt() provede blitting do zadního bufferu. Funkce přijímá dva obdélníky odkud a kam se bude kopírovat. V našem případě jsou oba obdélníky stejné. Druhý parametr je ukazatel na povrch, ze kterého data kopírujeme tzn. ukazatel na náš pomocný povrch pozadí. Parametr DDBLT_WAIT zajistí, že funkce počká až se dokončí všechny předchozí blittovací operace. Funkci Blt() budeme ještě dále rozebírat v další lekci.

6.5 Závěr

A už je tu zase konec. Po dnešní lekci uvidíte na monitoru vaší bitmapu, které se pořád dokola překresluje z našeho pomocného povrchu. Prohazování není vidět, jelikož vykreslujeme pořád tu samou bitmapu. Kdybyste si nahráli dvě bitmapy a po té je střídali, viděli byste jak DirectDraw nedovolí žádné "šeredné" problikávání. Příklad z dnešní lekce si samozřejmě můžete stáhnou v sekci Downloads.

Těším se příště nashledanou.

                                                                                                                                                                                                                                                                                                                    Jiří Formánek