Windows


Converting VCL Components to Windows Resources

Luigi Bianchi

Borland C++Builder has its own version of resource files, but they can be translated to more conventional .RC files.


Creating dialog boxes for GUI applications is one of the most common repetitive tasks in Windows development. Many class libraries, such as MFC (Microsoft Foundation Classes), reduce this burden on Windows developers. IDEs (Integrated Development Environments) can speed up this process even more by providing automatic code generation.

BCB (Borland C++Builder), a development environment, utilizes both ideas. It provides both a class library, VCL (Visual Component Library), and an IDE that automatically generates code to incorporate the components within a Windows application. However, such code generation requires strict cooperation between the IDE and VCL. As a result, VCL resource files have a format that cannot be used in non-VCL applications.

In this article, I describe a utility that I have written to convert VCL resource files into plain Windows resource script (.RC) files. The utility also automatically generates some of the code needed to initialize these converted components with default settings. Although turning a VCL application into a non-VCL application is not automatic, this tool greatly simplifies the task, with potentially large savings in application size. The tool also opens up new possibilities in application development, such as using BCB as a prototyping tool and shipping the final product as a non-VCL application.

Background on GUI Resources

A resource, in general GUI terms, is a data set that describes a component's variable parts, such as its size, position, color, etc. It should not be confused with the component's class definition or the code that instantiates such a component. A Windows resource, according to MSDN (Microsoft Developer Network), has the following definition:

A resource is binary data that you can add to the executable file of a Win32-based application. A resource can be either standard or defined. The data in a standard resource describes an icon, cursor, menu, dialog box, bitmap, enhanced metafile, font, accelerator table, message-table entry, string-table entry, or version information. An application-defined resource, also called a custom resource, contains any data required by a specific application.

Standard resource data is defined at the OS (operating system) level; the OS provides several APIs to manipulate the data. Custom resource data, instead, is defined by the module that uses it. The module must provide, or have access to, some functions to handle the custom resource.

Most Windows development tools come with a resource editor and a resource compiler. The resource editor allows you to visually design a dialog box and store its parameters in a script file (with the extension .RC, shown in Figure 1). The resource compiler translates the script into a binary file (a .RES file) that can be linked into the executable. The Win32 SDK and the most commonly used class libraries (MFC, OWL, etc.) are expected to work in this way.

The Problem with VCL Resources

Instead of creating a resource script file, BCB Resource Editor generates a binary file (with the extension .DFM) that only VCL can understand. Moreover, all the resources, even the standard ones, are stored as if they were custom, so you can't rely on direct OS support or resource de-compilers. Custom resource data generated by BCB Resource Editor is a copy of a .DFM file. VCL guarantees translation of these resources into OS calls, at the cost of extra code linked to the module. This was done to add functionality, such as automatic initialization of the controls to accelerate the GUI development process.

In addition to this inconvenience, some low-level API programming has been hidden in what Borland calls component "properties," in order to further simplify the design of the user interface. From Borland's documentation:

Properties are the most visible parts of components. The application developer can see and manipulate them at design time and get immediate feedback as the components react in the Form designer. A property can be of any type.

This raises the question of how to design a dialog box using BCB for a non-VCL application. Borland's answer is to use Resource Workshop, a separate tool that generates a standard resource script file. I tried to use it, but I discovered that Resource Workshop does not allow the use of a lot of the newer controls, such as ListViews, TreeNodes, MonthCalendars, TrackBars, etc. (Not surprisingly, the date that appears in the About Box is 1994, which explains a lot of things.) BCB is able to compile MFC, OWL, and WinApi code because there is a resource compiler for script files, but a visual .RC file generator is missing.

Exploring a .DFM file convinced me that the file could be translated into a standard resource script file. So, I created an extendable .DFM-to-.RC file converter that translates a VCL TForm into a standard Windows DIALOGBOX resource. Because a .DFM file contains more data than can be included in an .RC file, I use some of this extra data to automatically generate code to initialize the dialog box. Thus, I have more benefits than I could have using an updated version of Resource Workshop.

Exploring VCL Components

I began my development of the conversion process by using a resource editor to create a simple Dialog Box with a Push Button, a Static Text Field, and a Combo Box. This produced the script file shown in Figure 1. Without entering into .RC file details (for which wide documentation exists), I simply provide Figure 1, which shows a DIALOG resource described by a row that defines its nameID (IDD_DIALOG1), position (0, 0), and size (240, 120). The row is followed by statements that relate to the dialog styles, caption text, and font. Finally, a list of three statements describes each control that populates the DialogBox. These statements describe nameID, family, window style, position, size, and, eventually, extended style. The nameID is either a unique name or a unique 16-bit unsigned integer value that identifies the dialog box or the control.

After creating this resource file, I created a TForm with BCB that contained the same controls as the Dialog Box in order to obtain a .DFM file. Naturally, after gaining an understanding of about 99.9 percent of the binary file format, I discovered a VCL function, ObjectResourceToText, that converts the binary resource file into an equivalent, more readable text file. From here on, when I talk about a .DFM file, I will be referring to the equivalent text file, unless otherwise indicated.

In BCB, all the Palette icons that can be dragged onto a Form are called components and derive from the VCL TComponent class. They can be both visual and non-visual. The visual components are quite similar to SDK controls, while the non-visual components are, for example, Common Dialogs, Timers, Database Tables, and so on. These non-visual components don't have an equivalent representation in an .RC file. Note that TForm and TMenu also inherit from a TComponent.

When you start to design a new Form, the IDE creates three files with the same name, but with different extensions: .DFM, .CPP, and .HPP. These three files contain all the information about that form: definitions, declarations, resources, and implementation. When you then drag a component onto that Form, the IDE inserts the declaration of a pointer to the component into the private members of the Form and a section into the .DFM with the component's description and some of its properties. To reduce the size of a .DFM file, only those properties whose value differs from the default are stored. If you modify the component's name, the IDE reflects the change in the three files to keep them aligned.

Even using the text representation of the .DFM file, you still must deal with some binary data (for example, bitmaps) or other data that VCL uses internally during Form initialization.

A component can be included within another component (e.g., a Form is a component and includes all the other components). Also, non-visual components are included in the Form onto which they have been dragged.

In the text version of the resource files, every component description starts with the keyword object and terminates with the keyword end. Figure 2 shows the text version of a .DFM file generated for a TForm with the three controls described above. Similar to an .RC file, this file starts with a general description of the TForm and follows with a list of components.

One of those components is a TComboBox, which starts with a list of properties (Left, Top, etc.), each followed by an equal sign and a value. Within the source files, a pointer, ComboBox1, refers to this TComboBox. The IDE creates the declaration for TComboBox and initializes the pointer ComboBox1 with its address, keeping things synchronized among the three files associated with the form. Unlike .RC files, you don't need a nameID to identify this combo box, because you have access to it through the pointer variable; this pointer is a data member of TForm.

Properties in a .DFM file can be integers, strings, and lists. To complicate things, strings can represent Boolean values (as in the Sorted property), something that looks like an enum (as in the Style property), or something that appears to represent text (as in the Items.Strings case). Finally, this .DFM file shows four default property values ('I', 'Love', 'Chick', and 'Corea') for which there is no equivalent in an .RC file. These extra parameters are used by a magic VCL function that fills the Combo Box at run time with the four items defined at design time. From the resource information only, this function knows which strings must be inserted into the ComboBox1 instance of the TComboBox class. The programmer does not need to write any code to accomplish this initialization.

Conversion Classes

Because the information stored in an .RC file is a subset of the information in a .DFM file, it should be possible to convert a .DFM file into an .RC file. This process involves several tasks: the substitution of variable names with nameIDs, the conversion of the control metrics to account for different strategies between .DFM and .RC files, and the rearrangement of the controls' creation order to preserve the same tab-order navigation. In addition, the conversion process should create some code to initialize the controls and to set additional properties (which can be accessed only through API calls) without requiring users to manually write code. By generating this code, the conversion process preserves the added benefit of VCL components over ordinary Windows controls.

To perform these tasks, I created a hierarchy of classes, whose base class is pVCLComponent (Listing 1). Its purpose is to read and keep track of the VCL components stored in a .DFM file. All the other classes descend from pVCLComponent. The first one, pResComponent, automatically generates a unique nameID for every control. Note that there are some components, such as CommonDialog, that do not have a nameID and have no representation in an .RC file.

Two classes are derived from pResComponent: pVisualObject and pMenuBase. The former relates to objects that are windows, so it has a size, a style, etc. The latter relates to menus. pDialogBox and pVisualControl are derived from pVisualObject. All the controls, such as pRadioButton, pEdit, pComboBox, etc., inherit from pVisualControl, while an intermediate class, pCommCtrl is used for the common control. More than 50 components are defined.

The DfmConvert Function

.DFM-to-.RC conversion is implemented in the DfmConvert function (Listing 2). This function receives the name of the .DFM file to convert, two integers for generating the nameIDs of the Dialog Box and the controls, and two floating-point values required to resize the form. After all the necessary filenames have been set and the text version of the .DFM file has been generated and opened, DfmConvert creates a pDialogBox object and retrieves the required TForm properties, through the Parse function, which is a public member of pVCLComponent. The Parse function is the most important one; I'll come back to it later.

Next, the pointer to pDialogBox is added to a list of pVCLComponent pointers called obj_list. The RecurseFile function completes the task of retrieving the properties of all the other components. It continues to read the file, and whenever a new component is found, RecurseFile reads the component's name and type and passes them to GetVCLComponent. If the type is defined in the class hierarchy whose ancestor is pVCLComponent, an object of that type is created, and its pointer is returned; otherwise, a NULL value is returned. Next, program control returns to RecurseFile, which, if the component type has not been recognized, skips to the next one. Otherwise, RecurseFile inserts the component's pointer into obj_list, calls the Parse function to retrieve all the required properties of that specific object, and then recursively calls itself. This process stops when it reaches the end of the file.

At this point, all the information has been retrieved and preprocessed. Next, two more lists are created: one for the Menus and another for the Visual Controls. This extra step is performed because both component categories require some extra processing. The .RC file, for example, requires that controls be sorted in a particular order to properly handle the tab-key order. Therefore, two sort functions are required: the first for the tab-order arrangement and the second to avoid hiding one control beneath another — controls must be sorted according to the "level" they occupy on the screen (see Listing 2).

When all the components have been processed, it is relatively simple to convert them to resources. DfmConvert calls function WriteRcRh once for each element of the Menu list and the Visual Control list and once for the Dialog Box, to write the necessary entries into .RC and .RH files. WriteRcRh is declared virtual in pResComponent, so you can adapt its behavior by deriving a new class for the hierarchy and overriding the function in the derived class.

Special Processing

Some controls, such as images, require that items be added to the resource script file in different locations. For these controls, it is not possible to simply append some text at the end of the script file. For this reason, DfmConvert cannot store items directly into a file, but needs to prearrange the text in memory. To do this, DfmConvert uses a StrList object (Listing 3), which is a class that derives from a list<string> and adds a few new member functions. The public member function Insert_A_Before_B, for example, first looks for occurrence of the string B in the list and then inserts the string A before it. Another member function, WriteFile, stores all the strings in the list into a file.

Similar to writing resource script files, code files are automatically written by overriding the virtual function WriteCppHpp. The override provided by pDialogBox creates a Dialog Box procedure that contains only the famous switch(uMsg) statement and adds some of the unique comments defined in pvisualcomponent.h (not shown — provided in the online archive). All other components add the correct code at the desired position by calling StrList's Insert_A_After_B function and passing one of these comments as the B string.

The pResComponent member function

sl_It
HandleMessage(StrList& sl, const string& msg, const string& 
   code)

automatically inserts code into the case <MSG> clauses of message handlers, where MSG is a Windows message such as WM_PAINT, WM_INITIDIALOG, etc. If a case <MSG> statement does not already exist for that specific message, HandleMessage inserts it into the procedure's switch(uMsg) scope using the Insert_A_After_B function.

For a more detailed description of this mechanism, refer to the full source code listings (available at www.cuj.com/code).

The Parse Function

Now that I have demonstrated this utility's overall operation, I want to show how the Parse function works. This function retrieves the values associated with some of the component's properties and eventually processes them. Of course, a general approach, valid for all the derived classes, is required. The Parse function should read through the .DFM file and when it encounters the name of a property, store its value somewhere. For this purpose, I use STL maps to associate a property name string, which will be the key, with a pointer to a variable of a specific type (see Listing 3).

pVCLComponent declares four maps: one for Boolean values (BoolMap bm), one for integers (IntMap im), one for general text information that needs interpretation (StringMap sm_info), and one for quoted text (StringMap sm_text). In derived classes' constructors, I use the LookForXXXX member functions to create an association between the property name string and the pointer to the variable that will contain its value.

For example, the int version of the function

void pVCLComponent::LookForInt(
   const char *prop,
   int *dest,
   int def_val)
{  *dest = def_val;
   im.insert(make_pair(prop, dest));
}

first copies the value of def_val, an optional default value, into the variable pointed to by dest and then binds the pointer to that int to the string "prop".

Whenever the Parse function (Listing 4) encounters a property, it searches all the maps for the property's occurrence, using the property's name as a key. If Parse finds the property entry in a map, then it copies the property value read from the .DFM file into the associated variable. If Parse does not find the property entry in any of the maps, then it calls the virtual function ParseMore. This function can be overridden by derived classes to read more complex properties, such as arrays or binary data.

This lookup process is performed for every encountered property. Finally, when a component description is terminated, another virtual function, OnParseEnd, is called. This function gives derived classes the opportunity to perform some special processing that might be necessary before calling WriteXXXX. Such processing cannot be carried out until all the properties are read, because it requires knowledge of the complete set of properties.

A Pleasant Surprise

Because .DFM files are stored as unmodified custom resources within an executable, it is possible to extract them from programs and then apply the whole process of resource conversion and code generation. Because BCB and Delphi use the same resource format, you can also use this tool to convert a Delphi Form. For example I've extracted a Delphi freeware program's resources and converted them into a C program skeleton, reconstructing quite quickly the user interface. You'll find this utility in the full source code listings.

The Results

Figure 3 shows a form I built with VCL and then, with the help of this tool, converted to Windows controls. After converting the form, I wrote about 20 lines of code to initialize the Common Controls and the Rich Edit box, and to define a WinMain function that creates a Dialog Box with the resources and the generated window procedure. I added no other lines of code. Then, I built the non-VCL program with BCB, using the same compiler options. The results of the converted version appear in Figure 4. Even with some small differences, you can verify that the VCL form and the "standard Windows" version look very similar and a lot of controls are initialized.

Next, I compared the size of the two executables, both linked statically and with no debug information. While the file size of the original VCL application was more than 700 KB, the file size for the API version was only 64 KB (About an order of magnitude difference!). Also the differences in build times were impressive: 11.37 versus 4.95 seconds on my notebook.

Conclusion

This tool has some limitations. You may need to extend it to handle the situations that I haven't take into account. But I must confess, I didn't know where to stop in writing this article; there are many other features I didn't mention, in order to save space.

There are some Windows controls this tool does not support, and others it supports only partially. But because I see a lot of benefits in using this approach, especially for small applications, I invite you to contact me via email for an updated version of this tool or to contribute improvements. I will continue to use and update this tool. I'm also working on a version that generates code compatible with OWLNext, a freeware upgrade to Borland OWL.

The conversion of VCL events, which are also properties, into Windows messages remains to be explored.

Even with some limitations, I've really changed the way in which I actually work. There are several situations in which I don't need all the power of VCL, but just some BCB RAD facilities to generate a small executable or DLL. In my example, I've avoided writing many (hundreds) of lines of code, which were automatically generated by this utility; reduced the executable size to one-tenth; and cut the build time by more than two.

Luigi Bianchi lives in Rome and has a degree in Electronic Engineering from the University of Rome "La Sapienza." He is a freelance consultant and a researcher at the University of Rome "Tor Vergata," where he is involved in a Brain Computer Interface project and real-time processing of Biological data for disabled people. He has been working in C++ under Windows since 1994. He can be reached at theboss@luigibianchi.com.