Larry Osterman's WebLog

Confessions of an Old Fogey
Blog - Title

What’s wrong with this code, part 22 – Drawing Text…

What’s wrong with this code, part 22 – Drawing Text…

  • Comments 43

Recently I’ve been working on something that I’ve never done before in my almost 24 years at Microsoft. 

 

For the past 23ish years, I’ve been a plumber – all the work I’ve done has been under the covers.  But for the next version of Windows, I decided to stretch my boundaries a bit and try some UI programming.  I’ve just spent the past few days working on a cool change to the volume control (it’s not important what it is, and most people will never know about the change, but those that do will probably agree with me :)).

As part of the change, I needed to measure the dimensions of a text string.  This is a dummy version of some code I wrote, I simply called DrawText with the DT_CALCRECT into a memory DC that I created.

BOOL InitInstance(HINSTANCE hInstance, int nCmdShow)
{
   HWND hWnd;

   hInst = hInstance; // Store instance handle in our global variable

   hWnd = CreateWindow(szWindowClass, szTitle, WS_OVERLAPPEDWINDOW,
      CW_USEDEFAULT, 0, CW_USEDEFAULT, 0, NULL, NULL, hInstance, NULL);

   if (!hWnd)
   {
      return FALSE;
   }
<BEGIN LARRYS CODE>
   HDC hdc = CreateCompatibleDC(NULL);

   RECT rcText = {0, 0, 88, 34};

   DrawText(hdc, L"My Text String", -1, &rcText, DT_CENTER | DT_END_ELLIPSIS | DT_EDITCONTROL | DT_WORDBREAK | DT_NOPREFIX | DT_CALCRECT);

   CAtlString string;
   string.Format(L"Text String occupies: %d x %d pixels", rcText.right - rcText.left, rcText.bottom - rcText.top);
   MessageBox(hWnd, string, L"String Size", 0);
<END LARRYS CODE> 
   ShowWindow(hWnd, nCmdShow);
   UpdateWindow(hWnd);

   return TRUE;
}

This is just code I took by using Visual Studio to create a Windows Win32 project and inserting the code between “BEGIN LARRYS CODE” and “END LARRYS CODE”.  The meat of the code is just 3 lines of code.

Even though there’s almost no code here, it still has a bug in it that was quite subtle and took me several hours to find.

  • So it is the font then :)

    But I can't agree it is a subtle mistake. Regardless of large fonts/DPI (not the same thing btw, large fonts increases the point size of the font, and DPI increases the size of a point in pixels) using a different font for drawing text and measuring the rect is wrong, wrong, wrong. Even in your simple case when the system font and the control's font are the same size it will give the wrong result. For one thing, the system font is monospace and the control most likely uses a proportional font. Depending on the text, you'll get wider or narrower rectangle. It gets even worse with word-wrapping. You may get one more or one less line. And assuming this code will end into production, the text will most likely be localized. The system font can't handle some

    characters and will render them as squares (or will use some default font substitution). Either way, it will give wrong metrics.

    Ideally, you should get the font from the control. Just ask nicely: SendMessage(hWnd,WM_GETFONT,0,0). Assuming the control handles this message correctly.

  • Ivo, I know - that's what the final solution in my code does.

    And to me it was subtle - the code worked just fine in all my test scenarios before I went to high DPI.

  • Larry, completely off topic, but since you're toying with volume-control related UI now, maybe you can fix this bug that really annoys me every time I open the mixer in vista/server2k8: I have my default window colour set to light gray as I find it much easier on the eyes than the default white. When I open the mixer, the application names for each of the per-application volume controls all have black text on white backgrounds, rather than using the default window background colour.

    See http://www.dhiren.za.net/files/pictures/mixer.png

  • The amount of software that gets broken with large fonts is evidence to something terribly rotten in the API (including it's documentation in this context). The API merely providing means to achieve the goal of extent calculation simply isn't enough; it should lead the developer away from such pitfalls.

    Oh well, I guess this can be attributed to backward compatibility or legacy code or whatever.

  • Now that 3 or 4 bugs in the code have been pointed out, perhaps the original post can be updated to reflect the fixes?

    i would hate for someone googling for how to measure text to find this on MSDN and thinks it's the perferred method.

  • Ian: Typically I post an answers link with the corrected code in it.  

    Dhiren: You know, this is the first that anyone's pointed it out to me...  I'll file a bug to make sure it's fixed at some point in the future :).

    Eldan: It's more complicated than that.  My code was incorrect from the beginning, I just hadn't tested it enough.  Large fonts happened to point out the fundamental bug in my code, but the bug was there all along - it's entirely possible the bug would have been found with an international version of the code for example.

  • Ahh, and I figured you were talking about the race condition on the user switching fonts out from under you. (kidding)

  • Nils Arthur asked: why it's only possible to lower the volume in Windows (i.e. setting a volume between 0% and 100%) and not raise it (i.e setting it higher than 100%)?

    Because it doesn't go up to eleven ?

  • Leo Davidson, I think the comments here prove your point.  I agree with you.

  • Indeed large fonts is just a single facet of the problem.

    The whole font-handling segment of the GDI API is problematic. That bug which have been there all along wouldn't have been there if the API would guide you to obtain the correct font before you measure.

  • This is without running the code, just by reading:

    1. Isn't there a conflict between using DT_END_ELLIPSIS (which changes the text it does not fit in the rectangle) together with DT_CALCRECT (which expands the rectangle to fit the text)?

    2. The original rectangle is 88 pixels wide.

    And then we check the behavior of DT_CALCRECT:

    The rectangle gets expanded vertically and the text is wrapped, unless the largest word is wider than the rectangle. In that case the width gets increased to fit the largest word.

    So at big DPI the 88 pixels are too little and the width will grow.

    3. Using generic APIs and data types (MessageBox, CAtlString, etc.) with Unicode strings (L"...") instead of generic text ( _T("...") or TEXT("...") ). Probably does not matter in Win, I assume it's all build as Unicode (and is detected at compile time, and has nothing to do with the font :-)

  • Mihai, actually the wordwrap and endelipsis flags together mean that it won't actually add the elipsis until it has to.  From what I've seen, the API doesn't change the width.

  • The other day, I wrote about measuring the dimensions of a piece of text using the DrawText API . My

Page 3 of 3 (43 items) 123