Implement a Close Tab Button for a TTabControl (plus: a round button over hot tab)

Delphi’s TTabControl is a container type VCL control – allowing controls in its client area and displaying a set of tabs (as tabs or buttons). The TTabControl, unlike TPageControl, is not made up of several pages (for each tab), rather the TTabControl has one “page”. By design, the TTabControl is used when displaying/editing list of objects of the same type. When changing the active tab – you need to update the controls it hosts.

An example of a tabbed control UI is an Internet Browser – take a look at Chrome, IE, Edge, or Firefox, etc. tabs. See how each tab has a close button. No such similar close button, to “close a tab”, can be specified or set via properties or methods of the TTabControl. What if you need one (for whatever the purpose)?

So, how to implement a close button for a TTabControl’s Tab? Plus: how to display the close button only over the hot tab (under the mouse) and have the button nicely rounded.

Here’s one quick-and-dirty implementation:

In my case I wanted the close button for a tab to appear when the mouse hovers over a tab – not on each tab. The tab under the mouse is called the “hot tab” (and is actually rendered differently – different color used for it).

The first issue to be solved, is how to “draw” the button? One way to do it is to make it (unnecessary) complex and use owner drawing (OwnerDraw property) – this approach then also requires to draw tabs completely “by hand” (taking into account application styles and tab images and all that needs to be drawn in the OnDrawTab event handler). I do not want to go that road. I want to have a “real” button (TButton) to appear on the hot tab – so I can click it standardly and handle its OnClick event. So, for this purpose let’s use a TButton and ensure it gets positioned where it needs to be (“on” the tab) when needed.

Next, the “when needed”: I do not want the close button to be visible at all times and on all tabs. I want it visible when the mouse is over some tab and hidden when not. For this purpose, the standard TTabControl events: OnMouseEnter, OnMouseLeave and OnMouseMove can be used. When the mouse enters the TTabControl – make the button visible. When the mouse leaves the TTabControl – hide the button. When the mouse moves over the tabs – make sure to place the button onto the “hot” tab (the tab the mouse is over). The above events are fired only when the mouse moves over the tabs – not when it moves over control’s inside TTabControl’s “page”.

Finally, when the button is clicked, I need to know over what tab it was – so to know what tab to remove (Delete the item from the Tabs property). For this purpose I’m (omg, yes I know) using the Tag property of the button.

Let’s see some code. Here’s the procedure responsible for placing the button over the hot tab:

procedure TMainForm.ShowTabCloseButtonOnHotTab;
var
  iot : integer;
  cp : TPoint;
  rectOver: TRect;
begin
  cp := TabControl1.ScreenToClient(Mouse.CursorPos);
  iot := TabControl1.IndexOfTabAt(cp.X, cp.Y);

  if iot > -1 then
  begin
    rectOver := TabControl1.TabRect(iot);

    Button1.Left := rectOver.Right - Button1.Width;
    Button1.Top := rectOver.Top + ((rectOver.Height div 2) - (Button1.Height div 2));

    Button1.Tag := iot;
    Button1.Show;
  end
  else
  begin
    Button1.Tag := -1;
    Button1.Hide;
  end;
end;

The tab control has the name “TabControl1” and the button is named “Button1” (kids: don’t do that at home). Note that Button1’s parent *is* TTabControl.

In short: check where the mouse is, if over a tab of the tab control get hot tab’s index via IndexOfTabAt. If the tab is found (index > -1) get the tab’s rectangle and place/position the button to the far right side vertically centered. In my case the tabs are positioned at the top of the tab control (TabPosition property == tpTop). If you want tabs at the bottom or left/right – the positioning code needs to be altered.

When the mouse enters the tab control the above procedure is called:

procedure TMainForm.TabControl1MouseEnter(Sender: TObject);
begin
  ShowTabCloseButtonOnHotTab;
end;

When the mouse leaves the control, “simply” hide the button:

procedure TMainForm.TabControl1MouseLeave(Sender: TObject);
begin
  if Button1 <> FindVCLWindow(Mouse.CursorPos) then
  begin
    Button1.Hide;
    Button1.Tag := -1;
  end;
end;

Well, the above is not simply hiding the button. The thing is: when the mouse enters the client area of the button – the TabControl’s OnMouseLeave is fired – but if the mouse is over the button (and the button is over the hot tab) – we still want it visible. The super handy FindVCLWindow does the trick.

Next, here’s what I do when the mouse moves over tabs :

procedure TMainForm.TabControl1MouseMove(Sender: TObject; Shift: TShiftState; X, Y: Integer);
{$WRITEABLECONST ON}
const oldPos : integer = -2;
{$WRITEABLECONST OFF}
var
  iot : integer;
  ctrl : TWinControl;
begin
  inherited;

  iot := TTabControl(Sender).IndexOfTabAt(x,y);

  if (iot > -1) then
  begin
    if iot <> oldPos then
      ShowTabCloseButtonOnHotTab;
  end;

  oldPos := iot;
end;

As the mouse moves over tabs, check over which tab the mouse is. When the mouse enters a different tab – re-position the button.

Finally, when the button is clicked, do what’s needed:

procedure TMainForm.Button1Click(Sender: TObject);
begin
 TabControl1.Tabs.Delete(Button1.Tag);

  ShowTabCloseButtonOnHotTab;
end;

Is it nice? No, I said “quick and dirty”. Does it work? Yes.

In action:

For the TLDR readers: download the source code and refactor as needed 😊

p.s.
Yes, I wanted a nice round button (as you can see in my screen shot), and the trick used is:

  procedure MakeTabCloseButtonRound;
  var
    rgn: HRGN;
    bx, by : integer;
  begin
    bx := GetSystemMetrics(SM_CXEDGE);
    by := GetSystemMetrics(SM_CYEDGE);

    rgn := CreateRoundRectRgn(bx, by, Button1.ClientWidth-bx, Button1.ClientHeight-by, Button1.ClientWidth-bx, Button1.ClientHeight-by);

    SetWindowRgn(Button1.Handle, rgn, True);
  end;

And that’s all folks.

2 thoughts on “Implement a Close Tab Button for a TTabControl (plus: a round button over hot tab)

  1. zarkogajic Post author

    Hi Shlomo,

    Yes, there’s no hide/unhide tab with TTabControl. TPageControl has that.

    My approach with TTabControl is to use it to display a list of objects, where each object in the list is “assigned” to a tab. So, when adding tabs I’m using TabControl.Tabs.AddObject(tab_display_name, anObject). Then you can have a “Visible” property on the object. When a tab is deleted you set Visible to false (or you delete anObject from the list – if closing the tab would be “the same” as deleting the object). to “unhide” it, just add the tab to the Tabs property (and assign that object).

    In short, any changes to the list – you re-populate the Tabs of the TTabControl.

    -žarko

    Reply

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.