NewTabSet.pas 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603
  1. unit NewTabSet;
  2. {
  3. Inno Setup
  4. Copyright (C) 1997-2025 Jordan Russell
  5. Portions by Martijn Laan
  6. For conditions of distribution and use, see LICENSE.TXT.
  7. TNewTabSet - modern VS-style tabs with theme support
  8. }
  9. interface
  10. uses
  11. Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Math, Generics.Collections,
  12. ModernColors, NewUxTheme;
  13. type
  14. TTabPosition = (tpTop, tpBottom);
  15. TBoolList = TList<Boolean>;
  16. TCloseButtonClickEvent = procedure(Sender: TObject; Index: Integer) of object;
  17. TNewTabSet = class(TCustomControl)
  18. private
  19. FCloseButtons: TBoolList;
  20. FHints: TStrings;
  21. FMenuThemeData: HTHEME;
  22. FOnCloseButtonClick: TCloseButtonClickEvent;
  23. FTabs: TStrings;
  24. FTabIndex: Integer;
  25. FTabPosition: TTabPosition;
  26. FTabsOffset: Integer;
  27. FTheme: TTheme;
  28. FThemeDark: Boolean;
  29. FHotIndex: Integer;
  30. procedure EnsureCurrentTabIsFullyVisible;
  31. function GetTabRect(const Index: Integer; const ApplyTabsOffset: Boolean = True): TRect;
  32. function GetCloseButtonRect(const TabRect: TRect): TRect;
  33. procedure InvalidateTab(Index: Integer);
  34. procedure CloseButtonsListChanged(Sender: TObject; const Item: Boolean;
  35. Action: TCollectionNotification);
  36. procedure TabsListChanged(Sender: TObject);
  37. procedure HintsListChanged(Sender: TObject);
  38. procedure SetCloseButtons(Value: TBoolList);
  39. procedure SetTabs(Value: TStrings);
  40. procedure SetTabIndex(Value: Integer);
  41. procedure SetTabPosition(Value: TTabPosition);
  42. procedure SetTheme(Value: TTheme);
  43. procedure SetHints(const Value: TStrings);
  44. function ToCurrentPPI(const XY: Integer): Integer;
  45. procedure UpdateThemeData(const Open: Boolean);
  46. procedure CMFontChanged(var Message: TMessage); message CM_FONTCHANGED;
  47. procedure CMHintShow(var Message: TCMHintShow); message CM_HINTSHOW;
  48. procedure CMMouseLeave(var Message: TMessage); message CM_MOUSELEAVE;
  49. procedure WMMouseMove(var Message: TWMMouseMove); message WM_MOUSEMOVE;
  50. procedure WMThemeChanged(var Message: TMessage); message WM_THEMECHANGED;
  51. protected
  52. function CanAutoSize(var NewWidth, NewHeight: Integer): Boolean; override;
  53. procedure CreateParams(var Params: TCreateParams); override;
  54. procedure CreateWnd; override;
  55. procedure MouseDown(Button: TMouseButton; Shift: TShiftState; X, Y: Integer); override;
  56. procedure UpdateHotIndex(NewHotIndex: Integer);
  57. procedure Paint; override;
  58. procedure Resize; override;
  59. public
  60. constructor Create(AOwner: TComponent); override;
  61. destructor Destroy; override;
  62. property CloseButtons: TBoolList read FCloseButtons write SetCloseButtons;
  63. property Theme: TTheme read FTheme write SetTheme;
  64. published
  65. property Align;
  66. property AutoSize default True;
  67. property Font;
  68. property Hints: TStrings read FHints write SetHints;
  69. property ParentFont;
  70. property TabIndex: Integer read FTabIndex write SetTabIndex;
  71. property Tabs: TStrings read FTabs write SetTabs;
  72. property TabPosition: TTabPosition read FTabPosition write SetTabPosition default tpBottom;
  73. property PopupMenu;
  74. property OnClick;
  75. property OnCloseButtonClick: TCloseButtonClickEvent read FOnCloseButtonClick write FOnCloseButtonClick;
  76. end;
  77. procedure Register;
  78. implementation
  79. uses
  80. Types;
  81. procedure Register;
  82. begin
  83. RegisterComponents('JR', [TNewTabSet]);
  84. end;
  85. procedure RGBToHSV(const R, G, B: Integer; var H, S: Double; var V: Integer);
  86. var
  87. Max, Min, C: Integer;
  88. begin
  89. Max := R;
  90. if G > Max then Max := G;
  91. if B > Max then Max := B;
  92. Min := R;
  93. if G < Min then Min := G;
  94. if B < Min then Min := B;
  95. C := Max - Min;
  96. if C = 0 then begin
  97. H := 0;
  98. S := 0;
  99. end
  100. else begin
  101. if Max = R then
  102. H := (60 * (G - B)) / C
  103. else if Max = G then
  104. H := (60 * (B - R)) / C + 120
  105. else if Max = B then
  106. H := (60 * (R - G)) / C + 240;
  107. if H < 0 then
  108. H := H + 360;
  109. S := C / Max;
  110. end;
  111. V := Max;
  112. end;
  113. procedure HSVtoRGB(const H, S: Double; const V: Integer; var R, G, B: Integer);
  114. begin
  115. const I = Integer(Trunc(H / 60));
  116. const F = Frac(H / 60);
  117. const P = Integer(Round(V * (1.0 - S)));
  118. const Q = Integer(Round(V * (1.0 - S * F)));
  119. const T = Integer(Round(V * (1.0 - S * (1.0 - F))));
  120. case I of
  121. 0: begin R := V; G := t; B := p; end;
  122. 1: begin R := q; G := V; B := p; end;
  123. 2: begin R := p; G := V; B := t; end;
  124. 3: begin R := p; G := q; B := V; end;
  125. 4: begin R := t; G := p; B := V; end;
  126. 5: begin R := V; G := p; B := q; end;
  127. else
  128. { Should only get here with bogus input }
  129. R := 0; G := 0; B := 0;
  130. end;
  131. end;
  132. function LightenColor(const Color: Integer; const Amount: Integer): TColor;
  133. var
  134. H, S: Double;
  135. V, R, G, B: Integer;
  136. begin
  137. RGBtoHSV(Byte(Color), Byte(Color shr 8), Byte(Color shr 16), H, S, V);
  138. Inc(V, Amount);
  139. if V > 255 then
  140. V := 255;
  141. if V < 0 then
  142. V := 0;
  143. HSVtoRGB(H, S, V, R, G, B);
  144. Result := R or (G shl 8) or (B shl 16);
  145. end;
  146. { TNewTabSet }
  147. const
  148. TabSetMarginX = 4;
  149. TabPaddingX = 5;
  150. TabPaddingY = 3;
  151. CloseButtonSizeX = 12;
  152. constructor TNewTabSet.Create(AOwner: TComponent);
  153. begin
  154. inherited;
  155. FCloseButtons := TBoolList.Create;
  156. FCloseButtons.OnNotify := CloseButtonsListChanged;
  157. FTabs := TStringList.Create;
  158. TStringList(FTabs).OnChange := TabsListChanged;
  159. FTabPosition := tpBottom;
  160. FHints := TStringList.Create;
  161. TStringList(FHints).OnChange := HintsListChanged;
  162. FHotIndex := -1;
  163. ControlStyle := ControlStyle + [csOpaque];
  164. Width := 129;
  165. Height := 21;
  166. AutoSize := True;
  167. end;
  168. procedure TNewTabSet.CreateParams(var Params: TCreateParams);
  169. begin
  170. inherited;
  171. with Params.WindowClass do
  172. style := style and not CS_HREDRAW;
  173. end;
  174. procedure TNewTabSet.CreateWnd;
  175. begin
  176. inherited;
  177. UpdateThemeData(True);
  178. end;
  179. destructor TNewTabSet.Destroy;
  180. begin
  181. UpdateThemeData(False);
  182. FHints.Free;
  183. FTabs.Free;
  184. FCloseButtons.Free;
  185. inherited;
  186. end;
  187. procedure TNewTabSet.CMFontChanged(var Message: TMessage);
  188. begin
  189. inherited;
  190. if AutoSize then
  191. AdjustSize;
  192. end;
  193. procedure TNewTabSet.CMHintShow(var Message: TCMHintShow);
  194. var
  195. I: Integer;
  196. R: TRect;
  197. begin
  198. inherited;
  199. if Message.HintInfo.HintControl = Self then begin
  200. for I := 0 to FTabs.Count-1 do begin
  201. if I >= FHints.Count then
  202. Break;
  203. R := GetTabRect(I);
  204. if PtInRect(R, Message.HintInfo.CursorPos) then begin
  205. Message.HintInfo.HintStr := FHints[I];
  206. Message.HintInfo.CursorRect := R;
  207. Break;
  208. end;
  209. end;
  210. end;
  211. end;
  212. procedure TNewTabSet.WMMouseMove(var Message: TWMMouseMove);
  213. begin
  214. var Pos := SmallPointToPoint(Message.Pos);
  215. var NewHotIndex := -1;
  216. for var I := 0 to FTabs.Count-1 do begin
  217. if I <> TabIndex then begin
  218. var R := GetTabRect(I);
  219. if PtInRect(R, TPoint.Create(Pos.X, Pos.Y)) then begin
  220. NewHotIndex := I;
  221. Break;
  222. end;
  223. end;
  224. end;
  225. UpdateHotIndex(NewHotIndex);
  226. end;
  227. procedure TNewTabSet.WMThemeChanged(var Message: TMessage);
  228. begin
  229. { Don't Run to Cursor into this function, it will interrupt up the theme change }
  230. UpdateThemeData(True);
  231. inherited;
  232. end;
  233. procedure TNewTabSet.EnsureCurrentTabIsFullyVisible;
  234. begin
  235. const AdjacentTabVisiblePixels = ToCurrentPPI(30);
  236. const CR = ClientRect;
  237. const R = GetTabRect(FTabIndex, False);
  238. var Offset := FTabsOffset;
  239. { If the tab is overflowing to the right, scroll right }
  240. var Overflow := R.Right - Offset - CR.Right + AdjacentTabVisiblePixels;
  241. if Overflow > 0 then
  242. Inc(Offset, Overflow);
  243. { If there's extra space after the last tab, scroll left if possible }
  244. const LastTabRight = GetTabRect(FTabs.Count-1, False).Right +
  245. ToCurrentPPI(TabSetMarginX);
  246. Offset := Min(Offset, Max(0, LastTabRight - CR.Right));
  247. { If the tab is overflowing to the left, scroll left }
  248. Overflow := Offset - R.Left + AdjacentTabVisiblePixels;
  249. if Overflow > 0 then
  250. Offset := Max(0, Offset - Overflow);
  251. if FTabsOffset <> Offset then begin
  252. FTabsOffset := Offset;
  253. Invalidate;
  254. end;
  255. end;
  256. function TNewTabSet.CanAutoSize(var NewWidth, NewHeight: Integer): Boolean;
  257. begin
  258. { We need to manage our own height for correct results with non-default PPI }
  259. Canvas.Font.Assign(Font);
  260. NewHeight := Canvas.TextHeight('0') + (ToCurrentPPI(TabPaddingY) * 2) +
  261. ToCurrentPPI(2);
  262. Result := True;
  263. end;
  264. function TNewTabSet.GetTabRect(const Index: Integer;
  265. const ApplyTabsOffset: Boolean = True): TRect;
  266. var
  267. CR: TRect;
  268. I, SizeX, SizeY: Integer;
  269. Size: TSize;
  270. begin
  271. CR := ClientRect;
  272. Canvas.Font.Assign(Font);
  273. if FTabPosition = tpBottom then
  274. Result.Top := 0;
  275. Result.Right := ToCurrentPPI(TabSetMarginX);
  276. if ApplyTabsOffset then
  277. Dec(Result.Right, FTabsOffset);
  278. for I := 0 to FTabs.Count-1 do begin
  279. Size := Canvas.TextExtent(FTabs[I]);
  280. SizeX := Size.cx + (ToCurrentPPI(TabPaddingX) * 2);
  281. if (I < FCloseButtons.Count) and FCloseButtons[I] then
  282. Inc(SizeX, ToCurrentPPI(CloseButtonSizeX));
  283. SizeY := Size.cy + (ToCurrentPPI(TabPaddingY) * 2);
  284. if FTabPosition = tpTop then
  285. Result.Top := CR.Bottom - SizeY;
  286. Result := Bounds(Result.Right, Result.Top, SizeX, SizeY);
  287. if Index = I then
  288. Exit;
  289. end;
  290. SetRectEmpty(Result);
  291. end;
  292. function TNewTabSet.GetCloseButtonRect(const TabRect: TRect): TRect;
  293. begin
  294. Result := TRect.Create(TabRect.Right - ToCurrentPPI(CloseButtonSizeX) - ToCurrentPPI(TabPaddingX) div 2,
  295. TabRect.Top, TabRect.Right - ToCurrentPPI(TabPaddingX) div 2, TabRect.Bottom);
  296. end;
  297. procedure TNewTabSet.InvalidateTab(Index: Integer);
  298. var
  299. R: TRect;
  300. begin
  301. if HandleAllocated and (Index >= 0) and (Index < FTabs.Count) then begin
  302. R := GetTabRect(Index);
  303. InvalidateRect(Handle, @R, False);
  304. end;
  305. end;
  306. procedure TNewTabSet.CloseButtonsListChanged(Sender: TObject; const Item: Boolean;
  307. Action: TCollectionNotification);
  308. begin
  309. FHotIndex := -1;
  310. Invalidate;
  311. end;
  312. procedure TNewTabSet.TabsListChanged(Sender: TObject);
  313. begin
  314. FHotIndex := -1;
  315. Invalidate;
  316. end;
  317. procedure TNewTabSet.HintsListChanged(Sender: TObject);
  318. begin
  319. ShowHint := FHints.Count > 0;
  320. end;
  321. procedure TNewTabSet.MouseDown(Button: TMouseButton; Shift: TShiftState; X,
  322. Y: Integer);
  323. var
  324. I: Integer;
  325. R: TRect;
  326. begin
  327. if Button = mbLeft then begin
  328. for I := 0 to FTabs.Count-1 do begin
  329. R := GetTabRect(I);
  330. if (X >= R.Left) and (X < R.Right) then begin
  331. if ((I = TabIndex) or (I = FHotIndex)) and (I < FCloseButtons.Count) and FCloseButtons[I] then begin
  332. var R2 := GetCloseButtonRect(R);
  333. if PtInRect(R2, TPoint.Create(X, Y)) then begin
  334. if Assigned(OnCloseButtonClick) then
  335. OnCloseButtonClick(Self, I);
  336. Break;
  337. end;
  338. end;
  339. TabIndex := I;
  340. Break;
  341. end;
  342. end;
  343. end;
  344. end;
  345. procedure TNewTabSet.UpdateHotIndex(NewHotIndex: Integer);
  346. begin
  347. var OldHotIndex := FHotIndex;
  348. if NewHotIndex <> OldHotIndex then begin
  349. FHotIndex := NewHotIndex;
  350. if OldHotIndex <> -1 then
  351. InvalidateTab(OldHotIndex);
  352. if NewHotIndex <> -1 then
  353. InvalidateTab(NewHotIndex);
  354. end;
  355. end;
  356. procedure TNewTabSet.CMMouseLeave(var Message: TMessage);
  357. begin
  358. UpdateHotIndex(-1);
  359. inherited;
  360. end;
  361. procedure TNewTabSet.Paint;
  362. var
  363. HighColorMode: Boolean;
  364. procedure DrawCloseButton(const TabRect: TRect; const TabIndex: Integer);
  365. const
  366. MENU_SYSTEMCLOSE = 17;
  367. MSYSC_NORMAL = 1;
  368. begin
  369. if (TabIndex < FCloseButtons.Count) and FCloseButtons[TabIndex] then begin
  370. var R := GetCloseButtonRect(TabRect);
  371. if FMenuThemeData <> 0 then begin
  372. var Offset := ToCurrentPPI(1);
  373. Inc(R.Left, Offset);
  374. Inc(R.Top, Offset);
  375. DrawThemeBackground(FMenuThemeData, Canvas.Handle, MENU_SYSTEMCLOSE, MSYSC_NORMAL, R, nil);
  376. end else begin
  377. InflateRect(R, -ToCurrentPPI(3), -ToCurrentPPI(6));
  378. Canvas.Pen.Color := Canvas.Font.Color;
  379. Canvas.MoveTo(R.Left, R.Top);
  380. Canvas.LineTo(R.Right, R.Bottom);
  381. Canvas.MoveTo(R.Left, R.Bottom-1);
  382. Canvas.LineTo(R.Right, R.Top-1);
  383. end;
  384. end;
  385. end;
  386. procedure DrawTabs(const SelectedTab: Boolean);
  387. var
  388. I: Integer;
  389. R: TRect;
  390. begin
  391. for I := 0 to FTabs.Count-1 do begin
  392. R := GetTabRect(I);
  393. if SelectedTab and (FTabIndex = I) then begin
  394. if FTheme <> nil then
  395. Canvas.Brush.Color := FTheme.Colors[tcBack]
  396. else
  397. Canvas.Brush.Color := clBtnFace;
  398. Canvas.FillRect(R);
  399. if FTheme <> nil then
  400. Canvas.Font.Color := FTheme.Colors[tcFore]
  401. else
  402. Canvas.Font.Color := clBtnText;
  403. Canvas.TextOut(R.Left + ToCurrentPPI(TabPaddingX), R.Top + ToCurrentPPI(TabPaddingY), FTabs[I]);
  404. DrawCloseButton(R, I);
  405. ExcludeClipRect(Canvas.Handle, R.Left, R.Top, R.Right, R.Bottom);
  406. Break;
  407. end;
  408. if not SelectedTab and (FTabIndex <> I) then begin
  409. if FHotIndex = I then begin
  410. if FTheme <> nil then
  411. Canvas.Font.Color := FTheme.Colors[tcFore]
  412. else
  413. Canvas.Font.Color := clBtnText;
  414. end else if FTheme <> nil then
  415. Canvas.Font.Color := FTheme.Colors[tcMarginFore]
  416. else if HighColorMode and (ColorToRGB(clBtnFace) <> clBlack) then
  417. Canvas.Font.Color := LightenColor(ColorToRGB(clBtnShadow), -43)
  418. else begin
  419. { If the button face color is black, or if running in low color mode,
  420. use plain clBtnHighlight as the text color }
  421. Canvas.Font.Color := clBtnHighlight;
  422. end;
  423. Canvas.TextOut(R.Left + ToCurrentPPI(TabPaddingX), R.Top + ToCurrentPPI(TabPaddingY), FTabs[I]);
  424. if FHotIndex = I then
  425. DrawCloseButton(R, I);
  426. end;
  427. end;
  428. end;
  429. var
  430. CR: TRect;
  431. begin
  432. Canvas.Font.Assign(Font);
  433. HighColorMode := (GetDeviceCaps(Canvas.Handle, BITSPIXEL) *
  434. GetDeviceCaps(Canvas.Handle, PLANES)) >= 15;
  435. CR := ClientRect;
  436. { Work around an apparent NT 4.0/2000/??? bug. If the width of the DC is
  437. greater than the width of the screen, then any call to ExcludeClipRect
  438. inexplicably shrinks the DC's clipping rectangle to the screen width.
  439. Calling IntersectClipRect first with the entire client area as the
  440. rectangle solves this (don't ask me why). }
  441. IntersectClipRect(Canvas.Handle, CR.Left, CR.Top, CR.Right, CR.Bottom);
  442. { Selected tab }
  443. DrawTabs(True);
  444. { Top or bottom line }
  445. if FTheme <> nil then
  446. Canvas.Brush.Color := FTheme.Colors[tcBack]
  447. else
  448. Canvas.Brush.Color := clBtnFace;
  449. const LineRectHeight = ToCurrentPPI(1);
  450. var LineRect := CR;
  451. if FTabPosition = tpBottom then
  452. LineRect.Bottom := LineRect.Top + LineRectHeight
  453. else
  454. LineRect.Top := LineRect.Bottom - LineRectHeight;
  455. Canvas.FillRect(LineRect);
  456. { Background fill }
  457. if FTheme <> nil then
  458. Canvas.Brush.Color := FTheme.Colors[tcMarginBack]
  459. else if HighColorMode then
  460. Canvas.Brush.Color := LightenColor(ColorToRGB(clBtnFace), 35)
  461. else
  462. Canvas.Brush.Color := clBtnShadow;
  463. if FTabPosition = tpBottom then
  464. Inc(CR.Top, LineRectHeight)
  465. else
  466. Dec(CR.Bottom, LineRectHeight);
  467. Canvas.FillRect(CR);
  468. { Non-selected tabs }
  469. DrawTabs(False);
  470. end;
  471. procedure TNewTabSet.Resize;
  472. begin
  473. EnsureCurrentTabIsFullyVisible;
  474. inherited;
  475. end;
  476. procedure TNewTabSet.SetCloseButtons(Value: TBoolList);
  477. begin
  478. FCloseButtons.Clear;
  479. for var V in Value do
  480. FCloseButtons.Add(V);
  481. end;
  482. procedure TNewTabSet.SetHints(const Value: TStrings);
  483. begin
  484. FHints.Assign(Value);
  485. end;
  486. procedure TNewTabSet.SetTabIndex(Value: Integer);
  487. begin
  488. if FTabIndex <> Value then begin
  489. InvalidateTab(FTabIndex);
  490. FTabIndex := Value;
  491. InvalidateTab(Value);
  492. EnsureCurrentTabIsFullyVisible;
  493. Click;
  494. end;
  495. end;
  496. procedure TNewTabSet.SetTabPosition(Value: TTabPosition);
  497. begin
  498. if FTabPosition <> Value then begin
  499. FTabPosition := Value;
  500. Invalidate;
  501. end;
  502. end;
  503. procedure TNewTabSet.SetTabs(Value: TStrings);
  504. begin
  505. FTabs.Assign(Value);
  506. if FTabIndex >= FTabs.Count then
  507. SetTabIndex(FTabs.Count-1);
  508. end;
  509. procedure TNewTabSet.SetTheme(Value: TTheme);
  510. begin
  511. if FTheme <> Value then begin
  512. FTheme := Value;
  513. var NewThemeDark := (FTheme <> nil) and FTheme.Dark;
  514. if FThemeDark <> NewThemeDark then
  515. UpdateThemeData(True);
  516. FThemeDark := NewThemeDark;
  517. Invalidate;
  518. end;
  519. end;
  520. function TNewTabSet.ToCurrentPPI(const XY: Integer): Integer;
  521. begin
  522. Result := MulDiv(XY, CurrentPPI, 96);
  523. end;
  524. procedure TNewTabSet.UpdateThemeData(const Open: Boolean);
  525. begin
  526. if FMenuThemeData <> 0 then begin
  527. CloseThemeData(FMenuThemeData);
  528. FMenuThemeData := 0;
  529. end;
  530. if Open and UseThemes then begin
  531. if (FTheme <> nil) and FTheme.Dark then
  532. FMenuThemeData := OpenThemeData(Handle, 'DarkMode::Menu');
  533. if FMenuThemeData = 0 then
  534. FMenuThemeData := OpenThemeData(Handle, 'Menu');
  535. end;
  536. end;
  537. end.