{"id":112087,"date":"2026-02-25T07:00:00","date_gmt":"2026-02-25T15:00:00","guid":{"rendered":"https:\/\/devblogs.microsoft.com\/oldnewthing\/?p=112087"},"modified":"2026-02-25T08:38:27","modified_gmt":"2026-02-25T16:38:27","slug":"20260225-00","status":"publish","type":"post","link":"https:\/\/devblogs.microsoft.com\/oldnewthing\/20260225-00\/?p=112087","title":{"rendered":"Intercepting messages before <CODE>Is&shy;Dialog&shy;Message<\/CODE> can process them"},"content":{"rendered":"<p>Last time, we looked at <a title=\"Customizing the ways the dialog manager dismisses itself: Isolating the Close pathway\" href=\"https:\/\/devblogs.microsoft.com\/oldnewthing\/20260224-00\/?p=112082\"> recognizing that the user clicked the Close button (or equivalent nonclient gestures) on a dialog<\/a>. The other system-provided pathway to dismissing a dialog is pressing <kbd>ESC<\/kbd>, and we saw in our flow diagram that this is done by the <code>Is\u00adDialog\u00adMessage<\/code> function.<\/p>\n<p>If your dialog box doesn&#8217;t have an <code>IDCANCEL<\/code> button, then you can detect that the user hit <kbd>ESC<\/kbd> by simply recognizing that they didn&#8217;t click the Close button. If you shut off the <code>WM_<wbr \/>CLOSE<\/code> pathway, then the only other source of <code>IDCANCEL<\/code> is the <kbd>ESC<\/kbd> key.<\/p>\n<p>Now, you might be concerned that additional pathways for system-provided dialog dismissal may be added later. (Who knows, maybe a new touch gesture will be invented.) But you can&#8217;t predict what pathway that future system-provide dismissal will take, so you have nothing to code to. All you can do is cover the pathways that you know and hope that any future dismissal mechanisms will follow one of them.<\/p>\n<p>We saw from our diagram that the <kbd>ESC<\/kbd> pathway consists of the <code>Is\u00adDialog\u00adMessage<\/code> function processing a <code>WM_<wbr \/>KEY\u00adDOWN<\/code> for the <kbd>ESC<\/kbd> key and turning it into a <code>WM_<wbr \/>COMMAND<\/code> button click for <code>IDCANCEL<\/code>. By the time it is converted into a fake button click message, it&#8217;s too late to know what the source was, so we&#8217;ll have to do the work before then.<\/p>\n<p>One way to do this is to recognize the <kbd>ESC<\/kbd> key <i>before<\/i> calling <code>IsDialogMessage<\/code>. If your dialog was created as a modeless dialog, then you already have a custom dialog message loop. And if it was created as a modal dialog, you can <a title=\"The dialog manager, part 5: Converting a non-modal dialog box to modal\" href=\"https:\/\/devblogs.microsoft.com\/oldnewthing\/20050404-48\/?p=35983\"> convert it to a modeless one with a dialog message loop<\/a>. Once that&#8217;s done, you can <a title=\"The dialog manager, part 8: Custom navigation in dialog boxes\" href=\"https:\/\/devblogs.microsoft.com\/oldnewthing\/20050407-00\/?p=35953\"> treat the <kbd>ESC<\/kbd> key as if it were custom navigation<\/a>.<\/p>\n<p>Your first try might go like this:<\/p>\n<pre>while (\u27e6 dialog still active \u27e7 &amp;&amp;\r\n       GetMessage(&amp;msg, NULL, 0, 0, 0)) {\r\n if (msg.message == WM_KEYDOWN &amp;&amp;\r\n     msg.wParam == VK_ESCAPE) {\r\n  \u27e6 do custom ESC key handling \u27e7\r\n } else if (!IsDialogMessage(hdlg, &amp;msg)) {\r\n  TranslateMessage(&amp;msg);\r\n  DispatchMessage(&amp;msg);\r\n }\r\n}\r\n<\/pre>\n<p>However, this fails to honor controls that declare <code>DLGC_<wbr \/>WANT\u00adALL\u00adKEYS<\/code> or <code>DLGC_<wbr \/>WANT\u00adMESSAGE<\/code>. We&#8217;ll have to check with the focus control to see if it wants to use the <kbd>ESC<\/kbd> key for its own purposes. While we&#8217;re at it, we&#8217;ll also ensure that we are stealing <kbd>ESC<\/kbd> keys only from our own dialog. The code is getting complex enough that we&#8217;ll break it out into a helper function.<\/p>\n<pre>bool IsDialogESC(HDLG hdlg, MSG const* msg)\r\n{\r\n    if (msg-&gt;message != WM_KEYDOWN ||\r\n        msg-&gt;wParam != VK_ESCAPE) {\r\n        return false;\r\n    }\r\n\r\n    if (msg-&gt;hwnd != hdlg &amp;&amp;\r\n        !IsChild(hdlg, msg-&gt;hwnd)) {\r\n        return false;\r\n    }\r\n\r\n    auto code = SendMessage(msg-&gt;hwnd, WM_GETDLGCODE,\r\n                            msg-&gt;wParam, (LPARAM)msg);\r\n    if (code &amp; (DLGC_WANTALLKEYS | DLGC_WANTMESSAGE)) {\r\n        return false;\r\n    }\r\n\r\n    return true;\r\n}\r\n<\/pre>\n<p>In order for this to be a potential dialog-dismissing <kbd>ESC<\/kbd>, the message must be a <code>WM_<wbr \/>KEY\u00adDOWN<\/code> of <code>VK_<wbr \/>ESCAPE<\/code>, and the message must target the dialog or a child of the dialog. And the target window also must decline to handle the message itself.<\/p>\n<p>I carefully ordered the tests so that we can early-out as quickly as possible. Checking for the <kbd>ESC<\/kbd> key can be done by inspecting the message. Checking that the target window is acceptable is a little more work, but not too bad. Checking whether the target window wants to handle the message is the most expensive test, so we do that last.<\/p>\n<p>Now we can incorporate this helper function into our custom message loop.<\/p>\n<pre>while (\u27e6 dialog still active \u27e7 &amp;&amp;\r\n       GetMessage(&amp;msg, NULL, 0, 0, 0)) {\r\n if (<span style=\"border: solid 1px currentcolor;\">IsDialogESC(hdlg, &amp;msg)<\/span>) {\r\n  \u27e6 do custom ESC key handling \u27e7\r\n } else if (!IsDialogMessage(hdlg, &amp;msg)) {\r\n  TranslateMessage(&amp;msg);\r\n  DispatchMessage(&amp;msg);\r\n }\r\n}\r\n<\/pre>\n<p>This all sounds great, but you might not be able to make this change. For example, maybe the dialog box&#8217;s dialog procedure uses <code>End\u00adDialog()<\/code> to dismiss the dialog. The fact that it has been called is not exposed by the dialog box infrastructure. Or maybe you don&#8217;t control the code that calls <code>Dialog\u00adBox<\/code> in the first place, so you can&#8217;t make them switch to a modeless dialog box with a custom dialog loop.<\/p>\n<p>We&#8217;ll study this problem next time.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Process the message before you let <CODE>Is&shy;Dialog&shy;Message<\/CODE> see it.<\/p>\n","protected":false},"author":1069,"featured_media":111744,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"footnotes":""},"categories":[1],"tags":[25],"class_list":["post-112087","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-oldnewthing","tag-code"],"acf":[],"blog_post_summary":"<p>Process the message before you let <CODE>Is&shy;Dialog&shy;Message<\/CODE> see it.<\/p>\n","_links":{"self":[{"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/posts\/112087","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/users\/1069"}],"replies":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/comments?post=112087"}],"version-history":[{"count":0,"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/posts\/112087\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/media\/111744"}],"wp:attachment":[{"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/media?parent=112087"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/categories?post=112087"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/tags?post=112087"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}