Tisk a náhled před tiskem OpenGL scény v MFC

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ů.

Příprava OpenGL pro tisk mimo obrazovku - OnPrint1()

  1. Vypočítáme velikost DIBu pro tisk a náhled, která závisí na velikosti zobrazovacího zařízení. Pro náhled použijeme rozlišení tiskárny. Pro tisk použijeme redukované rozlišení tiskárny. V ideálním případě, pokud by velikost potřebné paměti a rychlost nebyla problém bychom použili plné rozlišení obrazovky. Nicméně pro tiskárnu s rozlišením 720 DPI a použitím papíru o velikosti "letter" přesáhne paměť, potřebná pro DIB sekci, snadno 100MB. Proto redukujeme rozlišení tisku.
  2. Pro vytvoření DIB sekce o velikosti zmíněné dříve voláme funkci CreateDIBSection().
  3. Vytvoření paměťového DC a jeho připojení k DIB sekci. Voláním Win32 funkce CreateCompatibleDC() vytvoříme paměťové DC, potom do něj vybereme DIB sekci.
  4. Nastavení pixel formátu paměťového DC je podobné nastavení pixel formátu obrazovkového DC. Jediný rozdíl je ve flagu, který nastavuje vlastnosti pixelového bufferu. Pro obrazovku nastavujieme PFD_DRAW_TO_WINDOW a PFD_DOUBLEBUFFER, ale pro paměťové DC potřebujeme PFD_DRAW_TO_BITMAP.
  5. Vytvoření paměťového RC. Použijeme dříve vytvořené paměťové DC pro vytvoření paměťového RC pro OpenGL mimoobrazovkového renderingu. Po skončení tisku budou uvolněny (tj. ve funkci OnPrint2).
  6. Staré DC a RC pro vykreslování na obrazovku si musíme uložit, protože je po skončení tisku musíme opět nastavit jako aktuální.
  7. Funkce wglMakeCurrent() nastaví paměťové RC jako aktuální. Od teď bude OpenGL kreslit do paměťového, ne obrazovkového, RC. Nicméně nejdříve musíme inicializovat paměťové RC stejně jako jsme to dělali s obrazovkovým.
  8. Inicializace paměťového RC. Pře vlastním kreslením do paměťového RC ještě musíme nastavit velikost plochy do které se může kreslit (velikost DIBu) a perspektivu.
  9. Vytvoření display listů pro paměťové RC. Pokud používáte display listy, tak je musíte znovu vytvořit pro nově vytvořené paměťové RC. Pamatujte si, že display listy nejsou znovupoužitelné pro různá RC.

Následuje zdrojový kód funkce CMyView::OnPrint1().

void CMyView::OnPrint1(CDC* pDC, CPrintInfo* pInfo, CView* pView)

{

1. Vypočítáme velikost DIBu pro tisk a náhled

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);

2. Vytvoření DIB sekce

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

3. Vytvoření paměťového DC a jeho připojení k DIB sekci

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

4. Nastvení pixel formátu 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);

5. Vytvoření paměťového RC

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;

}

6. Uložíme si staré DC a RC

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

7. Nastavíme paměťové RC jako aktuální

::wglMakeCurrent(m_hMemDC, m_hMemRC);

8. Inicializace paměťového RC je stejná jako u obrazovkového RC

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

9. Vytvoření display listů pro paměťové RC

// Žádné display listy nemáme

}

Vykreslení scény mimo obrazovku - OnDraw()

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.

Vlastní tisk na tiskárnu nebo do náhledu - OnPrint2()

Tato metoda provede "skutečný" tisk a poté uvolní paměť.

  1. Po vykreslení scény do paměti můžeme smazat paměťové RC a nastavit původní DC a RC jako aktuální.
  2. Výpočet cílové velikosti vzhledem k velikosti obrázku a orientaci papíru. Obrázek (scéna) je uložen v paměti (v DIB sekci). Ve skutečnosti je velikost a orientace papíru rozdílná od obrázku. Potřebujeme zjistit cílovou plochu na stránce, na kterou bud obrázek kopírován. Cílová plocha by měla mít stejnou orientaci a poměr stran jako obrázek v DIB sekci.
  3. Roztažení obrázku na velikost cílové plochy (vlastní tisk). Win32 API funkce StretchDIBits() zkopíruje DIB sekci do cíle, tj. na cílové DC (tiskárna nebo náhled). Obrázek je roztáhnut tak aby vyplnil cílovou plochu při zachování poměru stran.
  4. Práce je hotova. Uvolníme pamět DIBu a DC.

void CMyView::OnPrint2(CDC* pDC)

{

1. Uvolnění paměťového RC, a obnovení půvoního (starého) DC a RC

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

2. Výpočet cílové velikosti vzhledem k velikosti obrázku a orientaci papíru

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);

3. Roztažení obrázku na velikost cílové plochy (vlastní tisk)

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()");

4. Uvolnění paměti

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.

Zdrojové kódy