Recall that window DCs are most commonly used only temporarily. If you need to draw into a window, you call BeginPaint or, if outside of a paint cycle, GetDC, although painting outside of a paint cycle is generally to be avoided. The window manager produces a DC for the window and returns it. You use the DC, then restore it to its original state and return it to the window manager with EndPaint (or ReleaseDC). Internally, the window manager keeps a small cache of DCs which it dips into when people come asking for a window DC, and when the DC is returned, it goes back into the cache. Since window DCs are used only temporarily, the number of outstanding DCs is typically not more than a handful, and a small cache is sufficient to satisfy DC demands in a normally-running system.

If you register a window class and include the CS_OWNDC flag in the class styles, then the window manager creates a DC for the window and puts it into the DC cache with a special tag that means "Do not purge this DC from the DC cache because it's the CS_OWNDC for this window." If you call BeginPaint or GetDC to get a DC for a CS_OWNDC window, then that DC will always be found and returned (since it was marked as "never purge"). The consequences of this are good, bad, and worse.

The good part is that since the DC has been created specially for the window and is never purged, you don't have to worry about "cleaning up the DC" before returning it to the cache. Whenever you call BeginPaint or GetDC for a CS_OWNDC window, you always get that special DC back. Indeed, that's the whole point of CS_OWNDC windows: You can create a CS_OWNDC window, get its DC, set it up the way you like it (selecting fonts, setting colors, etc.), and even if you release the DC and get it again later, you will get that same DC back and it will be just the way you left it.

The bad part is that you're taking something that was meant to be used only temporarily (a window DC) and using it permanently. Early versions of Windows had a very low limit for DCs (eight or so), so it was crucial that DCs be released as soon as they weren't needed. That limit has since been raised significantly, but the underlying principle remains: DCs should not be allocated carelessly. You may have noticed that the implementation of CS_OWNDC still uses the DC cache; it's just that those DCs get a special marking so the DC manager knows to treat them specially. This means that a large number of CS_OWNDC DCs end up "polluting" the DC cache, slowing down future calls to functions like BeginPaint and ReleaseDC that need to search through the DC cache.

(Why wasn't the DC manager optimized to handle the case of a large number of CS_OWNDC DCs? First, as I already noted, the original DC manager didn't have to worry about the case of a large number of DCs since the system simply couldn't even create that many in the first place. Second, even after the limit on the number of DCs was raised, there wasn't much point in rewriting the DC manager to optimize the handling of CS_OWNDC DCs since programmers were already told to use CS_OWNDC sparingly. This is one of the practicalities of software engineering: You can do only so much. Everything you decide to do comes at the expense of something else. It's hard to justify optimizing a scenario that programmers were told to avoid and which they in fact were already avoiding. You don't optimize for the case where somebody is abusing your system. It's like spending time designing a car's engine so it maintained good gas mileage when the car has no oil.)

The worse part is that most windowing framework libraries and nearly all sample code assume that your windows are not CS_OWNDC windows. Consider the following code that draws text in two fonts, using the first font to guide the placement of characters in the second. It looks perfectly fine, doesn't it?

void FunnyDraw(HWND hwnd, HFONT hf1, HFONT hf2)
{
 HDC hdc1 = GetDC(hwnd);
 HFONT hfPrev1 = SelectFont(hdc1, hf1);
 UINT taPrev1 = SetTextAlign(hdc1, TA_UPDATECP);
 MoveToEx(hdc1, 0, 0, NULL);
  
 HDC hdc2 = GetDC(hwnd);
 HFONT hfPrev2 = SelectFont(hdc2, hf2);
  
 for (LPTSTR psz = TEXT("Hello"); *psz; psz++) {
  POINT pt;
  GetCurrentPositionEx(hdc1, &pt);
  TextOut(hdc2, pt.x, pt.y + 30, psz, 1);
  TextOut(hdc1, 0, 0, psz, 1);
 }
  
 SelectFont(hdc1, hfPrev1);
 SelectFont(hdc2, hfPrev2);
  
 SetTextAlign(hdc1, taPrev1);
  
 ReleaseDC(hwnd, hdc1);
 ReleaseDC(hwnd, hdc2);
}

We get two DCs for the window. In the first we select our first font; in the second, we select the second. In the first DC, we also set the text alignment to TA_UPDATECP which means that the coordinates passed to the TextOut function will be ignored. Instead the text will be drawn starting at the "current position" and the "current position" will be updated to the end of the string, so that the next call to TextOut will resume where the previous one left off.

Once the two DCs are set up, we draw our string one character at a time. We query the first DC for the current position and draw the character in the second font at that same x-coordinate (but a bit lower), then we draw the character in the first font (which also advances the current position).

After the text drawing loop is done, we restore the states of the two DCs as part of the standard bookkeeping.

The intent of the function is to draw something like this, where the first font is bigger than the second.

Hello
Hello

And if the window is not CS_OWNDC that's what you get. You can try it out by calling it from our scratch program:

HFONT g_hfBig;

BOOL
OnCreate(HWND hwnd, LPCREATESTRUCT lpcs)
{
 LOGFONT lf;
 GetObject(GetStockFont(ANSI_VAR_FONT),
           sizeof(lf), &lf);
 lf.lfHeight *= 2;
 g_hfBig = CreateFontIndirect(&lf);
 return g_hfBig != NULL;
}

void
OnDestroy(HWND hwnd)
{
 if (g_hfBig) DeleteObject(g_hfBig);
 PostQuitMessage(0);
}

void
PaintContent(HWND hwnd, PAINTSTRUCT *pps)
{
 FunnyDraw(hwnd, g_hfBig,
                 GetStockFont(ANSI_VAR_FONT));
}

But if the window is CS_OWNDC, then bad things happen. Try it yourself by changing the line wc.style = 0; to wc.style = CS_OWNDC; You get the following unexpected output:

HHeelllloo

Of course, if you understand how CS_OWNDC works, this is hardly unexpected at all. The key to understanding is remembering that when the window is CS_OWNDC then GetDC just returns the same DC back no matter how many times you call it. Now all you have to do is walk through the FunnyDraw function remembering that hdc1 and hdc2 are in fact the same thing.

void FunnyDraw(HWND hwnd, HFONT hf1, HFONT hf2)
{
 HDC hdc1 = GetDC(hwnd);
 HFONT hfPrev1 = SelectFont(hdc1, hf1);
 UINT taPrev1 = SetTextAlign(hdc1, TA_UPDATECP);
 MoveToEx(hdc1, 0, 0, NULL);

So far, execution of the function is pretty normal.

 HDC hdc2 = GetDC(hwnd);

Since the window is a CS_OWNDC window, the DC that is returned in hdc2 is the same one that was returned in hdc1. In other words, hdc1 == hdc2! Now things get exciting.

 HFONT hfPrev2 = SelectFont(hdc2, hf2);

Since hdc1 == hdc2, what this really does is deselect the font hf1 from the DC and select the font hf2 instead.

 for (LPTSTR psz = TEXT("Hello"); *psz; psz++) {
  POINT pt;
  GetCurrentPositionEx(hdc1, &pt);
  TextOut(hdc2, pt.x, pt.y + 30, psz, 1);
  TextOut(hdc1, 0, 0, psz, 1);
 }

Now this loop completely falls apart. At the first iteration, we retrieve the current position from the DC, which returns (0, 0) since we haven't moved it yet. We then draw the letter "H" at position (0, 30) into the second DC. But since the second DC is the same as the first one, what really happens is that we are calling TextOut into a DC that is in TA_UPDATECP mode. Thus, the coordinates are ignored, the letter "H" is displayed (in the second font), and the current position is updated to be after the "H". Finally, we draw the "H" into the first DC (which is the same as the second). We think we're drawing it with the first font, but in fact we're drawing with the second font. We think we're drawing at (0, 0), but in fact we're drawing at (x, 0), where x is the width of the letter "H", because the call to TextOut(hdc2, ...) updated the current position.

Thus, each time through the loop, the next character in the string is displayed twice, all in the second font.

But wait, the disaster isn't finished yet. Look at our cleanup code:

 SelectFont(hdc1, hfPrev1);

This restores the original font into the DC.

 SelectFont(hdc2, hfPrev2);

This re-selects the first font! We failed to restore the DC to its original state and ended up putting a "corrupted" DC into the cache.

That's why I described CS_OWNDC as "worse". It takes code that used to work and breaks it by violating assumptions that most people make (usually without realizing it) about DCs.

And you thought CS_OWNDC was bad. Next time I'll talk about the disaster that is known as CS_CLASSDC.