5
votes

Am i allowed to use a DC outside of a paint cycle? Is my window's DC guaranteed to be valid forever?

i'm trying to figure out how long my control's Device Context (DC) is valid.

i know that i can call:

GetDC(hWnd);

to get the device context of my control's window, but is that allowed?

When Windows sends me a WM_PAINT message, i am supposed to call BeginPaint/EndPaint to properly acknowledge that i've painted it, and to internally clear the invalid region:

BeginPaint(hWnd, {out}paintStruct);
try
   //Do my painting
finally
   EndPaint(hWnd, paintStruct);
end;

But calling BeginPaint also returns me a DC inside the PAINTSTRUCT structure. This is the DC that i should be painting on.

i cannot find anything in the documentation that says that the DC returned by BeginPaint() is the same DC that i would get from GetDC().

Especially now, in the days of Desktop Composition, is it valid to paint on a DC that i obtain outside of BeginPaint?

There seem to be 2 ways i can get a DC to paint on during a paint cycle:

  1. dc = GetDC(hWnd);

  2. BeginPaint(&paintStruct);

There is a 3rd way, but it seems to be a bug with the Borland Delphi that i develop with.

During WM_PAINT processing, Delphi believes that the wParam is a DC, and proceeds to paint on it. Whereas the MSDN says that the wParam of a WM_PAINT message is unused.

The Why

My real goal is to try to keep a persistent GDI+ Graphics object against an HDC, so that i can use some better performing features of GDI+ that depend on having a persistent DC.

During the WM_PAINT message handling i want to draw a GDI+ image to the canvas. The following nieve version is very slow:

WM_PAINT:
{
   PAINTSTRUCT ps;
   BeginPaint(m_hwnd, ps);
   Graphics g = new Graphics(ps.hdc);
   g.DrawImage(m_someBitmap, 0, 0);
   g.Destroy();
   EndPaint(h_hwnd, ps);
}

GDI contains a faster performing bitmap, a CachedBitmap. But using it without thinking gives no performance benefit:

WM_PAINT:
{
   PAINTSTRUCT ps;
   BeginPaint(m_hwnd, ps);

   Graphics g = new Graphics(ps.hdc);
   CachedBitmap bm = new CachedBitmap(m_someBitmap, g);
   g.DrawCachedBitmap(m_bm, 0, 0);
   bm.Destroy();
   g.Destroy();
   EndPaint(h_hwnd, ps);
}

The performance gain comes from creating the CachedBitmap once, so on program initialization:

m_graphics = new Graphics(GetDC(m_hwnd));
m_cachedBitmap = new CachedBitmap(b_someBitmap, m_graphcis);

And now on the paint cycle:

WM_PAINT:
{
   PAINTSTRUCT ps;
   BeginPaint(m_hwnd, ps);
   m_graphics.DrawCachedBitmap(m_cachedBitmap, 0, 0);
   EndPaint(h_hwnd, ps);
}        

Except now i'm trusting that the DC i obtained after program initializtion will be the same DC for my window as long as the application is running. This means that it survives through:

  • fast user switches
  • composition enabled/disabled
  • theme switching
  • theme disabling

i find nothing in MSDN that guarantees that the same DC will be used for a particular window for as long as the window exists.

Note: i am not using double-buffering, because i want to be a good developer, and do the right thing. * Sometimes that means you double-buffering is bad.

3

3 Answers

5
votes

There are exceptions, but in general, you may get a different DC each time you call GetDC or BeginPaint. Thus you shouldn't try to save state in the DC. (If you must do this for performance, there are special DCs you can create for a class of windows or a particular window instance, but it doesn't sound like that's what you really need or want.)

Most of the time, however, those DCs will be compatible. They will represent the same graphics mode, so your compatible bitmap should work, even if you get a different DC.

There are Windows messages that tell you when the graphics mode changes, like WM_DISPLAYCHANGE and WM_PALETTECHANGED. You can listen for these, and recreate your cached bitmap. Since those are rare events, you won't have to worry about the performance impact of recreating your cached bitmap at that point.

You can also get notifications for things like theme changes. Those don't change the graphics mode--they're a higher level concept--so your cached bitmap should still be compatible with any DC you get. But if you want to change bitmap when the theme changes, you can listen for WM_THEMECHANGED as well.

6
votes

The only way I know of that may (or may not) do what you are looking for is to create the window with the CS_OWNDC class style.

What that does is allocates a unique device context for each window in the class.

Edit

From the linked MSDN article:

A device context is a special set of values that applications use for drawing in the client area of their windows. The system requires a device context for each window on the display but allows some flexibility in how the system stores and treats that device context.

If no device-context style is explicitly given, the system assumes each window uses a device context retrieved from a pool of contexts maintained by the system. In such cases, each window must retrieve and initialize the device context before painting and free it after painting.

To avoid retrieving a device context each time it needs to paint inside a window, an application can specify the CS_OWNDC style for the window class. This class style directs the system to create a private device context — that is, to allocate a unique device context for each window in the class. The application need only retrieve the context once and then use it for all subsequent painting.

Windows 95/98/Me: Although the CS_OWNDC style is convenient, use it carefully, because each device context uses a significant portion of 64K GDI heap.

Perhaps this example will illustrate the use of CS_OWNDC better:

#include <windows.h>

static TCHAR ClassName[] = TEXT("BitmapWindow");
static TCHAR WindowTitle[] = TEXT("Bitmap Window");

HDC m_hDC;
HWND m_hWnd;

LRESULT CALLBACK WndProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
    static PAINTSTRUCT ps;

    switch (msg)
    {
    case WM_PAINT:
        {
            BeginPaint(hWnd, &ps);

            if (ps.hdc == m_hDC)
                MessageBox(NULL, L"ps.hdc == m_hDC", WindowTitle, MB_OK);
            else
                MessageBox(NULL, L"ps.hdc != m_hDC", WindowTitle, MB_OK);

            if (ps.hdc == GetDC(hWnd))
                MessageBox(NULL, L"ps.hdc == GetDC(hWnd)", WindowTitle, MB_OK);
            else
                MessageBox(NULL, L"ps.hdc != GetDC(hWnd)", WindowTitle, MB_OK);

            RECT r;
            SetRect(&r, 10, 10, 50, 50);
            FillRect(m_hDC, &r, (HBRUSH) GetStockObject( BLACK_BRUSH ));

            EndPaint(hWnd, &ps);
            return 0;
        }
    case WM_DESTROY:
        {
            PostQuitMessage(0);
            return 0;
        }
    }
    return DefWindowProc(hWnd, msg, wParam, lParam);
}

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nShowCmd)
{   
    WNDCLASSEX wcex;

    wcex.cbClsExtra = 0;
    wcex.cbSize = sizeof(WNDCLASSEX);
    wcex.cbWndExtra = 0;
    wcex.hbrBackground = (HBRUSH) GetStockObject( WHITE_BRUSH );
    wcex.hCursor = LoadCursor( NULL, IDC_ARROW );
    wcex.hIcon = LoadIcon( NULL, IDI_APPLICATION );
    wcex.hIconSm = NULL;
    wcex.hInstance = hInstance;
    wcex.lpfnWndProc = WndProc;
    wcex.lpszClassName = ClassName;
    wcex.lpszMenuName = NULL;
    wcex.style = CS_OWNDC;

    if (!RegisterClassEx(&wcex))
        return 0;

    DWORD dwExStyle = 0;
    DWORD dwStyle = WS_OVERLAPPEDWINDOW | WS_VISIBLE;

    m_hWnd = CreateWindowEx(dwExStyle, ClassName, WindowTitle, dwStyle, 0, 0, 300, 300, NULL, NULL, hInstance, NULL);

    if (!m_hWnd)
        return 0;

    m_hDC = GetDC(m_hWnd);

    MSG msg;
    while (GetMessage(&msg, NULL, 0, 0))
    {
        TranslateMessage(&msg);
        DispatchMessage(&msg);
    }

    return msg.wParam;
}

The CS_OWNDC flag is not to be confused with the CS_CLASSDC flag which:

Allocates one device context to be shared by all windows in the class. Because window classes are process specific, it is possible for multiple threads of an application to create a window of the same class. It is also possible for the threads to attempt to use the device context simultaneously. When this happens, the system allows only one thread to successfully finish its drawing operation.

If all else fails just reconstruct the CachedBitmap.

When you construct a CachedBitmap object, you must pass the address of a Graphics object to the constructor. If the screen associated with that Graphics object has its bit depth changed after the cached bitmap is constructed, then the DrawCachedBitmap method will fail, and you should reconstruct the cached bitmap. Alternatively, you can hook the display change notification message and reconstruct the cached bitmap at that time.

I'm not saying that CS_OWNDC is the perfect solution, but it is one step towards a better solution.

Edit

The sample program seemed to retain the same DC during screen resolution / bit depth change testing with the CS_OWNDC flag, however, when that flag was removed, the DC's were different (Window 7 64-bit Ultimate)(should work the same over differn OS versions... although it wouldn't hurt to test).

Edit2

This example doesn't call GetUpdateRect to check if the window needs to be painted during the WM_PAINT. That is an error.

2
votes

You can draw onto whichever window dc pleases you. They're both valid. A window does not have just one dc that can represent it at a time. So each time you call GetDC - and BeginPaint internally does so, you will get a new, unique dc, that nonetheless represents the same display area. Just ReleaseDC (or EndPaint) when you're done with them. In the days of Windows 3.1 device contexts were a limited, or very expensive system resource, so applications were encouraged to never hold onto them, but to retrieve them from the GetDC cache. nowadays its perfectly acceptable to create a dc at window creation, and cache it for the life of the window.

The only "problem" is, when handling WM_PAINT, the dc returned by BeginPaint will be clipped to the invalid rect, and the saved one will not.


I don't however understand what you are attempting to achieve with gdiplus. Usually, if an object is ... selected into a dc for a long period of time, that dc is a memory dc, not a window dc.


Each time GetDC is called you WILL get a new HDC representing a distinct device context with its own state. So, objects, background colors, text modes etc. set on one DC will NOT effect that state of another DC retrieved by a different call to GetDC or BeginPaint.

The system cannot randomly invalidate HDCs retrieved by the client, and actually does a lot of work in the background to ensure that HDCs retrieved before a display mode switch, continue to function. Even changing the bit depth, that technically makes the dc's incompatible, will not, in any way, prevent an application from continuing to use an hdc to blit.

That said, it is wise to watch at LEAST for WM_DISPLAYCHANGE, release any cached DCs and device bitmaps, and recreate them.