• The Old New Thing

    Logging the contents of every message box dialog via automation

    • 9 Comments

    Today's Little Program logs the contents of every message box dialog, or anything that vaguely resembles a message box dialog. (Since there's no way for sure to know whether a dialog box is a message box or not.)

    using System.Windows.Automation;
    
    class Program
    {
     [System.STAThread]
     public static void Main(string[] args)
     {
      Automation.AddAutomationEventHandler(
       WindowPattern.WindowOpenedEvent,
       AutomationElement.RootElement,
       TreeScope.Descendants,
       (sender, e) => {
        var element = sender as AutomationElement;
        if (element.GetCurrentPropertyValue(
         AutomationElement.ClassNameProperty) as string != "#32770") {
         return;
        }
    
        var text = element.FindFirst(TreeScope.Children,
         new PropertyCondition(AutomationElement.AutomationIdProperty, "65535"));
        if (text != null) {
         System.Console.WriteLine(text.Current.Name);
        }
       });
      System.Console.ReadLine();
      Automation.RemoveAllEventHandlers();
     }
    }
    

    This is the same pattern as the program we wrote last week, but with different guts when the window opens.

    This time, we see if the class name is #32770, which UI Spy tells us is the class name for dialog boxes. (That this is the numerical value of WC_DIALOG is no coincidence.)

    If we have a dialog, then we look for a child element whose automation ID is 65535, which UI Spy tells us is the automation ID for the text inside a message box dialog. (That the traditional control ID for static controls is -1 and 65535 is the the numerical value of (WORD)-1, is no coincidence.)

    If so, then we print the text.

    If we were cleverer, we could also confirm that the only buttons are OK, Cancel, and so on. Otherwise, we can get faked out by other dialog boxes that contain static text.

  • The Old New Thing

    The changing name of the Microsoft event held in conjunction with Martin Luther King, Jr. Day

    • 9 Comments

    Today is Martin Luther King, Jr. Day, a federal holiday in the United States honoring the civil rights leader and formally serving as a day to reflect on the principles of racial equality and nonviolent social change and more generally to honor Dr. King's legacy through service.

    At Microsoft, the day has been recognized with an event whose name is, um, well, the name keeps changing. Here are the names from recent years:

    • 2006: Martin Luther King, Jr. Day Event
    • 2007: Martin Luther King, Jr. Celebration
    • 2008: Dr. Martin Luther King, Jr. Day Event
    • 2009: MLK Day Event
    • 2010: Celebration to Honor Dr. Martin Luther King, Jr.
    • 2011: Martin Luther King Day of Celebration
    • 2012: Martin Luther King, Jr. Day of Celebration
    • 2013: Dr. Martin Luther King, Jr. Day of Celebration Event

    I like to think this is done intentionally just to keep people on their toes.

    Ironically, although the event "celebrates diversity and inclusion," the email announcing the event inadvertently excludes people with visual impairments because it consists of a giant JPG with no ALT text.

    (No dream report from me today. That would be disrespectful to the man himself.)

  • The Old New Thing

    Using accessibility to monitor windows as they come and go

    • 9 Comments
    Today's Little Program monitors windows as they come and go. When people contemplate doing this, they come up with ideas like installing a WH_CBT hook or a WH_SHELL hook, but one of the major problems with those types of hooks is that they are injected hooks. Injection is bad for a number of reasons.

    • It forces the hook to be in a DLL so it can be injected.
    • Hook activities need to be marshaled back to the main program.
    • Your DLL will capture events only in processes of the same bitness, because you cannot load a 32-bit DLL into a 64-bit process or vice versa.
    • You can inject into an elevated process only if your process is also elevated. If your process is non-elevated, then you will not capture events for windows belonging to elevated processes.

    This is where accessibility comes in handy, because accessibility lets you specify whether you want your hook to be an injected or non-injected one. And if you're non-injected, then the programming model is much simpler because everything happens in your process (indeed, on a single thread).

    Take the scratch program and make the following changes:

    #include <strsafe.h>
    
    BOOL
    OnCreate(HWND hwnd, LPCREATESTRUCT lpcs)
    {
     g_hwndChild = CreateWindow(TEXT("listbox"), NULL,
         LBS_HASSTRINGS | WS_CHILD | WS_VISIBLE | WS_VSCROLL,
         0, 0, 0, 0, hwnd, NULL, g_hinst, 0);
     if (!g_hwndChild) return FALSE;
     return TRUE;
    }
    
    void CALLBACK WinEventProc(
        HWINEVENTHOOK hWinEventHook,
        DWORD event,
        HWND hwnd,
        LONG idObject,
        LONG idChild,
        DWORD dwEventThread,
        DWORD dwmsEventTime
    )
    {
     if (hwnd &&
         idObject == OBJID_WINDOW &&
         idChild == CHILDID_SELF)
     {
      PCTSTR pszAction = NULL;
      TCHAR szBuf[80];
      switch (event) {
      case EVENT_OBJECT_CREATE:
       pszAction = TEXT("created");
       break;
      case EVENT_OBJECT_DESTROY:
       pszAction = TEXT("destroyed");
       break;
      }
      if (pszAction) {
       TCHAR szClass[80];
       TCHAR szName[80];
       szClass[0] = TEXT('\0');
       szName[0] = TEXT('\0');
       if (IsWindow(hwnd)) {
        GetClassName(hwnd, szClass, ARRAYSIZE(szClass));
        GetWindowText(hwnd, szName, ARRAYSIZE(szName));
       }
       TCHAR szBuf[80];
       StringCchPrintf(szBuf, ARRAYSIZE(szBuf),
                       TEXT("%p %s \"%s\" (%s)"), hwnd, pszAction,
                       szName, szClass);
       ListBox_AddString(g_hwndChild, szBuf);
      }
     }
    }
    
    int WINAPI WinMain(HINSTANCE hinst, HINSTANCE hinstPrev,
                       LPSTR lpCmdLine, int nShowCmd)
    {
     ...
      ShowWindow(hwnd, nShowCmd);
    
     HWINEVENTHOOK hWinEventHook = SetWinEventHook(
         EVENT_OBJECT_CREATE, EVENT_OBJECT_DESTROY,
         NULL, WinEventProc, 0, 0,
         WINEVENT_OUTOFCONTEXT | WINEVENT_SKIPOWNPROCESS);
    
      while (GetMessage(&msg, NULL, 0, 0)) {
       TranslateMessage(&msg);
       DispatchMessage(&msg);
      }
    
      if (hWinEventHook) UnhookWinEvent(hWinEventHook);
    ...
    }
    

    This is a generalization of our earlier program which waits for a specific window to be destroyed, except that we now are watching all windows for creation and destruction.

    When you run this program, you see that there is a lot of window activity, but maybe you are interested only in windows when they are shown and hidden. No problem, that's a small change:

      switch (event) {
      case EVENT_OBJECT_SHOW:
       pszAction = TEXT("shown");
       break;
      case EVENT_OBJECT_HIDE:
       pszAction = TEXT("hidden");
       break;
      }
    ...
    
     HWINEVENTHOOK hWinEventHook = SetWinEventHook(
         EVENT_OBJECT_SHOW, EVENT_OBJECT_HIDE,
         NULL, WinEventProc, 0, 0,
         WINEVENT_OUTOFCONTEXT | WINEVENT_SKIPOWNPROCESS);
    

    Notice that these notifications are received for windows from both 32-bit and 64-bit processes, and that they are received even for windows belonging to elevated processes. You can't do that with an injected hook.

  • The Old New Thing

    How can I write to a file only as long as nobody's looking?

    • 9 Comments

    A customer wanted to know how to detect that the user has opened Notepad to view a particular file. They had come up with method based on polling and sniffing the Notepad title bar, but they found that it consumed a lot of CPU. (They hadn't noticed yet that it doesn't localize, and that it can trigger false positives since Notepad shows only the file name and not the full path in the title bar.)

    Okay, let's step back and make sure we understand the problem, because this sounds like the sort of thing where the customer is looking for an answer rather than a solution. After all, why Notepad? What about some other text editor? What about Explorer's preview pane?

    The scenario here is that the program is generating some sort of output file, and they want the program to exit if somebody double-clicks the output file in an attempt to open it in Notepad. The customer wasn't specifically interested in Notepad, but since that was the most common case, that's all that they were really interested in.

    One option is to open the file in FILE_SHARE_READ mode. This allows other applications to open the file for reading even while you are writing. If, on the other hand, you expect the user to try to edit the output file and save the result, then they will either encounter a sharing violation (if you opened in deny-write mode) or overwrite the changes that the generator program had made in the meantime.

    The customer said that they were fine with the program just exiting if somebody tried to look at the output file while it was being generated. In that case, they could use an oplock, specifically a Read-Write oplock. The Read-Write oplock grants you exclusive access to the file, but notifies you if somebody else tries to access the same file, so that you can close your handle and let the other application gain access.

  • The Old New Thing

    A program for my nieces: The ABCs, part 3

    • 9 Comments

    One problem I discovered when my nieces ran my initial ABC program was that they had a habit of holding down a key, thereby triggering autorepeat. I had instructed them not to mash the keyboard but rather to press only one key at a time, and while they were good at adhering to the "one key at a time" rule, they also interpreted it as "type really slowly" and ended up autorepeating a lot.

    So let's disable keyboard autorepeat.

    Of course, one way to do this would be to change the system keyboard autorepeat setting, but that would be using global state to manage a local problem. Instead, we just filter the autorepeats out of our edit control:

    LRESULT CALLBACK EditSubclassProc(
        HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam,
        UINT_PTR uIdSubclass, DWORD_PTR dwRefData)
    {
      switch (uMsg) {
      case WM_NCDESTROY:
        RemoveWindowSubclass(hwnd, EditSubclassProc, uIdSubclass);
        break;
      case WM_CHAR:
        if ((lParam & 0x40000000) && wParam != VK_BACK) return 0;
        break;
      }
        return DefSubclassProc(hwnd, uMsg, wParam, lParam);
    }
    
    BOOL
    OnCreate(HWND hwnd, LPCREATESTRUCT lpcs)
    {
      ...
    
      SetWindowSubclass(g_hwndChild, EditSubclassProc, 0, 0);
      SetWindowFont(g_hwndChild, g_hfEdit, TRUE);
    
      return TRUE;
    }
    

    Bit 30 in the lParam of a WM_CHAR message says whether the key was already down. If we see that bit set, then we know that the message was an autorepeat and we throw the message away. (But I let the backspace key through because that lets me erase a lot of text quickly.)

    It's important that the subclass procedure be removed before the window is destroyed. One way of doing this is to remove the subclass procedure in the parent window's WM_DESTROY handler, but since I don't have one, and I'm too lazy to make one, I go for the alternate method of doing just-in-time deregistration by removing the subclass procedure in the subclass procedure itself.

    This version of the program managed to keep my nieces happy for quite some time. We'll tinker with it some more next week.

  • The Old New Thing

    A practical reason for shutting down for the Mayan apocalyse

    • 9 Comments

    I dreamed that Costco announced that they were closing for the Mayan apocalypse and would reopen two weeks later. Not because they believed in it. Rather, because that was their estimate as to how long it would take people to get through their stockpiles and be ready to go shopping again.

    Curiously, I had this dream several weeks after the apocalypse date had passed.

  • The Old New Thing

    The most expensive notepads in Microsoft history

    • 9 Comments

    Many years ago, I visited the office of a colleague who worked on Internet Explorer in order to work on some problem or other. As we investigated the issue, we took notes on a 5"×7" tear-off notepad which bore the logo Forms³.

    My colleague then pointed out to me that we were taking notes on the most expensive notepads in Microsoft history.

    Forms³ (pronounced "forms-cubed") was the code name for the project that was the precursor to Trident, the layout engine that powers Internet Explorer. As I recall the story as it was told to me, project management thought it would be cool to have custom notepads made for their project. The people responsible for converting this idea into reality designed a logo, laid it out, and found a vendor to produce the notepads. But there was some sort of misunderstanding (maybe the asked for too many colors? didn't order enough notepads to trigger the bulk discount?), and the price for the notepads ended up being something ridiculous like $10 for a 50-page notepad.

    That's how the most expensive notepads in Microsoft history came to be.

    Bonus chatter: Oh, by the way, the word "precursor" I used up there? Yeah, that was a euphemism for "cancelled (but used as a learning opportunity)." Microsoft engineers are fond of black humor when it comes to software (especially software made by Microsoft) as a way of coping with adversity, and after the Forms³ project was cancelled, there was a subculture of engineers who morbidly called it Forms tubed.

  • The Old New Thing

    How do I customize the console properties for a shortcut to a console application?

    • 9 Comments

    You already know how to create a shortcut:

    #include <windows.h>
    #include <tchar.h>
    #include <shlobj.h>
    #include <atlbase.h>
    
    // class CCoInitialize incorporated here by reference
    
    int __cdecl _tmain(int argc, TCHAR **argv)
    {
     // error checking elided for expository purposes
     CCoInitialize init;
     CComPtr<IShellLink> spsl;
     spsl.CoCreateInstance(CLSID_ShellLink);
     spsl->SetPath(TEXT("C:\\Windows\\system32\\cmd.exe"));
     CComQIPtr<IPersistFile>(spsl)->Save(L"Here.lnk", TRUE);
     return 0;
    }
    

    If you double-click the resulting shortcut from Explorer, it will run the command processor in a default console window.

    Today's Little Program customizes the other console properties, so you can control settings like the console buffer size and whether QuickEdit is enabled by default.

    We use the IShell­Data­List interface to attach "bonus data" to the shell link. The data we are interested in here is the NT_CONSOLE_PROPS. Remember, Little Programs perform little to no error checking, use hard-coded paths, and all that other stuff that make them unsuitable for shipping-quality code.

    int __cdecl _tmain(int argc, TCHAR **argv)
    {
     CCoInitialize init;
     CComPtr<IShellLink> spsl;
     spsl.CoCreateInstance(CLSID_ShellLink);
     spsl->SetPath(TEXT("C:\\Windows\\system32\\cmd.exe"));
    
     NT_CONSOLE_PROPS props;
     ZeroMemory(&props, sizeof(props));
     props.dbh.cbSize = sizeof(props);
     props.dbh.dwSignature = NT_CONSOLE_PROPS_SIG;
     props.wFillAttribute = FOREGROUND_BLUE | FOREGROUND_GREEN |
                            FOREGROUND_RED; // white on black
     props.wPopupFillAttribute = BACKGROUND_BLUE | BACKGROUND_GREEN |
                                 BACKGROUND_RED | BACKGROUND_INTENSITY |
                                 FOREGROUND_BLUE | FOREGROUND_RED;
                                 // purple on white
     props.dwWindowSize.X = 132; // 132 columns wide
     props.dwWindowSize.Y = 50; // 50 lines tall
     props.dwScreenBufferSize.X = 132; // 132 columns wide
     props.dwScreenBufferSize.Y = 1000; // large scrollback
     props.uCursorSize = 25; // small cursor
     props.bQuickEdit = TRUE; // turn QuickEdit on
     props.bAutoPosition = TRUE;
     props.uHistoryBufferSize = 25;
     props.uNumberOfHistoryBuffers = 4;
     props.ColorTable[ 0] = RGB(0x00, 0x00, 0x00);
     props.ColorTable[ 1] = RGB(0x00, 0x00, 0x80);
     props.ColorTable[ 2] = RGB(0x00, 0x80, 0x00);
     props.ColorTable[ 3] = RGB(0x00, 0x80, 0x80);
     props.ColorTable[ 4] = RGB(0x80, 0x00, 0x00);
     props.ColorTable[ 5] = RGB(0x80, 0x00, 0x80);
     props.ColorTable[ 6] = RGB(0x80, 0x80, 0x00);
     props.ColorTable[ 7] = RGB(0xC0, 0xC0, 0xC0);
     props.ColorTable[ 8] = RGB(0x80, 0x80, 0x80);
     props.ColorTable[ 9] = RGB(0x00, 0x00, 0xFF);
     props.ColorTable[10] = RGB(0x00, 0xFF, 0x00);
     props.ColorTable[11] = RGB(0x00, 0xFF, 0xFF);
     props.ColorTable[12] = RGB(0xFF, 0x00, 0x00);
     props.ColorTable[13] = RGB(0xFF, 0x00, 0xFF);
     props.ColorTable[14] = RGB(0xFF, 0xFF, 0x00);
     props.ColorTable[15] = RGB(0xFF, 0xFF, 0xFF);
     CComQIPtr<IShellLinkDataList>(spsl)->AddDataBlock(&props);
    
     CComQIPtr<IPersistFile>(spsl)->Save(L"Here.lnk", TRUE);
     return 0;
    }
    
  • The Old New Thing

    Displaying a property sheet for multiple files

    • 9 Comments

    Today's Little Program will show a property sheet that covers multiple files, just like the one you get from Explorer if you multi-select a bunch of files and right-click them all then select Properties.

    In fact, that description of how you do the operation interactively maps directly to how you do the operation programmatically!

    #define UNICODE
    #define _UNICODE
    #define STRICT_TYPED_ITEMIDS
    #include <windows.h>
    #include <ole2.h>
    #include <shlobj.h>
    #include <atlbase.h>
    #include <atlalloc.h>
    
    HRESULT GetUIObjectOf(
        IShellFolder *psf,
        HWND hwndOwner,
        UINT cidl,
        PCUITEMID_CHILD_ARRAY apidl, REFIID riid, void **ppv)
    {
     return psf->GetUIObjectOf(hwndOwner, cidl, apidl, riid, nullptr, ppv);
    }
    

    The Get­UI­Object­Of helper function merely wraps the IShell­Folder::Get­UI­Object­Of method to insert the pesky nullptr parameter between the riid and ppv. The riid and ppv parameters by convention go right next to each other, and the IID_PPV_ARGS macro assumes that the function you're calling follows that convention. Unfortunately, the people who designed IShell­Folder::Get­UI­Object­Of didn't get the memo, and we've been stuck with it ever since.

    HRESULT InvokeCommandByVerb(
        IContextMenu *pcm,
        HWND hwnd,
        LPCSTR pszVerb)
    {
     HMENU hmenu = CreatePopupMenu();
     HRESULT hr = hmenu ? S_OK : E_OUTOFMEMORY;
     if (SUCCEEDED(hr)) {
      hr = pcm->QueryContextMenu(hmenu, 0, 1, 0x7FFF, CMF_NORMAL);
      if (SUCCEEDED(hr)) {
       CMINVOKECOMMANDINFO info = { 0 };
       info.cbSize = sizeof(info);
       info.hwnd = hwnd;
       info.lpVerb = pszVerb;
       hr = pcm->InvokeCommand(&info);
      }
      DestroyMenu(hmenu);
     }
     return hr;
    }
    

    The Invoke­Command­By­Verb function merely hosts an IContext­Menu and invokes a single verb.

    Okay, those are the only two helper functions we need this week. The rest we can steal from earlier articles.

    For the purpose of illustration, the program will display a multi-file property sheet for the first two files in your My Documents folder folder. Remember, Little Programs do little to no error checking.

    int __cdecl wmain(int, wchar_t **)
    {
     CCoInitialize init;
     ProcessReference ref;
     CComPtr<IShellFolder> spsf;
     BindToCsidl(CSIDL_MYDOCUMENTS, IID_PPV_ARGS(&spsf));
     CComPtr<IEnumIDList> speidl;
     spsf->EnumObjects(nullptr, SHCONTF_NONFOLDERS, &speidl);
     if (!speidl) return 0;
     CComHeapPtr<ITEMID_CHILD> spidl1;
     CComHeapPtr<ITEMID_CHILD> spidl2;
     if (speidl->Next(1, &spidl1, nullptr) != S_OK) return 0;
     if (speidl->Next(1, &spidl2, nullptr) != S_OK) return 0;
     PCUITEMID_CHILD rgpidl[2] = { spidl1, spidl2 };
     CComPtr<IContextMenu> spcm;
     GetUIObjectOf(spsf, nullptr, 2, rgpidl, IID_PPV_ARGS(&spcm));
     if (!spcm) return 0;
     InvokeCommandByVerb(spcm, "properties");
     return 0;
    }
    

    Because everybody freaks out if I write code that doesn't run on Windows XP, I used the Bind­To­CSIDL function instead of one of its more modern equivalents to get access to the My Documents folder.

    Once we have My Documents, we ask to enumerate its non-folders. If the enumeration fails or says that there are no items (by returning S_FALSE), then we bail immediately.

    Next, we enumerate two items from the folder. If we can't get both, then we bail.

    We then create a two-item array and get the IContext­Menu UI object for the collection.

    Finally, we invoke the "properties" verb on the context menu.

    And that's it. If you run this program, you'll see a context menu for the first two files in your My Documents folder.

  • The Old New Thing

    Posted messages are processed ahead of input messages, even if they were posted later

    • 9 Comments

    Regardless of which interpretation you use, it remains the case that posted messages are processed ahead of input messages. Under the MSDN interpretation, posted messages and input messages all go into the message queue, but posted messages are pulled from the queue before input messages. Under the Raymond interpretation, posted messages and input messages are kept in separate queues, and the message retrieval functions will look first in the posted message queue before looking in the input queue.

    Let's run an experiment to see posted messages get processed ahead of input messages. Start with the new scratch program and make these changes:

    #include <strsafe.h>
    
    class RootWindow : public Window
    {
    public:
     virtual LPCTSTR ClassName() { return TEXT("Scratch"); }
     static RootWindow *Create();
    
     void AppendText(LPCTSTR psz)
     {
        ListBox_SetCurSel(m_hwndChild,
                          ListBox_AddString(m_hwndChild, psz));
     }
    
     void AppendFormat(LPCTSTR pszFormat, ...)
     {
      va_list ap;
      va_start(ap, pszFormat);
      TCHAR szMsg[256];
      StringCchVPrintf(szMsg, ARRAYSIZE(szMsg), pszFormat, ap);
      AppendText(szMsg);
      va_end(ap);
     }
    
     void LogMessage(const MSG *pmsg)
     {
       AppendFormat(TEXT("%d\t%04x\t%p\t%p"),
                    pmsg->time,
                    pmsg->message,
                    pmsg->wParam,
                    pmsg->lParam);
     }
    
     ...
    };
    
    LRESULT RootWindow::OnCreate()
    {
     m_hwndChild = CreateWindow(
          TEXT("listbox"), NULL,
          LBS_HASSTRINGS | LBS_USETABSTOPS |
          WS_CHILD | WS_VISIBLE | WS_TABSTOP | WS_VSCROLL,
          0, 0, 0,0, GetHWND(), (HMENU)1, g_hinst, 0);
     return 0;
    }
    
    int WINAPI WinMain(HINSTANCE hinst, HINSTANCE hinstPrev,
                       LPSTR lpCmdLine, int nShowCmd)
    {
       ...
       while (GetMessage(&msg, NULL, 0, 0)) {
    
        switch (msg.message) {
        case WM_KEYDOWN:
         prw->AppendText(TEXT("Sleeping"));
         UpdateWindow(prw->GetHWND());
         Sleep(1000);
         prw->AppendText(TEXT("Posting"));
         PostMessage(prw->GetHWND(), WM_USER, 0, 0);
         break;
        case WM_KEYUP:
        case WM_USER:
         prw->LogMessage(&msg);
         break;
        }
    
        TranslateMessage(&msg);
        DispatchMessage(&msg);
       ...
    }
    

    This program creates a list box so we can display some output. In the message loop, it sniffs at all the queued messages and does the following:

    • If the message is WM_KEY­UP or WM_USER, then it logs the message timestamp and some parameters.
    • If the message is WM_KEY­DOWN, then it sleeps without processing messages for one second, and then posts a WM_USER message to the main window (which ignores it).

    Run this program, and then tap the shift key.

    The window gets a WM_KEY­DOWN for the shift key. It sleeps for one second (plenty of time for you to release the shift key), and then posts a WM_USER message.

    The WM_USER and WM_KEY­UP messages arrive, and observe via the log window that they arrive out of order. WM_USER message arrived first!

    That's because of the rule that says that posted messages are processed ahead of input messages. (Depending on how you want to look at it, you might say that posted messages are "called out for preferential treatment" in the queue, or you might say that posted messages are placed in a different queue from input messages, and the posted message queue has higher priority.)

    Observe also that the timestamp on the WM_USER message is greater than the timestamp on the WM_KEY­UP message, because the key went up before the WM_USER message was posted. Time has gone backward.

    Make the following change to our program: Change the message we post from WM_USER to WM_KEY­UP:

          PostMessage(hwnd, WM_KEYUP, 0, 0);
    

    Run the program again, and again tap the shift key. Observe that the posted WM_KEY­UP message is processed ahead of the WM_KEY­UP input message. (You can see the difference because we posted the WM_KEY­UP message with wParam and lParam both zero, whereas the WM_KEY­UP input message has information in those parameters.)

    This little demonstration also reinforces some other things we already knew. For example, it once again shows that the input manager does not wiretap your posted messages. If you post a WM_KEY­UP message, it is treated like a posted message not an input message. We saw earlier that posting a keyboard message does not update internal input states. The keyboard shift states are not updated to match your prank call message. If somebody calls Get­Queue­Status, they will not be told that there is input waiting. It will not wake a Msg­Wait­For­Multiple­Objects function that is waiting for QS_INPUT. And as we saw here today, the message gets processed out of order.

Page 371 of 453 (4,526 items) «369370371372373»