From a3ac459abc434f5743490a389240cedf0639a5aa Mon Sep 17 00:00:00 2001 From: Uladzimir Karpenka Date: Thu, 2 Jul 2026 14:04:21 +0300 Subject: [PATCH] feat(overlay): flat themed buttons + full theme support for slice flags MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Share one button paint routine: extract TFlatButton.Paint into PaintFlatButton(canvas, rect, colors) and call it from both TFlatButton and VfoOverlay.DrawButton — flag buttons now render identically to the left panel (flat fill + border, no gloss), using theme Btn* colors. Theme the whole flag card: the structural CLR_* palette (card bg, border, text, dim, accents, freq, meter bg) is now derived from the active TAppTheme via ApplyThemeColors, so flags follow light/dark like the rest of the UI. Status accents (TX/SPLIT) stay fixed on purpose and use a fixed dark text over their bright badges. Theme is pushed to flags from ApplyDarkTheme and on creation via WireOverlayEvents. Co-Authored-By: Claude Opus 4.8 --- FlatButton.pas | 58 ++++++++++++++--------- MainForm.pas | 7 +++ VfoOverlay.pas | 123 ++++++++++++++++++++++--------------------------- 3 files changed, 98 insertions(+), 90 deletions(-) diff --git a/FlatButton.pas b/FlatButton.pas index ec6a228..6f675aa 100644 --- a/FlatButton.pas +++ b/FlatButton.pas @@ -51,6 +51,14 @@ type property Visible; end; +// Отрисовка кнопки в стиле TFlatButton на произвольном канвасе/прямоугольнике. +// Используется и TFlatButton.Paint, и оверлеями, которые рендерятся в битмап +// (VfoOverlay) — чтобы вид кнопок был одинаковым по всей программе. Шрифт +// берётся текущий у канваса (вызывающий выставляет Name/Size заранее). +procedure PaintFlatButton(C: TCanvas; const R: TRect; const ACaption: string; + AActive, AHot: Boolean; + ClrNorm, ClrActive, ClrHot, ClrBorder, ClrText, ClrTextAct: TColor); + // Фабрика — аналог MakeBtn, возвращает TFlatButton function MakeFlatBtn(AParent: TWinControl; const ACap: string; ALeft, ATop, AW, AH: Integer; @@ -85,39 +93,45 @@ begin Invalidate; end; -procedure TFlatButton.Paint; +procedure PaintFlatButton(C: TCanvas; const R: TRect; const ACaption: string; + AActive, AHot: Boolean; + ClrNorm, ClrActive, ClrHot, ClrBorder, ClrText, ClrTextAct: TColor); const CORNER = 3; var - R: TRect; - TW, TH: Integer; BG: TColor; + TW, TH: Integer; begin - R := ClientRect; - - if FActive then BG := FClrActive - else if FHot then BG := FClrHot - else BG := FClrNorm; + if AActive then BG := ClrActive + else if AHot then BG := ClrHot + else BG := ClrNorm; // Фон со скруглёнными углами - Canvas.Brush.Color := BG; - Canvas.Brush.Style := bsSolid; - Canvas.Pen.Color := BG; - Canvas.Pen.Style := psSolid; - Canvas.RoundRect(R.Left, R.Top, R.Right, R.Bottom, CORNER * 2, CORNER * 2); + C.Brush.Color := BG; + C.Brush.Style := bsSolid; + C.Pen.Color := BG; + C.Pen.Style := psSolid; + C.RoundRect(R.Left, R.Top, R.Right, R.Bottom, CORNER * 2, CORNER * 2); // Рамка - Canvas.Pen.Color := FClrBorder; - Canvas.Brush.Style := bsClear; - Canvas.RoundRect(R.Left, R.Top, R.Right - 1, R.Bottom - 1, CORNER * 2, CORNER * 2); + C.Pen.Color := ClrBorder; + C.Brush.Style := bsClear; + C.RoundRect(R.Left, R.Top, R.Right - 1, R.Bottom - 1, CORNER * 2, CORNER * 2); // Текст - if FActive then Canvas.Font.Color := FClrTextAct - else Canvas.Font.Color := FClrText; - Canvas.Brush.Style := bsClear; - TW := Canvas.TextWidth(Caption); - TH := Canvas.TextHeight('A'); - Canvas.TextOut((Width - TW) div 2, (Height - TH) div 2, Caption); + if AActive then C.Font.Color := ClrTextAct + else C.Font.Color := ClrText; + C.Brush.Style := bsClear; + TW := C.TextWidth(ACaption); + TH := C.TextHeight('A'); + C.TextOut(R.Left + (R.Right - R.Left - TW) div 2, + R.Top + (R.Bottom - R.Top - TH) div 2, ACaption); +end; + +procedure TFlatButton.Paint; +begin + PaintFlatButton(Canvas, ClientRect, Caption, FActive, FHot, + FClrNorm, FClrActive, FClrHot, FClrBorder, FClrText, FClrTextAct); end; procedure TFlatButton.MouseEnter; diff --git a/MainForm.pas b/MainForm.pas index c3bc315..5dfc88a 100644 --- a/MainForm.pas +++ b/MainForm.pas @@ -2853,6 +2853,11 @@ begin FSpecView.SetTheme(T); if Assigned(FWidebandView) then FWidebandView.SetTheme(T); + // Флаги слайсов (оверлеи) — цвета кнопок из темы, как левая панель. + if Assigned(FVfoOverlay) then FVfoOverlay.SetTheme(T); + for i := 0 to High(FSliceFlags) do + if Assigned(FSliceFlags[i]) then FSliceFlags[i].SetTheme(T); + // VFO display — вызывает UpdateVfoDisplay, который использует тему UpdateVfoDisplay; end; @@ -5451,6 +5456,8 @@ begin O.OnSliceSelect := OnVfoOverlaySliceSelect; O.OnClose := OnVfoOverlayClose; O.OnInvalidate := OnVfoOverlayInvalidate; + // Актуальная тема (цвета кнопок флага = как на левой панели). + if FLightTheme then O.SetTheme(LightTheme) else O.SetTheme(DarkTheme); end; function TMainForm.FindSliceFlag(SliceId: Integer): TVfoOverlay; diff --git a/VfoOverlay.pas b/VfoOverlay.pas index 49bd2be..5eea9fc 100644 --- a/VfoOverlay.pas +++ b/VfoOverlay.pas @@ -17,7 +17,7 @@ unit VfoOverlay; interface uses - Classes, SysUtils, Controls, Graphics, Types, Math; + Classes, SysUtils, Controls, Graphics, Types, Math, FlatButton, AppTheme; const VFO_OVERLAY_ALPHA = 210; @@ -88,6 +88,12 @@ type FOnTxSelect: TNotifyEvent; FOnInvalidate: TNotifyEvent; + FTheme: TAppTheme; // тема приложения (цвета кнопок и т.п.) + // Структурные цвета карточки, выведенные из FTheme (ApplyThemeColors). + CLR_CARD_BG, CLR_BORDER, CLR_SEP, CLR_TEXT, CLR_DIM, + CLR_ACCENT, CLR_ACCENT_D, CLR_BTN_NORM, CLR_BTN_HOT, + CLR_FREQ, CLR_SVAL, CLR_BAR_BG: TColor; + FSliceId: Integer; // логический id слайса (владелец — MainForm/контроллер) FIsMain: Boolean; // главный приёмник (слайс A) — без кнопки закрытия FOnSliceSelect: TNotifyEvent; // клик по бейджу буквы — сделать слайс активным @@ -116,6 +122,7 @@ type procedure DrawDspPanel(C: TCanvas; X, Y, RowW: Integer); procedure DrawAudPanel(C: TCanvas; X, Y, RowW: Integer); procedure RegisterHit(const HR: TRect; HV: Integer); + procedure ApplyThemeColors; // раскладывает FTheme → структурные CLR_* function FormatFreq(Hz: Double): string; function GetFilterBWForMode(ModeIdx, FilterIdx: Integer): Integer; function GetFilterLblForMode(ModeIdx, FilterIdx: Integer): string; @@ -157,6 +164,8 @@ type const CurName: string); procedure UpdateSMeter(DB: Double); procedure UpdateVfo(AVfoHz: Double); + // Тема приложения (цвета кнопок берутся отсюда — как левая панель). + procedure SetTheme(const T: TAppTheme); // Привязка к слайсу: id, буква флага, признак главного (слайс A). procedure SetSliceInfo(ASliceId: Integer; ALetter: Char; AIsMain: Boolean); property SliceId: Integer read FSliceId; @@ -237,22 +246,14 @@ const AUD_MAX_VIS = 6; // максимум видимых устройств (дальше — прокрутка) OVL_BASE_H = PAD + HDR_H + MET_H + FRQ_H + BAR_H + PAD; // = 96 - // Палитра - CLR_CARD_BG = TColor($00191919); - CLR_BORDER = TColor($00484848); - CLR_SEP = TColor($00303030); - CLR_TEXT = TColor($00E4E4E4); - CLR_DIM = TColor($00707070); - CLR_ACCENT = TColor($0040FF80); // зелёный акцент - CLR_ACCENT_D = TColor($00103018); // тёмный акцент (фон активной) - CLR_BTN_NORM = TColor($00262626); - CLR_BTN_HOT = TColor($00383838); - CLR_FREQ = TColor($0050FF80); - CLR_SVAL = TColor($0030C8FF); - CLR_BAR_BG = TColor($00101010); - CLR_TX_ON = TColor($000030E0); // красный TX идёт (BGR) - CLR_TX_SEL = TColor($0010A8F0); // янтарный: слайс выбран для передачи - CLR_SPLIT_ON = TColor($0020C0FF); // оранжевый SPLIT + // Палитра. Структурные цвета (фон/текст/акценты) берутся из темы — + // см. поля CLR_* класса + ApplyThemeColors. Здесь — только статус-акценты, + // которые намеренно одинаковы в обеих темах (яркие бейджи-статусы), и + // тёмный текст поверх них. + CLR_TX_ON = TColor($000030E0); // красный TX идёт (BGR) + CLR_TX_SEL = TColor($0010A8F0); // янтарный: слайс выбран для передачи + CLR_SPLIT_ON = TColor($0020C0FF); // оранжевый SPLIT + CLR_BADGE_TXT = TColor($00181818); // тёмный текст поверх ярких бейджей OVL_MODE_COUNT = 8; OVL_MODE_NAMES: array[0..7] of string = ( @@ -407,11 +408,38 @@ begin FCacheDirty := True; FMeterDirty := False; FLastSTextRect := Rect(0, 0, 0, 0); + FTheme := DarkTheme; // по умолчанию тёмная; MainForm выставит актуальную + ApplyThemeColors; Width := OVL_W; Height := OVL_BASE_H; Cursor := crHandPoint; end; +// Раскладывает тему в структурные цвета карточки. Статус-акценты (TX/SPLIT) +// намеренно НЕ тематизируются — они одинаковы в обеих темах. +procedure TVfoOverlay.ApplyThemeColors; +begin + CLR_CARD_BG := FTheme.Panel; + CLR_BORDER := FTheme.Border; + CLR_SEP := FTheme.Border; + CLR_TEXT := FTheme.Text; + CLR_DIM := FTheme.TextDim; + CLR_ACCENT := FTheme.BtnTextActive; // зелёный акцент (яркий/тёмный по теме) + CLR_ACCENT_D := FTheme.BtnActive; // фон активной строки + CLR_BTN_NORM := FTheme.BtnNorm; + CLR_BTN_HOT := FTheme.BtnHot; + CLR_FREQ := FTheme.FreqActive; + CLR_SVAL := FTheme.Amber; // числовое значение S-метра + CLR_BAR_BG := FTheme.BG; // фон полоски S-метра +end; + +procedure TVfoOverlay.SetTheme(const T: TAppTheme); +begin + FTheme := T; + ApplyThemeColors; + RequestInvalidate; // перерисовать хром флага с новыми цветами +end; + destructor TVfoOverlay.Destroy; begin FDevNames.Free; @@ -522,61 +550,20 @@ end; { ---- Примитивы отрисовки ---- } -// Масштаб яркости цвета (для стеклянного градиента/фаски). -function GlossColor(Base: TColor; F: Double): TColor; -var - R, G, B: Byte; -begin - RedGreenBlue(ColorToRGB(Base), R, G, B); - Result := RGBToColor(EnsureRange(Round(R * F), 0, 255), - EnsureRange(Round(G * F), 0, 255), - EnsureRange(Round(B * F), 0, 255)); -end; - procedure TVfoOverlay.DrawButton(C: TCanvas; const R: TRect; const Lbl: string; Active, Hot: Boolean; FontSz: Integer); var - Base: TColor; - Y, H: Integer; - RowT, Gloss: Double; + Brd: TColor; begin - if Active then Base := CLR_ACCENT - else if Hot then Base := CLR_BTN_HOT - else Base := CLR_BTN_NORM; - - // Скруглённая тёмная база = форма + рамка-фаска. - C.Brush.Color := GlossColor(Base, 0.58); - C.Brush.Style := bsSolid; - C.Pen.Color := GlossColor(Base, 0.42); - C.Pen.Style := psSolid; - C.RoundRect(R.Left, R.Top, R.Right, R.Bottom, 5, 5); - - // Стеклянный вертикальный градиент: ярче вверху, темнее внизу (как ползунок зума). - H := R.Bottom - R.Top; - C.Pen.Style := psClear; - for Y := R.Top + 1 to R.Bottom - 2 do - begin - if H > 3 then RowT := (Y - (R.Top + 1)) / (H - 3) else RowT := 0.0; - Gloss := 1.30 - 0.60 * RowT; - C.Brush.Color := GlossColor(Base, Gloss); - C.FillRect(Rect(R.Left + 2, Y, R.Right - 2, Y + 1)); - end; - - // Блик сверху + тень снизу (объём). - C.Pen.Style := psSolid; - C.Pen.Color := GlossColor(Base, 1.55); - C.Line(R.Left + 2, R.Top + 1, R.Right - 2, R.Top + 1); - C.Pen.Color := GlossColor(Base, 0.40); - C.Line(R.Left + 2, R.Bottom - 2, R.Right - 2, R.Bottom - 2); - - if Active then C.Font.Color := CLR_CARD_BG - else C.Font.Color := CLR_TEXT; + // Цвета берём из темы (как StyleButton на левой панели): рамка — по состоянию. + if Active then Brd := FTheme.BtnBorderActive else Brd := FTheme.BtnBorderNorm; C.Font.Name := 'Courier New'; C.Font.Size := FontSz; C.Font.Style := []; - C.Brush.Style := bsClear; - C.TextOut(R.Left + (R.Right - R.Left - C.TextWidth(Lbl)) div 2, - R.Top + (R.Bottom - R.Top - C.TextHeight('A')) div 2, Lbl); + // Общая отрисовка (FlatButton) — вид как у кнопок по всей программе. + PaintFlatButton(C, R, Lbl, Active, Hot, + FTheme.BtnNorm, FTheme.BtnActive, FTheme.BtnHot, Brd, + FTheme.BtnText, FTheme.BtnTextActive); end; procedure TVfoOverlay.DrawIndicator(C: TCanvas; X, Y: Integer; const Lbl: string; @@ -917,7 +904,7 @@ begin CurY := PAD; { --- Шапка: [A] BW ... [×] SPLIT [TX] --- } - DrawBadge(C, Rect(X, CurY, X+16, CurY+HDR_BADGE_H), FSlice, SliceColor(FSlice), CLR_CARD_BG); + DrawBadge(C, Rect(X, CurY, X+16, CurY+HDR_BADGE_H), FSlice, SliceColor(FSlice), CLR_BADGE_TXT); RegisterHit(Rect(X, CurY, X+16, CurY+HDR_BADGE_H), HIT_SLICE_SEL); // бейдж = выбор слайса // Кнопка закрытия × — только у неглавных слайсов (главный приёмник не закрыть). if not FIsMain then @@ -936,12 +923,12 @@ begin // передача идёт → красный; слайс выбран для TX → янтарный; иначе → нейтральный. R := Rect(W - PAD - 26, CurY, W - PAD, CurY + HDR_BADGE_H); if FTx then DrawBadge(C, R, 'TX', CLR_TX_ON, clWhite) - else if FTxSel then DrawBadge(C, R, 'TX', CLR_TX_SEL, CLR_CARD_BG) + else if FTxSel then DrawBadge(C, R, 'TX', CLR_TX_SEL, CLR_BADGE_TXT) else DrawBadge(C, R, 'TX', CLR_BTN_NORM, CLR_DIM); RegisterHit(R, HIT_TX); // SPLIT (левее TX) — кликабельно R := Rect(W - PAD - 26 - 44, CurY, W - PAD - 26 - 4, CurY + HDR_BADGE_H); - if FSplit then DrawBadge(C, R, 'SPLIT', CLR_SPLIT_ON, CLR_CARD_BG) + if FSplit then DrawBadge(C, R, 'SPLIT', CLR_SPLIT_ON, CLR_BADGE_TXT) else DrawBadge(C, R, 'SPLIT', CLR_BTN_NORM, CLR_DIM); RegisterHit(R, HIT_SPLIT); Inc(CurY, HDR_H);