Displaying the Input Language indicator in a WTL dialog

Introduction

I recently had to create a dialog containing a password field, in which I wanted to display the current Keyboard Input Language indicator. This is useful when the user is working in a multi-language environment and enters text in a password field and cannot see the text being entered. For example, the user has a US keyboard with Hebrew characters on the keys and can switch between English (EN) and Hebrew (HE). This indicator is the same as one sees in the standard Windows Logon or Change Password dialogs when more than one input language has been configured. When the user toggles the input language via the Language Bar (or using a hotkey combination such as left Alt-Shift), the indicator changes to match the Language Bar.

(To configure additional input languages open Control Panel, click on Regional and Language Options, click on the Languages tab and click the Details… button under Text services and input languages.)

WM_INPUTLANGCHANGE

After a bit of digging around I read in MSDN that whenever the user changes the keyboard input language, first the WM_INPUTLANGCHANGEREQUEST message is posted to the window that currently has the focus. The application accepts this change by passing the message to DefWindowProc (this happens automatically within the WTL framework by default), or rejects the change by handling the message (so that it effectively never arrives at DefWindowProc). Once the change is accepted the WM_INPUTLANGCHANGE message is posted to the topmost affected window which once again passes this to DefWindowProc, which in turn passes the message to all first-level child windows, and so on.

However one cannot simply add a handler to a dialog’s message map – these messages never enter the message map’s ProcessWindowMessage function! (I found the main application window will receive WM_INPUTLANGCHANGEREQUEST in it’s override of CMessageFilter::PreTranslateMessage but never receives WM_INPUTLANGCHANGE, and if a modal dialog is being displayed at the time then it receives neither. Similarly PreTranslateMessage in the modal dialog itself never sees either of these messages.)

What actually happens is exactly as stated in Microsoft’s documentation – these messages are “posted to the window that currently has the focus“. This does not mean one’s modal dialog, it means the control within the dialog that currently has the focus. In order for a dialog to receive these messages it has to superclass every child control that can potentially receive focus. For example if one has a password dialog with two edit text controls (username and password) and two buttons (OK and Cancel), then there are four controls that can receive focus by the user clicking on them (the text controls) or tabbing to them (all four). If one of these four controls has focus when the user presses Alt-Shift to change the input language, then the messages of interest are sent to that control only.

Intercepting Windows messages sent to a dialog’s child controls

To intercept these messages one adds a CContainedWindow member to the dialog class for each control and then subclasses each control in order to redirect it’s message map to that within the dialog class. The dialog’s message map will contain an alternate message map for these controls where a handler can be added for the WM_INPUTLANGCHANGE message. Any messages not handled by the alternate message map are routed to the controls, but now the dialog gets to see them first! One will have CContainedWindow members for all the controls (and perhaps other data members such as CString to conveniently initialise/retrieve the text values). (If one’s dialog contains other controls that cannot receive focus, such as a bitmap or disabled control, then no special handling is required for these.)

private:
   CContainedWindow   _cwUsername;
   CContainedWindow   _cwPassword;
   CContainedWindow   _cwOk;
   CContainedWindow   _cwCancel;

CContainedWindow’s constructor takes three arguments: the class name of an existing window class to base the control on (but here we want to attach to an existing control so this is left null), a pointer to the object containing the message map (the dialog class) and the message map ID of the message map to process the messages (the default message map has an ID of 0, so here one can specify an alternate message map ID such as 1). This is initialised in the constructor of the dialog class:

PasswordDlg::PasswordDlg() :
    _cwUsername(0, this, 1),
    _cwPassword(0, this, 1),
    _cwOk(0, this, 1),
    _cwCancel(0, this, 1) {}

Subclassing the controls (and retrieving the control text via CString members) is most easily achieved using ATL’s DDX support. So firstly stdafx.h should include these lines:

//...
#include <atlddx.h>     //For DDX support
#include <atlcrack.h>   //For message maps
//...

Then define the message and DDX maps in PasswordDlg.h:

//...
class PasswordDlg : public CDialogImpl<PasswordDlg>,
                    public CWinDataExchange<PasswordDlg>   //For DDX
{
//...
   BEGIN_MSG_MAP_EX(PasswordDlg)
      MSG_WM_INITDIALOG(OnInitDialog)                     //Needed to subclass the controls and set the initial language indicator
      MSG_WM_CTLCOLORSTATIC(OnCtlColorStatic)             //Needed to change the language indicator to white on blue
      COMMAND_HANDLER_EX(IDCANCEL, BN_CLICKED, OnCancel)
      COMMAND_HANDLER_EX(IDOK, BN_CLICKED, OnOk)
      //The alternate message map for the controls
      ALT_MSG_MAP(1)                                      //Default messageMapId is 0 so I use 1 for the first alternate messageMapId
         MSG_WM_INPUTLANGCHANGE(OnInputLangChange)        //The handler to be notified of the language change
   END_MSG_MAP()

   BEGIN_DDX_MAP(PasswordDlg)
      DDX_TEXT(IDC_USERNAME, _username)
      DDX_TEXT(IDC_PASSWORD, _password)
      DDX_TEXT(IDC_LANGUAGE, _inputLanguage)
      DDX_CONTROL(IDC_USERNAME, _cwUsername)
      DDX_CONTROL(IDC_PASSWORD, _cwPassword)
      DDX_CONTROL(IDOK, _cwOk)
      DDX_CONTROL(IDCANCEL, _cwCancel)
   END_DDX_MAP()

private:
//...
    BOOL OnInitDialog(CWindow wndFocus, LPARAM lInitParam);
    HBRUSH OnCtlColorStatic(CDCHandle dc, CStatic wndStatic);
    void OnCancel(UINT uNotifyCode, int nID, CWindow wndCtl);
    void OnOk(UINT uNotifyCode, int nID, CWindow wndCtl);
    void OnInputLangChange(DWORD dwCharSet, HKL hKbdLayout);

private:
    CString             _username;
    CString             _password;
    CString             _inputLanguage;
    CContainedWindow    _cwUsername;
    CContainedWindow    _cwPassword;
    CContainedWindow    _cwOk;
    CContainedWindow    _cwCancel;
};

The resource IDs are defined in resource.h:

//...
#define IDD_PASSWORD                130
#define IDC_USERNAME                1000
#define IDC_PASSWORD                1001
#define IDC_LANGUAGE                1002

The dialog resource is defined in PasswordDlg.rc. Notice the static text label to display the language (IDC_LANGUAGE):

IDD_PASSWORD DIALOGEX 0, 0, 201, 66
STYLE DS_SETFONT | DS_MODALFRAME | DS_FIXEDSYS | WS_POPUP | WS_CAPTION | WS_SYSMENU
CAPTION "Enter Password"
FONT 8, "MS Shell Dlg", 400, 0, 0x1
BEGIN
    DEFPUSHBUTTON   "&OK",IDOK,88,44,50,14
    PUSHBUTTON      "&Cancel",IDCANCEL,143,44,50,14
    EDITTEXT        IDC_USERNAME,65,8,127,12,ES_AUTOHSCROLL | NOT WS_TABSTOP
    EDITTEXT        IDC_PASSWORD,65,25,127,12,ES_PASSWORD | ES_AUTOHSCROLL
    LTEXT           "&User name:",IDC_STATIC,11,10,47,8
    LTEXT           "&Password:",IDC_STATIC,11,27,47,8
    CTEXT           "EN",IDC_LANGUAGE,9,50,12,10,SS_CENTERIMAGE
END

The initial value for the language indicator is set in OnInitDialog in PasswordDlg.cpp using GetKeyboardLayout to get the active input locale ID followed by GetLocaleInfo with the flag LOCALE_SABBREVLANGNAME to lookup the three-letter language name. The first two letters are what are diaplayed (the third letter is the sublanguage):

//...
    TCHAR langName[KL_NAMELENGTH] = {0};
    if(::GetLocaleInfo(MAKELCID(::GetKeyboardLayout(0), 0), LOCALE_SABBREVLANGNAME, langName, KL_NAMELENGTH))
    {
        _inputLanguage = langName;
        _inputLanguage = _inputLanguage.Left(2);
    }
    DoDataExchange();   //Subclass the controls and initialise the language indicator (and default username if used).

The language text is displayed as white on blue by adding a handler for WM_CTLCOLORSTATIC:

//...
HBRUSH PasswordDlg::OnCtlColorStatic(CDCHandle dc, CStatic wndStatic)
{
    int nId = wndStatic.GetDlgCtrlID();
    if(IDC_LANGUAGE == nId)
    {
        ::SetTextColor(dc, RGB(255, 255, 255));
        ::SetBkColor(dc, RGB(0, 0, 255));
        return (HBRUSH)GetStockObject(NULL_BRUSH);
    }
    SetMsgHandled(FALSE);
    return 0;
}

Finally the change notification is used to update the indicator in the handler for WM_INPUTLANGCHANGE:

//...
void PasswordDlg::OnInputLangChange(DWORD /*dwCharSet*/, HKL hKbdLayout)
{
    TCHAR langName[KL_NAMELENGTH] = {0};
    if(::GetLocaleInfo(MAKELCID(hKbdLayout, 0), LOCALE_SABBREVLANGNAME, langName, KL_NAMELENGTH))
    {
        _inputLanguage = langName;
        _inputLanguage = _inputLanguage.Left(2);
        DoDataExchange(false, IDC_LANGUAGE);
    }
    SetMsgHandled(FALSE);
}

The attached demo (see downloads below) was written in Visual Studio 2005 using the latest version of WTL (8.0).

More by Author

Get the Free Newsletter!

Subscribe to Developer Insider for top news, trends & analysis

Must Read