Display – práce s grafickým adaptérem v C++

V tomto článku si ukážeme pár užitečných věcí pro práci s obrazovkou. Ukážeme si například, jak zjistit všechny dostupné grafické režimy, jak přepnout do zvoleného režimu, zjistit aktuální nastavení apod.

Poznámka ke screen-shotu: Hodnota 60 Hz obnovovací frekvence není chyba programu, ale jedná se o LCD monitor, kde tato hodnota je zcela optimální a nikoli „sebevražedná“, jak by tomu bylo u klasického monitoru.

Zjištění aktuálního nastavení obrazovky

Ke zjištění nastavení rozlišení máme v zásadě 3 cesty. Jedna z nich je přes takzvanou systémovou metriku, tedy s použitím funkce GetSystemMetrics:

int GetSystemMetrics(
  int nIndex  // určení, který parametr máme zjistit
);

Tato funkce nám umožňuje zjistit hodnotu některého z mnoha parametrů systémové metriky, jako je třeba velikost ikon, velikosti různých prvků okna, jako titulkový pruh a mnoho dalšího. Pro nás nyní budou zajímavé hodnoty týkající se rozměrů obrazovky. Pokud chceme získat absolutní velikost (v pixelech) primárního monitoru, použijeme jako parametr nIndex hodnotu SM_CXSCREEN, resp. SM_CYSCREEN. Pokud nás zajímají rozměry tzv. virtuální obrazovky, tedy plochy tvořené všemi monitory, použijeme parametry SM_CXVIRTUALSCREEN, resp. SM_CYVIRTUALSCREEN. Dále můžeme zjistit velikost, do které se roztáhne maximalizované okno. Tato velikost se samozřejmě liší od rozměru celé obrazovky v závislosti na poloze a velikosti pruhu úloh. Pro tento případ použijeme parametry SM_CXMAXIMIZED, resp. SM_CYMAXIMIZED. Nakonec můžeme zjistit počet instalovaných monitorů pomocí parametru SM_CMINITORS. Ukažme si vše na konkrétním příkladě. Ukázkový projekt je aplikace MFC založená na dialogu. Pro zobrazení systémové metriky jsem přidal další dialog (CSysMetricsDlg) s několika static prvky, které naplníme v InitDialog zjištěnými hodnotami systémové metriky:

BOOL CSysMetricsDlg::OnInitDialog()
{
  CDialog::OnInitDialog();
 
  SetDlgItemInt(IDC_CXSCREEN,
    GetSystemMetrics(SM_CXSCREEN), FALSE); 
  SetDlgItemInt(IDC_CYSCREEN,
    GetSystemMetrics(SM_CYSCREEN), FALSE); 
  SetDlgItemInt(IDC_XVIRTUALSCREEN,
    GetSystemMetrics(SM_CXVIRTUALSCREEN), FALSE); 
  SetDlgItemInt(IDC_YVIRTUALSCREEN,
    GetSystemMetrics(SM_CYVIRTUALSCREEN), FALSE); 
  SetDlgItemInt(IDC_MONITOR_COUNT,
    GetSystemMetrics(SM_CMONITORS), FALSE); 
  SetDlgItemInt(IDC_CXMAXIMIZED,
    GetSystemMetrics(SM_CXMAXIMIZED), FALSE); 
  SetDlgItemInt(IDC_CYMAXIMIZED,
    GetSystemMetrics(SM_CYMAXIMIZED), FALSE); 
  return TRUE;
}

Na následujícím obrázku jsou výsledky získané na počítači s 2 monitory v režimu dual-head (Matrox). Na prvním je výsledek v případě, kdy na obou monitorech bylo nastaveno rozlišení 1152 x 864 pixelů. Výsledek rozměrů virtuální obrazovky je zcela podle očekávání (2304 = 2*1152 do šířky)

Na dalším obrázku je výsledek v případě, kdy na druhém monitoru bylo nastaveno rozlišení 1024 x 768, zatímco na prvním zůstalo 1152 x 864.

Jak vidíte, šířka virtuální obrazovky dává trochu „podivnou“ hodnotu, neboť výška na druhém monitoru je pouze 768. Nicméně jako virtuální obrazovka je brán „opsaný“ obdélník obou monitorů.

Tímto způsobem však nejsme schopni zjistit barevnou hloubku a obnovovací frekvenci monitoru. Proto se podívejme na druhý způsob, který nám umožní zjistit více o zobrazovacím adaptéru. Použijeme funkci GetDeviceCaps:

int GetDeviceCaps(
  HDC hdc,    // handle kontextu zařízení
  int nIndex  // index parametru
);

Tato funkce nám obdobně jako předchozí vrátí hodnotu zvoleného parametru, který určíme parametrem nIndex. Kromě toho musíme funkci dát platný handle na kontext zařízení. Klidně to může být kontext zařízení našeho okna, získaný funkcí GetDC (jak si hned ukážeme). Z množství možných parametrů nás budou zajímat následující:

  • HORZRES – šířka obrazovky ( v pixelech )
  • VERTRES – výška obrazovky
  • BITPIXEL – barevná hloubka (v bitech na pixel)
  • VREFRESH – obnovovací frekvence monitoru (toto funguje pouze ve Windows NT/2000)
Vytvořme si tedy na hlavním dialogu static, do něhož vypíšeme informace o aktuálně nastaveném grafickém režimu. Příslušná funkce bude vypadat takto:

void CUkazkaDlg::GetCurrentMode()
{
  TCHAR chLine[255];
  TCHAR chText[100];
  int xScreen, yScreen, bpp, freq;
  HDC hdc = ::GetDC(m_hWnd);
  xScreen = GetDeviceCaps(hdc, HORZRES);
  itoa(xScreen, chLine, 10);
  lstrcat(chLine, " x ");
  yScreen = GetDeviceCaps(hdc, VERTRES);
  itoa(yScreen, chText, 10);
  lstrcat(chLine, chText);
  lstrcat(chLine, ", ");
  bpp = GetDeviceCaps(hdc, BITSPIXEL);
  switch ( bpp )
  {
    case 2:
      lstrcpy(chText, "mono");
      break;
    case 4:
      lstrcpy(chText, "16 barev");
      break;
    case 8:
      lstrcpy(chText, "256 barev");
      break;
    case 16:
      lstrcpy(chText, "High color (16 bitů)");
      break;
    case 32:
      lstrcpy(chText, "True color (32 bitů)");
      break;
    default:
      itoa(bpp, chText, 10);
      break;
  }
  lstrcat(chLine, chText);
  freq = GetDeviceCaps(hdc, VREFRESH);
  itoa(freq, chText, 10);
  lstrcat(chLine, ", ");
  lstrcat(chLine, chText);
  lstrcat(chLine, " Hz");
  ::ReleaseDC(m_hWnd, hdc);
  SetDlgItemText(IDC_CURRENT_MODE, chLine);
}

Zjištění seznamu všech grafických režimů

Nyní nás ale může zajímat, které všechny grafické režimy „umí“ naše grafická karta, a dále, které z nich „zvládne“ náš monitor. K výčtu grafických režimů slouží funkce EnumDisplaySettingsEx:

BOOL EnumDisplaySettingsEx(
  LPCTSTR lpszDeviceName,  // zobrazovací zařízení
  DWORD iModeNum,          // číslo grafického režimu
  LPDEVMODE lpDevMode,  // nastavení grafického režimu
  DWORD dwFlags            // volby
);

resp. její „jednodušší“ varianta EnumDisplaySettings. Tato jednodušší varianta nám neumožňuje rozlišit, zda nás zajímá, jestli daný režim zvládne monitor.

Když se podíváte na parametr lpDevMode, zjistíte, že nám ukazuje na strukturu DEVMODE, která je poměrně rozsáhlá, neboť mimo jiné obsahuje i prvky týkající se vlastností tiskárny. Nás v případě grafického adaptéru budou zajímat následující prvky této struktury:

  • dmPelsWidth – šířka (v pixelech) viditelného povrchu zařízení, v našem případě tedy šířka obrazovky
  • dmPelsHeight – výška obrazovky
  • dmBitsPerPel – barevná hloubka, v bitech na pixel
  • dmDisplayFrequency – obnovovací frekvence obrazovky
  • dmFields – zde musíme specifikovat, které prvky této struktury jsou platné, pokud například měníme nastavení (viz dále)
Pokud chceme získat seznam všech dostupných grafických režimů, voláme tuto funkci opakovaně s tím, že parametr iModeNum zvyšujeme od 0 po 1, a pokračujeme tak dlouho, dokud funkce vrací TRUE. Po každém volání funkce máme ve struktuře lpDevMode informace o daném grafickém režimu. Jediné, co musíme ještě udělat, je před začátkem volání funkce nastavit prvek dmSize na velikost struktury, tedy sizeof(DEVMODE), a hodnotu dmDriverExtra na počet bytů, pokud bychom chtěli získat další data s informacemi o ovladači, svázaná s touto strukturou. V našem případě je nastavíme na 0, neboť tato data nás nebudou zajímat. Pokud chceme rozlišit, zda nás zajímají všechny režimy, které umí grafická karta, nebo pouze ty, které současně zvládne i monitor, můžeme to specifikovat v parametru dwFlags. Pokud uvedeme hodnotu EDS_RAWMODE, funkce nám bude vracet všechny režimy, v opačném případě (když zadáme hodnotu 0), pouze ty režimy, které jsou podporovány monitorem. Vytvořme si tedy na našem dialogu list-box, jehož položky si naplníme seznamem zjištěných grafických režimů s tím, že si vytvoříme funkci, která jako parametr bude mít volbu, jestli zobrazit všechny režimy nebo jen podporované monitorem:

void CUkazkaDlg::EnumDispSettings(BOOL all)
{
  TCHAR chLine[255];
  TCHAR chText[100];
  DWORD dwMode = 0;
  BOOL bResult = TRUE;
  DEVMODE devmode;
  SendDlgItemMessage(IDC_LIST_MODES, LB_RESETCONTENT, 0, 0);
  ZeroMemory(&devmode, sizeof(DEVMODE));
  devmode.dmSize = sizeof(DEVMODE);
  devmode.dmDriverExtra = 0;
  while ( bResult )
  {
    if ( all )
      bResult = EnumDisplaySettingsEx(NULL,
        dwMode,
        &devmode,
        EDS_RAWMODE);
    else
      bResult = EnumDisplaySettingsEx(NULL,
        dwMode,
        &devmode,
        0);
    dwMode++;
    itoa(devmode.dmPelsWidth, chLine, 10);
    lstrcat(chLine, " x ");
    itoa(devmode.dmPelsHeight, chText, 10);
    lstrcat(chLine, chText);
    switch ( devmode.dmBitsPerPel )
    {
      case 2:
        lstrcpy(chText, "mono");
        break;
      case 4:
        lstrcpy(chText, "16 barev");
        break;
      case 8:
        lstrcpy(chText, "256 barev");
        break;
      case 16:
        lstrcpy(chText, "High color (16 bitů)");
        break;
      case 32:
        lstrcpy(chText, "True color (32 bitů)");
        break;
      default:
        itoa(devmode.dmBitsPerPel, chText, 10);
        break;
    }
    lstrcat(chLine, " - ");
    lstrcat(chLine, chText);
    lstrcat(chLine, " - ");
    itoa(devmode.dmDisplayFrequency, chText, 10);
    lstrcat(chLine, chText);
    lstrcat(chLine, " Hz");
    SendDlgItemMessage(IDC_LIST_MODES, LB_ADDSTRING, 0, (LPARAM)(LPCTSTR)chLine);
  }
  if ( dwMode > 0 )
    SendDlgItemMessage(IDC_LIST_MODES, LB_SETCURSEL, 0, 0);
  SetDlgItemInt(IDC_MODES_COUNT, dwMode);
}

Ještě si řekněme, že tato funkce nám poskytuje také další způsob zjištění aktuálního grafického režimu. Když totiž jako parametr iModeNum uvedeme hodnotu ENUM_CURRENT_SETTINGS, funkce nám strukturu lpDevMode naplní údaji o aktuálním nastaveném režimu. Dále máme možnost uvést hodnotu ENUM_REGISTRY_SETTINGS, pak získáme nastavení režimu, které je uloženo v registrační databázi, nezávisle na aktuálním nastavení, které mohlo být nastaveno dynamicky beze změny údajů v registru. Ukažme si příklad funkce, která vypíše do textu ve static prvku aktuální grafický režim získaný tímto způsobem:

void CUkazkaDlg::GetCurrentMode2()
{
  TCHAR chLine[255];
  TCHAR chText[100];
  DEVMODE devmode;
  ZeroMemory(&devmode, sizeof(DEVMODE));
  devmode.dmSize = sizeof(DEVMODE);
  EnumDisplaySettingsEx(
    NULL,
    ENUM_CURRENT_SETTINGS,
    &devmode,
    0);

  itoa(devmode.dmPelsWidth, chLine, 10);
  lstrcat(chLine, " x ");
  itoa(devmode.dmPelsHeight, chText, 10);
  lstrcat(chLine, chText);
  switch ( devmode.dmBitsPerPel )
  {
    case 2:
      lstrcpy(chText, "mono");
      break;
    case 4:
      lstrcpy(chText, "16 barev");
      break;
    case 8:
      lstrcpy(chText, "256 barev");
      break;
    case 16:
      lstrcpy(chText, "High color (16 bitů)");
      break;
    case 32:
      lstrcpy(chText, "True color (32 bitů)");
      break;
    default:
      itoa(devmode.dmBitsPerPel, chText, 10);
      break;
  }
  lstrcat(chLine, " - ");
  lstrcat(chLine, chText);
  lstrcat(chLine, " - ");
  itoa(devmode.dmDisplayFrequency, chText, 10);
  lstrcat(chLine, chText);
  lstrcat(chLine, " Hz");
  SetDlgItemText(IDC_CURRENT_MODE, chLine);
}

Přepnutí do příslušného grafického režimu

Nyní tedy máme zobrazen seznam grafických režimů. Ukažme si, jak programově přepnout do zvoleného grafického režimu. K tomuto účelu slouží funkce ChangeDisplaySettingsEx:

LONG ChangeDisplaySettingsEx(
  LPCTSTR lpszDeviceName,  // jméno zobrazovacího zařízení
  LPDEVMODE lpDevMode,    // grafický režim
  HWND hwnd,              // nepoužito, musí být NULL
  DWORD dwflags,            // volby grafického režimu
  LPVOID lParam            // video parametry (or NULL)
);

Jako první parametr lpszDeviceName můžeme zadat jméno grafického zařízení získané funkcí EnumDisplayDevices, nebo můžeme zadat NULL, což znamená, že jde o primární zobrazovací zařízení, většinou samozřejmě monitor. Parametr lpDevMode je adresa již zmíněné struktury DEVMODE, ve které naplníme platnými hodnotami ty prvky, které chceme v této funkci použít, a dále nastavíme prvek dmFields, kde právě specifikujeme, které prvky struktury DEVMODE obsahují platná data. Parametr dwFlags upřesňuje způsob aplikace změn; uveďme si některé z možných hodnot a jejich význam:

  • 0 – grafický režim bude změněn dynamicky
  • CDS_GLOBAL – nové nastavení bude uloženo a projeví se u všech uživatelů, jinak je změněno pouze nastavení aktuálního uživatele. Tento příznak má efekt pouze tehdy, je-li současně uveden následující příznak:
  • CDS_UPDATEREGISTRY – grafický režim bude změněn a nastavení uloženo do registru.
  • CDS_NORESET – nové nastavení bude uloženo do registru, ale nebude ihned změněno. Současně musí být uveden příznak CDS_UPDATEREGISTRY.
  • CDS_TEST – systém pouze otestuje, zda uvedený režim může být nastaven
Parametr lParam specifikuje další podrobnosti týkající se především TV výstupu. Zde se jím nebudeme zabývat, pro případné zájemce o hlubší studium této problematiky je zde samozřejmě dokumentace v podobě MSDN, kde najdete všechny podrobnosti.

Vytvořme si tedy v našem ukázkovém projektu funkci, která dynamicky přepne do režimu, jenž si uživatel vybere v seznamu (z list-boxu):

void CUkazkaDlg::OnSwitchMode()
{
  DWORD dwMode = (DWORD)SendDlgItemMessage(IDC_LIST_MODES, LB_GETCURSEL, 0,0);
  DEVMODE devmode;
  ZeroMemory(&devmode, sizeof(DEVMODE));
  devmode.dmSize = sizeof(DEVMODE);
  if ( !EnumDisplaySettingsEx(NULL,
        dwMode,
        &devmode,
        0) )
    return; // cosi je v nepořádku, jdeme pryč
  devmode.dmFields = DM_PELSWIDTH | DM_PELSHEIGHT | DM_BITSPERPEL
    | DM_DISPLAYFREQUENCY;
  ChangeDisplaySettingsEx(NULL,
    &devmode,
    NULL,
    0,
    NULL);
}

Na závěr si ještě řekněme, jak v aplikaci zachytit změnu grafického režimu, kterou provedla jiná aplikace nebo uživatel. Mnoho aplikací potřebuje především na změnu rozlišení obrazovky nějak reagovat, je tu proto systémová zpráva WM_DISPLAYCHANGE, jejíž parametry jsou následující:

wParam – nová barevná hloubka

lParam – rozlišení obrazovky: dolní WORD je horizontální rozlišení a horní WORD je vertikální rozlišení. Tuto zprávu samozřejmě zachytíme v proceduře hlavního okna aplikace, neboť systém tuto zprávu automaticky posílá všem hlavním oknům spuštěných aplikací:

LRESULT CUkazkaDlg::WindowProc(UINT message, WPARAM wParam, LPARAM lParam)
{
  switch ( message )
  {
    case WM_DISPLAYCHANGE:
      MessageBox("A tady můžeme reagovat na změnu rozlišení");
      break;
  } 
  return CDialog::WindowProc(message, wParam, lParam);
}

Zde si můžete stáhnout ukázkový projekt display.zip.

Diskuze (5) Další článek: Amazon koupil Egghead

Témata článku: , , , , , , , , , , , , , , , , , , , , , , , , ,