Obalení OpenGL třídami MFC nám dovolí využít obou výhod API: rychlého vykreslování a elegantního rozhraní. Nicméně díky faktu, že mnoho ovladačů tiskáren nepracuje s API funkcí SetPixelFormat(), není možné tisknout OpenGL scénu přímo na tiskárnu. Velmi rozšířená technika je vykreslit OpenGL scénu do DIBu a poté ji zkopírovat do DC pro tisk nebo náhled. V tomto článku uvidíte jak to udělat.
Při vysvětlování použiji klasickou architekturu dokument/pohled. Třída CMyView je potomkem třídy COpenGLView, ve které je udělána veškerá inicializace OpenGL. V této nové třídě implementujeme tisk a náhled OpenGL scény. Tisk na tiskárnu nebo do náhledu se ve třídě pohledu provádí pomocí virtuální funkce OnPrint(). Upravíme ji následujícím způsobem.
void CMyView::OnPrint(CDC* pDC, CPrintInfo* pInfo)
{
OnPrint1(pDC, pInfo, this);// Příprava tisku scény
OnDraw(pDC);// Vykreslení scény je společné pro výstup do okna, tisk i náhled
OnPrint2(pDC);// Vlastní výstup na tiskárnu a úklid po tisku
}
Funkce OnPrint1() je vytvořena pro renderování mimo obrazovku. Hlavní úkoly této funkce jsou vytvořit DIB a paměťové DC i RC. Paměťové RC bude později použito pro vykreslení OpenGL scény mimo obrazovku. Funkce OnDraw() je standardní virtuální funkce třidy pohledu ve které provádíme vlastní vykreslení scény a to jak na tiskárnu a do náhledu, tak na obrazovku. Funkce OnPrint2() zkopíruje získaný DIB OpenGL scény na tiskárnu nebo do náhledu a provede úklid, tj. uvolnění DIBu a paměťových kontextů.
Následuje zdrojový kód funkce CMyView::OnPrint1().
void CMyView::OnPrint1(CDC* pDC, CPrintInfo* pInfo, CView* pView)
{
CRect rcClient;
pView->GetClientRect(&rcClient);// Zjištění velikosti okna
float fClientRatio = float(rcClient.Height())/rcClient.Width();// Poměr velikostí stran okna
Zjistíme velikost stránky. CSize m_szPage je pomocná členská proměnná třídy CMyView.
m_szPage.cx = pDC->GetDeviceCaps(HORZRES);
m_szPage.cy = pDC->GetDeviceCaps(VERTRES);
CSize szDIB;
Větvíme funkci podle toho, zda je proměnná m_bPreview true (náhled) nebo false (tisk). Pro náhled použijeme rozlišení okna.
if (pInfo->m_bPreview)// Náhled
{
szDIB.cx = rcClient.Width();
szDIB.cy = rcClient.Height();
}
else// Tisk
{
Pro tisk použijeme vyšší rozlišení. Musíme upravit jeho velikost tak, aby poměr stran byl stejný jako u okna.
if (m_szPage.cy > fClientRatio * m_szPage.cx)
{
// Plocha okna je širší než tisknutelná plocha
szDIB.cx = m_szPage.cx;
szDIB.cy = long(fClientRatio * m_szPage.cx);
}
else
{
// Plocha okna je užší než tisknutelná plocha
szDIB.cx = long(float(m_szPage.cy) / fClientRatio);
szDIB.cy = m_szPage.cy;
}
Pokud je DIB paměťově příliš velký, upravíme rozlišení. Určíme maximální velikost DIBu na 20 MB. Mělo by to záviset na tiskárně, ale bohužel nevím, jak programově zjistit velikost paměti tiskárny.
while (szDIB.cx * szDIB.cy > 20 * 1024 * 1024)
{
szDIB.cx = szDIB.cx / 2;
szDIB.cy = szDIB.cy / 2;
}
}
// Výpis zjištěných hodnot do okna debugeru
TRACE("Buffer size: %d x %d = %6.2f MB\n", szDIB.cx, szDIB.cy, szDIB.cx*szDIB.cy*0.000001);
BITMAPINFO m_bmi je pomocná členská proměnná třídy CMyView.
memset(&m_bmi, 0, sizeof(BITMAPINFO));// Nulování
m_bmi.bmiHeader.biSize = sizeof(BITMAPINFOHEADER);// Velikost této struktury
m_bmi.bmiHeader.biWidth = szDIB.cx;// Šířka DIBu
m_bmi.bmiHeader.biHeight = szDIB.cy;// Výška DIBu
m_bmi.bmiHeader.biPlanes = 1;
m_bmi.bmiHeader.biBitCount = 24;// Počet bitů na pixel
m_bmi.bmiHeader.biCompression = BI_RGB;// Typ komprese (závisí na počtu bitů na pixel) - bez komprese
m_bmi.bmiHeader.biSizeImage = szDIB.cx * szDIB.cy * 3;// Počet bytů pro uložení jednotlivých bodů DIBu (šířka*výška*počet bytů na bod)
HDC hDC = ::GetDC(pView->m_hWnd);// Získání DC okna
HANDLE m_hDib je pomocná členská proměnná třídy CMyView.
m_hDib = ::CreateDIBSection(hDC, &m_bmi, DIB_RGB_COLORS, &m_pBitmapBits, NULL, (DWORD)0);// Vytvoříme DIB
::ReleaseDC(pView->m_hWnd, hDC);// Uvolnění DC okna
HDC m_hMemDC je pomocná členská proměnná třídy CMyView.
m_hMemDC = ::CreateCompatibleDC(NULL);// Vytvoření paměťové DC
if (!m_hMemDC)// Pokud se jeho vytvoření nepovedlo smažeme jej a skončíme
{
DeleteObject(m_hDib);
m_hDib = NULL;
return;
}
SelectObject(m_hMemDC, m_hDib);// Vybereme DIB do paměťového DC
if (!SetDCPixelFormat(m_hMemDC, PFD_DRAW_TO_BITMAP | PFD_SUPPORT_OPENGL | PFD_STEREO_DONTCARE))
static PIXELFORMATDESCRIPTOR pfd =
{
sizeof(PIXELFORMATDESCRIPTOR),// Velikost této struktury
1,// Verze
PFD_DRAW_TO_BITMAP// Kreslení do DIBu
| PFD_SUPPORT_OPENGL// Podpora OpenGL
| PFD_STEREO_DONTCARE,
PFD_TYPE_RGBA// Používá se RGBA
24,// 24-bitová barevná hloubka
0, 0, 0, 0, 0, 0,// Barevné bity ignorovány
0,// Žádný alfa buffer
0,// Shift bit ignorován (?)
0,// Žádný akumulační bufer (?)
0, 0, 0, 0,// Akumulační bity ignorovány
32,// 32 bitový z-buffer
0,// Žádný stencil buffer
0,// Žádný pomocný buffer
PFD_MAIN_PLANE,// Hlavní hladina (vrstva)
0,// Rezervováno
0, 0, 0// Hladinová maska ignorována
};
Vyhledáme index pixel formátu, který je nejbližší předcházející struktuře.
int pixelformat;// Pomocná proměnná
if ((pixelformat = ChoosePixelFormat(m_pDC->GetSafeHdc(), &pfd)) == 0)
{
// Nepovedlo se, uvolníme paměť a skončíme
DeleteObject(m_hDib);
m_hDib = NULL;
DeleteDC(m_hMemDC);
m_hMemDC = NULL;
return;
}
if (SetPixelFormat(m_pDC->GetSafeHdc(), pixelformat, &pfd) == FALSE)// Nastaví formát pixelu
{
// Nepovedlo se, uvolníme paměť a skončíme
DeleteObject(m_hDib);
m_hDib = NULL;
DeleteDC(m_hMemDC);
m_hMemDC = NULL;
return;
}
int n = ::GetPixelFormat(m_pDC->GetSafeHdc());// Zjistí aktuální formát pixelu
// Naplní strukturu pfd informacemi o aktuálním formátu.
::DescribePixelFormat(m_pDC->GetSafeHdc(), n, sizeof(pfd), &pfd);
HGLRC m_hMemRC je pomocná členská proměnná třídy CMyView.
m_hMemRC = ::wglCreateContext(m_hMemDC);// Získáme paměťové RC
if (!m_hMemRC)
{
// Nepovedlo se, uvolníme paměť a skončíme
DeleteObject(m_hDib);
m_hDib = NULL;
DeleteDC(m_hMemDC);
m_hMemDC = NULL;
return;
}
HDC m_hOldDC a HGLRC m_hOldRC jsou pomocné členské proměnné třídy CMyView.
m_hOldDC = ::wglGetCurrentDC();// Získáme aktuální (staré) DC
m_hOldRC = ::wglGetCurrentContext();// Získáme aktuální (staré) RC
::wglMakeCurrent(m_hMemDC, m_hMemRC);
glClearDepth(1.0f);// Nastavení čisticí hodnoty pro hloubkový buffer
glEnable(GL_DEPTH_TEST);// Povolí kontrolu hloubky
GLfloat fAspect = (GLfloat)szDIB.cx / szDIB.cy;// Poměr šířky k výšce
glMatrixMode(GL_PROJECTION);// Nastaví aktuální matici pro výpočty potřebné pro vykreslování
glLoadIdentity();// Resetuje aktuální matici
gluPerspective(45.0f, fAspect, 1, 100);// Nastavení perspektivy
glMatrixMode(GL_MODELVIEW);// Nastavení aktuální matici pro výpočty potřebné pro vykreslování
glViewport(0, 0, cx, cy);// Nastavení obdélníku do kterého lze vykreslovat
// Žádné display listy nemáme
}
To provedeme vlastním kreslení. Je dobrým zvykem použít pro vykreslení do paměti stejnou funkci jako pro vykreslení na obrazovku. Jediným rozdílem bude, že nepoužijeme double buffering. Tuto funkci nebudu popisovat, protože si ji každý napíše sám.
Tato metoda provede "skutečný" tisk a poté uvolní paměť.
void CMyView::OnPrint2(CDC* pDC)
{
DIB je hotový. Už nepotřebujeme paměťové RC. Jenom zkopírujeme obrázek na DC pro tisk nebo náhled.
::wglMakeCurrent(NULL, NULL);// Jako aktuální nenastavíme nic
::wglDeleteContext(m_hMemRC);// Smazání RC
::wglMakeCurrent(m_hOldDC, m_hOldRC);// Obnovení původního DC a RC
float fBmiRatio = float(m_bmi.bmiHeader.biHeight) / m_bmi.bmiHeader.biWidth;
CSize szTarget;
if (m_szPage.cx > m_szPage.cy)// Stránka na šířku
{
if(fBmiRatio < 1)// Obrázek na šířku
{
szTarget.cx = m_szPage.cx;
szTarget.cy = long(fBmiRatio * m_szPage.cx);
}
else// Obrázek na výšku
{
szTarget.cx = long(m_szPage.cy / fBmiRatio);
szTarget.cy = m_szPage.cy;
}
}
else// Stránka na výšku
{
if(fBmiRatio<1)// Obrázek na šířku
{
szTarget.cx = m_szPage.cx;
szTarget.cy = long(fBmiRatio * m_szPage.cx);
}
else// Obrázek na výšku
{
szTarget.cx = long(m_szPage.cy/fBmiRatio);
szTarget.cy = m_szPage.cy;
}
}
// Výpočet posunutí pro vycentrování na stránce
CSize szOffset((m_szPage.cx - szTarget.cx) / 2, (m_szPage.cy - szTarget.cy) / 2);
int nRet = ::StretchDIBits(pDC->GetSafeHdc(),
szOffset.cx, szOffset.cy,
szTarget.cx, szTarget.cy,
0, 0,
m_bmi.bmiHeader.biWidth,
m_bmi.bmiHeader.biHeight,
GLubyte*) m_pBitmapBits,
m_bmi,
DIB_RGB_COLORS,
SRCCOPY);
if(nRet == GDI_ERROR)// Tisk byl neúspěšný
TRACE0("Chyba ve StretchDIBits()");
DeleteObject(m_hDib);// Smazání DIBu
m_hDib = NULL;
DeleteDC(m_hMemDC);//Smazání DC
m_hMemDC = NULL;
m_hOldDC = NULL;
}
Pro tento postup při tisku potřebuje nejméně 16 bitové barvy. Pokud se v náhledu zobrazuje černá plocha zkontrolujte nastavení barev. V tomto by mohla být chyba.
Tento článek je mírně upraveným překladem článku "Printing and Print Preview OpenGL in MFC" z anglického webu o programování http://www.codeguru.com/, kde si můžete stáhnout i zdrojový kód ukázkové aplikace. Hlavním rozdílem je základní třída pohledu a nastavení pixel formátu DC, které jsem musel vytvořit sám. V původním článku to bylo uděláno zavoláním dvou funkcí, ale já si nestáhl zdrojový kód a neměl jsem zrovna přístup k internetu.
napsal: Milan Turek <nalim.kerut (zavináč) email.cz>
Adresa anglického článku je http://www.codeguru.com/opengl/printpreview.html.