Extending Action Manager styles

By: Jeremy North

Abstract: Article describing how to extend the Action Manager components to draw new menu features.

Extending Action Manager styles

This article describes how to extend the Action Manager drawing styles focusing on new ways to draw menu items. The new menu items are rich content (menu items with descriptions) and textual separators (separators with captions) for applications that use the TActionManager component.



Brief History

The Action Manager components first appeared in Delphi 6. They were designed to be extensible and you will be taking advantage of this extensibility to enhance the XP Action Manager style.
The components in the Action Manager set are TActionMainMenuBar, TActionToolBar, TPopupActionBar, TActionManager, TXPColorMap, TStandardColorMap and TTwilightColorMap.


The two new menu item drawing styles are:

    Rich Content - menu item which includes the hint under the actual text for the menu item.
    Textual Separator - menu separator that has a caption. These separators can be used in either context or main menus. The existing separator style is also supported.

    Hide image


Implementation choices


There are three ways in which the new style could have been implemented:

  1. Using the Hint property as the rich content and modifying the caption to include the textual separators caption
  2. Defining a new TActionClientItem descendant and adding new properties to this class to draw the added items. Further effort is required to achieve design time support however.
  3. Defining new TAction descendants and publishing the new properties. I have actually done this for my personal implementation and while it does support design time drawing it isn't a good way to do this for existing applications. For new applications it is great because my Rich Content action type had a ContentFont property which allowed me to draw the Rich Content in a totally different font if I wished.

What is the best way then?
The way I am showing you of course! If the second way (custom TActionClientItem) worked at design time that might be the best way to go.


Getting Started

There are two ways the code in this article can be implemented. The first way is by compiling the updated style directly into your application and using it at run time. The second is to register a new Style and use it at design time.

  1. Download the CodeCentral submission
  2. Extract the XPExActnCtrls.pas unit and save to your project source folder
  3. Start your Delphi version that supports TActionManager and load your project
  4. Add the unit to your project (optional as long as it can be found on the library path)
  5. Add the unit to the implementation uses clause for the unit that has the TActionMainMenuBar and TPopupActionBar components
  6. Select your TActionMainMenuBar component and select the Events tab in the Object Inspector
  7. Generate an OnGetControlClass event by double clicking on it
  8. Add the following code to the Event:
    if not (Sender is TActionMainMenuBar) then
    ControlClass := TXPRichContentMenuItem;
  9. Repeat the same for the TPopupActionBar components OnGetControlClass event except you DO NOT need the if statement
  10. Run your application

You now might want to modify or add hints to your various menu items.

Where are my textual separators?
There is one thing that is required to get the textual separators working. You have to modify the caption of your separator items to be the following:

- <Separator Caption>

dash space caption to display

The full caption for the textual separator shown in the screenshot above is: - Edit Items

If the caption is just a - (dash) then the default separator is drawn.


As mentioned previously, the second approach to using the new menu item styles is to create a new Action Manager style.


Defining the new style

For the Action Manager to be able to draw these new menu items you need to create and register a new TActionManagerStyle. As this new style is just an extension of the existing XP Style, there are not many changes required.
You need to descend from the TXPStyleActionBars class because you want the new style to be based on the existing XP style.



Your unit should look something like:
uses
  XPStyleActnCtrls;

type
  TXPExStyleActionBars = class(TXPStyleActionBars)

  end;

For this implementation two methods need to be overridden; StyleName and GetControlClass.

Your class should look like the class declaration below:

  
TXPExStyleActionBars = class(TXPStyleActionBars)
public
  function GetControlClass(ActionBar: TCustomActionBar;
    AnItem: TActionClientItem): TCustomActionControlClass; override;
  function GetStyleName; override;
end;

Return the name of the new style in the GetStyleName method.

  
function TXPExStyleActionBars.GetStyleName: string;
begin
  result := 'New XP Style';
end;

Delphi 2005 and up users
You may want to press Ctrl+Shift+L inside the string and generate a resourcestring instead of hardcoding the result.


The GetControlClass method is implemented as follows:

function TXPExStyleActionBars.GetControlClass(ActionBar: TCustomActionBar;
  AnItem: TActionClientItem): TCustomActionControlClass;
begin
  if ActionBar is TCustomActionPopupMenu then
    result := TXPExStyleRichContent
  else
    result := inherited GetControlClass(ActionBar, AnItem);
end;

GetControlClass determines what class will be used to draw the ActionClientItem that is passed in. Since we only have one new control class, this method is straight forward. If the ActionBar is a TCustomActionPopupMenu then return the new control class. Notice how you only have to test for TCustomActionPopupMenu, this is because even though the menus appear on the main menu bar they are actually implmented using the custom popup menu class.

Registering the new style
To register the new style an instance of the Style has to be created and registered in the initialization section of the unit. Make sure you unregister and free the instance in the finalization section of the same unit.

var
  XPExStyle: TXPExStyleActionBars;

...
 
initialization
  XPExStyle := TXPExStyleActionBars.Create;
  RegisterActnBarStyle(XPExStyle);
  
finalization
  UnregisterActnBarStyle(XPExStyle);
  XPExStyle.Free;

To make this style the default style for the TActionManager include the following in the initialization section.

DefaultActnBarStyle := XPExStyle.GetStyleName;

It is now time to create the TXPExStyleRichContent class.



Creating the new control class
Create and save a new unit as XPExActnCtrls.pas.
The class in this unit is responsible for drawing the rich content and textual separators. Once again take advantage of the existing XP Style classes and descend from TXPStyleMenuItem.

Why descend from the TXPStyleMenuItem?
The TXPStyleMenuItem is the class responsible for drawing the main menu and context menu items.

The class heirarchy for TXPStyleMenuItem is.

TXPStyleMenuItem -> TCustomMenuItem -> TCustomActionControl -> TGraphicControl

There are a lot of methods that can be overridden by new control classes.


The following methods need to be overridden


    DrawBackground - control the drawing of the menu items in the selected state.
    DrawGlyph - limit the height of a checked menu items background.
    DrawShadowText - control the drawing of the disabled text.
    DrawText - the text needs to be bold and text appears underneath it.
    DrawUnusedEdges - only call inherited for this method when the menu item isn't a textual separator.
    CalcBounds - this is no normal menu item anymore, the height can be variable depending on content.

The final class definition has the following members. Private variables have been removed.

type
  TXPExStyleRichContent = class(TXPStyleMenuItem)
  private
    function IsSeparator: Boolean; {$IFDEF INLINE}inline;{$ENDIF}
    function SeparatorCaption: string; {$IFDEF INLINE}inline;{$ENDIF}
    function HasContent: Boolean; {$IFDEF INLINE}inline;{$ENDIF}
    function GetContent: string; {$IFDEF INLINE}inline;{$ENDIF}
    procedure UpdateShortCutBounds;
  protected
    function CaptionHeight: Integer; virtual;
    function ContentHeight: Integer; virtual;
    function ContentWidth: Integer; virtual;
    function CaptionWidth: Integer; virtual;
    procedure DrawBackground(var PaintRect: TRect); override;
    procedure DrawGlyph(const Location: TPoint); override;
    procedure DrawShadowedText(Rect: TRect; Flags: Cardinal; Text: String; 
	 	TextColor: TColor; ShadowColor: TColor); override;
    procedure DrawText(var Rect: TRect; var Flags: Cardinal; Text: String); override;
    procedure DrawUnusedEdges; override;
  public
    constructor Create(AOwner: TComponent); override;
    procedure CalcBounds; override;
  end;

Delphi 2005 and Delphi 2006 supports function inlining so make sure function inlining is used when either Delphi 2005 or Delphi 2006 is used.
INLINE is defined when Delphi 2005 or above is used

{$IF COMPILERVERSION > 17.0}
  {$DEFINE INLINE}
{$IFEND}



The code included in the download is commented but I will just highlight a couple of items that might be of interest.

The UpdateShortCutBounds method
This method updates the width allowed for drawing the Short Cut on the menu item. It is needed because in the default style (XP Style) the shortcut bounds is calculated on a non bolded font. The new style uses a bolded font so the bounds has to be recalculated. Unfortunately there was no method to override to do this within the style architecture but fortunately we are able to get at the properties needed to make sure the shortcut bounds is correct.

The DrawGlyph method looks the same as the code in the descendant control
Very true. There are only two places where it is changed and both are due to drawing checked menu items. The calculated rect for the check item uses the Height of the control to set the bounds. With rich content on a menu item the height is larger than what the original style expects and looks out of place. The CaptionHeight method is now called to set the rect height for any checked menu items.

IsSeparator, HasContent, GetContent and SeparatorCaption methods
These are all helper functions to see if the menu item should draw with the new style. The HasContent returns true if the Hint property length is greater than 0. If you are using Delphi 2005 or Delphi 2006 these methods are inlined.

CalcBounds
CalcBounds is called before the menu is displayed to determine the size of the required menu item. Unfortunately when CalcBounds is called, some property values or not yet set for the ActionClient property for the menu item. This causes us to delay the setting of the true height and width until a future CalcBounds call (it is called more than once prior to displaying the menu) is made and either the Action or Caption properties have been set. We test both the Action and Caption properties because some menu items might not have an action associated with them. For example, separators don't.

DrawBackground
The DrawBackground method is responsible for drawing the gradient effect when an item is selected. The colors used to draw this effect are from the TGradientColors class included in the unit. All are based around the XP Styles existing Selected Color. This method also prevents a selected background being drawn when the textual separators are being drawn.

CaptionHeight, ContentHeight, ContentWidth and CaptionWidth methods
These are new methods that are called often during the drawing process and return what they say they do. The ContentWidth returns a constant plus a little border value for the width of the content item. With other implementations the minimum width for the item could be a property set at design time.


Further enhancements

To further enhance the textual separator you might include:

  • A right aligned bitmap
  • Allowing the caption to drawn at different justifications (left, center or right)

Additional properties for a rich content menu item might include:

  • Specific width for the content
  • Storing the content in a TStrings type to allow for new lines
  • Drawing the content with a different Font (name, color or size)



Something Extra

Included in the download is an another package that can be installed into the IDE. This package enables the menu drawing for the IDE. Full source code is included.
Open the ExtendingIDE100 project, compile and install the package.

This was only tested under Delphi 2006 but should work for all IDE versions that use the Action Managers main menu toolbar.

Hide image



Source Code

The source code for this article is available from CodeCentral.
Included in the download is the package required to install the new Action Manager style as well as a simple demo application showing the action style being used.

The screenshots below are from the demo application.

Hide image

Hide image



A new Action Manager style is in progress...

NOTE: Very early screenshots.

Hide image

Hide image

Hide image

Some may not realise but my QualityCentral windows client (called JED, QC) also uses a custom Action Manager style. Based on the Office 2003 toolbar look and includes support for XP themes.

Further Details

For more details about what Jeremy is currently working on, be sure to check out his blog and website.

He is currently celebrating the release of a new Borland Developer Studio IDE add in called Visual Forms.

For more information on Visual Forms be sure to checkout the Visual Forms website.




Server Response from: ETNASC04