Home > Articles > Programming > C/C++

  • Print
  • + Share This
This chapter is from the book

Creating User Interfaces

As you saw in Chapter 1, it is easy to create a user interface in C++Builder by combining forms with components. You can put application code in event handlers on the controls so they can react to mouse and keyboard actions.

But, as your user interfaces become more complex, controls piled on forms are not enough to make your programs manageable. In this section, you explore the next-level techniques required to deal with complex user interfaces.

Component Templates and Frames

As you develop your applications, you might develop a style of components for your user interfaces. For instance, your database user interfaces might typically have a TDBGrid to its right, a splitter to its left, and a TDBRecordView to the left of that.

C++Builder offers three ways to solve this problem, any of which is better than repeatedly dropping the same configuration of components on a form.

  • Use Component Templates—these are combinations of components that you select, set properties for, and put on the component palette as a group for later reuse. The reused components are independent of the template after they have been dropped on the form.

  • Use Frames—these are combinations of components placed on a form-like object, which are considered a cohesive whole. You can set properties and event handlers for these components and for the frame as a whole. Frames can be placed on the component palette in which case they can be dropped onto other user interfaces (forms or frames). They can also be placed in the Object Repository, which enables them to also be used as the basis for inheritance. Such frames can be used to extend an already created frame, and then changes to the original frame will also affect the descendant frame.

  • Create your own combined component by programming the creation of subcomponents into a new component. Component creation is covered in Chapter 4, "Creating Custom Components."

Component Templates

You can see a pair of components about to be turned into a component template in Figure 3.3:

Figure 3.3Figure 3.3 Creating a component template from a pair of components.

Figure 3.4 shows the form from which the template was derived, after the template was dropped on the form. You can see the component on the template, where it shows the icon of the first of the two components selected to make the template; the hint shows that the component is called TEchoedEdit, demonstrating that you can set a desired name for this new component. You can also see in the editor window that the dropped component template has inserted a copy of the original event handler, with the referenced component names changed.

Figure 3.4Figure 3.4 Template on the form.

What happens when the event handler refers to a component outside the template? That name is not changed. Of course, that will cause a compilation problem if you drop the template on a form that does not include the named external component.

So, component templates are useful, but they should be constructed carefully if they are to be successfully reused.

Frames

The word Frame is used to describe an object of the TFrame class or one of its descendants. Conceptually, a Frame can be thought of as a child Form, although in reality, a Frame is more closely related to a ScrollBox. Let's examine this argument more closely.

The TFrame class is a direct descendant of the TCustomFrame class, serving only to publish selected properties of TCustomFrame; no implementation code is added. The TCustomFrame class, in turn, descends from the TScrollingWinControl class. Both the TCustomForm and TScrollBox classes are also descendants of the TScrollingWinControl class.

The TScrollingWinControl class, a direct descendant of the TWinControl class, extends its parent class by providing support for horizontal and vertical scrollbars and management of the controls that it contains. The TCustomForm class extends the TScrollingWinControl class by providing support for aspects specific to top-level windows. The TScrollBox class extends the TScrollingWinControl class only via the BorderStyle property. However, the TCustomFrame class presents no additional properties or member functions to its parent class.

In short, a Frame is little more than a TScrollingWinControl object—a child window with scrolling support. At design time, a Frame most closely resembles a TForm object. At runtime, a Frame most closely resembles a TScrollBox object with its BorderStyle property set to bsNone.

The TCustomFrame Class

Because the TFrame class is a descendant of the TCustomFrame class and serves only to publish selected properties of its parent class, it is worth examining the TCustomFrame class in $(BCB)\Source\VCL\Forms.hpp.

Within the TCustomFrame constructor, the Width property is assigned a value of 320 and the Height property is specified as 240. Also from within the constructor, the following state flags are added to the ControlState property: csSetCaption, csAcceptsControls, csCaptureMouse, csClickEvents, and csDoubleClicks. In fact, apart from the csAcceptsControls flag, the other state flags are automatically set for all TControl descendants. It is the csAcceptsControls state flag that makes the Frame object a container control. In addition, similar to the case when working with a Form (or a Data Module) at design time, the TCustomFrame class can also contain nonvisual components. Because these nonvisual components should be streamed to the .DFM file with the Frame object, the TCustomFrame class extends (by overriding) the DYNAMIC GetChildren() member function in which a direct call to the supplied TGetChildProc-type callback function is made for each owned nonvisual component. Note that this is the same technique performed by the TCustomForm and TDataModule classes.

ActionLists are supported in the TCustomFrame class via the private AddActionList() and RemoveActionList() member functions. These functions serve to append and delete any ActionList objects (contained within the TFrame descendant class) to and from the internal ActionList array of the parent Form, respectively. As such, each of these member functions is called appropriately from within the overridden SetParent() (when the parent Form changes) and Notification() (when an ActionList is added/removed) member functions.

Working with Frames at Design Time

When a new TFrame descendant class is added to a project at design time, the IDE presents an instance of this class contained inside a Form Designer (a TWinControlForm object). You can then work with this class just like a form. For example, you can change aspects of the TFrame descendant class by changing any of its published properties. In fact, except for the TScrollBox::BorderStyle property, the properties (including event types) of the default TFrame descendant are identical to those of the TScrollBox class.

Manipulation of a TFrame descendant class within its own Form Designer makes changes to the class itself. This is identical to what happens with a TForm descendant class in its Form Designer. For example, when you drop a new component on a Form at design time, the header file of the TForm descendant class is updated to reflect this change. Similarly, when you change a property of a TFrame descendant class in its Form Designer, the changes affect either the class header file or the corresponding .DFM file. This is in contrast to working with most other components, where design time manipulation only affects a particular instance of the component class. For example, when you drop a TPanel component on a Form at design time, and then change the Color property of this Panel, you are only altering a single instance of the TPanel class, not the class itself. When utilizing components you expect that: In many situations, the creation of a custom component class for each instance of a particular component is not warranted. There's no need to create, for example, a TRedPanel class just to use a red-colored TPanel variation.

Of course, the C++Builder IDE supports the design time manipulation of a particular instance of a TFrame descendant class. For example, you can select the Frames icon from the Standard page of the Component Palette to add an instance of any TFrame descendant class to another container control, such as a Form, a Panel, or even another Frame, and then modify it. Or, you can add a TFrame descendant class to the Component Palette by picking Add To Palette from the pop-up menu that appears when the Frame is right-clicked in its Form Designer, from which you can reuse it elsewhere.

The values set in properties of any TFrame descendant class or its components are considered defaults for the frame's instances and descendants. If you do not set those properties in an instance or descendant, changes to the ancestor will take immediate effect in the descendant or instance.

For example, imagine you have a button in your frame class that has the value Big Button as the default for its caption. Drop an instance of this frame on your form. Then, change the caption of the button in the class to Former Big Button. The instance you dropped on the form will change to match. However, if you change the caption on the button in the instance of the frame on your form to X, and then change the caption of that same button in the class to Y, the instance will retain the caption X.

Working with Frames at Runtime

There is nothing special about working with a TFrame descendant class instance at runtime—it is just another type of TWinControl descendant. Indeed, a Frame most closely resembles a ScrollBox at runtime. Without the design time enhancements presented by the IDE and TCustomFrame class, there would be little advantage to using a Frame over a ScrollBox.

However, frames have more sophisticated resource management than most other controls. For example, if you place a TImage component on a Form, load the TImage component with a 1MB bitmap file, and then make a dozen copies of this TImage component, the result would be a significant increase in the size of your application. On the other hand, if you add a new Frame to your project, drop a TImage component on the Frame in its Form Designer (in other words, modify the TFrame descendant class), load the TImage component with a 1MB bitmap file, and then use a dozen instances of this Frame instead of the individual TImage objects, you would not see a significant increase in application size. This results from the fact that each Frame instance will share only one copy of the compiled bitmap resource. This is in contrast to using a dozen TImage instances, where a dozen copies of the bitmap would be compiled into your application's resources.

Creating a TFrame Descendant Class

As you have seen, creating a TFrame descendant class is simplified by the visual editing features of the IDE. This is in contrast to creating, for example, a TScrollBox descendant, which requires programming. You are also not required to package and register the component class when working with frames.

Practical Example: Using Frames to Create a Pop-Up Window

Many of the latest applications present temporary pop-up windows that contain different types of controls. For example, many of the toolbar buttons in Microsoft Office 2000 present what appear to be standard pop-up menus, but are actually custom topmost, captionless windows. Another common example of such a window is the drop-down list portion of a combo box control.

You can create your own version of a pop-up window via a TCustomFrame descendant class (see Figure 3.5). The interface for this class is provided in Listing 3.8.

Figure 3.5Figure 3.5 Using a Frame as a pop-up window containing a TreeView control.

Listing 3.8 TPopupFrame.h, a TFrame Descendant Class

//---------------------------------------------------------------------------//
#ifndef PopupFrameUnitH
#define PopupFrameUnitH
//---------------------------------------------------------------------------//
#include <Classes.hpp>
#include <Controls.hpp>
#include <ComCtrls.hpp>
//---------------------------------------------------------------------------//

class TPopupFrame : public TFrame
{
__published:
  TTreeView *TreeView1;
  void __fastcall TreeView1MouseMove(TObject *Sender, TShiftState Shift,
   int X, int Y);
  void __fastcall TreeView1MouseUp(TObject *Sender, TMouseButton Button,
   TShiftState Shift, int X, int Y);

private:
  TNotifyEvent OnCloseUp_;
  MESSAGE void __fastcall CMMouseEnter(TMessage& AMsg)
  {
   ReleaseCapture();
  }
  MESSAGE void __fastcall CMMouseLeave(TMessage& AMsg)
  {
   if (Visible) SetCapture(TreeView1->Handle);
  }

protected:
  virtual void __fastcall CreateParams(TCreateParams& AParams)
  {
   TFrame::CreateParams(AParams);
   AParams.Style = AParams.Style | WS_BORDER;
   AParams.ExStyle = AParams.ExStyle | WS_EX_PALETTEWINDOW;
  }
  virtual void __fastcall CreateWnd()
  {
   TFrame::CreateWnd();
   ::SetParent(Handle, GetDesktopWindow());
   SNDMSG(TreeView1->Handle, WM_SETFOCUS, 0, 0);
  }
  DYNAMIC void __fastcall VisibleChanging()
  {
   TFrame::VisibleChanging();
   if (Visible) ReleaseCapture();
   else SetCapture(TreeView1->Handle);
  }

public:
BEGIN_MESSAGE_MAP
  VCL_MESSAGE_HANDLER(CM_MOUSEENTER, TMessage, CMMouseEnter)
  VCL_MESSAGE_HANDLER(CM_MOUSELEAVE, TMessage, CMMouseLeave)
END_MESSAGE_MAP(TFrame)

public:
  __fastcall TPopupFrame(TComponent* AOwner);
  __property TNotifyEvent OnCloseUp = {read = OnCloseUp_, write = OnCloseUp_};
};
//---------------------------------------------------------------------------//
extern PACKAGE TPopupFrame *PopupFrame;
//---------------------------------------------------------------------------//
#endif

A pop-up window is actually a child of the desktop window (the WS_CHILD style bit is set) that is created with a combination of the WS_EX_TOOLWINDOW and WS_EX_TOPMOST extended style bits (represented by WS_EX_PALETTEWINDOW). The WS_CHILD bit prevents your Form from losing activation when the pop-up window itself is activated. The WS_EX_PALETTEWINDOW bit prevents the pop-up window from being obscured by other windows (WS_EX_TOPMOST) and from appearing in the dialog that's displayed when the end user presses the Alt+Tab keystroke combination (WS_EX_TOOLWINDOW).

To realize the style bit manipulation, you override the virtual CreateParams() member function:

  virtual void __fastcall CreateParams(TCreateParams& AParams)
  {
   TFrame::CreateParams(AParams);
   AParams.Style = AParams.Style | WS_BORDER;
   AParams.ExStyle = AParams.ExStyle | WS_EX_PALETTEWINDOW;
  }

First, you call the CreateParams() member function of the parent class so that (up the heirarchy ladder) the TWinControl can add the WS_CHILD bit, among others. Next, you add the WS_BORDER bit to the Style data member, and the WS_EX_PALETTEWINDOW bit to the ExStyle data member of the TCreateParams-type argument.

To change the parent of the pop-up window to the desktop window, you override the virtual CreateWnd() member function:

  virtual void __fastcall CreateWnd()
  {
   TFrame::CreateWnd();
   ::SetParent(Handle, GetDesktopWindow());
   SNDMSG(TreeView1->Handle, WM_SETFOCUS, 0, 0);
  }

First, you call the CreateWnd() member function of the parent class so that (up the heirarchy ladder) the TWinControl class can register the class with Windows and create the window via CreateWindowHandle() member function. Next, you use the SetParent() API function along with the GetDesktopWindow() API function to change the parent of the pop-up window to the desktop window. This manipulation ensures that the contents of the pop-up window are not clipped to the bounds of its usual parent (a Form). You also send the child TreeView control the WM_SETFOCUS message to fool it into thinking it has keyboard focus.

In addition to manipulating the style and parent of the pop-up window, you also need to manage its mouse-related events—specifically, those that should trigger the pop-up window to close. To this end, you make use of the VCL CM_MOUSENTER and CM_MOUSELEAVE messages and extend the virtual VisibleChanging() member function. You also make use of the readily available TTreeView::OnMouseMove and TTreeView::OnMouseUp events. Mouse capture is accordingly granted/removed from the TreeView control so that its OnMouseUp event handler will fire, even when a mouse button is released when the cursor is beyond the bounds of your pop-up window. Specifically, unless your pop-up window has captured the mouse, the WM_*BUTTONUP messages will not be sent when the cursor is located outside the client area of your pop-up window.

In fact, every step that you performed to create your pop-up window class could just as well have been performed without the use of Frames. However, the goal of this example was to demonstrate that the use of Frames can significantly simplify the process of implementing such a class. For example, you did not have to manually declare, and then assign the appropriate member functions to the TreeView's OnMouseMove and OnMouseUp events. Such a task can quickly become cumbersome when a component contains several different child components, each with a vast number of event handlers. For a slightly more complex example of a TFrame descendant class, examine the TMCIWndFrame class included in the companion CD-ROM as part of the Proj_VideoDemo.bpr project in the VideoDemo folder.

Finally, note that the redefinition of any virtual or dynamic member function applies only at runtime. In this case, your redefined version will not be called at design time. This would not be the case if you are going to place your component in a design time package and register it with the IDE. Indeed, when a TFrame descendant class is registered as a design time component, use of the csDesigning ComponentState flag might be needed.

Inheriting from a TFrame Descendant Class

Descendants of a TFrame descendant class can also be visually manipulated at design time. For example, if you create a descendant of your TPopupFrame class, it too will open in its own Form Designer and reflect the attributes of its parent class. The aforementioned rules of inheritance apply here as well—subsequent design time changes made to the parent class will be reflected in the descendant, but not vice versa.

It seems, however, that the IDE is not well-suited to handle visual Frame inheritance. That is, the procedure, although straightforward, is not entirely user friendly. The following steps are required to create a descendant of a TFrame descendant class that can be visually designed:

  1. Add the parent TFrame descendant class to the project (for example, TFrame2).

  2. Choose File, New Frame to add a new TFrame descendant class to the project (for example TFrame3).

  3. Edit the header and source files of the new TFrame descendant class (Unit3.h, Unit3.cpp), replacing all occurrences of the original parent class (TFrame) with the name of the new parent class (for example, TFrame2).

  4. Edit the .DFM file of the new descendant class, changing the keyword object with the keyword inherited.

Reusing Frames

Like forms, frames can be added to the Object Repository for use in subsequent projects. Frames can also be distributed to other developers by simply supplying the source code (interface and implementation) and the .DFM file. Moreover, as previously mentioned, a TFrame descendant class can be placed in a design time package and registered with the IDE much like any other custom component class. For example, to register a new TFrame descendant class TMyFrame, you simply use the PACKAGE macro where necessary, and then define the familiar ValidCtrCheck() and Register() functions in the class source file:

static inline void ValidCtrCheck(TMyFrame*)
{
  new TMyFrame(NULL);
}

namespace Myframeunit
{
  void __fastcall PACKAGE Register()
  {
     TComponentClass classes[1] = {__classid(TMyFrame)};
     RegisterComponents("Samples", classes, 0);
  }
}

However, there are some caveats when working with a Frame in a package at design time. First, when utilizing with the Package Editor, the IDE insists on opening the .DFM file of a TFrame descendant class as if it was a standard Form object. Consequently, the entries of the .DFM file are changed to reflect those of a TForm descendant class. For example, a normal default TFrame descendant class will exhibit the following entries:

object Frame2: TFrame2
 Left = 0
 Top = 0
 Width = 320
 Height = 240
 TabOrder = 0
end

On the other hand, the .DFM file for the same TFrame descendant class that has been mangled by the IDE will appear as follows:

object Frame2: TFrame2
 Left = 0
 Top = 0
 Width = 320
 Height = 240
 Color = clBtnFace
 Font.Charset = DEFAULT_CHARSET
 Font.Color = clWindowText
 Font.Height = -11
 Font.Name = 'MS Sans Serif'
 Font.Style = []
 OldCreateOrder = True
 PixelsPerInch = 96
 TextHeight = 13
end

Aside from the removal of the entry for the TabOrder property, the IDE appends entries that apply to descendants of the TForm class, but not to TFrame descendants (OldCreateOrder, for example). Moreover, after a TFrame descendant class is registered, subsequent attempts to open its source file (and thus, its .DFM file) will result in the aforementioned mangling (regardless of whether the Package Editor is in use). Note that if a .DFM should become inadvertently changed, you can simply append an entry for the TabOrder property, and then remove all nonapplicable entries.

Another side effect of registering a TFrame descendant class is that its contained components cannot automatically be manipulated at design time. For example, when included as a component template or simply added to a project, the contained components of a TFrame descendant class instance can be individually manipulated at design time. However, when working with a design-time instance of a registered TFrame descendant class, these contained components are no longer accessible; instead, other measures must be implemented (for example, individual property editors).

Closing Remarks on Frames

Admittedly, the design-time functionality presented by Frames is far from that necessary for true visual component development. At this stage, Frames are perhaps more useful for simply managing groups of controls rather than for creating complex components. Indeed, when system resources are an issue, the use of a TFrame descendant class might not be the most robust approach. Recall that the TFrame class is a TWinControl descendant, and thus each instance consumes a window handle. Still, frames are a step in the right direction, and the concept is sound. Undoubtedly, you can expect to see an increased level of sophistication in subsequent versions of C++Builder.

Coping with Different Screen Conditions

Another important issue in user interface design is managing windows as they are resized so that their components continue to occupy an optimal arrangement in the space available.

Fortunately, C++Builder provides a variety of mechanisms for allocating the window real estate:

  • Alignment—controls the location of visual components by specifying the relative space they should occupy to the left, right, top, bottom, and client (remainder) of the parent.

  • Anchor—controls the specific positioning of a visual component by anchoring one or more corners of the component.

In addition to these properties, two components are especially important in managing window layouts:

  • TScrollbox—a panel that forms scrollbars when it gets too small to show all the components it contains.

  • TSplitter—a panel that enables you to change the size of aligned controls it separates by dragging the splitter.

Alignment

TPanel and many other controls support alignment through the Align property.

Figure 3.6 shows the variations in alignment of a TPanel on a form.

Figure 3.6Figure 3.6 Alignment of panels on a form.

Figure 3.7 shows how the panels adjust in size as the window changes shape.

Figure 3.7Figure 3.7 Panels on a form change shape when aligned.

Figure 3.8 shows a fully aligned user interface with many aligned elements: grids, panels, tabbed pages. Each panel is separated by splitters allowing their relative sizes to be changed.

Figure 3.8Figure 3.8 A user interface with many aligned elements.

Anchors

This figure also shows some anchored elements—TButton, TEdit, TUpDown components. Such components do not offer the Align property, but they do offer the Anchor property.

Figure 3.9 shows what happens to buttons anchored in a variety of ways as their parent changes shape and size.

Figure 3.9Figure 3.9 The behavior of anchored buttons.

Notice that, unlike alignment, anchoring does not prevent elements from overlapping. Under alignment, a component can shrink to invisibility.

Alignment, Anchors, Splitters, and Scroll Boxes

As you can see in Figure 3.10, the TSplitter components work with alignment to allow user flexibility in changing the shape and size of portions of the user interface.

Figure 3.10Figure 3.10 The behavior of aligned panels and anchored buttons as TSplitters are moved.

By moving the splitters (the dark vertical lines at the right edge of the Left panel and on the left edge of the Right panel), the proportion of the form given to each of the aligned components can be changed at runtime. Notice how the shapes and sizes of the anchored components also change—within limits. When the limits are transgressed, such components disappear over the edge of their parent panel and are unreachable.

The TSplitter component needs to be a sibling of, and aligned the same as, the component it controls. Thus, the left splitter, which controls the left pattern, has its alignment set to alLeft; the right splitter is aligned alRight.

Because splitters and alignment can cause unanchored and anchored components alike to become unreachable, it is a good idea to use the TScrollBox for panels where that can occur.

In Figure 3.10, for instance, the top and bottom panels cannot be affected by either resizing or by the splitters. But the left, right and client panels can be. In Figure 3.11, you can see the effect of replacing the client-aligned panel with a client-aligned TScrollBox.

Figure 3.11Figure 3.11 The behavior of a TScrollBox as TSplitters are moved.

Although the anchors can squeeze some of the controls out of existence, those not affected by the size of the TScrollBox are still able to be reached even when the scrollbox starts to cut them off, simply by using the scrollbar.

Coping with Complexity in the Implementation of the User Interface

Although much of the complexity of the user interface resides in the layout and behavior of visual components, most of the real work of the application is done in the component event handlers.

Unfortunately, this can lead to various problems, especially when it is desired to share the actions of the user interface with several different visual representations. For instance, a typical application might reveal a particular feature as a menu entry, a toolbar icon, and a control in a dialog. For even more controllability, it might offer a macro language that enables control from external applications, perhaps through a COM interface.

C++Builder does allow event handlers to be shared. The event handlers can interrogate the Sender argument to determine the source of their invocation. This offers some ability to reduce the complexity of the user interface implementation.

There is another element of such user interfaces that presents a problem. Sometimes a feature might be disabled or an option can be checked off. Managing this in conventional event handlers usually results in the event handler having code that sets the appearance of every user interface element that represents the feature. That adds complexity to the event handler, and can lead to errors in the behavior of the user interface.

To ease the implementation of user interfaces, Borland introduced the TAction classes. These actions, are components that can respond to control events. Unlike event handlers, which are attached to components, components attach to actions. This reversal of direction helps to ensure that any number of components can share and be affected by the state of the action—for instance, whether it is enabled or checked.

Action instances can be organized into lists, represented by the TActionList class. Usually a given form or application will have a single action list.

One of the easiest ways to see a TActionList in action is to create an MDI (Multiple Document Interface) Application in the File, New menu dialog tab Projects.

Figure 3.12 shows the resulting interface with the action list open, and its File category selected. The File New action within that category is selected, and the Object Inspector is displaying the event properties for the action. Below all that is the Source Code Editor showing the default code automatically created for the event handler on the action.

Figure 3.12Figure 3.12 Editing an action event handler.

Naturally, you could add any additional code you might need to the event handler. Also of interest is that the application's menu entry for File, New and the toolbar icon for creating a new file (the white sheet of paper icon) both share the action and, through it, the event handler.

It is simple to create your own action list with actions, and associate each action with a user interface element like a menu entry or button.

Figure 3.13 shows a simple program with two actions and an action list.

Figure 3.13Figure 3.13 A simple Action List Program.

This example shows how the action contributes the caption to the menu and the check box label. If you change the caption on the action, all the controls displaying that text change to match the text you provided.

You can also see from the event handler for the EnableTheButtonAction that it sets the action for the button to be enabled or disabled based on its Checked state. When the action is disabled, both the button and the menu item will be disabled—and vice versa.

Action Manager

C++Builder 6 introduced the Action Manager and Action Bands. Action Bands enable you to create user interfaces by dragging and dropping actions onto special visual components: the TActionMainMenuBar and the TActionToolBar.

NOTE

TActionManager, TactionMainMenuBar, and TActionToolBar are not allowed in CLX programs at this time.

Figure 3.14 shows a design session with an Action Manager–oriented program. You can see the Action Manager window, which is displaying the list of actions and action categories. These actions are no different from those you create for an ActionList–oriented program.

Figure 3.14Figure 3.14 A session with the Action Manager.

What is different is the use of TActionMainMenuBar and TActionToolBar. Actions from Action Manager have been dragged and dropped onto the TActionToolBar, automatically forming buttons (the images are from the image list and are attached to the action so that they are consistent across the application. The categories have been dropped on the menu bar to form menu headings with items underneath them.

A number of properties affect the appearance and behavior of the menus, menu items, and the buttons. For instance, the Action Window contains Caption Options that control whether buttons on the toolbar show captions, and if captions are shown, whether they are always shown or if showing captions is selective.

Action Manager–driven menus and toolbars offer the same features present in many modern applications such as Microsoft office—including the capability to order menu items based on the usage during the session.

Another feature of the Action Manager is the capability to present the Action Manager to the user at runtime so that they can customize the tool bars and menu bars. Adding the Customize Action Bars standard action to the Action Manager and dragging and dropping it on the menu or tool bar are all you need to do.

Standard Actions

C++Builder offers a wide variety of standard actions such as Copy, Paste, File Open, and File Save As.

Some of these actions can be very useful either as everything you need, or as building blocks for enhancement. Here are some of the more interesting types:

Format actions—These actions affect the active TRichEdit on the form (if there is one) and alter the attributes of its selected text. They are attached to the button or menu item that causes the action.

  • TRichEditBold

  • TRichEditItalic

  • TRichEditUnderline

  • TRichEditStrikeOut

  • TRichEditBullets

  • TRichEditAlignLeft

  • TRichEditAlignRight

  • TRichEditAlignCenter

Help Actions

  • THelpContextAction—If this action is assigned to the Action List, the currently selected control's HelpContext property is forwarded to the Help Manager so that the appropriate help can be displayed. Note that this is not assigned to a control, but to the list, and that it operates with all the controls on the form.

File Actions

  • TFileOpen—Attach this to a control or menu item and put appropriate code in the OnAccept and OnCancel event of the action to make sure that the right things happen after the dialog is presented. The action's Dialog property can be used to find information about the file to be opened, and, at design or runtime, you can also use this to set the various properties that pertain to which files will be displayed.

  • TFileSaveAs—This is much like FileOpen, with an OnAccept and OnCancel event to be filled in with what you need to have done.

  • TFilePrintSetup—This action, like the other file actions, presents a dialog, which, in this case sets up the printer. Because the dialog reaches directly into the printer parameters, there is no need for additional processing.

  • TFileRun—This runs the specified application or file.

  • TFileExit—This closes the main form.

Search actions—Like the formatting actions, the search actions pertain to the active control, assuming it can accept search and/or replace operations. The dialogs automatically move the selection appropriately.

  • TSearchFind

  • TSearchFindFirst

  • TSearchReplace

  • TSearchFindNext

Tab (page control) Actions—These operate on the currently active tab or page control

  • TPreviousTab

  • TNextTab

List actions—These operate on the currently selected list control.

  • TListControlCopySelection

  • TListControlDeleteSelection

  • TListControlSelectAll

  • TListControlClearSelection

  • TListControlMoveSelection

  • TStaticListAction—This action supplies items to the target control or controls. On a TActionToolBar, it provides a drop-down list.

  • TVirtualListAction—This is similar to the TStaticListAction, except that it uses the OnGetItem event handler as its way of supplying items. This means it can get the items from other controls or from a database or some other source.

Dialog Actions—These actions provide the specified dialog, and offer the appropriate events to enable you to process the selection.

  • TOpenPicture

  • TSavePicture

  • TColorSelect

  • TFontEdit

  • TPrintDlg

Internet Actions

  • TBrowseURL—Launches the system default browser on the specified URL.

  • TDownLoadURL—This causes the specified URL to be downloaded to a local file. A periodic event occurs to report progress, and you can write code to do things like update a progress bar.

  • TSendMail—This enables the user to send a MAPI email message made from the material in the Text property.

Tools Actions

  • TCustomizeActionBars—Provides an Action Manager–based customization dialog so that the user can rearrange the content of the TActionMainMenuBar and the TActionToolBar.

Enhancing Usability by Allowing Customization of the User Interface

A good way to improve the usability of your interface is to enable the user to customize its appearance. This can be as simple as changing the color of different elements of the interface, or it can be as complex as allowing the user to undock parts of the interface or rearrange others. The ability to resize an interface is important, as is the ability to make only certain parts of the interface visible at any given time. Of all these, using color is probably the simplest. All you need to do is give the user access to the Color properties of the controls you use to create the interface. In some cases this might not be appropriate; for instance, when the interface is highly graphical because there might only be small areas of the interface suitable for such customization. A good way to meet the user's expectations in terms of color is to use the system colors when possible. The system colors are shown in Table 3.2 along with a brief description of what they are for.

Table 3.2 System Colors

System Color

Description

clBackground

Current background color of the Windows desktop

clActiveCaption

Current color of the title bar of the active window

clInactiveCaption

Current color of the title bar of inactive windows

clMenu

Current background color of menus

clWindow

Current background color of windows

clWindowFrame

Current color of window frames

clMenuText

Current color of text on menus

clWindowText

Current color of text in windows

clCaptionText

Current color of the text on the title bar of the active window

clActiveBorder

Current border color of the active window

clInactiveBorder

Current border color of inactive windows

clAppWorkSpace

Current color of the application workspace

clHighlight

Current background color of selected text

clHighlightText

Current color of selected text

clBtnFace

Current color of a button face

clBtnShadow

Current color of a shadow cast by a button

clBtnShadow

Current color of text that is dimmed

clBtnText

Current color of text on a button

clInactiveCaptionText

Current color of the text on the title bar of an inactive window

clBtnHighlight

Current color of the highlighting on a button

cl3DDkShadow

Dark shadow for three-dimensional display elements

cl3DLight

Light color for three-dimensional display elements (for edges facing the light source)

clInfoText

Text color for ToolTip controls

clInfoBk

Background color for ToolTip controls


For example, when displaying text in a window, use the clWindowText color. If the text is highlighted, use clHighlightText. These colors will already be specified to the user's preference and should, therefore, be a good choice for the interface. This section concentrates on the resizing, aligning, visibility, and docking capabilities of a user interface. The MiniCalculator provides all these features, so it is used as an example. The remainder of this section is broken into subsections, each giving an example of a particular technique.

Docking

In the some programs, the portions of the display can be undocked from the rest of the interface, and then positioned and resized independently. To make it possible to undock a panel from the main form, you must do three things:

  1. Set DragKind to dkDock.

  2. Set DragMode to dmAutomatic.

  3. Set DockSite to true.

You can do all this at design time using the Object Inspector.

This is enough to make a panel dockable, but to make it really do the job, a little more work is required.

Consider what changes, if any, you need to make when the panel is undocked from the form. A first thought might be to write a handler for the form's OnUnDock event. However, this might not be suitable if you are using a version of C++Builder that has the bug in the VCL that results in OnUnDock not being fired the first time a control is undocked. If you require any resizing, clearly it will not work as you expect. A better approach is to write a handler for panel's OnDockEnd event and check the value of the Floating property of the panel. If Floating is true and this is the first call to OnDockEnd, the control has been undocked. This event occurs at the same time as the OnUnDock event, so there is no perceptible difference to the user. The only additional requirement of using this method is that you must use a variable to indicate whether the call to OnEndDock is the first call in the docking action. This is because OnEndDock is called at the end of every move made by a docking control. You can use a bool variable, for instance FirstPanelEndDock, to indicate if the OnEndDock event is the first in the current docking sequence. This requires you to add the line

bool FirstPanelEndDock;

to the form's class definition and initialize it to true in the constructor:

FirstPanelEndDock = true;

The code required in the panel's OnEndDock event is shown in Listing 3.9.

Listing 3.9 Implementation of OnEndDock

void __fastcall TMainForm::PanelEndDock(TObject *Sender,
                      TObject *Target,
                      int X,
                      int Y)
{
  if(Panel->Floating)
  {
   SetFocus();
  }
  if(FirstPanelEndDock)
  {
   if(Panel->Floating) FirstPanelEndDock = false;
   Height = Height - Panel->Height;
  }
}

If this is the first time that Panel's OnEndDock event is fired in the current docking sequence (that is, Panel has just been undocked and FirstPanelEndDock is true), you resize the form by subtracting the Height of Panel from The form's current Height. You do this even if the control is not floating because you add the Height of Panel back to the form in the form's OnDockDrop event, which will be fired if Panel is not loating. This can occur the first time you try to undock Panel where it is possible to undock and dock Panel in the same docking action.

You can now undock Panel, and the form will be automatically resized appropriately. Notice that before you resize the form you first reset the FirstPanelEndDock to false, but only if Panel is Floating. Again, this is because the first time you undock the panel it is possible to undock and dock in the same action. Panel might not be Floating, and setting FirstPanelUnDock to false would mean that this code would not be executed the next time the panel is actually undocked.

Note that every time PanelEndDock() is called and Panel->Floating is true, you call SetFocus() for the form. This ensures that the form never loses input focus from the keyboard.

Docking Panel back onto the form is a bit more complicated than undocking it. First you must implement the form's OnGetSiteInfo event handler. This event passes a TRect parameter, InfluenceRect, by reference. This TRect specifies where on the form docking will be activated if a dockable control is released over it. This enables you to specify docking regions on a control for specific controls. You can specify a dockable region equal to the Height of Panel and the ClientWidth of the form starting at the top of the main form. The event handler is shown in Listing 3.10.

Listing 3.10 Implementation of the form ->OnGetSiteInfo

void __fastcall TMainForm::FormGetSiteInfo(TObject* Sender,
                      TControl* DockClient,
                      TRect& InfluenceRect,
                      TPoint& MousePos,
                      bool& CanDock)
{
  if(DockClient->Name == "Panel")
  {
   InfluenceRect.Left  = ClientOrigin.x;
   InfluenceRect.Top  = ClientOrigin.y;
   InfluenceRect.Right = ClientOrigin.x + ClientWidth;
   InfluenceRect.Bottom = ClientOrigin.y + DockClient->Height;
  }
}

The first thing you do inside FormGetSiteInfo() is check to see if the DockClient—the TControl pointer to the object that caused the event to be fired—is Panel. If it is, you define the docking site above which Panel can be dropped by specifying suitable values for the InfluenceRect parameter. You do not use the remaining parameters: MousePos and CanDock. MousePos is a reference to the current cursor position, and CanDock is used to determine if the dock is allowed. With CanDock set to false, the DockClient cannot dock.

You must now implement the form's OnDockOver event. This event enables you to provide visual feedback to the user as to where the control will be docked if the control is currently over a dock site (the mouse is inside InfluenceRect) and the control is dockable (CanDock == true). You use the DockRect property of the Source parameter, a TDragDropObject pointer, to define the docking rectangle that appears to the user. The implementation of OnDockOver is shown in Listing 3.11.

Listing 3.11 Implementation of OnDockOver

void __fastcall TMainForm::FormDockOver(TObject* Sender,
                    TDragDockObject* Source,
                    int X,
                    int Y,
                    TDragState State,
                    bool& Accept)
{
  if(Source->Control->Name == "Panel")
  {
   TRect DockingRect( ClientOrigin.x,
             ClientOrigin.y,
             ClientOrigin.x + ClientWidth,
             ClientOrigin.y + Source->Control->Height );

   Source->DockRect = DockingRect;
  }
}

When the docking control moves over its InfluenceRect (as defined in OnGetSiteInfo), the outline rectangle that signifies the control's position is snapped to the Source->DockRect defined in OnDockOver. This gives the user visual confirmation of where the docking control will be docked if the control is released. In this case, Source->DockRect is set equal to the Height of the control and the ClientWidth of the main form, with TRect starting at ClientOrigin. In fact, this is the same as the InfluenceRect specified in OnGetsiteInfo.

The remaining parameters are not used: X, the horizontal cursor position; Y, the vertical cursor position; State, of type TDragState, the movement state of the mouse in relation to the control; and Accept. Setting Accept to false prevents the control from docking.

Finally, you implement OnDockDrop. This event enables you to resize the control to fit the DockRect specified in the OnDockOver handler. It also enables you to perform any other processing that is needed, such as resizing the form or resetting the Anchors or Align property. The implementation for FormDockDrop is shown in Listing 3.12.

Listing 3.12 Implementation of the form ->OnDockDrop

void __fastcall TMainForm::FormDockDrop(TObject* Sender,
                    TDragDockObject* Source,
                    int X,
                    int Y)
{
  if(Source->Control->Name == "Panel")
  {
   Source->Control->Top = 0;
   Source->Control->Left = 0;
   Source->Control->Width = ClientWidth;

   // Allow space...
   Height = Height + Source->Control->Height;

   // Must reset the Align of Panel 
   Source->Control->Align = alTop;

   // Reset the FirstPanelEndDock flag
   FirstPanelEndDock = true;
  }
}

The implementation of FormDockDrop() as shown in Listing 3.12 is not as simple as it first appears. First you resize Panel to fit the top of the form. Then, you allow space for the docked panel by increasing the Height of the form by the Height of Panel. Next reset Panel->Align to alTop. You must do this as the Align property is set to alNone when Panel is undocked. Finally, you reset FirstPanelEndDock to true in readiness for the next time Panel is undocked.

Note that you must adjust the Height of the form before you reset the Align property of Panel to alTop. If Panel->Align is set to alTop before the form's Height is adjusted, the form's Height might be adjusted twice. This is because the form will be automatically resized to accommodate Panel if Panel->Align is alTop and there is not sufficient room. Subsequently, changing the form's Height manually results in twice as much extra height as was needed. Changing the Height of the form first circumvents this problem because there will always be enough room for Panel. When its Align property is set to alTop, no automatic resizing is required.

In many ways, the docking capabilities of this example are small, but they are sufficient. For a more involved example of docking in C++Builder, you should study the example project dockex.bpr in the $(BCB)\Examples\Docking folder of your C++Builder 5 or above installation.

Controlling Visibility

Offering users the ability to show or hide parts of the interface is a relatively easy way to allow user customization. By simply changing the Visible property of a control, you can control whether the control appears in the interface. This enables you to provide functionality that some users want, but that others might find a nuisance. Those that need the functionality can make the required controls visible, and those that don't want it can hide the controls. The main consideration with showing and hiding controls is that you must ensure that the appearance of the interface remains acceptable. In other words, hiding a control should not leave a large gap in the interface, and showing a control should not affect the current layout any more than necessary.

Customizing the Client Area of an MDI Parent Form

Allowing the user to customize the background of an MDI parent form, typically by adding an image to it, is not as easy as it first appears and, therefore, deserves a special mention. To do this, you must subclass the window procedure of the client window of the parent form. This is because the client window of the parent form is the background for the MDI child windows. You must draw on the client window, not the form itself. For more information about this, refer to the Win32 SDK online help under "Frame, Client, and Child Windows." To access the client window, use the form's ClientHandle property. To draw on the client window, you must respond to the WM_ERASEBKGND message. The image can be centered, tiled, or stretched. You should draw onto an offscreen bitmap, and then you use either the WinAPI BitBlt() or StretchBlt() function to draw the image onto the client window. This minimizes flicker. Second, you use the Draw() method to draw your image onto the Canvas of the offscreen bitmap. You do this rather than use BitBlt() because you want to support JPEG images. TJPEGImage derives from TGraphic and so implements the Draw() method, but TJPEGImage does not have a Canvas and so cannot be used with BitBlt().

Working with Drag and Drop

One of the very early features on Microsoft's Windows operating system was drag and drop. The nature of the mouse makes dragging and dropping things on the screen seem a very natural extension of human behavior. It is one of the very first things that new Windows users grasp and, as such, should be implemented in all your applications whenever it makes sense to do so. Fortunately the concept is pretty simple and C++Builder makes the implementation very easy.

The Solution

To enable drag-and-drop in your application, you must first inform the operating system that your application is ready to receive dropped files. You do this by calling the DragAcceptFiles() method from the Win32 API. You then need to handle the events that are created by the drop action. You do this by creating a message map and an event handler for the WM_DROPFILES message that will read the name of the dropped file and act accordingly.

The Code

To illustrate this concept, you can build an application that closely resembles the System Configuration Editor that ships with Windows. To see it in action, click your Start button, select Run, type Sysedit in the Run dialog box, and click OK. If you play with it a little bit, you will notice that the Sysedit application does not handle dropped files, but yours will. However, in the interest of brevity, that little application will not handle most of the other functionality of the Sysedit application, including allowing you to save edited files. That functionality is simple enough for you to implement yourself if you want. Notice in Figure 3.15 the DragDrop application displaying some of the readme files that come with C++Builder.

WARNING

Do not edit the contents of any of the child windows in the System Configuration Editor unless you know what you're doing. Depending on your version of Windows, these files tell the operating system how to start up properly. Making any mistakes in these files or entering improper values can result in a long night.

If you opened up the System Configuration Editor to look at it, close it now and let's go to work. Follow the instructions to create the DragDrop application, or you can just load it from the CD-ROM that accompanies this book.

  1. Start C++Builder and create a new application.

  2. Change Form1's name to MainForm.

  3. Create a new form called ChildForm. Set its ClientHeight and ClientWidth properties to about 250 and 350, respectively.

  4. Add a TRichEdit component from the Component Palette's Win32 tab to ChildForm, and set its Align property to alClient.

  5. Save the application by clicking the floppy disk stack on the C++Builder toolbar. Save the main form's unit as MainUnit.cpp, the ChildForm's unit as ChildUnit.cpp, and the project file as DragDrop.bpr.

Figure 3.15Figure 3.15 DragDrop at runtime.

Now that your form is done, it's time to add the code to the event handlers.

To inform the operating system that you want to accept dropped files, you need to call the DragAcceptFiles() method. The best place to do this is in the constructor for the main form. Select the MainForm from the tabs on the Source Code Editor and put the following line in its constructor:

DragAcceptFiles(Handle, True);

To create an event handler for the DragDrop event, open the header file for the main form by right-clicking the MainUnit.cpp tab in the code editor and select Open Header, Source File from the pop-up menu. Insert the following code in the public section of the TMainForm class declaration:

class TMainForm : public Tform
{
__published:  // IDE-managed Components
  void __fastcall FormCreate(TObject *Sender);
private:    // User declarations
  void virtual __fastcall WMDropFiles(TWMDropFiles &message);
public:     // User declarations
  __fastcall TMainForm(TComponent* Owner);
  BEGIN_MESSAGE_MAP
  MESSAGE_HANDLER(WM_DROPFILES, TWMDropFiles, WMDropFiles)
  END_MESSAGE_MAP(TForm);
};

Now switch back to the MainUnit.cpp file and add the code from Listing 3.13 to the end. You can leave out all the comments if you want.

Listing 3.13 WMDropFiles Event Handler

// fires an event when a file, or files are dropped onto the application.
void __fastcall TMainForm::WMDropFiles(TWMDropFiles &message)
{
  AnsiString FileName;
  FileName.SetLength(MAX_PATH);

  int Count = DragQueryFile((HDROP)message.Drop, 0xFFFFFFFF, NULL, MAX_PATH);

  // index through the files and query the OS for each file name...
  for (int index = 0; index < Count; ++index)
  {
    // the following code gets the FileName of the dropped file. it
    // looks cryptic but that's only because it is. Hey, Why do you think
    // Delphi and C++Builder are so popular anyway? Look up DragQueryFile
    // the Win32.hlp Windows API help file.
    FileName.SetLength(DragQueryFile((HDROP)message.Drop, index,FileName.c_str(), MAX_PATH));

    // examine the filename's extension.
    // If it's a text or Rich Text file then ...
    if (UpperCase(ExtractFileExt(FileName)) == ".TXT" || UpperCase(ExtractFileExt(FileName)) == ".RTF")
    {
      // create a new child form...
      TChildForm *Viewer = new TChildForm(Application);
      // display the file...
      Viewer->Caption = FileName;
      Viewer->RichEdit1->Lines->LoadFromFile(FileName);
      Viewer->Show();
    }
  }
  // tell the OS that you're finished...
  DragFinish((HDROP) message.Drop);
}

To prevent the application from leaking memory, you need to ensure that the memory is properly freed when each viewer is closed. Select the ChildForm in the Object Inspector. Switch to the Events tab and double-click the OnClose event to create the OnClose event handler. Insert the following code into the event handler:

Action = caFree;

Open MainForm.cpp in the code editor, select File, Include Unit Header and select the child form. This makes MainForm aware of the ChildForm so that the compiler knows what you are talking about when you refer to the child form.

Compile and execute the application. When you drag a plain text or rich text file into the application, it will open a simple text viewer in its client area.

How Does It Work?

When the application initializes and creates the MainForm, it calls the Win32 API method DragAcceptFiles(), passing the application's handle and the value true indicating to the OS that the application is ready to accept dropped files. Passing false to the OS will disable drag and drop in your application.

If drag and drop is enabled in your application, the application will receive a WM_DROPFILES message from Windows for each file it receives. For the application to properly handle these messages, you must define a MESSAGE_HANDLER macro, which is a structure that associates a particular Windows message with one of the application's custom message handlers. The DragDrop application's message map associates the WM_DROPFILES message with the WMDropFiles message handler.

Inside the WMDropFiles message handler, the DragQueryFile() method will query the OS for information about the dropped files. The following is Microsoft's definition of the DragQueryFile() method. It can be found in Win32.hlp.

The DragQueryFile() function retrieves the filenames of dropped files.
UINT DragQueryFile(
  HDROP hDrop,  // handle to structure for dropped files
  UINT iFile,  // index of file to query
  LPTSTR lpszFile,  // buffer for returned filename
  UINT cch   // size of buffer for filename
  );
Parameters:
hDrop
Identifies the structure containing the filenames of the dropped files.
iFile
Specifies the index of the file to query. If the value of the iFile parameter is 0xFFFFFFFF, DragQueryFile() returns a count of the files dropped. If the value of the iFile parameter is between zero and the total number of files dropped, DragQueryFile() copies the filename with the corresponding value to the buffer pointed to by the lpszFile parameter.
lpszFile
Points to a buffer to receive the filename of a dropped file when the function returns. This filename is a null-terminated string. If this parameter is NULL, DragQueryFile() returns the required size, in characters, of the buffer.
cch
Specifies the size, in characters, of the lpszFile buffer.
Return Values:
When the function copies a filename to the buffer, the return value is a count of the characters copied, not including the terminating null character.
If the index value is 0xFFFFFFFF, the return value is a count of the dropped files.
If the index value is between zero and the total number of dropped files and the lpszFile buffer address is NULL, the return value is the required size, in characters, of the buffer, not including the terminating null character.

When the DragDrop application receives a dropped file, it fires the WMDropFiles() method, which uses the Message handle to query the operating system for the number of files dropped. It then iterates through the file list, examining each file's extension looking for a .txt or .rtf extension. If the file has one of those extensions, the application creates an instance of ChildForm and loads the file into the TRichText component for display to the user. As each ChildWindow is closed, it calls the caFree action, which releases the memory associated with the ChildForm's instance.

Wrapping Up Drag and Drop

Although there's no default VCL wrapper for drag and drop, C++Builder makes it pretty easy to implement it in your applications. If you have neglected adding this capability because you thought it would be too hard, you've just been empowered.

  • + Share This
  • 🔖 Save To Your Account