add light/dark theme with global persistence

- AppTheme.pas: новый файл, TAppTheme record + DarkTheme()/LightTheme()
- FlatSlider: цвета через SetThemeColors вместо хардкода
- SpectrumView: все цвета из TAppTheme (SetTheme)
- SettingsForm: ApplyTheme(T) с рекурсивным обходом контролов
- DeviceForm: SetTheme(T) — перекраска всех элементов включая TEdit
- Settings: SaveTheme/LoadTheme в корневой секции preferences (не привязано к устройству)
- MainForm: тема загружается при старте, сохраняется при изменении

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-02 19:00:45 +03:00
co-authored by Claude Sonnet 4.6
parent f89b15730e
commit d9a2157719
8 changed files with 785 additions and 173 deletions
+69 -44
View File
@@ -18,7 +18,7 @@ interface
uses
Classes, SysUtils, Graphics, ExtCtrls, Controls, Math,
IntfGraphics, FPImage, LCLIntf, LCLType, GraphType;
IntfGraphics, FPImage, LCLIntf, LCLType, GraphType, AppTheme;
const
SV_CLR_BG = TColor($00101010);
@@ -44,6 +44,9 @@ type
FWfIntfImg: TLazIntfImage;
FWfPixels: array of LongWord;
FSpPts: array of TPoint;
// ── Тема ─────────────────────────────────────────────────────────────────
FTheme: TAppTheme;
FLightTheme: Boolean;
FSpPtsLen: Integer;
FSmBitmap: TBitmap;
FRulerLastFreq: Double;
@@ -197,6 +200,8 @@ type
// ── Утилиты ───────────────────────────────────────────────────────────────
procedure InvalidateGridCache;
// ── Тема ─────────────────────────────────────────────────────────────────
procedure SetTheme(const T: TAppTheme);
procedure InvalidateRulerCache;
procedure ResetSpectrumBuf;
procedure ResetWfAvgBuf;
@@ -360,6 +365,8 @@ begin
FWaterfallDirty := True;
FLastSMeter := -130; FSMeterPeak := -130; FSMeterMin := -130;
FLastFwdW := 0; FLastSWR := 1;
FLightTheme := False;
FTheme := DarkTheme;
ResetSpectrumBuf;
end;
@@ -439,14 +446,17 @@ begin
Alpha := Round((1.0 - Sqr((Row - YMin) / Max(1.0, H - YMin - 1.0))) * 200);
if Alpha <= 0 then Continue;
if Alpha > 200 then Alpha := 200;
GG := Byte(Alpha shr 1); RG := Byte(Alpha);
GG := Byte(FTheme.SpecGradB * Alpha div 200);
RG := Byte(FTheme.SpecGradG * Alpha div 200);
RowPtr := FSpecGradImg.GetDataLineStart(Row);
if RowPtr = nil then Continue;
for Col := 0 to W - 1 do
begin
if SpPts[Col].Y <= Row then
begin
RowPtr[0] := GG; RowPtr[1] := RG; RowPtr[2] := 0;
RowPtr[0] := GG;
RowPtr[1] := RG;
RowPtr[2] := Byte(FTheme.SpecGradR * Alpha div 200);
if BPPg >= 4 then RowPtr[3] := Byte(Alpha);
end else begin
RowPtr[0] := 0; RowPtr[1] := 0; RowPtr[2] := 0;
@@ -495,7 +505,7 @@ procedure TSpectrumView.SetSpectrumBitmapSize(W, H: Integer);
begin
if (W <= 0) or (H <= 0) then Exit;
FSpectrumBitmap.SetSize(W, H);
FSpectrumBitmap.Canvas.Brush.Color := SV_CLR_BG;
FSpectrumBitmap.Canvas.Brush.Color := FTheme.BG;
FSpectrumBitmap.Canvas.FillRect(Rect(0, 0, W, H));
FreeAndNil(FSpecGradImg);
InvalidateGridCache;
@@ -532,6 +542,16 @@ begin
FSpectrumDirty := True;
end;
procedure TSpectrumView.SetTheme(const T: TAppTheme);
begin
FTheme := T;
FLightTheme := T.BG > TColor($00808080); // светлая тема если фон светлее 50%
FreeAndNil(FSpecGradImg); // пересчитать градиент
InvalidateGridCache;
InvalidateRulerCache;
FWaterfallDirty := True;
end;
procedure TSpectrumView.InvalidateRulerCache;
begin
FRulerLastFreq := -1.0; FRulerLastVfo := -1.0; FRulerLastSpan := -1.0;
@@ -606,20 +626,20 @@ begin
FGridBitmapW := W; FGridBitmapH := H;
FGridBitmap.SetSize(W, H);
GC := FGridBitmap.Canvas;
PaintVerticalGradient(GC, W, H, TColor($0020160B), TColor($006C4F1F));
PaintVerticalGradient(GC, W, H, FTheme.SpecGradTop, FTheme.SpecGradBot);
LabelBandW := 28;
GC.Pen.Style := psClear; GC.Brush.Style := bsSolid;
GC.Brush.Color := TColor($00543F18);
GC.Brush.Color := FTheme.SpecLabelBand;
GC.FillRect(Rect(0, 0, LabelBandW, H));
GC.Pen.Style := psSolid;
GC.Font.Size := 7; GC.Font.Name := 'Courier New';
GC.Font.Color := TColor($00A8C9D9);
GC.Font.Color := FTheme.SpecLabelText;
GC.Brush.Style := bsClear;
dB := DBmax - FSpecGridStep;
while dB >= DBmin do
begin
Yp := Round((DBmax - dB) * InvRange * H);
GC.Pen.Color := TColor($00685624); GC.Pen.Width := 1;
GC.Pen.Color := FTheme.SpecGrid; GC.Pen.Width := 1;
GC.MoveTo(0, Yp); GC.LineTo(W-1, Yp);
GC.TextOut(1, Yp - 9, Format('%4.0f', [dB]));
dB := dB - FSpecGridStep;
@@ -627,7 +647,7 @@ begin
for i := 0 to 8 do
begin
GX := ScaleX(i, 8, W);
GC.Pen.Color := TColor($00685624);
GC.Pen.Color := FTheme.SpecGrid;
GC.MoveTo(GX, 0); GC.LineTo(GX, H);
end;
end;
@@ -646,7 +666,7 @@ begin
end;
X1 := Max(0, Min(W-1, VfoX + Round(Lo_Hz / FSpanHz * W)));
X2 := Max(0, Min(W-1, VfoX + Round(Hi_Hz / FSpanHz * W)));
C.Brush.Color := TColor($006A4E22); C.Brush.Style := bsSolid; C.Pen.Style := psClear;
C.Brush.Color := FTheme.SpecFilter; C.Brush.Style := bsSolid; C.Pen.Style := psClear;
if X2 > X1 then C.FillRect(Rect(X1, 0, X2, H));
C.Pen.Style := psSolid;
@@ -656,26 +676,26 @@ begin
AGCy := Round((DBmax - FAGCThresh) * InvRange * H);
if (AGCy >= 0) and (AGCy < H) then
begin
C.Pen.Color := TColor($0000AAFF); C.Pen.Width := 1; C.Pen.Style := psDash;
C.Pen.Color := FTheme.SpecAgcColor; C.Pen.Width := 1; C.Pen.Style := psDash;
C.MoveTo(0, AGCy); C.LineTo(W, AGCy);
C.Pen.Style := psSolid; C.Font.Color := TColor($0000AAFF); C.Font.Size := 6;
C.Pen.Style := psSolid; C.Font.Color := FTheme.SpecAgcColor; C.Font.Size := 6;
C.TextOut(4, AGCy - 9, 'AGC T');
end;
AGCHangY := Round((DBmax - FAGCHangLevel) * InvRange * H);
if (AGCHangY >= 0) and (AGCHangY < H) and (Abs(AGCHangY - AGCy) > 4) then
begin
C.Pen.Color := TColor($00FFCC00); C.Pen.Width := 1; C.Pen.Style := psDot;
C.Pen.Color := FTheme.SpecAgcHangColor; C.Pen.Width := 1; C.Pen.Style := psDot;
C.MoveTo(0, AGCHangY); C.LineTo(W, AGCHangY);
C.Pen.Style := psSolid; C.Font.Color := TColor($00FFCC00); C.Font.Size := 6;
C.Pen.Style := psSolid; C.Font.Color := FTheme.SpecAgcHangColor; C.Font.Size := 6;
C.TextOut(4, AGCHangY + 2, 'AGC H');
end;
end else begin
AGCy := Round((DBmax - (-FAGCTop)) * InvRange * H);
if (AGCy >= 0) and (AGCy < H) then
begin
C.Pen.Color := TColor($0000AAFF); C.Pen.Width := 1; C.Pen.Style := psDash;
C.Pen.Color := FTheme.SpecAgcColor; C.Pen.Width := 1; C.Pen.Style := psDash;
C.MoveTo(0, AGCy); C.LineTo(W, AGCy);
C.Pen.Style := psSolid; C.Font.Color := TColor($0000AAFF); C.Font.Size := 6;
C.Pen.Style := psSolid; C.Font.Color := FTheme.SpecAgcColor; C.Font.Size := 6;
C.TextOut(4, AGCy - 9, Format('AGC -%ddB', [FAGCTop]));
end;
end;
@@ -700,16 +720,16 @@ begin
DrawSpectrumGradient(FSpPts, W, H);
// ── 6. Линия спектра ──────────────────────────────────────────────────────
C.Pen.Style := psSolid; C.Pen.Color := TColor($0040FF80); C.Pen.Width := 1;
C.Pen.Style := psSolid; C.Pen.Color := FTheme.SpecLine; C.Pen.Width := 1;
C.Polyline(Slice(FSpPts, W));
// ── 7. Края фильтра + VFO ─────────────────────────────────────────────────
C.Pen.Color := TColor($0040FFCC); C.Pen.Width := 1; C.Pen.Style := psSolid;
C.Pen.Color := FTheme.SpecFilterEdge; C.Pen.Width := 1; C.Pen.Style := psSolid;
C.MoveTo(X1, 0); C.LineTo(X1, H);
C.MoveTo(X2, 0); C.LineTo(X2, H);
C.Pen.Color := TColor($0000AAFF); C.Pen.Width := 2;
C.Pen.Color := FTheme.SpecVfoCursor; C.Pen.Width := 2;
C.MoveTo(VfoX, 0); C.LineTo(VfoX, H - 12);
C.Brush.Color := TColor($0000AAFF); C.Brush.Style := bsSolid; C.Pen.Width := 1;
C.Brush.Color := FTheme.SpecVfoCursor; C.Brush.Style := bsSolid; C.Pen.Width := 1;
C.Polygon([Point(VfoX-5,0), Point(VfoX+5,0), Point(VfoX,8)]);
if FMarkerActive then DrawMarkerLine(C, W, H);
@@ -862,8 +882,8 @@ begin
if (FRulerBitmap.Width <> W) or (FRulerBitmap.Height <> H) then
FRulerBitmap.SetSize(W, H);
C := FRulerBitmap.Canvas;
PaintVerticalGradient(C, W, H, TColor($00372A12), TColor($00705A2A));
C.Pen.Color := TColor($00685624); C.Pen.Width := 1;
PaintVerticalGradient(C, W, H, FTheme.RulerGradTop, FTheme.RulerGradBot);
C.Pen.Color := FTheme.RulerBorder; C.Pen.Width := 1;
C.MoveTo(0, 0); C.LineTo(W, 0);
C.MoveTo(0, H-1); C.LineTo(W, H-1);
C.Font.Name := 'Courier New'; C.Font.Size := 7;
@@ -872,18 +892,18 @@ begin
for i := 0 to 8 do
begin
X := ScaleX(i, 8, W);
C.Pen.Color := TColor($00685624);
C.Pen.Color := FTheme.RulerBorder;
C.MoveTo(X, 0); C.LineTo(X, H div 2);
FreqHz := FreqStart + i * FSpanHz / 8;
Lbl := Format('%.3f', [FreqHz / 1e6]);
TW := C.TextWidth(Lbl);
C.Font.Color := TColor($00C8D8E2);
C.Font.Color := FTheme.RulerText;
C.TextOut(X - TW div 2, H div 2 - 1, Lbl);
end;
VfoX := Round((ActiveVfoFreq - FCenterFreq + FSpanHz/2) / FSpanHz * W);
if (VfoX >= 0) and (VfoX < W) then
begin
C.Pen.Color := TColor($0000FF80); C.Pen.Width := 2;
C.Pen.Color := FTheme.RulerVfo; C.Pen.Width := 2;
C.MoveTo(VfoX, 0); C.LineTo(VfoX, H - 1); C.Pen.Width := 1;
end;
end;
@@ -896,7 +916,7 @@ procedure TSpectrumView.DrawBarMeter(ACanvas: TCanvas; R: TRect;
Value, MaxVal: Double; BarColor: TColor);
var Pct, BarW: Integer;
begin
ACanvas.Brush.Color := TColor($00080808);
ACanvas.Brush.Color := FTheme.SMeterBG;
ACanvas.FillRect(R);
if MaxVal > 0 then
Pct := Round(Max(0.0, Min(1.0, Value / MaxVal)) * (R.Right - R.Left - 2))
@@ -918,22 +938,13 @@ const
DBM_LABELS: array[0..5] of string = ('-120','-100','-80','-60','-40','-20');
S_MARKS_DBM: array[0..7] of Double = (-121,-109,-97,-85,-73,-53,-33,-13);
S_MARKS_LBL: array[0..7] of string = ('S1','S3','S5','S7','S9','+20','+40','+60');
CLR_SMETER_BG = TColor($00181818);
CLR_BAR_GREEN = TColor($00091F00);
CLR_BAR_OVER = TColor($00091528);
CLR_PEAK_MARKER = TColor($00F8F8FF);
CLR_TICK_GREEN = TColor($00708070);
CLR_TICK_WHITE = TColor($00E0E0DC);
CLR_TICK_BLUE = TColor($0070A090);
CLR_LABEL_DBM = TColor($0096D8DC);
CLR_LABEL_S = TColor($00F0F0F0);
CLR_SCALE_DBM = TColor($00D0DCDC);
CLR_SCALE_S_GREEN = TColor($00E0E0E0);
CLR_SCALE_S_BLUE = TColor($0090D090);
CLR_BDR = TColor($00303028);
LEFT_INFO_MIN = 62; RIGHT_PAD = 8; LABEL_PAD = 4;
TICK_LONG = 5; TICK_MED = 3;
var
CLR_SMETER_BG, CLR_BAR_GREEN, CLR_BAR_OVER: TColor;
CLR_PEAK_MARKER, CLR_TICK_GREEN, CLR_TICK_WHITE, CLR_TICK_BLUE: TColor;
CLR_LABEL_DBM, CLR_LABEL_S, CLR_SCALE_DBM: TColor;
CLR_SCALE_S_GREEN, CLR_SCALE_S_BLUE, CLR_BDR: TColor;
W, H, BX, BW, LeftInfo: Integer;
Y_BAR_TOP, Y_BAR_BOT: Integer;
BarEnd, PeakLeft, PeakRight, S9X, OvrX: Integer;
@@ -960,6 +971,20 @@ var
end;
begin
CLR_SMETER_BG := FTheme.SMeterBG;
CLR_BAR_GREEN := FTheme.SMeterBarGreen;
CLR_BAR_OVER := FTheme.SMeterBarOver;
CLR_PEAK_MARKER := FTheme.SMeterPeakMarker;
CLR_TICK_GREEN := FTheme.SMeterTickGreen;
CLR_TICK_WHITE := FTheme.SMeterTickWhite;
CLR_TICK_BLUE := FTheme.SMeterTickBlue;
CLR_LABEL_DBM := FTheme.SMeterLabelDbm;
CLR_LABEL_S := FTheme.SMeterLabelS;
CLR_SCALE_DBM := FTheme.SMeterScaleDbm;
CLR_SCALE_S_GREEN:= FTheme.SMeterScaleSGreen;
CLR_SCALE_S_BLUE := FTheme.SMeterScaleSBlue;
CLR_BDR := FTheme.SMeterBdr;
W := R.Right - R.Left; H := R.Bottom - R.Top;
ZX1 := 0; ZX2 := 0; ZY1 := 0; ZY2 := 0;
if (W < 80) or (H < 16) then Exit;
@@ -997,9 +1022,9 @@ begin
ACanvas.Brush.Color := CLR_SMETER_BG; ACanvas.Brush.Style := bsSolid;
ACanvas.Pen.Style := psClear; ACanvas.FillRect(R); ACanvas.Pen.Style := psSolid;
FillBar(BX, Y_BAR_TOP, S9X, Y_BAR_BOT, TColor($00061006));
FillBar(S9X, Y_BAR_TOP, OvrX, Y_BAR_BOT, TColor($000A1006));
FillBar(OvrX, Y_BAR_TOP, BX + BW, Y_BAR_BOT, TColor($00060A10));
FillBar(BX, Y_BAR_TOP, S9X, Y_BAR_BOT, CLR_BAR_GREEN);
FillBar(S9X, Y_BAR_TOP, OvrX, Y_BAR_BOT, CLR_BAR_GREEN);
FillBar(OvrX, Y_BAR_TOP, BX + BW, Y_BAR_BOT, CLR_BAR_OVER);
BarEnd := DBtoX(Value);
PeakLeft := DBtoX(MinVal);
@@ -1029,7 +1054,7 @@ begin
ACanvas.Brush.Style := bsClear; ACanvas.Pen.Color := CLR_BDR;
ACanvas.Rectangle(LblZoneL, Y_BAR_TOP, BX + BW, Y_BAR_BOT);
// разделитель между зоной надписей и шкалой
ACanvas.Pen.Color := TColor($00404040);
ACanvas.Pen.Color := FTheme.SMeterDivider;
ACanvas.MoveTo(BX, Y_BAR_TOP + 1); ACanvas.LineTo(BX, Y_BAR_BOT - 1);
// --- Шкальные тики и надписи ---
@@ -1066,7 +1091,7 @@ begin
// "-85.3" над "dBm", снизу прижаты к верхнему краю полосы
ACanvas.Font.Size := 10; ACanvas.Font.Style := [fsBold]; ACanvas.Font.Color := CLR_LABEL_DBM;
ACanvas.TextOut(LblZoneL + 4, Y_BAR_TOP - TH10 - 2, LblDbm);
ACanvas.Font.Size := 6; ACanvas.Font.Style := []; ACanvas.Font.Color := TColor($00507070);
ACanvas.Font.Size := 6; ACanvas.Font.Style := []; ACanvas.Font.Color := CLR_TICK_BLUE;
ACanvas.TextOut(LblZoneL + 4 + TWDbm + 2, Y_BAR_TOP - TH6 - 4, 'dBm');
// S-значение под полосой, левый край у LblZoneL+4
ACanvas.Font.Size := 10; ACanvas.Font.Style := [fsBold]; ACanvas.Font.Color := CLR_LABEL_S;