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).