Along lines similar to last time, you can also add custom accelerators to your dialog box. (In a sense, this is a generalization of custom navigation, since you can make your navigation keys be accelerators.)

So let's use accelerators to navigate instead of picking off the keys manually. Our accelerator table might look like this:

IDA_PROPSHEET ACCELERATORS
BEGIN
    VK_TAB      ,IDC_NEXTPAGE       ,VIRTKEY,CONTROL
    VK_TAB      ,IDC_PREVPAGE       ,VIRTKEY,CONTROL,SHIFT
END

Here you can see my comma placement convention for tables. I like to put commas at the far end of the field rather than jamming it up against the last word in the column. Doing this makes cut/paste a lot easier, since you can cut a column and paste it somewhere else without having to go back and twiddle all the commas.

Assuming you've loaded this accelerator table into the variable "hacc", you can use that table in your custom dialog loop:

while (<dialog still active> &&
       GetMessage(&msg, NULL, 0, 0, 0)) {
 if (!TranslateAccelerator(hdlg, hacc, &msg) &&
     !IsDialogMessage(hdlg, &msg)) {
  TranslateMessage(&msg);
  DispatchMessage(&msg);
 }
}

The TranslateAccelerator function checks if the message matches any entries in the accelerator table. If so, then it posts a WM_COMMAND message to the window passed as its first parameter. In our case, we pass the dialog box handle. Not shown above is the WM_COMMAND handler in the dialog box that responds to IDC_NEXTPAGE and IDC_PREVPAGE by performing a navigation.

The same as last time, if you think there might be modeless dialogs owned by this message loop, you will have to do filtering so that you don't pick off somebody else's navigation keys.

while (<dialog still active> &&
       GetMessage(&msg, NULL, 0, 0, 0)) {
 if (!((hdlg == msg.hwnd || IsChild(hdlg, msg.hwnd)) &&
       !TranslateAccelerator(hdlg, hacc, &msg)) &&
     !IsDialogMessage(hdlg, &msg)) {
  TranslateMessage(&msg);
  DispatchMessage(&msg);
 }
}

Okay, I think that's enough of dialog boxes for now.