123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606 |
- unit NewTabSet;
- {
- Inno Setup
- Copyright (C) 1997-2024 Jordan Russell
- Portions by Martijn Laan
- For conditions of distribution and use, see LICENSE.TXT.
- TNewTabSet - modern VS-style tabs with theme support
- }
- interface
- uses
- Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Math, Generics.Collections,
- ModernColors, NewUxTheme;
- type
- TTabPosition = (tpTop, tpBottom);
- TBoolList = TList<Boolean>;
- TCloseButtonClickEvent = procedure(Sender: TObject; Index: Integer) of object;
- TNewTabSet = class(TCustomControl)
- private
- FCloseButtons: TBoolList;
- FHints: TStrings;
- FMenuThemeData: HTHEME;
- FOnCloseButtonClick: TCloseButtonClickEvent;
- FTabs: TStrings;
- FTabIndex: Integer;
- FTabPosition: TTabPosition;
- FTabsOffset: Integer;
- FTheme: TTheme;
- FThemeDark: Boolean;
- FHotIndex: Integer;
- procedure EnsureCurrentTabIsFullyVisible;
- function GetTabRect(const Index: Integer; const ApplyTabsOffset: Boolean = True): TRect;
- function GetCloseButtonRect(const TabRect: TRect): TRect;
- procedure InvalidateTab(Index: Integer);
- procedure CloseButtonsListChanged(Sender: TObject; const Item: Boolean;
- Action: TCollectionNotification);
- procedure TabsListChanged(Sender: TObject);
- procedure HintsListChanged(Sender: TObject);
- procedure SetCloseButtons(Value: TBoolList);
- procedure SetTabs(Value: TStrings);
- procedure SetTabIndex(Value: Integer);
- procedure SetTabPosition(Value: TTabPosition);
- procedure SetTheme(Value: TTheme);
- procedure SetHints(const Value: TStrings);
- function ToCurrentPPI(const XY: Integer): Integer;
- procedure UpdateThemeData(const Open: Boolean);
- procedure CMFontChanged(var Message: TMessage); message CM_FONTCHANGED;
- procedure CMHintShow(var Message: TCMHintShow); message CM_HINTSHOW;
- procedure CMMouseLeave(var Message: TMessage); message CM_MOUSELEAVE;
- procedure WMMouseMove(var Message: TWMMouseMove); message WM_MOUSEMOVE;
- procedure WMThemeChanged(var Message: TMessage); message WM_THEMECHANGED;
- protected
- function CanAutoSize(var NewWidth, NewHeight: Integer): Boolean; override;
- procedure CreateParams(var Params: TCreateParams); override;
- procedure CreateWnd; override;
- procedure MouseDown(Button: TMouseButton; Shift: TShiftState; X, Y: Integer); override;
- procedure UpdateHotIndex(NewHotIndex: Integer);
- procedure Paint; override;
- procedure Resize; override;
- public
- constructor Create(AOwner: TComponent); override;
- destructor Destroy; override;
- property CloseButtons: TBoolList read FCloseButtons write SetCloseButtons;
- property Theme: TTheme read FTheme write SetTheme;
- published
- property Align;
- property AutoSize default True;
- property Font;
- property Hints: TStrings read FHints write SetHints;
- property ParentFont;
- property TabIndex: Integer read FTabIndex write SetTabIndex;
- property Tabs: TStrings read FTabs write SetTabs;
- property TabPosition: TTabPosition read FTabPosition write SetTabPosition default tpBottom;
- property PopupMenu;
- property OnClick;
- property OnCloseButtonClick: TCloseButtonClickEvent read FOnCloseButtonClick write FOnCloseButtonClick;
- end;
- procedure Register;
- implementation
- uses
- Types;
- procedure Register;
- begin
- RegisterComponents('JR', [TNewTabSet]);
- end;
- procedure RGBToHSV(const R, G, B: Integer; var H, S: Double; var V: Integer);
- var
- Max, Min, C: Integer;
- begin
- Max := R;
- if G > Max then Max := G;
- if B > Max then Max := B;
- Min := R;
- if G < Min then Min := G;
- if B < Min then Min := B;
- C := Max - Min;
- if C = 0 then begin
- H := 0;
- S := 0;
- end
- else begin
- if Max = R then
- H := (60 * (G - B)) / C
- else if Max = G then
- H := (60 * (B - R)) / C + 120
- else if Max = B then
- H := (60 * (R - G)) / C + 240;
- if H < 0 then
- H := H + 360;
- S := C / Max;
- end;
- V := Max;
- end;
- procedure HSVtoRGB(const H, S: Double; const V: Integer; var R, G, B: Integer);
- var
- I, P, Q, T: Integer;
- F: Double;
- begin
- I := Trunc(H / 60);
- F := Frac(H / 60);
- P := Round(V * (1.0 - S));
- Q := Round(V * (1.0 - S * F));
- T := Round(V * (1.0 - S * (1.0 - F)));
- case I of
- 0: begin R := V; G := t; B := p; end;
- 1: begin R := q; G := V; B := p; end;
- 2: begin R := p; G := V; B := t; end;
- 3: begin R := p; G := q; B := V; end;
- 4: begin R := t; G := p; B := V; end;
- 5: begin R := V; G := p; B := q; end;
- else
- { Should only get here with bogus input }
- R := 0; G := 0; B := 0;
- end;
- end;
- function LightenColor(const Color: TColorRef; const Amount: Integer): TColorRef;
- var
- H, S: Double;
- V, R, G, B: Integer;
- begin
- RGBtoHSV(Byte(Color), Byte(Color shr 8), Byte(Color shr 16), H, S, V);
- Inc(V, Amount);
- if V > 255 then
- V := 255;
- if V < 0 then
- V := 0;
- HSVtoRGB(H, S, V, R, G, B);
- Result := R or (G shl 8) or (B shl 16);
- end;
- { TNewTabSet }
- const
- TabSetMarginX = 4;
- TabPaddingX = 5;
- TabPaddingY = 3;
- CloseButtonSizeX = 12;
- constructor TNewTabSet.Create(AOwner: TComponent);
- begin
- inherited;
- FCloseButtons := TBoolList.Create;
- FCloseButtons.OnNotify := CloseButtonsListChanged;
- FTabs := TStringList.Create;
- TStringList(FTabs).OnChange := TabsListChanged;
- FTabPosition := tpBottom;
- FHints := TStringList.Create;
- TStringList(FHints).OnChange := HintsListChanged;
- FHotIndex := -1;
- ControlStyle := ControlStyle + [csOpaque];
- Width := 129;
- Height := 21;
- AutoSize := True;
- end;
- procedure TNewTabSet.CreateParams(var Params: TCreateParams);
- begin
- inherited;
- with Params.WindowClass do
- style := style and not CS_HREDRAW;
- end;
- procedure TNewTabSet.CreateWnd;
- begin
- inherited;
- UpdateThemeData(True);
- end;
- destructor TNewTabSet.Destroy;
- begin
- UpdateThemeData(False);
- FHints.Free;
- FTabs.Free;
- FCloseButtons.Free;
- inherited;
- end;
- procedure TNewTabSet.CMFontChanged(var Message: TMessage);
- begin
- inherited;
- if AutoSize then
- AdjustSize;
- end;
- procedure TNewTabSet.CMHintShow(var Message: TCMHintShow);
- var
- I: Integer;
- R: TRect;
- begin
- inherited;
- if Message.HintInfo.HintControl = Self then begin
- for I := 0 to FTabs.Count-1 do begin
- if I >= FHints.Count then
- Break;
- R := GetTabRect(I);
- if PtInRect(R, Message.HintInfo.CursorPos) then begin
- Message.HintInfo.HintStr := FHints[I];
- Message.HintInfo.CursorRect := R;
- Break;
- end;
- end;
- end;
- end;
- procedure TNewTabSet.WMMouseMove(var Message: TWMMouseMove);
- begin
- var Pos := SmallPointToPoint(Message.Pos);
- var NewHotIndex := -1;
- for var I := 0 to FTabs.Count-1 do begin
- if I <> TabIndex then begin
- var R := GetTabRect(I);
- if PtInRect(R, TPoint.Create(Pos.X, Pos.Y)) then begin
- NewHotIndex := I;
- Break;
- end;
- end;
- end;
- UpdateHotIndex(NewHotIndex);
- end;
- procedure TNewTabSet.WMThemeChanged(var Message: TMessage);
- begin
- { Don't Run to Cursor into this function, it will interrupt up the theme change }
- UpdateThemeData(True);
- inherited;
- end;
- procedure TNewTabSet.EnsureCurrentTabIsFullyVisible;
- begin
- const AdjacentTabVisiblePixels = ToCurrentPPI(30);
- const CR = ClientRect;
- const R = GetTabRect(FTabIndex, False);
- var Offset := FTabsOffset;
- { If the tab is overflowing to the right, scroll right }
- var Overflow := R.Right - Offset - CR.Right + AdjacentTabVisiblePixels;
- if Overflow > 0 then
- Inc(Offset, Overflow);
- { If there's extra space after the last tab, scroll left if possible }
- const LastTabRight = GetTabRect(FTabs.Count-1, False).Right +
- ToCurrentPPI(TabSetMarginX);
- Offset := Min(Offset, Max(0, LastTabRight - CR.Right));
- { If the tab is overflowing to the left, scroll left }
- Overflow := Offset - R.Left + AdjacentTabVisiblePixels;
- if Overflow > 0 then
- Offset := Max(0, Offset - Overflow);
- if FTabsOffset <> Offset then begin
- FTabsOffset := Offset;
- Invalidate;
- end;
- end;
- function TNewTabSet.CanAutoSize(var NewWidth, NewHeight: Integer): Boolean;
- begin
- { We need to manage our own height for correct results with non-default PPI }
- Canvas.Font.Assign(Font);
- NewHeight := Canvas.TextHeight('0') + (ToCurrentPPI(TabPaddingY) * 2) +
- ToCurrentPPI(2);
- Result := True;
- end;
- function TNewTabSet.GetTabRect(const Index: Integer;
- const ApplyTabsOffset: Boolean = True): TRect;
- var
- CR: TRect;
- I, SizeX, SizeY: Integer;
- Size: TSize;
- begin
- CR := ClientRect;
- Canvas.Font.Assign(Font);
- if FTabPosition = tpBottom then
- Result.Top := 0;
- Result.Right := ToCurrentPPI(TabSetMarginX);
- if ApplyTabsOffset then
- Dec(Result.Right, FTabsOffset);
- for I := 0 to FTabs.Count-1 do begin
- Size := Canvas.TextExtent(FTabs[I]);
- SizeX := Size.cx + (ToCurrentPPI(TabPaddingX) * 2);
- if (I < FCloseButtons.Count) and FCloseButtons[I] then
- Inc(SizeX, ToCurrentPPI(CloseButtonSizeX));
- SizeY := Size.cy + (ToCurrentPPI(TabPaddingY) * 2);
- if FTabPosition = tpTop then
- Result.Top := CR.Bottom - SizeY;
- Result := Bounds(Result.Right, Result.Top, SizeX, SizeY);
- if Index = I then
- Exit;
- end;
- SetRectEmpty(Result);
- end;
- function TNewTabSet.GetCloseButtonRect(const TabRect: TRect): TRect;
- begin
- Result := TRect.Create(TabRect.Right - ToCurrentPPI(CloseButtonSizeX) - ToCurrentPPI(TabPaddingX) div 2,
- TabRect.Top, TabRect.Right - ToCurrentPPI(TabPaddingX) div 2, TabRect.Bottom);
- end;
- procedure TNewTabSet.InvalidateTab(Index: Integer);
- var
- R: TRect;
- begin
- if HandleAllocated and (Index >= 0) and (Index < FTabs.Count) then begin
- R := GetTabRect(Index);
- InvalidateRect(Handle, @R, False);
- end;
- end;
- procedure TNewTabSet.CloseButtonsListChanged(Sender: TObject; const Item: Boolean;
- Action: TCollectionNotification);
- begin
- FHotIndex := -1;
- Invalidate;
- end;
- procedure TNewTabSet.TabsListChanged(Sender: TObject);
- begin
- FHotIndex := -1;
- Invalidate;
- end;
- procedure TNewTabSet.HintsListChanged(Sender: TObject);
- begin
- ShowHint := FHints.Count > 0;
- end;
- procedure TNewTabSet.MouseDown(Button: TMouseButton; Shift: TShiftState; X,
- Y: Integer);
- var
- I: Integer;
- R: TRect;
- begin
- if Button = mbLeft then begin
- for I := 0 to FTabs.Count-1 do begin
- R := GetTabRect(I);
- if (X >= R.Left) and (X < R.Right) then begin
- if ((I = TabIndex) or (I = FHotIndex)) and (I < FCloseButtons.Count) and FCloseButtons[I] then begin
- var R2 := GetCloseButtonRect(R);
- if PtInRect(R2, TPoint.Create(X, Y)) then begin
- if Assigned(OnCloseButtonClick) then
- OnCloseButtonClick(Self, I);
- Break;
- end;
- end;
- TabIndex := I;
- Break;
- end;
- end;
- end;
- end;
- procedure TNewTabSet.UpdateHotIndex(NewHotIndex: Integer);
- begin
- var OldHotIndex := FHotIndex;
- if NewHotIndex <> OldHotIndex then begin
- FHotIndex := NewHotIndex;
- if OldHotIndex <> -1 then
- InvalidateTab(OldHotIndex);
- if NewHotIndex <> -1 then
- InvalidateTab(NewHotIndex);
- end;
- end;
- procedure TNewTabSet.CMMouseLeave(var Message: TMessage);
- begin
- UpdateHotIndex(-1);
- inherited;
- end;
- procedure TNewTabSet.Paint;
- var
- HighColorMode: Boolean;
- procedure DrawCloseButton(const TabRect: TRect; const TabIndex: Integer);
- const
- MENU_SYSTEMCLOSE = 17;
- MSYSC_NORMAL = 1;
- begin
- if (TabIndex < FCloseButtons.Count) and FCloseButtons[TabIndex] then begin
- var R := GetCloseButtonRect(TabRect);
- if FMenuThemeData <> 0 then begin
- var Offset := ToCurrentPPI(1);
- Inc(R.Left, Offset);
- Inc(R.Top, Offset);
- DrawThemeBackground(FMenuThemeData, Canvas.Handle, MENU_SYSTEMCLOSE, MSYSC_NORMAL, R, nil);
- end else begin
- InflateRect(R, -ToCurrentPPI(3), -ToCurrentPPI(6));
- Canvas.Pen.Color := Canvas.Font.Color;
- Canvas.MoveTo(R.Left, R.Top);
- Canvas.LineTo(R.Right, R.Bottom);
- Canvas.MoveTo(R.Left, R.Bottom-1);
- Canvas.LineTo(R.Right, R.Top-1);
- end;
- end;
- end;
- procedure DrawTabs(const SelectedTab: Boolean);
- var
- I: Integer;
- R: TRect;
- begin
- for I := 0 to FTabs.Count-1 do begin
- R := GetTabRect(I);
- if SelectedTab and (FTabIndex = I) then begin
- if FTheme <> nil then
- Canvas.Brush.Color := FTheme.Colors[tcBack]
- else
- Canvas.Brush.Color := clBtnFace;
- Canvas.FillRect(R);
- if FTheme <> nil then
- Canvas.Font.Color := FTheme.Colors[tcFore]
- else
- Canvas.Font.Color := clBtnText;
- Canvas.TextOut(R.Left + ToCurrentPPI(TabPaddingX), R.Top + ToCurrentPPI(TabPaddingY), FTabs[I]);
- DrawCloseButton(R, I);
- ExcludeClipRect(Canvas.Handle, R.Left, R.Top, R.Right, R.Bottom);
- Break;
- end;
- if not SelectedTab and (FTabIndex <> I) then begin
- if FHotIndex = I then begin
- if FTheme <> nil then
- Canvas.Font.Color := FTheme.Colors[tcFore]
- else
- Canvas.Font.Color := clBtnText;
- end else if FTheme <> nil then
- Canvas.Font.Color := FTheme.Colors[tcMarginFore]
- else if HighColorMode and (ColorToRGB(clBtnFace) <> clBlack) then
- Canvas.Font.Color := LightenColor(ColorToRGB(clBtnShadow), -43)
- else begin
- { If the button face color is black, or if running in low color mode,
- use plain clBtnHighlight as the text color }
- Canvas.Font.Color := clBtnHighlight;
- end;
- Canvas.TextOut(R.Left + ToCurrentPPI(TabPaddingX), R.Top + ToCurrentPPI(TabPaddingY), FTabs[I]);
- if FHotIndex = I then
- DrawCloseButton(R, I);
- end;
- end;
- end;
- var
- CR: TRect;
- begin
- Canvas.Font.Assign(Font);
- HighColorMode := (GetDeviceCaps(Canvas.Handle, BITSPIXEL) *
- GetDeviceCaps(Canvas.Handle, PLANES)) >= 15;
- CR := ClientRect;
- { Work around an apparent NT 4.0/2000/??? bug. If the width of the DC is
- greater than the width of the screen, then any call to ExcludeClipRect
- inexplicably shrinks the DC's clipping rectangle to the screen width.
- Calling IntersectClipRect first with the entire client area as the
- rectangle solves this (don't ask me why). }
- IntersectClipRect(Canvas.Handle, CR.Left, CR.Top, CR.Right, CR.Bottom);
- { Selected tab }
- DrawTabs(True);
- { Top or bottom line }
- if FTheme <> nil then
- Canvas.Brush.Color := FTheme.Colors[tcBack]
- else
- Canvas.Brush.Color := clBtnFace;
- const LineRectHeight = ToCurrentPPI(1);
- var LineRect := CR;
- if FTabPosition = tpBottom then
- LineRect.Bottom := LineRect.Top + LineRectHeight
- else
- LineRect.Top := LineRect.Bottom - LineRectHeight;
- Canvas.FillRect(LineRect);
- { Background fill }
- if FTheme <> nil then
- Canvas.Brush.Color := FTheme.Colors[tcMarginBack]
- else if HighColorMode then
- Canvas.Brush.Color := LightenColor(ColorToRGB(clBtnFace), 35)
- else
- Canvas.Brush.Color := clBtnShadow;
- if FTabPosition = tpBottom then
- Inc(CR.Top, LineRectHeight)
- else
- Dec(CR.Bottom, LineRectHeight);
- Canvas.FillRect(CR);
- { Non-selected tabs }
- DrawTabs(False);
- end;
- procedure TNewTabSet.Resize;
- begin
- EnsureCurrentTabIsFullyVisible;
- inherited;
- end;
- procedure TNewTabSet.SetCloseButtons(Value: TBoolList);
- begin
- FCloseButtons.Clear;
- for var V in Value do
- FCloseButtons.Add(V);
- end;
- procedure TNewTabSet.SetHints(const Value: TStrings);
- begin
- FHints.Assign(Value);
- end;
- procedure TNewTabSet.SetTabIndex(Value: Integer);
- begin
- if FTabIndex <> Value then begin
- InvalidateTab(FTabIndex);
- FTabIndex := Value;
- InvalidateTab(Value);
- EnsureCurrentTabIsFullyVisible;
- Click;
- end;
- end;
- procedure TNewTabSet.SetTabPosition(Value: TTabPosition);
- begin
- if FTabPosition <> Value then begin
- FTabPosition := Value;
- Invalidate;
- end;
- end;
- procedure TNewTabSet.SetTabs(Value: TStrings);
- begin
- FTabs.Assign(Value);
- if FTabIndex >= FTabs.Count then
- SetTabIndex(FTabs.Count-1);
- end;
- procedure TNewTabSet.SetTheme(Value: TTheme);
- begin
- if FTheme <> Value then begin
- FTheme := Value;
- var NewThemeDark := (FTheme <> nil) and FTheme.Dark;
- if FThemeDark <> NewThemeDark then
- UpdateThemeData(True);
- FThemeDark := NewThemeDark;
- Invalidate;
- end;
- end;
- function TNewTabSet.ToCurrentPPI(const XY: Integer): Integer;
- begin
- Result := MulDiv(XY, CurrentPPI, 96);
- end;
- procedure TNewTabSet.UpdateThemeData(const Open: Boolean);
- begin
- if FMenuThemeData <> 0 then begin
- CloseThemeData(FMenuThemeData);
- FMenuThemeData := 0;
- end;
- if Open and UseThemes then begin
- if (FTheme <> nil) and FTheme.Dark then
- FMenuThemeData := OpenThemeData(Handle, 'DarkMode::Menu');
- if FMenuThemeData = 0 then
- FMenuThemeData := OpenThemeData(Handle, 'Menu');
- end;
- end;
- end.
|