Creating a Metropolis UI ToolTip

From RAD Studio
Jump to: navigation, search

Go Up to Developing Metropolis UI Applications


A Metropolis UI style tooltip is a pop-up window that displays Help-like information when the mouse or touch device hovers over or touches a control. This tutorial explains how to create and use a tooltip using FireMonkey.

Create a new FireMonkey project:

  • Create a new Metropolis UI Desktop Application:
    • For Delphi: File > New > FireMonkey Metropolis UI Desktop Application - Delphi > Blank Metropolis UI Desktop Application.
    • For C++: File > New > FireMonkey Metropolis UI Desktop Application - C++ > Blank Metropolis UI Desktop Application.
  • Save the project.

Define ToolTip Class Object

Create a new class TToolTipPanel for your application.

In the your unit, do the following steps:

1. In the uses section, add the following units:
  • Delphi:
uses
  FMX.Edit;
  • C++:
    • In the header file (.h file), add the following code:
#include <FMX.Edit.hpp>
2. Define the TToolTipPanel class derived from TPanel.
  • Delphi:
 TToolTipPanel = class(TPanel)
  • C++: define this new class in the header file (.h file):
class TToolTipPanel : public TPanel { 
//class definition goes here
}
3. To add text to the tooltip, add a TLabel as a private field: FLabel. To access the tooltip text, define a public Text property along with the getter and setter functions of the Text property.
  • Delphi:
type
  TToolTipPanel = class(TPanel)
  private
    FLabel: TLabel;
    function GetToolTipText: string;
    procedure SetToolTipText(const Value: string);
  public
    property Text: string read GetToolTipText write SetToolTipText;
  end;
  • C++:
    • In the header file (.h file), add the following code:
class TToolTipPanel : public TPanel {
private:
    TLabel *FLabel;
    UnicodeString _fastcall GetToolTipText();
    void _fastcall SetToolTipText (const UnicodeString Value);
public:
	
__published:
    __property UnicodeString Text = {read = GetToolTipText, write = SetToolTipText};     
	 
};
4. Implement the getter and setter functions of the Text property as following:
function TToolTipPanel.GetToolTipText: string;
begin
  Result := FLabel.Text;
end;
procedure TToolTipPanel.SetToolTipText(const Value: string);
begin
  FLabel.Text := Value ;
end;
  • C++:
    • In the .cpp file, add the following code:
UnicodeString _fastcall TToolTipPanel::GetToolTipText() {
    return FLabel->Text;
}
// ---------------------------------------------------------------------------

void _fastcall TToolTipPanel::SetToolTipText(const UnicodeString Value) {
    FLabel->Text = Value;
}
5. Add the following private members to the TToolTipPanel class, in addition to the ones added above:
  • Delphi:
  private
    FMousePoint : TPointF; //keeps the current position for the mouse cursor
    FActiveControl : TFmxObject; //keeps the current active control for which the tooltip is displayed
    FTimer : TTimer; //the timer is used to decide when, in time, the tooltip is displayed and for how long,
                     // for the FActiveControl
    FCounter : Cardinal;//keeps a counter used on timer execution
    FOnlyInputFields : Boolean ; //this flag is used to decides if the tooltip is displayed only for
                                 // input controls 
    procedure OnTimer(Sender: TObject); //define the OnTime event handler for the FTimer
  • C++:
    • In the header file (.h file), add the following code to the TToolTipPanel class:
    TPointF FMousePoint;
    TFmxObject *FActiveControl;
    TTimer *FTimer;
    unsigned FCounter;
    bool FOnlyInputFields;
	
    void _fastcall OnTimer (TObject *Sender);
To expose the FOnlyInputFields flag:
  • Delphi: add the OnlyInputFields public property as follows:
  public
    property OnlyInputFields : Boolean read FOnlyInputFields write FOnlyInputFields;
  • C++: add the OnlyInputFields as a published property, as follows:
    __property  bool OnlyInputFields = {read = FOnlyInputFields, write = FOnlyInputFields };
6. For design purposes, add a private field FBorderWidth and a public property BorderWidth to set and get the tooltip borders' width. The borders are taken into consideration when the tooltip is displayed.
  • Delphi:
  private
    FBorderWidth : Single;
  public
    property BorderWidth : Single read FBorderWidth write FBorderWidth;
  • C++:
    • In the header file (.h file), add the following code to the TToolTipPanel class:
private:
    float FBorderWidth;
__published:	 
    __property float BorderWidth = {read = FBorderWidth, write = FBorderWidth };
7. Define and implement the constructor and destructor for the TToolTipPanel class as follows:
  • Declaration
  • Delphi:
  public
    constructor Create(AOwner: TComponent); override;
    destructor Destroy; override;
  • C++:
      • In the header file (.h file), add the following code to the TToolTipPanel class:
public:
    __fastcall TToolTipPanel(TComponent* Owner);
    __fastcall virtual ~TToolTipPanel(void);
  • Implementation
  • Delphi:
constructor TToolTipPanel.Create(AOwner: TComponent);
begin
  inherited; //inherits the behavior from TPanel 
  Visible := False; //initially, the tootltip is not visible
  StyleLookup := 'tooltippanel'; // sets the name of the ToolTip style
  //initialize FLabel
  FLabel := TLabel.Create(AOwner);
  FLabel.Parent := Self;
  FLabel.StyleLookup := 'tooltiplabel';
  FLabel.Text := Self.ToString;
  if assigned(FLabel.Canvas) then
    Height := FLabel.Canvas.TextHeight(FLabel.Text);
  FLabel.Align := TAlignLayout.Client;
  FLabel.TextAlign := TTextAlign.Center;
  FLabel.VertTextAlign := TTextAlign.Center;
  //initialize FTimer
  FTimer := TTimer.Create(AOwner);
  FTimer.OnTimer := OnTimer;
  FTimer.Enabled := True;
  FTimer.Interval := 500;
  FActiveControl := nil; //initially, there is no control for which to display the tootltip
  FCounter := 1000; //FCounter is initially set to a high value
  FBorderWidth := 10; //an initial value for the tooltip borders
end;

destructor TToolTipPanel.Destroy;
begin
  inherited;
end;
  • C++:
    • In the .cpp file, add the following code to the TToolTipPanel class:
__fastcall TToolTipPanel::TToolTipPanel(TComponent* Owner) : TPanel(Owner){

    Visible = False;
    StyleLookup =  "tooltippanel";
    FLabel = new TLabel(this);
    FLabel->Parent = this;
    FLabel->StyleLookup =  "tooltiplabel";
    FLabel->Text = this->ToString();
    if (FLabel->Canvas != NULL) {  
        Height = FLabel->Canvas->TextHeight(FLabel->Text);
    }
    FLabel->Align = TAlignLayout::Client;
    FLabel->TextAlign = TTextAlign::Center;
    FLabel->VertTextAlign = TTextAlign::Center;
    FTimer = new TTimer(Owner);
    FTimer->OnTimer = OnTimer;
    FTimer->Enabled = true;
    FTimer->Interval = 500;
    FActiveControl = NULL;
    FCounter = 1000;
    FBorderWidth = 10;
    }
	//---------------------------------------------------------------------------
	
__fastcall TToolTipPanel::~TToolTipPanel() {

    }
8. Add a public method to display the tooltip at a specified position:
  • Delphi:
public
   procedure ShowToolTip(AX, AY: Single);

implementation
procedure TToolTipPanel.ShowToolTip(AX, AY: Single);
begin
  Position.Point := PointF(AX,AY); //sets the tooltip position to the specified point
  //calculates the size of the tooltip depending on the text to be displayed
  Height := FLabel.Canvas.TextHeight(FLabel.Text) + 2 * FBorderWidth;
  Width  := FLabel.Canvas.TextWidth(FLabel.Text) + 2 * FBorderWidth;
  //sets the tooltip as visible
  Visible := True;
end;
  • C++:
    • In the header file (.h file), add the following code to the TToolTipPanel class:
public:
    __fastcall void ShowToolTip(float AX, float AY);
  • In the .cpp file , add the following code to the TToolTipPanel class:
void __fastcall TToolTipPanel::ShowToolTip(float AX, float AY){
    Position->Point = PointF(AX,AY);
    Height = FLabel->Canvas->TextHeight(FLabel->Text)+ 2 * FBorderWidth;
    Width = FLabel->Canvas->TextWidth(FLabel->Text) + 2 * FBorderWidth;
    Visible = True;
    }
9. A tooltip is typically displayed for the current position of the cursor. A tooltip also displays text that provides some information about the current position.
  • In this case, the tooltip displays the following text:
"ToolTip for component: "
and the name of the component on which the mouse cursor is positioned.
  • If no component is found, the display is:
"ToolTip for mouse pos "
and the coordinates of the mouse cursor.
  • If the tooltip is set to be displayed only for input controls, the tooltip displays the text:
"ToolTip for "
and the name of the input control in focus. Also, the tooltip is positioned under the input control, to avoid covering the control and to let the user set the input text.
The displayed text can, of course, be changed to suit your needs.

The OnTimer implementation should look as follows:

  • Delphi:
procedure TToolTipPanel.OnTimer;
var
  LActiveControl : IControl;
  LControl : TControl;
  LMousePos : TPointF;
  LObject : IControl ;
begin
  //test to detect for which kind of controls to display the control  
  if not FOnlyInputFields then
  begin
    // tests if the FMousePoint is actualized
    if Screen.MousePos <> FMousePoint then
    begin
      FMousePoint := Screen.MousePos ;
      FCounter := 0;
      Visible := False;
    end ;
    Inc(FCounter);
    case FCounter of
      0..2: Visible := False ;//simulates a delay on displaying the tooltip
      3:
      begin
        Text := '';
        if Parent is TForm then
        begin
          //identifies the object on which the mouse cursor is located
          LObject := (Parent as TForm).ObjectAtPoint(FMousePoint) ;
          if Assigned(LObject) then
            Text := LObject.GetObject.Name;
        end;
        //if no object is found, the tooltip displays the mouse cursor coordinates  
        if Text = '' then
          Text := 'ToolTip for mouse pos ' + PointToString(FMousePoint)
        else
          Text := 'ToolTip for component: ' + Text ;
        LMousePos := (Parent as TForm).ScreenToClient(FMousePoint);
        //displays the tooltip
        ShowToolTip(LMousePos.X, LMousePos.Y);
      end;
      // the tooltip is displayed for a limited time. In this case it is displayed until FCounter reaches 10
      4..10:;
    else
      FCounter := 1000;
      Visible := False ;
    end;
  end
  else
  begin
    //identifies the active control (the control in focus)
    if Parent is TForm then
      LActiveControl := (Parent as TForm).Focused;
    // checks if the object in focus is an input control (and a TEdit or a TEdit descendant) 
    if Assigned(LActiveControl) and (LActiveControl.GetObject <> FActiveControl) then
    begin
      Visible := False ;
      FActiveControl := LActiveControl.GetObject;
      if (FActiveControl is TEdit) then
        FCounter := 0;
    end;
    Inc(FCounter);
    case FCounter of
      0..2: Visible := False ;//simulates a delay on displaying the tooltip
      3..10: // the tooltip is displayed for the FActiveControl control, if it exists, under the input control,
             // so the tooltip doesn't cover the input area 
      begin
        if assigned(LActiveControl) then
        begin
          LControl := (LActiveControl as TControl);
          Text := 'ToolTip for ' + LControl.Name ;
          ShowToolTip(LControl.Position.X + 20, LControl.Position.Y + LControl.Height);
        end;
      end
    else
      FCounter := 1000;
      Visible := False ;
    end;
  end;
end;
  • C++:
    • In the header file (.h file), add the following code to the TToolTipPanel class:
void __fastcall TToolTipPanel::OnTimer(TObject* Sender) {

    IControl *LActiveControl;
    TControl *LControl;
    TPointF LMousePos;
    IControl *LObject;

    if (!FOnlyInputFields) {
        if (Screen->MousePos() != FMousePoint) {
            FMousePoint = Screen->MousePos();
            FCounter = 0;
            Visible = False;
        }
        FCounter++;

        switch (FCounter) {
        case 0:
        case 1:
        case 2:
            Visible = False;
            break;
        case 3:
            Text = "";
            if ((dynamic_cast<TForm*>(Parent)) != NULL) {
                TForm& ref_object = dynamic_cast<TForm&>(*Parent);
                LObject = ref_object.ObjectAtPoint(FMousePoint);
                if (LObject != NULL) {
                    Text = LObject->GetObjectW()->Name;
                }
            }
            if (Text == "") {
                Text = "ToolTip for mouse pos " + PointToString(FMousePoint);
            }
            else {
                Text = "ToolTip for component: " + Text;
            }
            LMousePos = dynamic_cast<TForm*>(Parent)->ScreenToClient(FMousePoint);
            ShowToolTip(LMousePos.X, LMousePos.Y);
            break;
        case 4:
        case 5:
        case 6:
        case 7:
        case 8:
        case 9:
        case 10:
            break;
        default:
            FCounter = 1000;
            Visible = False; ;
        }
    }
    else {
        if ((dynamic_cast<TForm*>(Parent)) != NULL) {
            TForm& ref_LObject = dynamic_cast<TForm&>(*Parent);
            LActiveControl = ref_LObject.Focused;
            if ((LActiveControl != NULL) & (LActiveControl->GetObjectW()!= FActiveControl)) {
                Visible = False;
                FActiveControl = LActiveControl->GetObjectW();
                if ((dynamic_cast<TEdit*>(FActiveControl)) != NULL) {
                    FCounter = 0;
                }
            }
            FCounter++;
            switch (FCounter) {
            case 0:
            case 1:
            case 2:
                Visible = False;
                break;
            case 3:
            case 4:
            case 5:
            case 6:
            case 7:
            case 8:
            case 9:
            case 10:
                if (LActiveControl != NULL) {
                    LControl = System::interface_cast<TControl, IControl>(LActiveControl);
                    Text = "ToolTip for " + LControl->Name;
                    ShowToolTip(LControl->Position->X + 20, LControl->Position->Y + LControl->Height);
                }
                break;
            default:
                FCounter = 1000;
                Visible = False; ;
            }
        }
    }
}
10. The final interface of the TToolTipPanel should look like this:
  • Delphi:
type
  TToolTipPanel = class(TPanel)
  private
    FOnlyInputFields: Boolean;
    FMousePoint: TPointF;
    FCounter: Cardinal;
    FActiveControl: TFmxObject;
    FLabel: TLabel;
    FTimer: TTimer;
    FBorderWidth: Single;
    function GetToolTipText: string;
    procedure SetToolTipText(const Value: string);
    procedure OnTimer(Sender: TObject);
  public
    constructor Create(AOwner: TComponent); override;
    destructor Destroy; override;
    procedure ShowToolTip(AX, AY: Single);
    property Text: string read GetToolTipText write SetToolTipText;
    property BorderWidth: Single read FBorderWidth write FBorderWidth;
    property OnlyInputFields: Boolean read FOnlyInputFields
      write FOnlyInputFields;
  end;
  • C++:
class TToolTipPanel : public TPanel{
private:
    TLabel *FLabel;
    TPointF FMousePoint;
    TFmxObject *FActiveControl;
    TTimer *FTimer;
    unsigned FCounter;
    bool FOnlyInputFields;
    float FBorderWidth;
    UnicodeString _fastcall GetToolTipText();
    void _fastcall SetToolTipText (const UnicodeString Value);
    void _fastcall OnTimer (TObject *Sender);

public:
    __fastcall TToolTipPanel(TComponent* Owner);
    __fastcall virtual ~TToolTipPanel(void);
    __fastcall void ShowToolTip(float AX, float AY);   
__published:
     __property UnicodeString Text = {read = GetToolTipText, write = SetToolTipText};     
     __property  bool OnlyInputFields = {read = FOnlyInputFields, write = FOnlyInputFields }; 
     __property float BorderWidth = {read = FBorderWidth, write = FBorderWidth };   
};


Using the ToolTip

1. In the Form Designer, add your components, for example: a TButton, a TCheckBox, a TEdit, a TMemo, a TRectangle.
2. Include in the form unit declaration the ToolTip unit defined above.
3. Add a private member, of type TToolTipPanel, to the form.
  • Delphi:
  private
   TT : TToolTipPanel;
  • C++:
    • In the private declaration of TForm, in the header file (.h file) add the following code:
private:	// User declarations
    TToolTipPanel *TT;
4. In the OnCreate event of the form, create a TToolTipPanel.
  • Delphi:
procedure TForm1.FormCreate(Sender: TObject);
begin
  TT := TToolTipPanel.Create(Form1);
  TT.Parent := Self ;
end;
  • C++:
__fastcall TForm1::TForm1(TComponent* Owner): TForm(Owner){
    TT = new TToolTipPanel(this);
    TT->Parent = this;
}
5. To test the tooltip behavior when the OnlyInputFields property is set to True or False, add the following in the OnChange event of the TCheckBox:
  • Delphi:
procedure TForm1.CheckBox1Change(Sender: TObject);
begin
   TT.OnlyInputFields := CheckBox1.IsChecked;
end;
  • C++:
void __fastcall TForm1::CheckBox1Change(TObject *Sender){
    TT->OnlyInputFields = CheckBox1->IsChecked;
}
6. Run the application.
Here are some sample images of the created tooltip:
Tooltip over a component.png Tooltip mouse position.png
The next image shows the inference of a tooltip for an input component for the OnlyInputFields property set to True:
OnlyInputFields False.png OnlyInputFields True.png

See Also