March, 2012

  • The Old New Thing

    Memory allocation functions can give you more memory than you ask for, and you are welcome to use the freebies too, but watch out for the free lunch

    • 44 Comments

    Memory allocation functions like Heap­Alloc, Global­Alloc, Local­Alloc, and Co­Task­Mem­Alloc all have the property that they can return more memory than you requested. For example, if you ask for 13 bytes, you may very well get a pointer to 16 bytes. The corresponding Xxx­Size functions return the actual size of the memory block, and you are welcome to use all the memory in the block up to the actual size (even the bytes beyond the ones you requested). But watch out for the free lunch.

    Consider the following code:

    BYTE *GetSomeZeroBytes(SIZE_T size)
    {
     BYTE *bytes = (BYTE*)HeapAlloc(GetProcessHeap(), 0, size);
     if (bytes) ZeroMemory(bytes, size);
     return bytes;
    }
    

    So far so good. We allocate some memory, and then fill it with zeroes. That gives us our zero-initialized memory.

    Or does it?

    BYTE *bytes = GetSomeZeroBytes(13);
    SIZE_T actualSize = HeapSize(GetProcessHeap(), 0, bytes);
    for (SIZE_T i = 0; i < actualSize; i++) {
     assert(bytes[i] == 0); // assertion fires!?
    }
    

    When you ask the heap manager for 13 bytes, it's probably going to round that up to 16, and when you call Heap­Size, it may very well say, "Hey, I gave you three extra bytes. Don't need to thank me."

    The problem comes when you try to reallocate the memory:

    BYTE *ReallocAndZero(BYTE *bytes, SIZE_T newSize)
    {
     return (BYTE*)HeapReAlloc(bytes, GetProcessHeap(),
                               HEAP_ZERO_MEMORY, newSize);
    }
    

    Here, you said, "Dear heap manager, please make this memory block bigger, and zero out the new bytes. Kthxbai." And, assuming the heap manager was successful, you will indeed have a larger memory block, and the new bytes will have been zeroed out.

    But the memory manager won't zero out the three bonus bytes it gave you when you called Heap­Alloc, because those bytes aren't new. In fact, the heap manager assumes that you knew about those three extra bytes and were actively using them, and it would be rude to zero out those bytes behind your back.

    Those bytes you didn't know about since you didn't check.

    You might think the problem is that you mixed zero-allocation modes. You allocated the memory as "Go ahead and give me garbage, I'll zero it out myself", and then you reallocated it as "Can you zero it out for me?" The problem is that you and the heap manager disagree on how big it is. While you assume that the size of it is "the exact number of bytes I asked for", the heap manager assumes that the size of it is "the exact number of bytes I gave you." Those bytes in the middle fall through the cracks.

    Therefore, you might try to fix it by changing your function like this:

    BYTE *ReallocAndZero(BYTE *bytes, SIZE_T newSize)
    {
     SIZE_T oldSize = HeapSize(GetProcessHeap(), bytes);
     BYTE *newBytes = (BYTE*)HeapReAlloc(bytes, GetProcessHeap(),
                                         0, size);
     if (newBytes && newSize > oldSize) {
      ZeroMemory(newBytes + oldSize, newSize - oldSize);
     }
     return newBytes;
    }
    

    But this doesn't work, because of the reason we gave above: Your call to Heap­Size will return the actual block size, not the requested size. You will therefore forget to zero out those three bytes you didn't know about.

    The real problem is in the Get­Some­Zero­Bytes function. It decided to manually zero out the bytes it received, but it zeroed out only the bytes that were requested, not the actual bytes received.

    One solution is to make sure to zero out everything, so that if it is reallocated, the extra bytes gained in the reallocation will also be zero.

    BYTE *GetSomeZeroBytes(SIZE_T size)
    {
     BYTE *bytes = (BYTE*)HeapAlloc(GetProcessHeap(), 0, size);
     if (bytes) ZeroMemory(bytes,
                           HeapSize(GetProcessHeap(), bytes));
     return bytes;
    }
    

    Another solution is to take advantage of the memory manager's HEAP_ZERO_MEMORY flag, which tells the memory manager to zero out the entire block of memory when it is allocated:

    BYTE *GetSomeZeroBytes(SIZE_T size)
    {
     return (BYTE*)HeapAlloc(GetProcessHeap(),
                             HEAP_ZERO_MEMORY, size);
    }
    

    … and to use the same flag when reallocating:

    BYTE *ReallocAndZero(BYTE *bytes, SIZE_T newSize)
    {
     return (BYTE*)HeapReAlloc(bytes, GetProcessHeap(),
                               HEAP_ZERO_MEMORY, size);
    }
    

    Most of the heap functions let you specify that you want the heap manager to zero out the memory for you, and that includes the bonus bytes. For example, you can use GMEM_ZERO­INIT with the Global­Alloc family of functions, and LMEM_ZERO­INIT with the Local­Alloc family of functions. The annoying one is Co­Task­Mem­Alloc, since it does not provide a flag for zero-allocation. You have to zero out the memory yourself, and you have to do it right. (The inspiration for today's article was a bug caused by not zeroing out the memory correctly.)

    There are other implications of these bonus bytes. For example, if you use Create­Stream­On­HGlobal to create a stream on an existing HGLOBAL, the function uses Global­Size to determine the size of the stream it should create. And that value includes the bonus bytes, even though you may not have realized that they were there. Result: You create a stream of 13 bytes, but somebody who tries to read from it will get 16 bytes. You need to make sure that the code which reads from the stream won't get upset by those extra bytes. (For example, if you passed it to a function that concatenates streams, you just inserted three bytes of garbage between the streams.) You also need to be careful that those extra bytes don't leak any sensitive information if you, say, put the memory block on the clipboard for everyone to see.

    Bonus chatter: It appears that at some point, the kernel folks decided that these "bonus bytes" were more hassle than they were worth, and now they spend extra effort remembering not only the actual size of the memory block but also the requested size. When you ask, "How big is this memory block?" they lie and return the requested size rather than the actual size. In other words, the free bonus bytes are no longer exposed to applications by the kernel heap functions. Note, however, that this behavior is not contractual; future versions of Windows may start handing out free bonus bytes again. Note also that not all heap managers have done the extra work to remember the requested size, and they will continue to hand out bonus bytes. Therefore, you must continue to code defensively and assume that bonus bytes may exist (even if they usually don't). (And note that heap debugging tools may intentionally generate "bonus bytes" to help flush out bugs.)

    Double extra bonus chatter: Note that this gotcha is not specific to Windows.

    // resize a block of memory originally allocated by calloc
    // and zero out the new bytes
    void *crealloc(void *bytes, size_t new_size)
    {
     size_t old_size = malloc_size(bytes);
     void *new_bytes = realloc(bytes, new_size);
     if (new_bytes && new_size > old_size) {
      memset((char*)new_bytes + old_size, 0, new_size - old_size);
     }
     return new_bytes;
    }
    

    Virtually all heap libraries have bonus bytes.

  • The Old New Thing

    Converting to Unicode usually involves, you know, some sort of conversion

    • 36 Comments

    A colleague was investigating a problem with a third party application and found an unusual window class name: L"整瑳整瑳". He remarked, "This looks quite odd and could be some problem with the application."

    The string is nonsense in Chinese, but I immediately recognized what was up.

    Here's a hint: Rewrite the string as

    L"\x6574" L"\x7473" L"\x6574" L"\x7473"

    Still don't see it? How about looking at the byte sequence, remembering that Windows uses UTF-16LE.

    0x74 0x65 0x73 0x74 0x74 0x65 0x73 0x74

    Okay, maybe you don't have your ASCII table memorized.

    0x74 0x65 0x73 0x74 0x74 0x65 0x73 0x74
    t e s t t e s t

    That's right, the application took the ASCII string "testtest" and just treated it as a Unicode string without actually converting it to Unicode. When the compiler complained "Cannot convert char * to wchar_t *" they just stuck a cast to make the compiler shut up.

    // Code in italics is wrong
    WNDCLASSW wc;
    wc.lpszClassName = (LPWSTR)"testtest";
    

    They were lucky that the compiler happened to put two null bytes at the end of the "testtest" string.

    Bonus psychic powers: Actually, I have a theory as to how this happened that doesn't involve maliciousness. (This is generally a good mindset to maintain, since most of the time, when people cause a problem, it's not willful; it's accidental.) Consider a library with the following interface header file:

    // mylib.h
    
    #ifdef __cplusplus
    extern "C" {
    #endif
    
    BOOL RegisterWindowClass(LPCTSTR pszClassName);
    
    #ifdef __cplusplus
    }; // extern "C"
    #endif
    

    Somebody uses this header file like this:

    #include <mylib.h>
    
    BOOL Initialize()
    {
        return RegisterWindowClass(TEXT("testtest"));
    }
    

    So far so good.

    Meanwhile, the library implementation goes like this:

    #define UNICODE
    #define _UNICODE
    
    #include <mylib.h>
    
    LRESULT CALLBACK StandardWndProc(HWND, UINT, WPARAM, LPARAM);
    
    BOOL RegisterWindowClass(LPCTSTR pszClassName)
    {
        WNDCLASS wc = { 0, StandardWndProc, 0, 0, g_hInstance,
                        LoadIcon(IDI_APPLICATION),
                        LoadCursor(IDC_ARROW),
                        (HBRUSH)(COLOR_WINDOW + 1),
                        NULL, pszClassName);
        return RegisterClass(&wc);
    }
    

    The two files both compile successfully, and they even link together. Unfortunately, one of them was compiled with Unicode disabled, and the other was compiled with Unicode enabled. Since the header file uses LPCTSTR, the actual declaration of RegisterWindowClass changes depending on whether the code that includes the header file is compiled as Unicode or ANSI.

    Result: If one file is compiled as ANSI and the other is compiled as Unicode, then one will pass an ANSI string, which the other will receive and treat as Unicode.

    This is why functions in Windows which are dependent on whether the caller is compiled as ANSI or Unicode are really two functions, one with the A suffix (for ANSI) and another with the W suffix (for Wnicode?), and the generic name is really a macro that forwards to one or the other. It prevents TCHARs from sneaking past the compiler and ending up being interpreted differently by the two sides.

  • The Old New Thing

    Why does holding the Ctrl key when selecting New Task from Task Manager open a command prompt?

    • 16 Comments

    Commenter Adam S wonders why holding the Ctrl key when selecting New Task from Task Manager will open a command prompt.

    It's a rogue feature.

    Windows XP introduced visual styles, and one of the tricky parts of debugging visual styles is that if the visual style engine goes berzerk, you can't see anything! One of the problems that the visual styles folks encountered when developing their feature was that sometimes they would get into a state where the Run dialog would stop working. And without a Run dialog, you couldn't install or launch a debugger to begin investigating what went wrong.

    The solution: Add the rogue feature where holding the Ctrl key when selecting New Task from Task Manager opened a command prompt directly, without involving the Run dialog. From that command prompt, you can then install the debugger and start debugging. (This technique also took advantage of the fact that console windows were not themed in Windows XP. If the visual style system got all messed up, at least your console windows worked!)

    Over time, the bugs in the visual style system got worked out, and this rogue escape hatch was no longer needed, but for whatever reason, it never got removed.

  • The Old New Thing

    Amusing message on a whiteboard in the hallway

    • 28 Comments

    It is common to see whiteboards mounted in hallways. Sometimes they have official purposes; other times they are just placed for the convenience of hallway conversations or impromptu meetings.

    One of the hallways near my office has a very large whiteboard, completely blank, save for one note clearly written in the corner.

    DO NOT ERASE















  • The Old New Thing

    In 1993, Microsoft stole my colleague's car

    • 28 Comments

    I remember walking between buildings at Microsoft back in the 1990's and seeing a moss-covered, rusted-out jalopy in one of the parking spaces. It clearly hadn't moved in ages. The person I was with said, "Oh, yeah, Microsoft owns that car. They stole it from Bob." (Bob is my generic name for a Microsoft employee.)

    The Inaugural Day Storm of 1993 left felled trees and other wind damage in its wake on the Microsoft Redmond campus. One of my colleagues was out of town when the storm hit, and he returned to stories of fallen trees, wind damage, and howling winds.

    Bob also returned to find that his car had been stolen out of the parking lot outside his building. (It was at the time a common practice to use Microsoft's parking lots as personal vehicle storage.)

    Bob filed a stolen-car report with his insurance company and received his payment. As far as Bob was concerned, that was the end of that.

    But that's not the whole story.

    Right after the storm, the Facilities department set about cleaning up all the trees and branches and leaves that were strewn across the parking lots. They waited until after work hours so the parking lot would be empty, but that didn't work in one particular lot, because Bob's car was still there. To permit the cleanup to proceed, they towed the car to the far corner of the parking lot to get it out of the way.

    Thus, when Bob returned from his trip, he found that his car was gone. It was actually not that far away, but this particular parking lot was nestled in a densely-wooded area, with the lot divided into sub-lots, each separated by a small stand of trees, so if you didn't know where to look, you could easily have missed the car.

    This left the car sitting abandoned in a Microsoft parking lot. Technically, the car was owned by Bob's insurance company, and technically Microsoft stole it.

    Pre-emptive snarky comment: "Wouldn't be the first time Microsoft stole something."

  • The Old New Thing

    Why do program files go into the Program Files directory?

    • 74 Comments

    Some of Microsoft's software certification programs (such as the Windows Logo) require that applications set their default installation location to the Program Files directory. What is the reason for this?

    One technical reason is that this ensures that the directory receives an appropriate default security descriptor. But the Program Files directory was introduced in Windows 95, which didn't have security descriptors, so that can't be the entire reason.

    Rewind the clock to Windows 3.1. Microsoft didn't provide guidance on where applications should install by default. As a result, they went everywhere. Some installed into the root of your C: drive. Some installed to a C:\LitWare directory. Some installed into the Windows directory. It was total chaos.

    Program Files was introduced in an attempt to bring order to chaos. Think of it as painting lines in a parking garage.

    Bonus chatter: I recall an application compatibility investigation from the Windows 95 days. After you installed a particular program, it refused to run. This was clearly a serious problem, even more so when you realized that the program in question was a very popular commercial program. Eventually the source of the problem was identified: When you installed the program, you must accept the default installation location. If you tried to install the program somewhere else, it refused to run. The problem was not caused by Windows 95; you had the same problem if you installed the program on Windows 3.1 to a non-default directory.

  • The Old New Thing

    Why doesn't the Maximize button maximize across all monitors?

    • 42 Comments

    Cheong wonders why there isn't a way for the Maximize button to maximize a window across multiple monitors. (Troll asks a similar question: Why doesn't Windows support spanned mode for multiple monitors?)

    We tried it that way at first. And we quickly discovered why it was a bad idea.

    Having multiple monitors behave as a single giant display surface creates a bunch of problems, because there's this annoying line down the center that breaks up everything it touches. Everything on one side of the line is on one monitor and everything on the other side of the line is on the other monitor. (And add additional annoying lines if you have more than two monitors.) If that line intersects text, then you have letters chopped in half, and then you have to mentally glue the two pieces back together in order to read them.

    What's even worse, the broken-up-text problem shows up more often than you might expect, because a lot of programs like to do things like center their dialog boxes. The result is that nearly every dialog box that you see is perfectly bisected by the annoying line. The dialog boxes consistently appear in the worst possible location.

    (Accessibility note: The text in this table cell is identical to the text in the previous cell.)

    Having multiple monitors behave as a single giant display surface creates a bunch of problems, because there's this annoying line down the center that breaks up everything it touches. Everything on one side of the line is on one monitor and everything on the other side of the line is on the other monitor. (And add additional annoying lines if you have more than two monitors.) If that line intersects text, then you have letters chopped in half, and then you have to mentally glue the two pieces back together in order to read them.

    What's even worse, the broken-up-text problem shows up more often than you might expect, because a lot of programs like to do things like center their dialog boxes. The result is that nearly every dialog box that you see is perfectly bisected by the annoying line. The dialog boxes consistently appear in the worst possible location.

    It gets worse if the two monitors do not have exactly the same dot pitch, because it means that no matter how you position your monitors, you will never get all the lines of text to line up perfectly. If you carefully align the monitors so that, say, the baselines of the first lines of text match up, you'll find that the bottom lines don't. Not only do your eyes have to navigate the horizontal gap between the monitors, they also have to navigate the vertical gap created by the pixel density mismatch. Which is even harder because the vertical gap varies from line to line.

    It's like that trick where you put a pencil in a glass of water and observe it from the side. Now imagine a glass filled with pencils, and each pencil refracts differently. And now imagine each pencil is a line of text you're trying to read.

    (Accessibility note: The text in this table cell is identical to the text in the previous cell.)

    It gets worse if the two monitors do not have exactly the same dot pitch, because it means that no matter how you position your monitors, you will never get all the lines of text to line up perfectly. If you carefully align the monitors so that, say, the baselines of the first lines of text match up, you'll find that the bottom lines don't. Not only do your eyes have to navigate the horizontal gap between the monitors, they also have to navigate the vertical gap created by the pixel density mismatch. Which is even harder because the vertical gap varies from line to line.

    It's like that trick where you put a pencil in a glass of water and observe it from the side. Now imagine a glass filled with pencils, and each pencil refracts differently. And now imagine each pencil is a line of text you're trying to read.

    Wait, I'm not finished yet. Things get still worse if your two monitors are not the same size. In that case, the virtual screen is larger than the visible region. For example, my monitor arrangement has a landscape monitor on the left and a portrait monitor on the right, with the bottoms of the monitors aligned.

       Virtual monitor   
       
    Monitor 2
       
    Virtual
    Monitor
       
    Monitor 1

    If a window were maximized across the virtual screen, the contents of the upper left corner would not be visible at all!

    Now, there may be specific cases where it would be meaningful to maximize a window across the virtual monitor, and if a program wants to do that, it can certainly implement that on its own. But even for a picture-viewing application, maximizing across the virtual monitor may not be a great idea: Pictures often have people in the center, and then you end up with somebody's head cut in half.

  • The Old New Thing

    How do I make it so that users can copy static text on a dialog box to the clipboard easily?

    • 20 Comments

    Given that you have a Win32 dialog box with static text in an LTEXT control, how do you make it so that users can easily copy that text to the clipboard?

    The traditional solution is to create a borderless read-only edit control (which draws as static text by default). Add it to the tab order by setting the WS_TABSTOP style, and maybe even give it a keyboard accelerator for accessibility.

    Starting in Windows Vista, version 6 of the common controls provides an alternative. (A less accessible alternative, mind you.) Static text controls automatically copy their contents to the clipboard when you double-click them if you set the SS_NOTIFY style.

    Let's try it:

    #include <windows.h>
    #include <windowsx.h>
    #include <commctrl.h>
    
    #pragma comment(linker, \
     "\"/manifestdependency:type='Win32' "\
     "name='Microsoft.Windows.Common-Controls' "\
     "version='6.0.0.0' "\
     "processorArchitecture='*' "\
     "publicKeyToken='6595b64144ccf1df' "\
     "language='*'\"")
    
    INT_PTR CALLBACK DlgProc(
        HWND hdlg, UINT uMsg, WPARAM wParam, LPARAM lParam)
    {
     switch (uMsg) {
     case WM_INITDIALOG:
      return TRUE;
     case WM_COMMAND:
      switch (GET_WM_COMMAND_ID(wParam, lParam)) {
      case IDCANCEL:
       EndDialog(hdlg, 0);
       break;
      }
     }
     return FALSE;
    }
    
    int WINAPI WinMain(HINSTANCE hinst, HINSTANCE hinstPrev,
                       LPSTR lpCmdLine, int nShowCmd)
    {
     return DialogBox(hinst, MAKEINTRESOURCE(1), NULL, DlgProc);
    }
    
    // resource file
    #include <windows.h>
    
    1 DIALOG 50, 50, 100, 50
    STYLE DS_SHELLFONT | WS_SYSMENU
    CAPTION "Sample"
    FONT 8, "MS Shell Dlg"
    BEGIN
     LTEXT "Sample text 1",100,10,10,80,10,SS_NOTIFY
     LTEXT "Sample text 2",101,10,20,80,10,SS_NOTIFY
     LTEXT "Sample text 3",102,10,30,80,10
    END
    

    Run this program and double-click on the text controls, and observe that the text gets copied to the clipboard, or at least it does for the first two, since I set the SS_NOTIFY style on them.

    Now, when the double-click to copy feature was added to the static control, there was no way to suppress it. The STN_DBLCLK notification is documented as ignoring its return code, so it would be a compatibility problem if suddenly it started studying its return code so that the parent could respond "No, I handled the click, don't do your default action." Instead, if you want to disable the double-click to copy feature on a SS_NOTIFY static control, you have to subclass the static control and eat the clicks yourself.

    LRESULT CALLBACK SuppressCopyOnClick(
        HWND hwnd, UINT uMsg, WPARAM wParam,
        LPARAM lParam, UINT_PTR uIdSubclass, DWORD_PTR dwRefData)
    {
     switch (uMsg) {
     case WM_LBUTTONDBLCLK: return 0; // eat the double-click
     case WM_NCDESTROY:
      RemoveWindowSubclass(hwnd, SuppressCopyOnClick, uIdSubclass);
      break;
     }
     return DefSubclassProc(hwnd, uMsg, wParam, lParam);
    }
    
    INT_PTR CALLBACK DlgProc(
        HWND hdlg, UINT uMsg, WPARAM wParam, LPARAM lParam)
    {
     switch (uMsg) {
     case WM_INITDIALOG:
      SetWindowSubclass(GetDlgItem(hdlg, 101),
                        SuppressCopyOnClick, 0, 0);
      return TRUE;
     ...
    

    "Why would you add a style that enabled a feature, and then disable the feature?"

    Maybe you want some other aspects of the feature but not the copy-on-double-click behavior. Maybe somebody else is adding the SS_NOTIFY style behind your back. For example, a UI framework might add it automatically to all static controls.

    And actually, in that UI framework case, you probably want the STN_DBLCLK notification to be fired when a double-click occurs, because you added an OnDoubleClick handler to your class. You just don't want the copy-to-clipboard behavior. We can fix that by firing the notification in our subclass procedure.

     case WM_LBUTTONDBLCLK:
      if (GetWindowStyle(hwnd) & SS_NOTIFY) {
       FORWARD_WM_COMMAND(GetParent(hwnd), GetDlgCtrlID(hwnd), hwnd,
                          STN_DBLCLK, SendMessage);
      }
      return 0; // message handled
    

    To illustrate this change, we'll make our dialog box beep when it gets a double-click notification. In real life, of course, you would do whatever you want to happen on the "double click on a static control" event. Actually, in real life, the code that responds to the STN_DBLCLK lives inside your framework, and it turns around and raises an OnDoubleClick event, but for simplicity, we'll just code it inline.

     case WM_COMMAND:
         switch (GET_WM_COMMAND_ID(wParam, lParam)) {
         case IDCANCEL:
            EndDialog(hdlg, 0);
            break;
         case 100:
         case 101:
         case 102:
          switch (GET_WM_COMMAND_CMD(wParam, lParam)) {
          // Obviously we would do something more interesting here
          case STN_DBLCLK: MessageBeep(MB_OK); break;
          }
         }
    

    Each of the static controls on the dialog behaves differently. The first one is SS_NOTIFY with no subclassing, so double-clicking copies the text to the clipboard and also beeps. The second one is SS_NOTIFY with subclassing to disable the copy-to-clipboard, so double-clicking merely beeps. And the third one doesn't have the SS_NOTIFY style at all, so it neither copies the next nor responds to double-click.

  • The Old New Thing

    How do I get mouse messages faster than WM_MOUSEMOVE?

    • 11 Comments
    We saw some time ago that the rate at which you receive WM_MOUSE­MOVE messages is entirely up to how fast your program calls Get­Message. But what if your program is calling Get­Message as fast as it can, and it's still not fast enough?

    You can use the Get­Mouse­Move­Points­Ex function to ask the window manager, "Hey, can you tell me about the mouse messages I missed?" I can think of two cases where you might want to do this:

    • You are a program like Paint, where the user is drawing with the mouse and you want to capture every nuance of the mouse motion.
    • You are a program that supports something like mouse gestures, so you want the full mouse curve information so you can do your gesture recognition on it.

    Here's a program that I wrote for a relative of mine who is a radiologist. One part of his job consists of sitting in a dark room studying medical images. He has to use his years of medical training to identify the tumor (if there is one), and then determine what percentage of the organ is afflicted. To use this program, run it and position the circle so that it matches the location and size of the organ under study. Once you have the circle positioned properly, use the mouse to draw an outline of the tumor. When you let go of the mouse, the title bar will tell you the size of the tumor relative to the entire organ.

    (Oh great, now I'm telling people to practice medicine without a license.)

    First, we'll do a version of the program that just calls Get­Message as fast as it can. Start with the new scratch program and make the following changes:

    class RootWindow : public Window
    {
    public:
     virtual LPCTSTR ClassName() { return TEXT("Scratch"); }
     static RootWindow *Create();
    protected:
     LRESULT HandleMessage(UINT uMsg, WPARAM wParam, LPARAM lParam);
     void PaintContent(PAINTSTRUCT *pps);
     BOOL WinRegisterClass(WNDCLASS *pwc);
    
    private:
     RootWindow();
     ~RootWindow();
     void OnCreate();
     void UpdateTitle();
     void OnSizeChanged(int cx, int cy);
     void AlwaysAddPoint(POINT pt);
     void AddPoint(POINT pt);
     void OnMouseMove(LPARAM lParam);
     void OnButtonDown(LPARAM lParam);
     void OnButtonUp(LPARAM lParam);
    
     // arbitrary limit (this is just a demo!)
     static const int cptMax = 1000;
    private:
     POINT  m_ptCenter;
     int    m_radius;
     BOOL   m_fDrawing;
     HPEN   m_hpenInside;
     HPEN   m_hpenDot;
     POINT  m_ptLast;
     int    m_cpt;
     POINT  m_rgpt[cptMax];
    };
    
    RootWindow::RootWindow()
     : m_fDrawing(FALSE)
     , m_hpenInside(CreatePen(PS_INSIDEFRAME, 3,
                                      GetSysColor(COLOR_WINDOWTEXT)))
     , m_hpenDot(CreatePen(PS_DOT, 1, GetSysColor(COLOR_WINDOWTEXT)))
    {
    }
    
    RootWindow::~RootWindow()
    {
     if (m_hpenInside) DeleteObject(m_hpenInside);
     if (m_hpenDot) DeleteObject(m_hpenDot);
    }
    
    BOOL RootWindow::WinRegisterClass(WNDCLASS *pwc)
    {
     pwc->style |= CS_VREDRAW | CS_HREDRAW;
     return __super::WinRegisterClass(pwc);
    }
    
    void RootWindow::OnCreate()
    {
     SetLayeredWindowAttributes(m_hwnd, 0, 0xA0, LWA_ALPHA);
    }
    
    void RootWindow::UpdateTitle()
    {
     TCHAR szBuf[256];
    
     // Compute the area of the circle using a surprisingly good
     // rational approximation to pi.
     int circleArea = m_radius * m_radius * 355 / 113;
    
     // Compute the area of the region, if we have one
     if (m_cpt > 0 && !m_fDrawing) {
      int polyArea = 0;
      for (int i = 1; i < m_cpt; i++) {
       polyArea += m_rgpt[i-1].x * m_rgpt[i  ].y -
                   m_rgpt[i  ].x * m_rgpt[i-1].y;
      }
      if (polyArea < 0) polyArea = -polyArea; // ignore orientation
      polyArea /= 2;
      wnsprintf(szBuf, 256,
               TEXT("circle area is %d, poly area is %d = %d%%"),
               circleArea, polyArea,
               MulDiv(polyArea, 100, circleArea));
     } else {
      wnsprintf(szBuf, 256, TEXT("circle area is %d"), circleArea);
     }
     SetWindowText(m_hwnd, szBuf);
    }
    
    void RootWindow::OnSizeChanged(int cx, int cy)
    {
     m_ptCenter.x = cx / 2;
     m_ptCenter.y = cy / 2;
     m_radius = min(m_ptCenter.x, m_ptCenter.y) - 6;
     if (m_radius < 0) m_radius = 0;
     UpdateTitle();
    }
    
    void RootWindow::PaintContent(PAINTSTRUCT *pps)
    {
     HBRUSH hbrPrev = SelectBrush(pps->hdc,
                                        GetStockBrush(HOLLOW_BRUSH));
     HPEN hpenPrev = SelectPen(pps->hdc, m_hpenInside);
     Ellipse(pps->hdc, m_ptCenter.x - m_radius,
                       m_ptCenter.y - m_radius,
                       m_ptCenter.x + m_radius,
                       m_ptCenter.y + m_radius);
     SelectPen(pps->hdc, m_hpenDot);
     Polyline(pps->hdc, m_rgpt, m_cpt);
     SelectPen(pps->hdc, hpenPrev);
     SelectBrush(pps->hdc, hbrPrev);
    }
    
    void RootWindow::AddPoint(POINT pt)
    {
     // Ignore duplicates
     if (pt.x == m_ptLast.x && pt.y == m_ptLast.y) return;
    
     // Stop if no room for more
     if (m_cpt >= cptMax) return;
    
     AlwaysAddPoint(pt);
    }
    
    void RootWindow::AlwaysAddPoint(POINT pt)
    {
     // Overwrite the last point if we can't add a new one
     if (m_cpt >= cptMax) m_cpt = cptMax - 1;
    
     // Invalidate the rectangle connecting this point
     // to the last point
     RECT rc = { pt.x, pt.y, pt.x+1, pt.y+1 };
     if (m_cpt > 0) {
      RECT rcLast = { m_ptLast.x,   m_ptLast.y,
                      m_ptLast.x+1, m_ptLast.y+1 };
      UnionRect(&rc, &rc, &rcLast);
     }
     InvalidateRect(m_hwnd, &rc, FALSE);
    
     // Add the point
     m_rgpt[m_cpt++] = pt;
     m_ptLast = pt;
    }
    
    void RootWindow::OnMouseMove(LPARAM lParam)
    {
     if (m_fDrawing) {
      POINT pt = { GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam) };
      AddPoint(pt);
     }
    }
    
    void RootWindow::OnButtonDown(LPARAM lParam)
    {
     // Erase any previous polygon
     InvalidateRect(m_hwnd, NULL, TRUE);
    
     m_cpt = 0;
     POINT pt = { GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam) };
     AlwaysAddPoint(pt);
     m_fDrawing = TRUE;
    }
    
    void RootWindow::OnButtonUp(LPARAM lParam)
    {
     if (!m_fDrawing) return;
    
     OnMouseMove(lParam);
    
     // Close the loop, eating the last point if necessary
     AlwaysAddPoint(m_rgpt[0]);
     m_fDrawing = FALSE;
     UpdateTitle();
    }
    
    LRESULT RootWindow::HandleMessage(
                              UINT uMsg, WPARAM wParam, LPARAM lParam)
    {
     switch (uMsg) {
      case WM_CREATE:
       OnCreate();  
       break;
    
      case WM_NCDESTROY:
       // Death of the root window ends the thread
       PostQuitMessage(0);
       break;
    
      case WM_SIZE:
       if (wParam == SIZE_MAXIMIZED || wParam == SIZE_RESTORED) {
        OnSizeChanged(GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam));
       }
       break;
    
      case WM_MOUSEMOVE:
       OnMouseMove(lParam);
       break;
    
      case WM_LBUTTONDOWN:
       OnButtonDown(lParam);
       break;
    
      case WM_LBUTTONUP:
       OnButtonUp(lParam);
       break;
     }
    
     return __super::HandleMessage(uMsg, wParam, lParam);
    }
    
    RootWindow *RootWindow::Create()
    {
     RootWindow *self = new(std::nothrow) RootWindow();
     if (self && self->WinCreateWindow(WS_EX_LAYERED,
           TEXT("Scratch"), WS_OVERLAPPEDWINDOW,
           CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT,
           NULL, NULL)) {
          return self;
      }
     delete self;
     return NULL;
    }
    

    This program records every mouse movement while the button is down and replays them in the form of a dotted polygon. When the mouse button goes up, it calculates the area both in terms of pixels and in terms of a percentage of the circle.

    This program works well. My relative's hand moves slowly enough (after all, it has to trace a tumor) that the Get­Message loop is plenty fast enough to keep up. But just for the sake of illustration, suppose it isn't. To make the effect easier to see, let's add some artificial delays:

    void RootWindow::OnMouseMove(LPARAM lParam)
    {
     if (m_fDrawing) {
      POINT pt = { GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam) };
      AddPoint(pt);
      UpdateWindow(m_hwnd);
      Sleep(100);
     }
    }
    

    Now, if you try to draw with the mouse, you see all sorts of jagged edges because our program can't keep up. (The Update­Window is just to make the most recent line visible while we are sleeping.)

    Enter Get­Mouse­Move­Points­Ex. This gives you all the mouse activity that led up to a specific point in time, allowing you to fill in the data that you missed because you weren't pumping messages fast enough. Let's teach our program how to take advantage of this:

    class RootWindow : public Window
    {
    ...
     void AlwaysAddPoint(POINT pt);
     void AddMissingPoints(POINT pt, DWORD tm);
     void AddPoint(POINT pt);
    ...
     POINT m_ptLast;
     DWORD m_tmLast;
     int   m_cpt;
    };
    
    void RootWindow::AddMissingPoints(POINT pt, DWORD tm)
    {
     // See discussion for why this code is wrong
     ClientToScreen(m_hwnd, &pt);
     MOUSEMOVEPOINT mmpt = { pt.x, pt.y, tm };
     MOUSEMOVEPOINT rgmmpt[64];
     int cmmpt = GetMouseMovePointsEx(sizeof(mmpt), &mmpt,
                                rgmmpt, 64, GMMP_USE_DISPLAY_POINTS);
    
     POINT ptLastScreen = m_ptLast;
     ClientToScreen(m_hwnd, &ptLastScreen);
     int i;
     for (i = 0; i < cmmpt; i++) {
      if (rgmmpt[i].time < m_tmLast) break;
      if (rgmmpt[i].time == m_tmLast &&
          rgmmpt[i].x == ptLastScreen.x &&
          rgmmpt[i].y == ptLastScreen.y) break;
     }
     while (--i >= 0) {
       POINT ptClient = { rgmmpt[i].x, rgmmpt[i].y };
       ScreenToClient(m_hwnd, &ptClient);
       AddPoint(ptClient);
     }
    }
    
    void RootWindow::AlwaysAddPoint(POINT pt)
    {
    ...
     // Add the point
     m_rgpt[m_cpt++] = pt;
     m_ptLast = pt;
     m_tmLast = GetMessageTime();
    }
    
    void RootWindow::OnMouseMove(LPARAM lParam)
    {
     if (m_fDrawing) {
      POINT pt = { GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam) };
      AddMissingPoints(pt, GetMessageTime());
      AddPoint(pt);
      UpdateWindow(m_hwnd);
      Sleep(100); // artificial delay to simulate unresponsive app
     }
    }
    

    Before updating the the current mouse position, we check to see if there were other mouse motions that occurred while we weren't paying attention. We tell Get­Mouse­Move­Points­Ex, "Hey, here is a mouse message that I have right now. Please tell me about the stuff that I missed." It fills in an array with recent mouse history, most recent events first. We go through that array looking for the previous point, and give up either when we find it, or when the timestamps on the events we received take us too far backward in time. Once we find all the points that we missed, we play them into the Add­Point function.

    Notes to people who like to copy code without understanding it: The code fragment above works only for single-monitor systems. To work correctly on multiple-monitor systems, you need to include the crazy coordinate-shifting code provided in the documentation for Get­Mouse­Move­Points­Ex. (I omitted that code because it would just be distracting.) Also, the management of m_tmLast is now rather confusing, but I did it this way to minimize the amount of change to the original program. It would probably be better to have added a DWORD tm parameter to Add­Point instead of trying to infer it from the current message time.

    The Get­Mouse­Move­Points­Ex technique is also handy if you need to refer back to the historical record. For example, if the user dragged the mouse out of your window and you want to calculate the velocity with which the mouse exited, you can use Get­Mouse­Move­Points­Ex to get the most recent mouse activity and calculate the velocity. This saves you from having to record all the mouse activity yourself on the off chance that the mouse might leave the window.

  • The Old New Thing

    Why does a maximized window have the wrong window rectangle?

    • 22 Comments

    Commenter configurator wonders why the maximum size for a form is the screen size plus (12,12). Somebody else wonders why it's the screen size plus (16,16).

    Let's start by rewinding the clock to Windows 3.0.

    When you maximize a window, the default position of the window is such that all the resizing borders hang "off the edges of the screen". The client area extends from the left edge of the screen to the right edge of the screen, and also goes all the way to the bottom. It doesn't go all the way to the top, since it needs to leave room for the caption, but the resizing border that sits above the caption area is not visible either.

    The reason for this should be obvious: Since the window is maximized, there's no point wasting screen real estate on the resizing borders. You want the client area to be as large as possible; that's why you maximized the window.

    The result of this window positioning is that the window rectangle itself is slightly larger than the screen. The parts that "hang off the edges of the screen" are not visible because, well, they're off the screen. (Of course, if your window had a maximum size smaller than the screen, then those borders stay visible.) The size of these borders might not be 12 pixels, mind you.

    This is how things stood for a long time. Even the introduction of multiple monitors in Windows 98 didn't affect the way maximized windows were positioned. Multiple monitors, however, altered one of the assumptions that lay behind the positioning of maximized windows, namely the assumption that edges beyond the screen were not visible. I mean, they weren't visible on the screen that held the maximized window, but they were visible on the adjacent monitor. As a result, when you maximized a window, its borders appeared as a sliver on the adjacent monitor.

    Why didn't Windows get rid of the sliver when multiple monitors were introduced? You probably know the reason already: Because there are applications which relied on the sliver. For example, an application might detect that it is maximized by checking whether its edges hang off the screen, rather than checking the WS_MAXIMIZED style. Why would they do it that way? Probably because they fumbled around until they found something that seemed to work, sort of like the people who detect whether the mouse buttons are swapped by calling SwapMouseButton instead of GetSystemMetrics(SM_SWAPBUTTON). (Or maybe because they wanted to treat as "logically maximized" windows which the user had manually resized to be larger than the screen.)

    The introduction of the Desktop Window Manager in Windows Vista gave the window manager team a chance to solve the problem without impairing compatibility: The Desktop Window Manager controls how windows appear on the screen, which can be different from the actual window properties. For example, the Desktop Window Manager typically animates a window into position when it becomes visible, yet if an application calls GetWindowRect, it will just see the window at its normal position with no animation.

    This decoupling of logical and physical characteristics permits all sorts of visual tricks. The visual trick relevant here is the removal of the overhang borders from a maximized window. The borders are still there: If you call GetWindowRect, you will get the same coordinates you always did. But they don't appear on the screen. The sliver is gone.

Page 1 of 3 (24 items) 123