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
+168 -82
View File
@@ -22,7 +22,7 @@ unit MainForm;
interface
uses
Classes, SysUtils, StrUtils, FreqDisplay, DeviceForm, VfoOverlay, FlatButton, FlatSlider, Forms, Controls, Graphics, Dialogs,
Classes, SysUtils, StrUtils, FreqDisplay, DeviceForm, VfoOverlay, FlatButton, FlatSlider, AppTheme, Forms, Controls, Graphics, Dialogs,
StdCtrls, ExtCtrls, ComCtrls, Buttons, Menus, Math, Types,
LCLIntf, LCLType, GraphType,
HPSDRProtocol, HPSDRNetwork,
@@ -249,6 +249,7 @@ type
FPendingDev: THPSDRDevice; // устройство ожидающее открытия WDSP
FBandCache: array[0..CFG_BAND_COUNT-1] of TBandSettings; // кэш диапазонов
FDevConnected: Boolean; // True после первого подключения
FLightTheme: Boolean; // True = светлая тема
FCenterFreq: Double; // центр спектра = LO (DDC). При CTUN ON не меняется при кручении VFO
FSpanHz: Double;
FSampleRate: Integer; // текущий DDC sample rate (Hz)
@@ -345,6 +346,7 @@ type
// ---- Helpers ----
procedure BuildUI;
procedure ApplyDarkTheme;
procedure SetLightTheme(V: Boolean);
procedure StyleButton(B: TFlatButton; Active: Boolean = False);
procedure StyleSpanButton(B: TFlatButton; Active: Boolean = False);
procedure PositionSpanOverlayButtons;
@@ -845,6 +847,7 @@ begin
Result.ShowSpectrum := FShowSpectrum;
Result.ShowWaterfall := FShowWaterfall;
Result.DisplayFPS := FDisplayFPS;
Result.LightTheme := FLightTheme;
// Audio device names
Result.AudioOutDevice := FAudioOutDevName;
Result.AudioInDevice := FAudioInDevName;
@@ -1053,6 +1056,7 @@ begin
{$ENDIF}
end;
FSettings.LoadCATSettings(FCATLastGlobal);
FLightTheme := FSettings.LoadTheme;
InitCATEngine;
CATApplySettings(FCATLastGlobal);
FSMeterPeak := -130;
@@ -2125,12 +2129,20 @@ end;
// ===========================================================================
procedure TMainForm.ApplyDarkTheme;
var T: TAppTheme; i: Integer;
procedure DP(P: TPanel);
begin P.Color := T.Panel; P.Font.Color := T.Text; end;
procedure DL(L: TLabel; Dim: Boolean = True);
begin if Dim then L.Font.Color := T.TextDim else L.Font.Color := T.Text; end;
procedure DS(S: TFlatSlider);
begin
P.Color := CLR_PANEL; P.Font.Color := CLR_TEXT;
S.SetThemeColors(T.SliderBG, T.SliderTrackEmpty, T.SliderTrackFill,
T.SliderThumbNorm, T.SliderThumbHot, T.SliderThumbDrag, T.SliderThumbBdr);
end;
begin
Color := CLR_BG; Font.Color := CLR_TEXT;
if FLightTheme then T := LightTheme else T := DarkTheme;
Color := T.BG; Font.Color := T.Text;
DP(PanelToolbar); DP(PanelLeft);
DP(PanelTopVfoGroup); DP(PanelTopVfoA); DP(PanelTopVfoB);
DP(PanelVfoA); DP(PanelVfoB); DP(PanelVfoButtons);
@@ -2138,38 +2150,107 @@ begin
DP(PanelRX); DP(PanelTX);
DP(PanelRight); DP(PanelSpanButtons);
PbSpectrum.Color := T.BG;
PbWaterfall.Color := T.BG;
if PanelSMeterRight <> nil then PanelSMeterRight.Color := T.Panel;
if PbSMeterRight <> nil then PbSMeterRight.Color := T.Panel;
PbFwdPower.Color := T.BG;
PbSWR.Color := T.BG;
PbSpectrum.Color := CLR_BG;
PbWaterfall.Color := CLR_BG;
if PanelSMeterRight <> nil then PanelSMeterRight.Color := CLR_PANEL;
if PbSMeterRight <> nil then PbSMeterRight.Color := CLR_PANEL;
PbFwdPower.Color := CLR_BG;
PbSWR.Color := CLR_BG;
// Метки
DL(LblAGCTop, False);
DL(LblDrv); DL(LblSpan);
LblFwdPwr.Font.Color := T.MeterOn;
LblSWRVal.Font.Color := T.TextDim;
// Кнопки тулбара
BtnDiscover.ClrNorm := T.TbDiscoverNorm;
BtnDiscover.ClrHot := T.TbDiscoverHot;
BtnDiscover.ClrActive := T.TbDiscoverHot;
BtnDiscover.ClrBorder := T.TbDiscoverBorder;
BtnDiscover.ClrText := T.TbDiscoverText;
BtnDiscover.ClrTextAct := T.TbDiscoverText;
BtnDiscover.Invalidate;
BtnStartStop.ClrNorm := T.TbStartNorm;
BtnStartStop.ClrBorder := T.TbStartBorder;
BtnStartStop.ClrText := T.TbStartText;
BtnStartStop.Invalidate;
// Обычные кнопки — все массивы
for i := 0 to BAND_COUNT-1 do StyleButton(BtnBand[i], BtnBand[i].Active);
for i := 0 to MODE_COUNT-1 do StyleButton(BtnMode[i], BtnMode[i].Active);
for i := 0 to FILT_COUNT-1 do StyleButton(BtnFilter[i], BtnFilter[i].Active);
for i := 0 to 4 do StyleButton(BtnAGCMode[i],BtnAGCMode[i].Active);
StyleButton(BtnCTun, BtnCTun.Active);
StyleButton(BtnNR, BtnNR.Active);
StyleButton(BtnNB, BtnNB.Active);
StyleButton(BtnSNB, BtnSNB.Active);
StyleButton(BtnANF, BtnANF.Active);
StyleButton(BtnMute, BtnMute.Active);
StyleButton(BtnWfAGC, BtnWfAGC.Active);
StyleButton(BtnWfNF, BtnWfNF.Active);
StyleButton(BtnMOX, BtnMOX.Active);
// Span / VFO-copy / HidePanel
StyleSpanButton(BtnSpan48k, BtnSpan48k.Active);
StyleSpanButton(BtnSpan96k, BtnSpan96k.Active);
StyleSpanButton(BtnSpan192k, BtnSpan192k.Active);
StyleSpanButton(BtnSpan384k, BtnSpan384k.Active);
StyleSpanButton(BtnSpan768k, BtnSpan768k.Active);
StyleSpanButton(BtnSpan1536k, BtnSpan1536k.Active);
StyleSpanButton(BtnVfoACopyB, False);
StyleSpanButton(BtnVfoSwap, False);
StyleSpanButton(BtnVfoBCopyA, False);
if BtnHidePanel <> nil then StyleSpanButton(BtnHidePanel, False);
// Ползунки
DS(TrkAGC); DS(TrkVolume); DS(TrkDrive);
// Спектр
FSpecView.SetTheme(T);
// VFO display — вызывает UpdateVfoDisplay, который использует тему
UpdateVfoDisplay;
end;
procedure TMainForm.SetLightTheme(V: Boolean);
begin
if FLightTheme = V then Exit;
FLightTheme := V;
ApplyDarkTheme;
FSettings.SaveTheme(V);
FSettings.Save;
if (FSettingsForm <> nil) and TSettingsForm(FSettingsForm).Visible then
TSettingsForm(FSettingsForm).LoadLightTheme(V);
end;
procedure TMainForm.StyleButton(B: TFlatButton; Active: Boolean);
var T: TAppTheme;
begin
if FLightTheme then T := LightTheme else T := DarkTheme;
B.Active := Active;
B.ClrNorm := CLR_INACTIVE;
B.ClrActive := CLR_ACTIVE;
B.ClrHot := TColor($00282828);
if Active then B.ClrBorder := CLR_ACTIVE_BDR
else B.ClrBorder := TColor($00444444);
B.ClrText := CLR_TEXT;
B.ClrTextAct := TColor($0000FF88);
B.ClrNorm := T.BtnNorm;
B.ClrActive := T.BtnActive;
B.ClrHot := T.BtnHot;
if Active then B.ClrBorder := T.BtnBorderActive
else B.ClrBorder := T.BtnBorderNorm;
B.ClrText := T.BtnText;
B.ClrTextAct := T.BtnTextActive;
B.Font.Name := 'Courier New';
B.Font.Size := 8;
end;
procedure TMainForm.StyleSpanButton(B: TFlatButton; Active: Boolean);
var T: TAppTheme;
begin
if FLightTheme then T := LightTheme else T := DarkTheme;
B.Active := Active;
B.ClrNorm := TColor($00543F18);
B.ClrActive := TColor($00624B1D);
B.ClrHot := TColor($00624B1D);
B.ClrBorder := TColor($00685624);
B.ClrText := TColor($00D8E6EE);
B.ClrTextAct := TColor($00F0FAFF);
B.ClrNorm := T.SpanNorm;
B.ClrActive := T.SpanActive;
B.ClrHot := T.SpanActive;
B.ClrBorder := T.SpanBorder;
B.ClrText := T.SpanText;
B.ClrTextAct := T.SpanText;
B.Font.Name := 'Courier New';
B.Font.Size := 7;
end;
@@ -2246,7 +2327,10 @@ end;
procedure TMainForm.UpdateVfoDisplay;
const
TOP_VFO_FREQ_H = 47;
var T: TAppTheme;
begin
if FLightTheme then T := LightTheme else T := DarkTheme;
FreqDispA.Frequency := Round(FVfoA);
FreqDispB.Frequency := Round(FVfoB);
FreqDispA.FontSize := TOP_VFO_FONT;
@@ -2256,56 +2340,52 @@ begin
if FActiveVfo = 0 then
begin
FreqDispA.ColorNormal := CLR_FREQ;
FreqDispB.ColorNormal := CLR_FREQ_DIM;
LblVfoALabel.Font.Color := CLR_FREQ;
LblVfoBLabel.Font.Color := CLR_TEXTDIM;
// Активный VFO-A: зелёный акцент
FreqDispA.ColorNormal := T.FreqActive;
FreqDispB.ColorNormal := T.FreqDim;
LblVfoALabel.Font.Color := T.FreqActive;
LblVfoBLabel.Font.Color := T.TextDim;
if BtnTopVfoASelect <> nil then begin
BtnTopVfoASelect.ClrNorm := TColor($00101C10);
BtnTopVfoASelect.ClrActive := TColor($00182818);
BtnTopVfoASelect.ClrHot := TColor($00182818);
BtnTopVfoASelect.ClrBorder := TColor($004A7A4A);
BtnTopVfoASelect.ClrText := TColor($0060B860);
BtnTopVfoASelect.ClrTextAct := TColor($0040FF60);
BtnTopVfoASelect.ClrNorm := T.VfoSelActNorm;
BtnTopVfoASelect.ClrActive := T.VfoSelActNorm;
BtnTopVfoASelect.ClrHot := T.VfoSelActNorm;
BtnTopVfoASelect.ClrBorder := T.VfoSelActBorder;
BtnTopVfoASelect.ClrText := T.VfoSelActText;
BtnTopVfoASelect.ClrTextAct := T.VfoSelActTextAct;
BtnTopVfoASelect.Active := True;
BtnTopVfoASelect.Invalidate;
end;
// Неактивный VFO-B: приглушённый
if BtnTopVfoBSelect <> nil then begin
BtnTopVfoBSelect.ClrNorm := TColor($001A1A1A);
BtnTopVfoBSelect.ClrActive := TColor($00282828);
BtnTopVfoBSelect.ClrHot := TColor($00252525);
BtnTopVfoBSelect.ClrBorder := TColor($00404040);
BtnTopVfoBSelect.ClrText := CLR_TEXTDIM;
BtnTopVfoBSelect.ClrTextAct := CLR_TEXT;
BtnTopVfoBSelect.ClrNorm := T.VfoSelInaNorm;
BtnTopVfoBSelect.ClrActive := T.VfoSelInaNorm;
BtnTopVfoBSelect.ClrHot := T.VfoSelInaNorm;
BtnTopVfoBSelect.ClrBorder := T.VfoSelInaBorder;
BtnTopVfoBSelect.ClrText := T.VfoSelInaText;
BtnTopVfoBSelect.ClrTextAct := T.VfoSelInaTextAct;
BtnTopVfoBSelect.Active := False;
BtnTopVfoBSelect.Invalidate;
end;
end else begin
FreqDispA.ColorNormal := CLR_FREQ_DIM;
FreqDispB.ColorNormal := CLR_FREQ;
LblVfoALabel.Font.Color := CLR_TEXTDIM;
LblVfoBLabel.Font.Color := CLR_FREQ;
// Неактивный VFO-A: приглушённый
FreqDispA.ColorNormal := T.FreqDim;
FreqDispB.ColorNormal := T.FreqActive;
LblVfoALabel.Font.Color := T.TextDim;
LblVfoBLabel.Font.Color := T.FreqActive;
if BtnTopVfoASelect <> nil then begin
BtnTopVfoASelect.ClrNorm := TColor($001A1A1A);
BtnTopVfoASelect.ClrActive := TColor($00282828);
BtnTopVfoASelect.ClrHot := TColor($00252525);
BtnTopVfoASelect.ClrBorder := TColor($00404040);
BtnTopVfoASelect.ClrText := CLR_TEXTDIM;
BtnTopVfoASelect.ClrTextAct := CLR_TEXT;
BtnTopVfoASelect.ClrNorm := T.VfoSelInaNorm;
BtnTopVfoASelect.ClrActive := T.VfoSelInaNorm;
BtnTopVfoASelect.ClrHot := T.VfoSelInaNorm;
BtnTopVfoASelect.ClrBorder := T.VfoSelInaBorder;
BtnTopVfoASelect.ClrText := T.VfoSelInaText;
BtnTopVfoASelect.ClrTextAct := T.VfoSelInaTextAct;
BtnTopVfoASelect.Active := False;
BtnTopVfoASelect.Invalidate;
end;
// Активный VFO-B: зелёный акцент
if BtnTopVfoBSelect <> nil then begin
BtnTopVfoBSelect.ClrNorm := TColor($00101C10);
BtnTopVfoBSelect.ClrActive := TColor($00182818);
BtnTopVfoBSelect.ClrHot := TColor($00182818);
BtnTopVfoBSelect.ClrBorder := TColor($004A7A4A);
BtnTopVfoBSelect.ClrText := TColor($0060B860);
BtnTopVfoBSelect.ClrTextAct := TColor($0040FF60);
BtnTopVfoBSelect.ClrNorm := T.VfoSelActNorm;
BtnTopVfoBSelect.ClrActive := T.VfoSelActNorm;
BtnTopVfoBSelect.ClrHot := T.VfoSelActNorm;
BtnTopVfoBSelect.ClrBorder := T.VfoSelActBorder;
BtnTopVfoBSelect.ClrText := T.VfoSelActText;
BtnTopVfoBSelect.ClrTextAct := T.VfoSelActTextAct;
BtnTopVfoBSelect.Active := True;
BtnTopVfoBSelect.Invalidate;
end;
@@ -2314,22 +2394,22 @@ begin
begin
if not FSplitTxB then
begin
BtnTopVfoATX.ClrNorm := TColor($00000060);
BtnTopVfoATX.ClrHot := TColor($00000090);
BtnTopVfoATX.ClrActive := TColor($00000090);
BtnTopVfoATX.ClrBorder := TColor($001010B0);
BtnTopVfoATX.ClrText := TColor($00C8D8FF);
BtnTopVfoATX.ClrTextAct := TColor($00E8F0FF);
BtnTopVfoATX.ClrNorm := T.VfoTxActNorm;
BtnTopVfoATX.ClrHot := T.VfoTxActNorm;
BtnTopVfoATX.ClrActive := T.VfoTxActNorm;
BtnTopVfoATX.ClrBorder := T.VfoTxActBorder;
BtnTopVfoATX.ClrText := T.VfoTxActText;
BtnTopVfoATX.ClrTextAct := T.VfoTxActText;
BtnTopVfoATX.Active := True;
end
else
begin
BtnTopVfoATX.ClrNorm := TColor($00303030);
BtnTopVfoATX.ClrHot := TColor($00404040);
BtnTopVfoATX.ClrActive := TColor($00404040);
BtnTopVfoATX.ClrBorder := TColor($00505050);
BtnTopVfoATX.ClrText := CLR_TEXTDIM;
BtnTopVfoATX.ClrTextAct := CLR_TEXT;
BtnTopVfoATX.ClrNorm := T.VfoTxInaNorm;
BtnTopVfoATX.ClrHot := T.VfoTxInaNorm;
BtnTopVfoATX.ClrActive := T.VfoTxInaNorm;
BtnTopVfoATX.ClrBorder := T.VfoTxInaBorder;
BtnTopVfoATX.ClrText := T.VfoTxInaText;
BtnTopVfoATX.ClrTextAct := T.VfoTxInaText;
BtnTopVfoATX.Active := False;
end;
BtnTopVfoATX.Invalidate;
@@ -2338,22 +2418,22 @@ begin
begin
if FSplitTxB then
begin
BtnTopVfoBTX.ClrNorm := TColor($00000060);
BtnTopVfoBTX.ClrHot := TColor($00000090);
BtnTopVfoBTX.ClrActive := TColor($00000090);
BtnTopVfoBTX.ClrBorder := TColor($001010B0);
BtnTopVfoBTX.ClrText := TColor($00C8D8FF);
BtnTopVfoBTX.ClrTextAct := TColor($00E8F0FF);
BtnTopVfoBTX.ClrNorm := T.VfoTxActNorm;
BtnTopVfoBTX.ClrHot := T.VfoTxActNorm;
BtnTopVfoBTX.ClrActive := T.VfoTxActNorm;
BtnTopVfoBTX.ClrBorder := T.VfoTxActBorder;
BtnTopVfoBTX.ClrText := T.VfoTxActText;
BtnTopVfoBTX.ClrTextAct := T.VfoTxActText;
BtnTopVfoBTX.Active := True;
end
else
begin
BtnTopVfoBTX.ClrNorm := TColor($00303030);
BtnTopVfoBTX.ClrHot := TColor($00404040);
BtnTopVfoBTX.ClrActive := TColor($00404040);
BtnTopVfoBTX.ClrBorder := TColor($00505050);
BtnTopVfoBTX.ClrText := CLR_TEXTDIM;
BtnTopVfoBTX.ClrTextAct := CLR_TEXT;
BtnTopVfoBTX.ClrNorm := T.VfoTxInaNorm;
BtnTopVfoBTX.ClrHot := T.VfoTxInaNorm;
BtnTopVfoBTX.ClrActive := T.VfoTxInaNorm;
BtnTopVfoBTX.ClrBorder := T.VfoTxInaBorder;
BtnTopVfoBTX.ClrText := T.VfoTxInaText;
BtnTopVfoBTX.ClrTextAct := T.VfoTxInaText;
BtnTopVfoBTX.Active := False;
end;
BtnTopVfoBTX.Invalidate;
@@ -2907,6 +2987,8 @@ begin
if not Assigned(FDeviceDialog) then
FDeviceDialog := TDeviceDialog.Create(Self);
FDeviceDialog.OnDiscover := BtnDiscoverFromDialog;
if FLightTheme then FDeviceDialog.SetTheme(LightTheme)
else FDeviceDialog.SetTheme(DarkTheme);
FDeviceDialog.ShowModal;
// Результат диалога не обрабатываем здесь — START сам спросит если нужно
end;
@@ -2986,6 +3068,8 @@ begin
if not Assigned(FDeviceDialog) then
FDeviceDialog := TDeviceDialog.Create(Self);
FDeviceDialog.OnDiscover := BtnDiscoverFromDialog;
if FLightTheme then FDeviceDialog.SetTheme(LightTheme)
else FDeviceDialog.SetTheme(DarkTheme);
if FDeviceDialog.ShowModal <> mrOk then Exit;
if not FDeviceDialog.DialogResult.Accepted then Exit;
FPendingIP := FDeviceDialog.DialogResult.IPAddress;
@@ -5016,6 +5100,7 @@ begin
SF.OnFPSChange := ApplyFPS;
SF.OnPAChange := OnPASettingsChange;
SF.OnCATChange := OnCATSettingsChange;
SF.OnThemeChange := SetLightTheme;
end;
SF := TSettingsForm(FSettingsForm);
SF.LoadPASettings(FPAMaxPower, FPABandCal);
@@ -5032,6 +5117,7 @@ begin
SF.RefreshAudioDevices(FAudioOut, FAudioIn);
SF.LoadVisibility(FShowSpectrum, FShowWaterfall);
SF.LoadFPS(FDisplayFPS);
SF.LoadLightTheme(FLightTheme);
// Загружаем текущие значения
if FWDSPReady then
SF.LoadValues(