NewTabSet.pas 17 KB

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