• The Old New Thing

    A twenty-foot-long computer

    • 20 Comments

    Back in the days of Windows 95, when Plug and Play was in its infancy, one of the things the Plug and Play team did was push the PCI specification to an absurd extreme.

    They took a computer and put it at one end of a hallway. They then built a chain of PCI bridge cards that ran down the hallway, and at the end of the chain, plugged in a video card.

    And then they turned it on.

    Amazingly, it actually worked. The machine booted and used a video card twenty feet away. (I'm guessing at the distance. It was a long time ago.) It took two people to operate this computer, one to move the mouse and type, and another to watch the monitor at the other end and report where the pointer was and what was happening on the screen.

    And the latency was insane.

    But it did work and thereby validated the original design.

    Other Plug and Play trivia: The phrase "Plug and Play" had already been trademarked at the time, and Microsoft had to obtain the rights to the phrase from the original owners.

  • The Old New Thing

    Slightly closer to a proper football (i.e., soccer) match

    • 25 Comments

    This weekend, I attended a soccer match between Chelsea FC and Celtic FC at Seahawks Stadium Qwest Field. The game was the opener of the 2004 ChampionsWorld Series, wherein some of the top soccer teams from Europe tour North America to give us yanks a chance to see how football is done for real.

    From reading the team's web sites before the match, you'd think that Chelsea had already thrown in the towel, because even before the game started, they were making excuses, blaming jet lag for their loss. The excuses weren't necessary, though, because Chelsea dominated the match and ultimately won 4–2.

    It was interesting comparing the two teams' styles. When Chelsea scored a goal, you could watch it develop. A through ball, a man open, a pass or two, and the ball is in the back of the net. Celtic's goals were more of a "And he's got the ball, and... hey, how'd he score?" It all happened so fast it was over before it began.

    Attendance was 30,000, only half the capacity of Seahawks Stadium Qwest Field, but a decent turnout I think for a sport that is still considered fringe here in the States.

    Here's Raymond's checklist comparing the match to what he figures a "proper" British football match to be like.

    What
    Raymond
    Experienced
    A Proper
    British
    Football
    Match
    Segregated cheering sections
    Singing
    "Neutral" cheering section ?
    Post-game riot  
    Alcohol sold during match  
    Police officers around pitch 
    Streaker interrupts match

    One aspect of the British football divisions that I find fascinating is relegation, wherein the worst teams of the year are demoted to the next lower league, replaced by the top teams of that next lower league. This doesn't happen in U.S. sports leagues (at least not any of the major ones). In fact, in U.S. sports leagues, the team that has the worst record is rewarded with an early draft pick! How's that for reverse incentive.

  • The Old New Thing

    Why do some process stay in Task Manager after they've been killed?

    • 35 Comments

    When a process ends (either of natural causes or due to something harsher like TerminateProcess), the user-mode part of the process is thrown away. But the kernel-mode part can't go away until all drivers are finished with the thread, too.

    For example, if a thread was in the middle of an I/O operation, the kernel signals to the driver responsible for the I/O that the operation should be cancelled. If the driver is well-behaved, it cleans up the bookkeeping for the incomplete I/O and releases the thread.

    If the driver is not as well-behaved (or if the hardware that the driver is managing is acting up), it may take a long time for it to clean up the incomplete I/O. During that time, the driver holds that thread (and therefore the process that the thread belongs to) hostage.

    (This is a simplification of what actually goes on. Commenter Skywing gave a more precise explanation, for those who like more precise explanations.)

    If you think your problem is a wedged driver, you can drop into the kernel debugger, find the process that is stuck and look at its threads to see why they aren't exiting. You can use the !irp debugger command to view any pending IRPs to see what device is not completing.

    After all the drivers have acknowledged the death of the process, the "meat" of the process finally goes away. All that remains is the "process object", which lingers until all handles to the process and all the threads in the process have been closed. (You did remember to CloseHandle the handles returned in the PROCESS_INFORMATION structure that you passed to the CreateProcess function, didn't you?)

    In other words, if a process hangs around after you've terminated it, it's really dead, but its remnants will remain in the system until all drivers have cleaned up their process bookkeeping, and all open handles to the process have been closed.

  • The Old New Thing

    Why can't you trap TerminateProcess?

    • 25 Comments

    If a user fires up Task Manager and clicks "End Task" on your program, Windows first tries to shut down your program nicely, by sending WM_CLOSE messages to GUI programs and CTRL_CLOSE_EVENT events to console programs. But you don't get a chance to intercept TerminateProcess. Why not?

    TerminateProcess is the low-level process killing function. It bypasses DLL_PROCESS_DETACH and anything else in the process. Once you kill with TerminateProcess, no more user-mode code will run in that process. It's gone. Do not pass go. Do not collect $200.

    If you could intercept TerminateProcess, then you would be escalating the arms race between programs and users. Suppose you could intercept it. Well, then if you wanted to make your program unkillable, you would just hang in your TerminateProcess handler!

    And then people would ask for "a way to kill a process that is refusing to be killed with TerminateProcess," and we'd be back to where we started.

    Tomorrow: About those processes that don't go away even though you've killed them. They're really dead, but they won't go away.

  • The Old New Thing

    Why was nine the maximum number of monitors in Windows 98?

    • 44 Comments

    Windows 98 was the first version of Windows to support multiple monitors. And the limit was nine.

    Why nine?

    Because that allowed you to arrange your monitors like this. You have early seventies television to thank.

    [Raymond is currently on vacation; this message was pre-recorded.]

  • The Old New Thing

    Querying information from an Explorer window

    • 37 Comments

    Sometimes software development is inventing new stuff. But often, it's just putting together the stuff you already have. Today's puzzle is one of the latter type of problem.

    Given a window handle, you can you determine (1) whether it is an Explorer window, and if so (2) what folder it is viewing, and (3) what item is currently focused.

    This is not an inherently difficult task. You just have to put together lots of small pieces.

    Start with the ShellWindows object which represents all the open shell windows. You can enumerate through them all with the Item property. This is rather clumsy from C++ because the ShellWindows object was designed for use by a scripting language like JScript or Visual Basic.

     IShellWindows *psw;
     if (SUCCEEDED(CoCreateInstance(CLSID_ShellWindows, NULL, CLSCTX_ALL,
                                    IID_IShellWindows, (void**)&psw))) {
      VARIANT v;
      V_VT(&v) = VT_I4;
      IDispatch  *pdisp;
      BOOL fFound = FALSE;
      for (V_I4(&v) = 0; !fFound && psw->Item(v, &pdisp) == S_OK;
           V_I4(&v)++) {
        ...
        pdisp->Release();
      }
      psw->Release();
     }
    

    From each item, we can ask it for its window handle and see if it's the one we want.

       IWebBrowserApp *pwba;
       if (SUCCEEDED(pdisp->QueryInterface(IID_IWebBrowserApp, (void**)&pwba))) {
         HWND hwndWBA;
         if (SUCCEEDED(pwba->get_HWND((LONG_PTR*)&hwndWBA)) &&
           hwndWBA == hwndFind) {
           fFound = TRUE;
           ...
         }
         pwba->Release();
       }
    

    Okay, now that we have found the folder via its IWebBrowserApp, we need to get to the top shell browser. This is done by querying for the SID_STopLevelBrowser service and asking for the IShellBrowser interface.

           IServiceProvider *psp;
           if (SUCCEEDED(pwba->QueryInterface(IID_IServiceProvider, (void**)&psp))) {
             IShellBrowser *psb;
             if (SUCCEEDED(psp->QueryService(SID_STopLevelBrowser,
                                  IID_IShellBrowser, (void**)&psb))) {
               ...
               psb->Release();
             }
             psp->Release();
           }
    

    From the IShellBrowser, we can ask for the current shell view via the QueryActiveShellView method.

               IShellView *psv;
               if (SUCCEEDED(psb->QueryActiveShellView(&psv))) {
                 ...
                 psv->Release();
               }
    

    Of course, what we really want is the IFolderView interface, which is the automation object that contains all the real goodies.

                 IFolderView *pfv;
                 if (SUCCEEDED(psv->QueryInterface(IID_IFolderView,
                                                   (void**)&pfv))) {
                   ...
                   pfv->Release();
                 }
    

    Okay, now we're golden. What do you want to get from the view? How about the location of the IShellFolder being viewed. To do that, we need to use IPersistFolder2::GetCurFolder. The GetFolder method will give us access to the shell folder, from which we ask for IPersistFolder2. (Most of the time you want the IShellFolder interface, since that's where most of the cool stuff hangs out.)

                   IPersistFolder2 *ppf2;
                   if (SUCCEEDED(pfv->GetFolder(IID_IPersistFolder2,
                                                (void**)&ppf2))) {
                     LPITEMIDLIST pidlFolder;
                     if (SUCCEEDED(ppf2->GetCurFolder(&pidlFolder))) {
                       ...
                       CoTaskMemFree(pidlFolder);
                     }
                     ppf2->Release();
                   }
    

    Let's convert that pidl into a path, for display purposes.

                       if (!SHGetPathFromIDList(pidlFolder, g_szPath)) {
                         lstrcpyn(g_szPath, TEXT("<not a directory>"), MAX_PATH);
                       }
                       ...
    

    What else can we do with what we've got? Oh right, let's see what the currently-focused object is.

                       int iFocus;
                       if (SUCCEEDED(pfv->GetFocusedItem(&iFocus))) {
                         ...
                       }
    

    Let's display the name of the focused item. To do that we need the item's pidl and the IShellFolder. (See, I told you the IShellFolder is where the cool stuff is.) The item comes from the Item method (surprisingly enough).

                         LPITEMIDLIST pidlItem;
                         if (SUCCEEDED(pfv->Item(iFocus, &pidlItem))) {
                           ...
                           CoTaskMemFree(pidlItem);
                         }
    

    (If we had wanted a list of selected items we could have used the Items method, passing SVGIO_SELECTION.)

    After we get the item's pidl, we also need the IShellFolder:

                           IShellFolder *psf;
                           if (SUCCEEDED(ppf2->QueryInterface(IID_IShellFolder,
                                                              (void**)&psf))) {
                             ...
                             psf->Release();
                           }
    

    Then we put the two together to get the item's display name, with the help of the GetDisplayNameOf method.

                             STRRET str;
                             if (SUCCEEDED(psf->GetDisplayNameOf(pidlItem,
                                                       SHGDN_INFOLDER,
                                                       &str))) {
                               ...
                             }
    

    We can use the helper function StrRetToBuf to convert the kooky STRRET structure into a boring string buffer. (The history of the kooky STRRET structure will have to wait for another day.)

                               StrRetToBuf(&str, pidlItem, g_szItem, MAX_PATH);
    

    Okay, let's put this all together. It looks rather ugly because I put everything into one huge function instead of breaking them out into subfunctions. In "real life" I would have broken things up into little helper functions to make things more manageable.

    Start with the scratch program and add this new function:

    #include <shlobj.h>
    #include <exdisp.h>
    
    TCHAR g_szPath[MAX_PATH];
    TCHAR g_szItem[MAX_PATH];
    
    void CALLBACK RecalcText(HWND hwnd, UINT, UINT_PTR, DWORD)
    {
     HWND hwndFind = GetForegroundWindow();
     g_szPath[0] = TEXT('\0');
     g_szItem[0] = TEXT('\0');
    
     IShellWindows *psw;
     if (SUCCEEDED(CoCreateInstance(CLSID_ShellWindows, NULL, CLSCTX_ALL,
                                    IID_IShellWindows, (void**)&psw))) {
      VARIANT v;
      V_VT(&v) = VT_I4;
      IDispatch  *pdisp;
      BOOL fFound = FALSE;
      for (V_I4(&v) = 0; !fFound && psw->Item(v, &pdisp) == S_OK;
           V_I4(&v)++) {
       IWebBrowserApp *pwba;
       if (SUCCEEDED(pdisp->QueryInterface(IID_IWebBrowserApp, (void**)&pwba))) {
         HWND hwndWBA;
         if (SUCCEEDED(pwba->get_HWND((LONG_PTR*)&hwndWBA)) &&
           hwndWBA == hwndFind) {
           fFound = TRUE;
           IServiceProvider *psp;
           if (SUCCEEDED(pwba->QueryInterface(IID_IServiceProvider, (void**)&psp))) {
             IShellBrowser *psb;
             if (SUCCEEDED(psp->QueryService(SID_STopLevelBrowser,
                                  IID_IShellBrowser, (void**)&psb))) {
               IShellView *psv;
               if (SUCCEEDED(psb->QueryActiveShellView(&psv))) {
                 IFolderView *pfv;
                 if (SUCCEEDED(psv->QueryInterface(IID_IFolderView,
                                                   (void**)&pfv))) {
                   IPersistFolder2 *ppf2;
                   if (SUCCEEDED(pfv->GetFolder(IID_IPersistFolder2,
                                                (void**)&ppf2))) {
                     LPITEMIDLIST pidlFolder;
                     if (SUCCEEDED(ppf2->GetCurFolder(&pidlFolder))) {
                       if (!SHGetPathFromIDList(pidlFolder, g_szPath)) {
                         lstrcpyn(g_szPath, TEXT("<not a directory>"), MAX_PATH);
                       }
                       int iFocus;
                       if (SUCCEEDED(pfv->GetFocusedItem(&iFocus))) {
                         LPITEMIDLIST pidlItem;
                         if (SUCCEEDED(pfv->Item(iFocus, &pidlItem))) {
                           IShellFolder *psf;
                           if (SUCCEEDED(ppf2->QueryInterface(IID_IShellFolder,
                                                              (void**)&psf))) {
                             STRRET str;
                             if (SUCCEEDED(psf->GetDisplayNameOf(pidlItem,
                                                       SHGDN_INFOLDER,
                                                       &str))) {
                               StrRetToBuf(&str, pidlItem, g_szItem, MAX_PATH);
                             }
                             psf->Release();
                           }
                           CoTaskMemFree(pidlItem);
                         }
                       }
                       CoTaskMemFree(pidlFolder);
                     }
                     ppf2->Release();
                   }
                   pfv->Release();
                 }
                 psv->Release();
               }
               psb->Release();
             }
             psp->Release();
           }
         }
         pwba->Release();
       }
        pdisp->Release();
      }
      psw->Release();
     }
     InvalidateRect(hwnd, NULL, TRUE);
    }
    

    Now all we have to do is call this function periodically and print the results.

    BOOL
    OnCreate(HWND hwnd, LPCREATESTRUCT lpcs)
    {
        SetTimer(hwnd, 1, 1000, RecalcText);
        return TRUE;
    }
    
    void
    PaintContent(HWND hwnd, PAINTSTRUCT *pps)
    {
      TextOut(pps->hdc, 0, 0, g_szPath, lstrlen(g_szPath));
      TextOut(pps->hdc, 0, 20, g_szItem, lstrlen(g_szItem));
    }
    

    We're ready to roll. Run this program and set it to the side. Then launch an Explorer window and watch the program track the folder you're in and what item you have focused.

    Okay, so I hope I made my point: Often, the pieces you need are already there; you just have to figure out how to put them together. Notice that each of the pieces is in itself not very big. You just had to recognize that they could be put together in an interesting way.

    Exercise: Change this program so it takes the folder and switches it to details view.

    [Raymond is currently on vacation; this message was pre-recorded.]

  • The Old New Thing

    Wrapper templates to make writing callback functions slightly easier

    • 18 Comments

    I previously discussed why callback functions must be static if they are member functions.

    Writing the correct prototype for the callback function is usually somewhat clumsy. It's not hard. Just clumsy.

    class Sample {
     static DWORD CALLBACK StaticThreadProc(LPVOID *lpParameter)
     {
      Sample *self = reinterpret_cast<Sample*>(lpParameter);
      return self->RealThreadProc();
     }
     DWORD __stdcall RealThreadProc()
     {
       ... do stuff ...
     }
     void DoSomething()
     {
       ... CreateThread(NULL, 0, StaticThreadProc, this, 0, &dwTid); ...
     }
    };
    

    (If you read my previous article, you'd recognizing sticking a __stdcall in the declaration for RealThreadProc as a micro-optimization.)

    Every class that has a thread procedure needs this "trampoline" function StaticThreadProc that has the correct function signature, then massages it into something that is easier to work with (in this case, an object pointer and method call). Well, okay, you could do the work directly in the trampoline if you wanted to, but it's usually much more convenient to put the bulk of the work in a proper member function so you have access to all the "this" shorthand.

    If you do this a lot, you can write a template function to do the boring grunt work, freeing your brain to do "real thinking".

    template<class T, DWORD (__stdcall T::*F)()>
    DWORD CALLBACK ThreadProc(void *p)
    {
        return ((reinterpret_cast<T*>(p))->*F)();
    }
    

    This template function declares a templatized thread procedure. Notice that the calling convention for the ThreadProc template function is correct for a thread function, so this guy can be passed straight to CreateThread. Your class then would look like this:

    class Sample {
     DWORD __stdcall Background()
     {
       ... do stuff ...
     }
     void DoSomething()
     {
       ... CreateThread(NULL, 0, ThreadProc<Sample, &Sample::Background>, this, 0, &dwTid); ...
     }
    };
    

    This takes the trampoline function out of the class declaration. Instead, it is auto-generated by the compiler via the template.

    Okay, so maybe this isn't much of a typing-savings after all, considering the rather clumsy expression for the template invocation. I wonder if it can be made simpler.

    [Raymond is currently on vacation; this message was pre-recorded.]

    [2004 July 31 - fix obvious typos.]

  • The Old New Thing

    How to display a string without those ugly boxes

    • 17 Comments

    You've all seen those ugly boxes. When you try to display a string and the font you have doesn't support all of the characters in it, you get an ugly box for the characters that aren't available in the font.

    Start with our scratch program and add this to the PaintContent function:

    void
    PaintContent(HWND hwnd, PAINTSTRUCT *pps)
    {
        TextOutW(pps->hdc, 0, 0,
                L"ABC\x0410\x0411\x0412\x0E01\x0E02\x0E03", 9);
    }
    

    That string contains the first three letters from three different alphabets: "ABC" from the Roman alphabet; "АБВ" from the Cyrillic alphabet; and "กขฃ" from the Thai alphabet.

    If you run this program, you get a bunch of ugly boxes for the non-Roman characters because the SYSTEM font is very limited in its character set support.

    But how to pick the right font? What if the string contained Korean or Japanese characters? There is no single font that contains every character defined by Unicode. (Or at least, none that is commonly available.) What do you do?

    This is where font linking comes in.

    Font linking allows you to take a string and break it into pieces, where each piece can be displayed in an appropriate font.

    The IMLangFontLink2 interface provides the methods necessary to do this breaking. GetStrCodePages takes the string apart into chunks, such that all the characters in a chunk can be displayed by the same font, and MapFont creates the font.

    Okay, so let's write our font-link-enabled version of the TextOut function. We'll do this in stages, starting with the idea kernel.

    #include <mlang.h>
    
    HRESULT TextOutFL(HDC hdc, int x, int y, LPCWSTR psz, int cch)
    {
      ...
      while (cch > 0) {
        DWORD dwActualCodePages;
        long cchActual;
        pfl->GetStrCodePages(psz, cch, 0, &dwActualCodePages, &cchActual);
        HFONT hfLinked;
        pfl->MapFont(hdc, dwActualCodePages, 0, &hfLinked);
        HFONT hfOrig = SelectFont(hdc, hfLinked);
        TextOut(hdc, ?, ?, psz, cchActual);
        SelectFont(hdc, hfOrig);
        pfl->ReleaseFont(hfLinked);
        psz += cchActual;
        cch -= cchActual;
      }
      ...
    }
    

    After figuring out which code pages the default font supports, we walk through the string asking GetStrCodePages to give us the next chunk of characters. From that chunk, we create a matching font and draw the characters in that font at "the right place". Repeat until all the characters are done.

    The rest is refinement and paperwork.

    First of all, what is "the right place"? We want the next chunk to resume where the previous chunk left off. For that, we take advantage of the TA_UPDATECP text alignment style, which says that GDI should draw the text at the current position, and update the current position to the end of the drawn text (therefore, in position for the next chunk).

    Therefore, part of the paperwork is to set the DC's current position and set the text mode to TA_UPDATECP:

      SetTextAlign(hdc, GetTextAlign(hdc) | TA_UPDATECP);
      MoveToEx(hdc, x, y, NULL);
    

    Then we can just pass "0,0" as the coordinates to TextOut, because the coordinates passed to TextOut are ignored if the text alignment mode is TA_UPDATECP; it always draws at the current position.

    Of course, we can't just mess with the DC's settings like this. If the caller did not set TA_UPDATECP, then the caller is not expecting us to be meddling with the current position. Therefore, we have to save the original position and restore it (and the original text alignment mode) afterwards.

      POINT ptOrig;
      DWORD dwAlignOrig = GetTextAlign(hdc);
      SetTextAlign(hdc, dwAlignOrig | TA_UPDATECP);
      MoveToEx(hdc, x, y, &ptOrig);
      while (cch > 0) {
        ...
        TextOut(hdc, 0, 0, psz, cchActual);
        ...
      }
      // if caller did not want CP updated, then restore it
      // and restore the text alignment mode too
      if (!(dwAlignOrig & TA_UPDATECP)) {
        SetTextAlign(hdc, dwAlignOrig);
        MoveToEx(hdc, ptOrig.x, ptOrig.y, NULL);
      }
    

    Next is a refinement: We should take advantage of the second parameter to GetStrCodePages, which specifies the code pages we would prefer to use if a choice is avialable. Clearly we should prefer to use the code pages supported by the font we want to use, so that if the character can be displayed in that font directly, then we shouldn't map an alternate font.

      HFONT hfOrig = (HFONT)GetCurrentObject(hdc, OBJ_FONT);
      DWORD dwFontCodePages = 0;
      pfl->GetFontCodePages(hdc, hfOrig, &dwFontCodePages);
      ...
      while (cch > 0) {
        pfl->GetStrCodePages(psz, cch, dwFontCodePages, &dwActualCodePages, &cchActual);
        if (dwActualCodePages & dwFontCodePages) {
          // our font can handle it - draw directly using our font
          TextOut(hdc, 0, 0, psz, cchActual);
        } else {
          ... MapFont etc ...
        }
      }
      ...
    

    Of course, you probably wonder this magical pfl comes from. It comes from the Multilanguage Object in mlang.

      IMLangFontLink2 *pfl;
      CoCreateInstance(CLSID_CMultiLanguage, NULL,
                       CLSCTX_ALL, IID_IMLangFontLink2, (void**)&pfl);
      ...
      pfl->Release();
    

    And of course, all the errors we've been ignoring need to be taken care of. This does create a big of a problem if we run into an error after we have already made it through a few chunks. What should we do?

    I'm going to handle the error by drawing the string in the original font, ugly boxes and all. We can't erase the characters we already drew, and we can't just draw half of the string (for our caller won't know where to resume). So we just draw with the original font and hope for the best. At least it's no worse than it was before font linking.

    Put all of these refinements together and you get this final function:

    HRESULT TextOutFL(HDC hdc, int x, int y, LPCWSTR psz, int cch)
    {
      HRESULT hr;
      IMLangFontLink2 *pfl;
      if (SUCCEEDED(hr = CoCreateInstance(CLSID_CMultiLanguage, NULL,
                          CLSCTX_ALL, IID_IMLangFontLink2, (void**)&pfl))) {
        HFONT hfOrig = (HFONT)GetCurrentObject(hdc, OBJ_FONT);
        POINT ptOrig;
        DWORD dwAlignOrig = GetTextAlign(hdc);
        if (!(dwAlignOrig & TA_UPDATECP)) {
          SetTextAlign(hdc, dwAlignOrig | TA_UPDATECP);
        }
        MoveToEx(hdc, x, y, &ptOrig);
        DWORD dwFontCodePages = 0;
        hr = pfl->GetFontCodePages(hdc, hfOrig, &dwFontCodePages);
        if (SUCCEEDED(hr)) {
          while (cch > 0) {
            DWORD dwActualCodePages;
            long cchActual;
            hr = pfl->GetStrCodePages(psz, cch, dwFontCodePages, &dwActualCodePages, &cchActual);
            if (FAILED(hr)) {
              break;
            }
    
            if (dwActualCodePages & dwFontCodePages) {
              TextOut(hdc, 0, 0, psz, cchActual);
            } else {
              HFONT hfLinked;
              if (FAILED(hr = pfl->MapFont(hdc, dwActualCodePages, 0, &hfLinked))) {
                break;
              }
              SelectFont(hdc, hfLinked);
              TextOut(hdc, 0, 0, psz, cchActual);
              SelectFont(hdc, hfOrig);
              pfl->ReleaseFont(hfLinked);
            }
            psz += cchActual;
            cch -= cchActual;
          }
          if (FAILED(hr)) {
            //  We started outputting characters so we have to finish.
            //  Do the rest without font linking since we have no choice.
            TextOut(hdc, 0, 0, psz, cch);
            hr = S_FALSE;
          }
        }
    
        pfl->Release();
    
        if (!(dwAlignOrig & TA_UPDATECP)) {
          SetTextAlign(hdc, dwAlignOrig);
          MoveToEx(hdc, ptOrig.x, ptOrig.y, NULL);
        }
      }
    
      return hr;
    }
    

    Finally, we can wrap the entire operation inside a helper function that first tries with font linking and if that fails, then just draws the text the old-fashioned way.

    void TextOutTryFL(HDC hdc, int x, int y, LPCWSTR psz, int cch)
    {
      if (FAILED(TextOutFL(hdc, x, y, psz, cch)) {
        TextOut(hdc, x, y, psz, cch);
      }
    }
    

    Okay, now that we have our font-linked TextOut with fallback, we can go ahead and adjust our PaintContent function to use it.

    void
    PaintContent(HWND hwnd, PAINTSTRUCT *pps)
    {
      TextOutTryFL(pps->hdc, 0, 0,
                   TEXT("ABC\x0410\x0411\x0412\x0E01\x0E02\x0E03"), 9);
    }
    

    Observe that the string is now displayed with no black boxes.

    One refinement I did not do was to avoid creating the IMlangFontLink2 pointer each time we want to draw text. In a "real program" you would probably create the multilanguage object once per drawing context (per window, perhaps) and re-use it to avoid going through the whole object creation codepath each time you want to draw a string.

    [Raymond is currently on vacation; this message was pre-recorded.]

  • The Old New Thing

    Don't forget to #define UNICODE if you want Unicode

    • 9 Comments

    I answered this comment directly, but it deserves reiteration with wider visibility.

    If you don't #define UNICODE, you get ANSI by default.

    If you want to see characters beyond the boring 7-bit ASCII, make sure you are using a font that can display those characters.

    I am assuming a level of competence where issues like this go without saying, so that I can dig into the more advanced topics without having to explain all the basics, but I have to accept that people of all levels of programming experience read my stuff.

    But the second part raises an advanced question: How do you find a font that can display the characters you want? What if the characters can come from a source outside your control? We'll look at this tomorrow.

  • The Old New Thing

    Why "Under Construction" should be made illegal on the web

    • 29 Comments

    The site www.emr.fr has been "under construction" for over eight years. I noticed it in June 1996 and check back on it periodically. Still Under Construction.

    Another site that I noticed as "Under Construction" in June 1996 is www.leary.com. And as of this writing, it is still under construction.

Page 385 of 431 (4,310 items) «383384385386387»