C++ At Work

Create Dynamic Dialogs, Satellite DLLs, and More

Paul DiLascia

Code download available at:CatWork2006_09.exe(4479 KB)

Q To create a modal dialog box and some push buttons indirectly, I use DLGTEMPLATE and DLGITEMTEMPLATE. Then I call the (empty) CDialog constructor to construct my dialog object. Next, I call InitModalIndirect and DoModal to create and display my dialog box. How and where can I define functions for my push buttons, and how can I handle BN_CLICKED notifications for them? (Since I'm using CDialog and not a CDialog-derived class I can't override its functions—for example, DoDataExchange—or add message map entries for the ID of the push buttons.)

Mona Fatholahi

Q To create a modal dialog box and some push buttons indirectly, I use DLGTEMPLATE and DLGITEMTEMPLATE. Then I call the (empty) CDialog constructor to construct my dialog object. Next, I call InitModalIndirect and DoModal to create and display my dialog box. How and where can I define functions for my push buttons, and how can I handle BN_CLICKED notifications for them? (Since I'm using CDialog and not a CDialog-derived class I can't override its functions—for example, DoDataExchange—or add message map entries for the ID of the push buttons.)

Mona Fatholahi

A You're so close, you're almost there. Your only mistake is thinking you have to use the CDialog class directly. In fact, you don't and shouldn't. You have to derive a new class—say, CMyDynamicDialog. Once you do that, everything works the same as a normal (resource-based) dialog. The only difference is that your dynamic dialog has to initialize itself by calling InitModalIndirect, instead of passing a resource ID to the constructor.

A You're so close, you're almost there. Your only mistake is thinking you have to use the CDialog class directly. In fact, you don't and shouldn't. You have to derive a new class—say, CMyDynamicDialog. Once you do that, everything works the same as a normal (resource-based) dialog. The only difference is that your dynamic dialog has to initialize itself by calling InitModalIndirect, instead of passing a resource ID to the constructor.

Back in my August 2005 column I showed how to write a CStringDialog class that uses InitModalIndirect to create an input dialog on the fly (see msdn.microsoft.com/msdnmag/issues/05/08/CAtWork). It has an Init function the program must call to initialize the dialog. CStringDialog::Init builds the dialog template and calls InitModalIndirect; you should write a similar function for your dynamic dialog. Once you instantiate and initialize your dialog, you can use it like any other—for example, CStringDialog overrides DoDataExchange to transfer the user's input to and from an internal data member.

Longtime readers of my column will know I'm not a fan of complex IDEs and code generators like the Class Wizard. To my mind, they only hide what's actually going on. If you understand the code the Class Wizard generates, you don't need a wizard because you can write the code yourself. To add a message handler all you need is a couple of lines to declare the handler and add an entry in your message map. In this case, however, the problem isn't the Class Wizard—it's thinking you have to use CDialog directly.

In general, you should rarely use MFC window classes directly. You almost always want to derive your own class because you almost always want to do something different, even if it's just to handle one message. There are two common exceptions to this rule. First, if you're writing a dialog and you want to access the dialog controls, it's appropriate to use CEdit, CListBox, CStatic, CComboBox, and all the other standard control classes without deriving. (But if you add functionality, you should derive your own classes—CMyEdit, CMyListBox, and so on.) Second, you might sometimes want to access a window that's either not part of your app or that you access only temporarily. In that case, you can call CWnd::FromHandle to create a temporary CWnd object that wraps the HWND. This is mostly a matter of convenience, so you can call CWnd functions instead of the Win32® API directly.

Q I've just learned that MFC lets you implement satellite DLLs for language localization. I was not aware of this—how does it work?

Q I've just learned that MFC lets you implement satellite DLLs for language localization. I was not aware of this—how does it work?

Robert Muller

A Starting in version 7.0, MFC added support for satellite DLLs that you can use to localize your app. The system is similar, but not identical, to the Microsoft®.NET Framework-based system of satellite assemblies. A satellite DLL is a resource-only DLL that contains resources localized for a particular language. So, for example, you might have English, French, and German versions of your dialogs. The name of the DLL must be MyResourcesXXX.dll, where MyResources is the base name of your resource .dll, and XXX is the three-letter language code—for example, ENU for United States English, FRA for French (France) and DEU for German. When your program starts, MFC looks for the appropriate DLL based on the user's current language, as returned by GetUserDefaultUILanguage. For details on MFC's lookup algorithm, see "Localized Resources in MFC Applications: Satellite DLLs" at msdn2.microsoft.com/en-us/library/8fkteez0.aspx.

A Starting in version 7.0, MFC added support for satellite DLLs that you can use to localize your app. The system is similar, but not identical, to the Microsoft®.NET Framework-based system of satellite assemblies. A satellite DLL is a resource-only DLL that contains resources localized for a particular language. So, for example, you might have English, French, and German versions of your dialogs. The name of the DLL must be MyResourcesXXX.dll, where MyResources is the base name of your resource .dll, and XXX is the three-letter language code—for example, ENU for United States English, FRA for French (France) and DEU for German. When your program starts, MFC looks for the appropriate DLL based on the user's current language, as returned by GetUserDefaultUILanguage. For details on MFC's lookup algorithm, see "Localized Resources in MFC Applications: Satellite DLLs" at msdn2.microsoft.com/en-us/library/8fkteez0.aspx.

GetUserDefaultUILanguage returns a LANGID, which is a WORD that encodes the primary language and sublanguage IDs. How do you know which three-letter abbreviation to use? You can look it up, but Figure 1 shows a function that reproduces the same algorithm MFC uses to compute the abbreviation from the LANGID.

Figure 1 GetLanguageCode.cpp

////////////////// // Function to get language code from LANGID // CString GetLanguageCode(LANGID langid) { int nPrimaryLang = PRIMARYLANGID(langid); int nSubLang = SUBLANGID(langid); // locale ID LCID lcid = MAKELCID(MAKELANGID(nPrimaryLang, nSubLang), SORT_DEFAULT); TCHAR szLangCode[4]; ::GetLocaleInfo(lcid, LOCALE_SABBREVLANGNAME, szLangCode, 4); return CString(szLangCode); }

The trick is to create a locale ID (LCID) and then call ::GetLocaleInfo with LCTYPE = LOCALE_SABBREVLANGNAME to get the language abbreviation. I wrote a little program called ShowLang (Figure 2) that displays the current language ID and abbreviation. You can download it from the MSDN®Magazine Web site. If you live in the U.S., did you ever notice that the MSDN Library always installs itself in a folder called 1033? That's the language ID for U.S. English.

Figure 2 Language ID

Figure 2** Language ID **

If you want to track the gory details of how MFC loads your language DLL, you can look in the MFC source file appcore.cpp. It starts in CWinApp::InitInstance, which contains the following line of code:

m_hLangResourceDLL = LoadAppLangResourceDLL();

If you peer inside LoadAppLangResourceDLL, you'll discover that MFC goes through some lengthy logic to handle different Windows versions before eventually arriving at _AfxLoadLangDLL, which is the function that actually builds the DLL name and then calls ::LoadLibrary.

Q I have to develop an MFC Visual C++ 6.0 dialog application where the user can choose the preferred language by clicking on a flag (Italian, German, English, French). As he clicks, I have to change all control labels and pick warning messages and other strings from a different, language-dependent resource. I understand I can use external resource-only DLLs, one for each language. What is the best way to do this?

Q I have to develop an MFC Visual C++ 6.0 dialog application where the user can choose the preferred language by clicking on a flag (Italian, German, English, French). As he clicks, I have to change all control labels and pick warning messages and other strings from a different, language-dependent resource. I understand I can use external resource-only DLLs, one for each language. What is the best way to do this?

Antonio Polo

A My answer to the previous question points the way for this one. MFC has all the code you need to load the appropriate DLL based on a language code. Unfortunately, this code is buried inside CWinApp, in appcore.cpp, without any virtual functions that let you override the behavior. For example, it would nice if there were a function that took a LANGID and loaded the appropriate resource DLL— but there is not. The code in appcore.cpp is hard-wired to use the language ID returned from GetUserDefaultUILanguage. The function _AfxLoadLangDLL takes a format string like "MyApp%s.dll" and LCID, and converts the LCID to a language abbreviation like ENU. It then loads "MyAppENU.dll"—but this function is not exposed (it's static). So I would copy it to your app and adapt it for your own needs.

A My answer to the previous question points the way for this one. MFC has all the code you need to load the appropriate DLL based on a language code. Unfortunately, this code is buried inside CWinApp, in appcore.cpp, without any virtual functions that let you override the behavior. For example, it would nice if there were a function that took a LANGID and loaded the appropriate resource DLL— but there is not. The code in appcore.cpp is hard-wired to use the language ID returned from GetUserDefaultUILanguage. The function _AfxLoadLangDLL takes a format string like "MyApp%s.dll" and LCID, and converts the LCID to a language abbreviation like ENU. It then loads "MyAppENU.dll"—but this function is not exposed (it's static). So I would copy it to your app and adapt it for your own needs.

Once you load the proper resource-only DLL, don't forget to call AfxSetResourceHandle, which sets the default HINSTANCE from which MFC loads your resources. Then you can have your dialog post a message that causes it to close and reload itself from the new DLL.

Q I'm trying to find the size of a folder. I'm using MFC's CFileFind class to enumerate the folders and add up the total size for all files, but this is taking too long for large directories with lots of files. Is there an easier way to find the size of a folder quickly?

Q I'm trying to find the size of a folder. I'm using MFC's CFileFind class to enumerate the folders and add up the total size for all files, but this is taking too long for large directories with lots of files. Is there an easier way to find the size of a folder quickly?

Numerous readers

A Yes, if you're willing to add a dependency on Windows®scripting. The Windows Script Host (WSH) lets you automate Windows—think of it as batch files (BAT) for Windows. One of the basic objects in WSH is FileSystemObject, which provides access to the file system. It has all the classes and methods you could ever want to manipulate files and folders. One of the classes it provides is Folder, which has a method Folder::Size that returns the size of the folder. If you're already using the .NET Framework, using FileSystemObject to get the size of a folder is simple and straightforward. Figure 3 shows a C++/CLI console program that uses FileSystemObject to get the size of a folder and display it in a command window.

A Yes, if you're willing to add a dependency on Windows®scripting. The Windows Script Host (WSH) lets you automate Windows—think of it as batch files (BAT) for Windows. One of the basic objects in WSH is FileSystemObject, which provides access to the file system. It has all the classes and methods you could ever want to manipulate files and folders. One of the classes it provides is Folder, which has a method Folder::Size that returns the size of the folder. If you're already using the .NET Framework, using FileSystemObject to get the size of a folder is simple and straightforward. Figure 3 shows a C++/CLI console program that uses FileSystemObject to get the size of a folder and display it in a command window.

Figure 3 GetFolderSize

#include "stdafx.h" #include "GetFolderSize.h" #using <mscorlib.dll> using namespace System; using namespace System::Reflection; using namespace std; //////////////////////////////////////////////////////////////// // Display size of folder passed as command-line argument. // Shows how to use Scripting.FileSystemObject to get the size of a // folder. // int _tmain(int argc, TCHAR* argv[], TCHAR* envp[]) { if (argc>=2) { LPCTSTR lpszPath = argv[1]; String^ path = gcnew String(lpszPath); Type^ fsoType = Type::GetTypeFromProgID( "Scripting.FileSystemObject"); Object^ fso = Activator::CreateInstance(fsoType); Object^ folder = fsoType->InvokeMember("GetFolder", BindingFlags::InvokeMethod, nullptr, fso, gcnew array<Object^>{path}); Object^ size = folder->GetType()->InvokeMember("Size", BindingFlags::GetProperty, nullptr, folder, nullptr); Console::WriteLine("Size of {0} is {1} bytes.", path, size); } else { Console::WriteLine("Please specify a folder"); } return 0; }

If you're not programming with the .NET Framework, you can use MFC's COM dispatch interface to encapsulate FileSystemObject. I wrote a little program called FileOpenFS that shows how. FileOpenFS has a customized File Open dialog that shows the folder size whenever you select a folder. The easiest way to use FileSystemObject (or any other COM object) from MFC is to use Visual Studio® to generate a C++ wrapper that encapsulates it. Select Project | Add Class, then select MFC and MFC Class From TypeLib.

Visual Studio displays a list from which to select the type library. The choice can be somewhat perplexing if you don't know the name you're looking for, as a typical machine can have hundreds of COM classes installed. Figure 4 shows the dialog with the Scripting object selected.

Figure 4 Selecting a Type Library

Figure 4** Selecting a Type Library **

Once you select the TypeLib, Visual Studio displays a list of all the classes it exports. Select the ones you want to wrap and Visual Studio generates a wrapper for each one. For example, it creates CFolder to wrap the Folder class and CDictionary to wrap Dictionary. One of the most common problems you'll have with these generated wrappers is name collisions. For example, CFileSystem has a function MoveFile, which conflicts with the global MoveFile already defined as a macro in Windows (in WinBase.h). There are a couple of ways to deal with this. You can use the rename option of the #import directive to rename the function, like so:

#import "foo.lib" no_namespace rename("OldName", "NewName")

But since that requires editing the source files that Visual Studio generates, I chose plan B, which disables warnings and undefines the offending macros. Figure 5 shows my top-level include file with all the Visual Studio-generated classes inside a namespace FileSystem. I like this approach because it puts everything in a separate file, FileSystem.h, which in turn includes all the wrapper files into a namespace. By separating all my own code from the Visual Studio-generated files, I can always regenerate them later without having to edit anything. Of course, this won't work if you also want to use the original global symbols like ::MoveFile.

Figure 5 FileSystem.h

#pragma once #pragma warning (push) // ignore global macro name conflict #pragma warning (disable: 4278) // undefine Windows global macros. If you care about these, // use "rename" in #import directive in .h files instead. #undef GetFreeSpace #undef DeleteFile #undef MoveFile #undef CopyFile namespace FileSystem { #include "CDictionary.h" #include "CDrive.h" #include "CDriveCollection.h" #include "CFile0.h" #include "CFileCollection.h" #include "CFileSystem.h" #include "CFileSystem3.h" #include "CFolder.h" #include "CFolderCollection.h" #include "CScriptEncoder.h" #include "CTextStream.h" inline HRESULT CreateFileSystemObject(CFileSystem3& fso) { CLSID clsid; HRESULT hr = AfxGetClassIDFromString( "Scripting.FileSystemObject", &clsid); if (SUCCEEDED(hr)) { BOOL bRet = fso.CreateDispatch(clsid); VERIFY(bRet); } return hr; } }; // namespace FileSystem #pragma warning (pop)

Visual Studio creates the wrapper classes. To use them, you have to write some lines to instantiate your object. I wrote a short function in FileSystem.h that does it:

inline HRESULT CreateFileSystemObject(CFileSystem3& fso) { CLSID clsid; HRESULT hr = AfxGetClassIDFromString( "Scripting.FileSystemObject", &clsid); if (SUCCEEDED(hr)) { VERIFY(fso.CreateDispatch(clsid)); } return hr; }

So now to create a FileSystemObject, all I have to do is write:

CFileSystem3 fso; CreateFileSystemObject(fso);

Happy programming!

Send your questions and comments for Paul to cppqa@microsoft.com.

Paul DiLascia is a freelance software consultant and Web/UI designer-at-large. He is the author of Windows++: Writing Reusable Windows Code in C++ (Addison-Wesley, 1992). In his spare time, Paul develops PixieLib, an MFC class library available from his Web site, www.dilascia.com.