The Road to Delphi

Delphi – Free Pascal – Oxygene


11 Comments

Exploring Delphi XE2 – VCL Styles Part II

The TStyleHook Class

The VCL Styles uses the TStyleHook class to intercept the paint methods and the Windows messages related to the Vcl Styles operations, If you have a custom control or need change the way how a control like a TEdit, TMemo, TListView, etc.  is painted you must create a new Style hook class inheriting from TStyleHook or using a existing  like TEditStyleHook, TComboBoxStyleHook, TMemoStyleHook ans so on .  After of creating your own hook you need to register your new style hook class using the RegisterStyleHook method. Also you can unregister a register style hook using the UnRegisterStyleHook procedure.

Check these samples of using custom TStyleHook classes

A Real World Sample

All the above links are about fixing bugs related to the VCL Styles. but that is not all what you can do do with the TStyleHook classes, for example check this image of a TSynEdit component inside of an VCL application with has the Carbon Vcl Style applied.

As you can see the scrollbars are not painted using the selected VCL style. So what I can do? In this case you can write a new style hook class or use an existing hook style. After of quick look for the existing style hook classes in the Vcl.StdCtrls unit, you will discover that the TMemoStyleHook class can do the work.

So writting just one line of code

TStyleManager.Engine.RegisterStyleHook(TCustomSynEdit, TMemoStyleHook);

The magic is done.

A Little of hack

When you need register a style hook using the RegisterStyleHook method, if you try to register the same hook class twice, an EStyleEngineException will be raised, So before to try to register a new hook class you must check if is the hook is already registered for a specific control or use a place like the initialization part of a unit to register the hooks . Unfortunally an extensive part of the VCL styles logic and the collections containing the registered style hooks, is contained in sealed classes, strict private vars and strict private static classes. So that information is not accessible directly. Because that, tasks how list the registered hooks, check if a hook has previously registered or if a class has a register style hook are not trivial.

The TCustomStyleEngine class (of the Vcl.Themes unit) has a strict protected class property called RegisteredStyleHooks this property points to a TDictionary declarated (as a private type) like this

    TStyleHookList = TList<TStyleHookClass>;
    TStyleHookDictionary = TDictionary<TClass, TStyleHookList>;

The info contained in this property is very usefull, but due to his visibility (strict protected class property) you need to use a hack to extract such info.

So using a helper class for the TCustomStyleEngine class you can gain access to the TDictionary with the registered hooks

type
  TStyleHookList = TList<TStyleHookClass>;  //you must need declare this type again because are declarated in the private section of the TCustomStyleEngine and are not visible
  TStyleHookDictionary = TDictionary<TClass, TStyleHookList>;//you must need declare this type again because are declarated in the private section of the TCustomStyleEngine and are not visible
  TCustomStyleEngineHelper = Class Helper for TCustomStyleEngine
  public
    class function GetRegisteredStyleHooks : TStyleHookDictionary;
  end;

And now using this helper class you can construct additional functions to work with the Style hooks.

Procedure ApplyEmptyVCLStyleHook(ControlClass :TClass); 
Procedure RemoveEmptyVCLStyleHook(ControlClass :TClass);
function  IsStyleHookRegistered(ControlClass: TClass; StyleHookClass: TStyleHookClass) : Boolean;
function  GetRegisteredStylesHooks(ControlClass: TClass) : TStyleHookList;

Using these functions you can list all the registerd style hooks in the system like so

var
 RttiType  : TRttiType;
 Item      : TListItem;
 List      : TStyleHookList;
 StyleClass: TStyleHookClass;
begin
  for RttiType in TRttiContext.Create.GetTypes do
    if RttiType.IsInstance and RttiType.AsInstance.MetaclassType.InheritsFrom(TComponent) then
    begin
       List:=GetRegisteredStylesHooks(RttiType.AsInstance.MetaclassType);
       if Assigned(List) then
       begin
         Item:=ListViewStyleHooks.Items.Add;
         Item.Caption:=RttiType.Name;

         for StyleClass in List do
          Item.SubItems.Add(StyleClass.ClassName);
       end;
    end;
end;

This is the code of the unit containing all the above code

unit uVCLStyleUtils;

interface

Uses
  Vcl.Themes,
  Vcl.Styles,
  Generics.Collections,
  Classes;

type
  TStyleHookList = TList<TStyleHookClass>;

Procedure ApplyEmptyVCLStyleHook(ControlClass :TClass);
Procedure RemoveEmptyVCLStyleHook(ControlClass :TClass);
function  IsStyleHookRegistered(ControlClass: TClass; StyleHookClass: TStyleHookClass) : Boolean;
function  GetRegisteredStylesHooks(ControlClass: TClass) : TStyleHookList;

implementation

uses
 Sysutils;

type
  TStyleHookDictionary = TDictionary<TClass, TStyleHookList>;
  TCustomStyleEngineHelper = Class Helper for TCustomStyleEngine
  public
    class function GetRegisteredStyleHooks : TStyleHookDictionary;
  end;


class function TCustomStyleEngineHelper.GetRegisteredStyleHooks: TStyleHookDictionary;
begin
  Result:= Self.FRegisteredStyleHooks;
end;

function  IsStyleHookRegistered(ControlClass: TClass; StyleHookClass: TStyleHookClass) : Boolean;
var
  List : TStyleHookList;
begin
 Result:=False;
    if TCustomStyleEngine.GetRegisteredStyleHooks.ContainsKey(ControlClass) then
    begin
      List := TCustomStyleEngine.GetRegisteredStyleHooks[ControlClass];
      Result:=List.IndexOf(StyleHookClass) <> -1;
    end;
end;

function  GetRegisteredStylesHooks(ControlClass: TClass) : TStyleHookList;
begin
 Result:=nil;
    if TCustomStyleEngine.GetRegisteredStyleHooks.ContainsKey(ControlClass) then
      Result:=TCustomStyleEngine.GetRegisteredStyleHooks[ControlClass];
end;

Procedure ApplyEmptyVCLStyleHook(ControlClass :TClass);
begin
   if not IsStyleHookRegistered(ControlClass, TStyleHook) then
    TStyleManager.Engine.RegisterStyleHook(ControlClass, TStyleHook);
end;

Procedure RemoveEmptyVCLStyleHook(ControlClass :TClass);
begin
   if IsStyleHookRegistered(ControlClass, TStyleHook) then
    TStyleManager.Engine.UnRegisterStyleHook(ControlClass, TStyleHook);
end;


end.

Check the full source code on Github.


6 Comments

Adding a Standard Context Popup Menu to a SynEdit

When you uses an Edit control like a TMemo or TEdit component a context menu pops up with options to undo, copy, paste, select all, etc.

Unfortunally the TSynEdit component doesn’t include a menu like this, so you must write you own. You can fix this in a few lines of code using a interposer class. Check this unit which implements a standard context menu for a TSynEdit component, based on a TActionList (only be sure to include the uSynEditPopupEdit unit after of the SynEdit unit in your uses list).

unit uSynEditPopupEdit;

interface

uses
 ActnList,
 Menus,
 Classes,
 SynEdit;

type
  TSynEdit = class(SynEdit.TSynEdit)
  private
    FActnList: TActionList;
    FPopupMenu : TPopupMenu;
    procedure CreateActns;
    procedure FillPopupMenu(APopupMenu : TPopupMenu);
    procedure CutExecute(Sender: TObject);
    procedure CutUpdate(Sender: TObject);
    procedure CopyExecute(Sender: TObject);
    procedure CopyUpdate(Sender: TObject);
    procedure PasteExecute(Sender: TObject);
    procedure PasteUpdate(Sender: TObject);
    procedure DeleteExecute(Sender: TObject);
    procedure DeleteUpdate(Sender: TObject);
    procedure SelectAllExecute(Sender: TObject);
    procedure SelectAllUpdate(Sender: TObject);
    procedure RedoExecute(Sender: TObject);
    procedure RedoUpdate(Sender: TObject);
    procedure UndoExecute(Sender: TObject);
    procedure UndoUpdate(Sender: TObject);
    procedure SetPopupMenu_(const Value: TPopupMenu);
    function  GetPopupMenu_: TPopupMenu;
  public
    constructor Create(AOwner: TComponent); override;
    destructor Destroy; override;
  published
    property PopupMenu: TPopupMenu read GetPopupMenu_ write SetPopupMenu_;
  end;

implementation

uses
 SysUtils;

const
 MenuName='uSynEditPopupMenu';

procedure TSynEdit.CopyExecute(Sender: TObject);
begin
  Self.CopyToClipboard;
end;

procedure TSynEdit.CopyUpdate(Sender: TObject);
begin
  TAction(Sender).Enabled :=Self.SelAvail;
end;

procedure TSynEdit.CutExecute(Sender: TObject);
begin
  Self.CutToClipboard;
end;

procedure TSynEdit.CutUpdate(Sender: TObject);
begin
  TAction(Sender).Enabled :=Self.SelAvail and not Self.ReadOnly;
end;

procedure TSynEdit.DeleteExecute(Sender: TObject);
begin
  Self.SelText := '';
end;

procedure TSynEdit.DeleteUpdate(Sender: TObject);
begin
  TAction(Sender).Enabled :=Self.SelAvail and not Self.ReadOnly;
end;

procedure TSynEdit.PasteExecute(Sender: TObject);
begin
 Self.PasteFromClipboard;
end;

procedure TSynEdit.PasteUpdate(Sender: TObject);
begin
 TAction(Sender).Enabled :=Self.CanPaste;
end;

procedure TSynEdit.RedoExecute(Sender: TObject);
begin
 Self.Redo;
end;

procedure TSynEdit.RedoUpdate(Sender: TObject);
begin
 TAction(Sender).Enabled :=Self.CanRedo;
end;

procedure TSynEdit.SelectAllExecute(Sender: TObject);
begin
 Self.SelectAll;
end;

procedure TSynEdit.SelectAllUpdate(Sender: TObject);
begin
 TAction(Sender).Enabled :=Self.Lines.Text<>'';
end;

procedure TSynEdit.UndoExecute(Sender: TObject);
begin
 Self.Undo;
end;

procedure TSynEdit.UndoUpdate(Sender: TObject);
begin
 TAction(Sender).Enabled :=Self.CanUndo;
end;

constructor TSynEdit.Create(AOwner: TComponent);
begin
  inherited;
  FActnList:=TActionList.Create(Self);
  FPopupMenu:=TPopupMenu.Create(Self);
  FPopupMenu.Name:=MenuName;
  CreateActns;
  FillPopupMenu(FPopupMenu);
  PopupMenu:=FPopupMenu;
end;

procedure TSynEdit.CreateActns;

 procedure AddActItem(const AText:string;AShortCut : TShortCut;AEnabled:Boolean;OnExecute,OnUpdate:TNotifyEvent);
 Var
    ActionItem  : TAction;
  begin
    ActionItem:=TAction.Create(FActnList);
    ActionItem.ActionList:=FActnList;
    ActionItem.Caption:=AText;
    ActionItem.ShortCut:=AShortCut;
    ActionItem.Enabled :=AEnabled;
    ActionItem.OnExecute :=OnExecute;
    ActionItem.OnUpdate  :=OnUpdate;
  end;

begin
  AddActItem('&Undo',Menus.ShortCut(Word('Z'), [ssCtrl]),False,UndoExecute, UndoUpdate);
  AddActItem('&Redo',Menus.ShortCut(Word('Z'), [ssCtrl,ssShift]),False,RedoExecute, RedoUpdate);
  AddActItem('-',0,False,nil,nil);
  AddActItem('Cu&t',Menus.ShortCut(Word('X'), [ssCtrl]),False,CutExecute, CutUpdate);
  AddActItem('&Copy',Menus.ShortCut(Word('C'), [ssCtrl]),False,CopyExecute, CopyUpdate);
  AddActItem('&Paste',Menus.ShortCut(Word('V'), [ssCtrl]),False,PasteExecute, PasteUpdate);
  AddActItem('De&lete',0,False,DeleteExecute, DeleteUpdate);
  AddActItem('-',0,False,nil,nil);
  AddActItem('Select &All',Menus.ShortCut(Word('A'), [ssCtrl]),False,SelectAllExecute, SelectAllUpdate);
end;

procedure TSynEdit.SetPopupMenu_(const Value: TPopupMenu);
Var
  MenuItem : TMenuItem;
begin
  SynEdit.TSynEdit(Self).PopupMenu:=Value;
  if CompareText(MenuName,Value.Name)<>0 then
  begin
   MenuItem:=TMenuItem.Create(Value);
   MenuItem.Caption:='-';
   Value.Items.Add(MenuItem);
   FillPopupMenu(Value);
  end;
end;

function TSynEdit.GetPopupMenu_: TPopupMenu;
begin
  Result:=SynEdit.TSynEdit(Self).PopupMenu;
end;

destructor TSynEdit.Destroy;
begin
  FPopupMenu.Free;
  FActnList.Free;
  inherited;
end;

procedure TSynEdit.FillPopupMenu(APopupMenu : TPopupMenu);
var
  i        : integer;
  MenuItem : TMenuItem;
begin
  if Assigned(FActnList) then
  for i := 0 to FActnList.ActionCount-1 do
  begin
    MenuItem:=TMenuItem.Create(APopupMenu);
    MenuItem.Action  :=FActnList.Actions[i];
    APopupMenu.Items.Add(MenuItem);
  end;
end;

end.

And this is the final result.