Changing the User Interface at Run Time

On Windows NT, most standard resource calls retrieve resources based on the language ID of the calling thread locale. (See Figure 4-11.) Since Windows NT supports multiple-language resources and allows you to change the thread locale setting, it follows that programs running on Windows NT can change the language of the user interface at any time. For example, you could allow the user to change the user interface language with a keyboard combination or a menu selection. Automatic teller machines in many US cities have this function. If your user interface contains dozens of dialog boxes and hundreds of messages, it might not be possible to install several language editions of the user interface without using up a lot of disk space. In that case, you could allow the user to choose a user interface language during installation. The setup program would customize the default resources of the executable to a particular language using the UpdateResource functions, which are described in detail in the section titled "Adding, Deleting, and Replacing Resources" in Volume 2 of the Microsoft Win32 Programmer's Reference.

CreateDialog DialogBoxIndirectParam LoadCursor
CreateDialogIndirect DialogBoxParam LoadIcon
CreateDialogIndirectParam FindResource LoadMenu
CreateDialogParam FindResourceEx LoadResource
CreateIconFromResource FormatMessage LoadString
DialogBox LoadAccelerators MessageBox
DialogBoxIndirect LoadBitmap MessageBoxEx

Figure 4-11. Standard Windows resource calls. Functions in italics take a language ID as a parameter. All others retrieve items based on the calling thread locale (on Windows NT) or based on the default system locale (on Windows 95).

To change the user interface language at run time on Windows NT, you need to either call API functions that let you specify a language ID (shown in italics in Figure 4-11) or change the thread locale setting with the function SetThreadLocale.

BOOL rc = SetThreadLocale(lcid);

Only Windows NT supports SetThreadLocale and GetThreadLocale. You'll need to explore other mechanisms to change the user interface at run time for Windows 95–based applications. The section titled "Multiple-Language DLLs" discusses one alternative.

Regardless of whether a user can select a user interface language for your program only during installation or can change it at run time, your program will need to tell the user which language choices are available. Win32 provides the function EnumResourceLanguages, which enumerates all language IDs associated with a given resource type and resource ID. To create a list of available languages, pick a resource that exists in all sets of language resources and enumerate the language IDs associated with it. The sample procedure MyFindAllLanguages below shows the syntax for calling EnumResourceLanguages.

Message Boxes

Message boxes make it convenient for you to display error messages in a dialog box without having to create dozens of individual dialog templates. The system creates a message box based on title bar text and message text supplied by the program. Windows provides several standard combinations of predefined buttons and icons. (See Figures 4-12 and 4-13.)

Icon Type Windows NT Windows 95
     
Exclamation point
(for warning messages)
     
Stop sign/Do not enter
(for critical messages)
     
Question mark
(for messages that require a Yes or No answer)


Figure 4-12. Message-box icons predefined by Windows NT and Windows 95.

Constant Message-Box Buttons
MB_ABORTRETRYIGNORE Abort, Retry, and Ignore
MB_OK OK
MB_OKCANCEL OK and Cancel
MB_RETRYCANCEL Retry and Cancel
MB_YESNO Yes and No
MB_YESNOCANCEL Yes, No, and Cancel


Figure 4-13 Predefined Windows button combinations.

The system handles the display of the dialog box and passes the application a flag indicating which button the user clicks to close the dialog box. You can call either MessageBox or MessageBoxEx to create a message box. MessageBoxEx takes an additional parameter—a language ID—that allows you to specify the language of the text in the predefined buttons. Currently Windows provides buttons only in the language of the system user interface, but future versions of Windows will probably provide additional translations.

Multiple-Language DLLs

Including multiple-language resources in an executable is convenient for small programs but might prove impractical for programs with large user interfaces. (You'd have to ship your product on CD-ROM to customers who had enormous hard drives.) An alternative is to isolate a program's user interface in a DLL that can be localized into different editions. Putting multiple-language resources into DLLs makes an application more extensible than if you linked everything into a single executable. A single executable supports a fixed number of languages, whereas additional DLLs can be released at any time. You could even sell localized user interface packages as separate add-on products.

You can create a mechanism for switching the user interface language during installation or at run time by giving each language edition of a DLL a unique filename. Distinct names are necessary because Windows cannot distinguish between two DLLs with the same module name, even if they contain resources in different languages. For example, you could incorporate the ISO-defined, three-letter abbreviation for the appropriate language into the filename. The sample code below, for Windows NT, attempts to load a user interface DLL that corresponds to a locale ID passed into the procedure. It constructs a DLL filename by calling GetLocaleInfo on the locale ID.

If the sample function LoadLanguage finds a DLL, it stores the DLL's module handle in the static global variable hModRes. The program passes this variable as the first parameter to functions such as LoadString and EnumResourceLanguages so that the functions will access the resources contained in the correct DLL. Usually the first parameter to such functions is NULL, indicating that the current executable file contains the resources.

///////////////////////////////
// LoadLanguage
//
// Load a language module. This function assumes that the language
// modules are DLLs and that those DLLs are named using three-letter
// abbreviations for a language name (for example, deu==German)
// stored in the Windows system registry.
//
// If the function cannot find a DLL that corresponds exactly to the
// LCID, it attempts to load any DLL associated with the same primary // language.
//
// LoadLanguage returns TRUE if a module is loaded successfully; else // it returns FALSE.

static HANDLE hModRes;
BOOL LoadLanguage(LCID lcid) // locale ID
{
SetThreadLocale(lcid); // change thread locale; Windows NT only

TCHAR lpszLang[4];

GetLocaleInfo(lcid, LOCALE_SABBREVLANGNAME, lpszLang, 4);

if ( (int)(hModRes = LoadLibrary(lpszLang)) < 32 )
{
// Search for any sublanguage.
lcid = MAKELANGID(PRIMARYLANGID(lcid), SUBLANG_DEFAULT);
GetLocaleInfo(lcid, LOCALE_SABBREVLANGNAME, lpszLang, 4);

if ( (int)(hModRes = LoadLibrary(lpszLang)) < 32 )
{
hModRes = NULL; // not found; use EXE file for resources
return (FALSE);
}
}
return (TRUE); // If we got here, we found a DLL for the
// requested language.
}

Occasionally, a user moves or deletes a file. To ensure that your program always has a working user interface, you can bind one set of language resources into the executable and use DLLs only for additional languages, or you can bind in a single error message telling the user that the user interface module is missing.

The sample code below searches for all possible language resources contained in both the current executable and possible DLL files and builds a list box that displays the possible user interface language choices to the user. It uses the FindFirstFile and FindNextFile API functions to search for DLLs named with three-letter ISO codes. Once the user selects a language, the program calls the LoadLanguage function described earlier to load the appropriate DLL. Both EnumResLangProc and EnumResourceLanguages are fully documented in the Windows API Quick Reference topics of the same names.

/////////////////////
// struct for use by MyFindAllLanguages and EnumResLangProc

typedef struct _tagLBData{ // list box data

HWND hWnd; // handle to window/dialog box
// containing the list box
int nListBox; // ID of the list box to fill
DWORD dwErrCode; // reason for failure of EnumResLangProc
} LBDATA, *PLBDATA;

#define NAMESTART 12 // offset to filename in list box item
// string (list box item strings will
// not be translated)

/////////////////////
// The following code will locate all the available languages for a
// resource, whether you are using resources in separate DLLs,
// resources in the EXE, or both. This example assumes that the DLLs
// follow the naming convention from the earlier example. With a
// sufficiently unique combination of resource name and type as a
// flag, you can search for your DLLs anywhere on the path.

BOOL CALLBACK EnumResLangProc(
HMODULE hModule, // resource-module handle
LPCTSTR lpszType, // address of resource type
LPCTSTR lpszName, // address of resource name
LANGID wIDLanguage, // resource language identifier
LPARAM lParam) // application-defined
// parameter
{
PLBDATA pLBData = (PLBDATA)lParam;
static TCHAR szItemText[NAMESTART + MAX_PATH];

// Display language ID components in the list box text. The
// format of the text will be: "PriLang SubLang - FileName".

wsprintf(szItemText, TEXT("%#02x %#02x - "),
PRIMARYLANGID(wIDLanguage),
SUBLANGID(wIDLanguage));

// Append the name of the file containing the resource.
if (0 < GetModuleFileName(hModule,
&szItemText[ lstrlen(szItemText) ],
MAX_PATH))
{
// Add the line to the list box.
SendDlgItemMessage(pLBData->hWnd, pLBData->nListBox,
LB_ADDSTRING, 0, (LPARAM)szItemText);
}
else
{ // Send reason for the failure of the call to GetModuleFileName
// back to our caller.
pLBData->dwErrCode = GetLastError();
}
// If this call failed, cause the enumeration process in
// EnumResourceLanguages to halt.
return (pLBData->dwErrCode == ERROR_SUCCESS ? TRUE : FALSE);
}

///////////////////////////
// MyFindAllLanguages
//
// Fill the given list box with the languages in which the given
// resource is available. This function will add the data to the
// given list box. No assumption is made about any of the list box's
// attributes (such as whether it is sorted). This function
// first empties the list box.
//
// MyFindAllLanguages returns ERROR_SUCCESS if successful or
// results of GetLastError() if unsuccessful.

DWORD MyFindAllLanguages(
HWND hWnd, // handle to window or dialog box
// containing the list box
int nListBox, // ID number of the list box to fill
LPCTSTR lpszType, // name or ID number of resource
// type to be enumerated
LPCTSTR lpszName) // name or ID number of resource
// name to be enumuerated
{
// Make sure the list box is empty.
SendDlgItemMessage(hWnd, nListBox, LB_RESETCONTENT, 0, 0);

LBDATA LBData;
LBData.hWnd = hWnd; // Fill in the LBData fields
LBData.nListBox = nListBox; // so the data gets sent to
// EnumResLangProc.

LBData.dwErrCode = ERROR_SUCCESS; // Assume we will be
// successful.

WIN32_FIND_DATA ffd; // for info from FindFirstFile and
// FindNextFile
HANDLE hFind = FindFirstFile(TEXT("???.DLL"), &ffd);

if (hFind != INVALID_HANDLE_VALUE)
{
do
{
HINSTANCE hModule = LoadLibraryEx(ffd.cFileName, hFind,
LOAD_LIBRARY_AS_DATAFILE);

if ( !EnumResourceLanguages(hModule, // Look in the DLL.
lpszType, // given resource type
lpszName, // given resource name
(ENUMRESLANGPROC) // address of
EnumResLangProc, // callback function
(LPARAM) &LBData) ) // application-defined
// parameter
{
LBData.dwErrCode = GetLastError();
}
FreeLibrary(hModule);
} while (LBData.dwErrCode == ERROR_SUCCESS &&
FindNextFile(hFind, &ffd) );

FindClose( hFind );
}
else
{
LBData.dwErrCode = GetLastError();
}

if (LBData.dwErrCode == ERROR_SUCCESS ||
LBData.dwErrCode == ERROR_FILE_NOT_FOUND )
{
LBData.dwErrCode = ERROR_SUCCESS;

if ( !EnumResourceLanguages( NULL, // Look in the current EXE.
lpszType, // given resource type
lpszName, // given resource name
(ENUMRESLANGPROC) // address of
EnumResLangProc, // callback function
(LPARAM) &LBData) ) // application-defined
{ // parameter
LBData.dwErrCode = GetLastError();
}
}
return (LBData.dwErrCode);
}