Files
ewsdr/VfoOverlay.pas
T
ew8bakandClaude Sonnet 4.6 5a74017e8a Fix VfoOverlay FM mode: NFM/FM filters, deviation sync, button layout
- FM filter row now shows only NFM (11 kHz) and FM (16 kHz) matching MainForm
- OnModeFilterSelect updates FFMDeviation and FFilter index for FM mode
- Mode and filter button widths distributed evenly (proportional, not fixed BtnW)
- S-meter inset by extra 4 px on each side

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 12:32:33 +03:00

486 lines
14 KiB
ObjectPascal

unit VfoOverlay;
{$mode objfpc}{$H+}
interface
uses
Classes, SysUtils, Controls, Graphics, Types, Math;
type
TModeFilterEvent = procedure(Mode: Integer; FilterBW: Integer) of object;
TVfoOverlay = class(TCustomControl)
private
FMode: Integer;
FFilterBW: Integer;
FVfoHz: Double;
FSMeterDB: Double;
FOnSelect: TModeFilterEvent;
FModeFilter: array[0..7] of Integer; // запомненный фильтр для каждого режима
FHitRects: array[0..15] of TRect;
FHitActions: array[0..15] of Integer; // <0 = режим (-1=idx0..), >0 = BW фильтра
FHitCount: Integer;
FHotIdx: Integer;
procedure DrawSelf(C: TCanvas; W, H: Integer);
procedure DrawSMeterBar(C: TCanvas; X, Y, BW, BH: Integer);
procedure DrawModeRow(C: TCanvas; X, Y, RowW, RowH: Integer);
procedure DrawFilterRow(C: TCanvas; X, Y, RowW, RowH: Integer);
procedure RegisterHit(HR: TRect; HV: Integer);
function CalcSLabel: string;
function FormatFreq(Hz: Double): string;
function GetFilterBWForMode(ModeIdx, FilterIdx: Integer): Integer;
function GetFilterLblForMode(ModeIdx, FilterIdx: Integer): string;
function GetFilterCountForMode(ModeIdx: Integer): Integer;
protected
procedure Paint; override;
procedure MouseDown(Button: TMouseButton; Shift: TShiftState;
X, Y: Integer); override;
procedure MouseMove(Shift: TShiftState; X, Y: Integer); override;
procedure MouseLeave; override;
public
constructor Create(AOwner: TComponent); override;
procedure SetState(AMode, ABW: Integer; AVfoHz, ASMeterDB: Double);
procedure UpdateSMeter(DB: Double);
procedure UpdateVfo(AVfoHz: Double);
property OnSelect: TModeFilterEvent read FOnSelect write FOnSelect;
end;
implementation
const
CLR_PANEL_BG = TColor($00141414);
CLR_BORDER = TColor($00505050);
CLR_TEXT = TColor($00E8E8E8);
CLR_DIM = TColor($00787878);
CLR_ACCENT = TColor($0040FF80);
CLR_BTN_NORM = TColor($00282828);
CLR_BTN_HOT = TColor($00383838);
CLR_BTN_ACT = TColor($00183828);
CLR_FREQ = TColor($0050FF80);
CLR_SVAL = TColor($0030C8FF);
CLR_BAR_GRN = TColor($0018A030);
CLR_BAR_RED = TColor($000030C0);
CLR_BAR_BG = TColor($00101010);
// Режимы — ТОЧНО как в MainForm.pas
// 0=LSB, 1=USB, 2=DSB, 3=CWL, 4=CWU, 5=FM, 6=AM, 7=SAM
OVL_MODE_COUNT = 8;
OVL_MODE_NAMES: array[0..7] of string = (
'LSB','USB','DSB','CWL','CWU','FM','AM','SAM');
// Фильтры SSB (LSB=0, USB=1)
SSB_BW: array[0..5] of Integer = (1800,2100,2400,2700,3300,3800);
SSB_LBL: array[0..5] of string = ('1.8K','2.1K','2.4K','2.7K','3.3K','3.8K');
// Фильтры DSB (DSB=2)
DSB_BW: array[0..5] of Integer = (16000,12000,10000,8000,5200,4000);
DSB_LBL: array[0..5] of string = ('16K','12K','10K','8K','5.2K','4K');
// Фильтры CW (CWL=3, CWU=4)
CW_BW: array[0..5] of Integer = (500,250,150,100,50,1000);
CW_LBL: array[0..5] of string = ('500','250','150','100','50','1K');
// Фильтры FM (FM=5) — только NFM и FM, как в MainForm
FM_BW: array[0..1] of Integer = (11000, 16000);
FM_LBL: array[0..1] of string = ('NFM', 'FM');
// Фильтры AM/SAM (AM=6, SAM=7)
AM_BW: array[0..5] of Integer = (20000,16000,12000,10000,8000,6000);
AM_LBL: array[0..5] of string = ('20K','16K','12K','10K','8K','6K');
// S-шкала: S1..S9
DB_S: array[1..9] of Double = (-121,-115,-109,-103,-97,-91,-85,-79,-73);
function TVfoOverlay.GetFilterBWForMode(ModeIdx, FilterIdx: Integer): Integer;
begin
case ModeIdx of
0,1: Result := SSB_BW[FilterIdx];
2: Result := DSB_BW[FilterIdx];
3,4: Result := CW_BW[FilterIdx];
5: Result := FM_BW[FilterIdx];
else Result := AM_BW[FilterIdx]; // 6=AM, 7=SAM
end;
end;
function TVfoOverlay.GetFilterLblForMode(ModeIdx, FilterIdx: Integer): string;
begin
case ModeIdx of
0,1: Result := SSB_LBL[FilterIdx];
2: Result := DSB_LBL[FilterIdx];
3,4: Result := CW_LBL[FilterIdx];
5: Result := FM_LBL[FilterIdx];
else Result := AM_LBL[FilterIdx];
end;
end;
function TVfoOverlay.GetFilterCountForMode(ModeIdx: Integer): Integer;
begin
if ModeIdx = 5 then Result := 2 // FM: только NFM и FM
else Result := 6;
end;
function DBmToSLabel(DB: Double): string;
var
I, Over: Integer;
begin
if DB >= DB_S[9] then
begin
Over := Round(DB - DB_S[9]);
if Over < 5 then
Result := 'S9'
else
begin
Over := ((Over + 5) div 10) * 10;
if Over = 0 then Over := 10;
Result := 'S9+' + IntToStr(Over);
end;
end
else if DB <= DB_S[1] then
Result := 'S1'
else
begin
Result := 'S1';
for I := 1 to 8 do
if DB >= DB_S[I] then
Result := 'S' + IntToStr(I);
end;
end;
constructor TVfoOverlay.Create(AOwner: TComponent);
begin
inherited Create(AOwner);
ControlStyle := ControlStyle + [csOpaque];
FMode := 1; // USB по умолчанию (как в MainForm)
FFilterBW := 2700;
FVfoHz := 14200000;
FSMeterDB := -121;
FHitCount := 0;
FHotIdx := -1;
// Дефолтные фильтры для каждого режима (запоминаются при переключении)
FModeFilter[0] := 2700; // LSB
FModeFilter[1] := 2700; // USB
FModeFilter[2] := 8000; // DSB
FModeFilter[3] := 500; // CWL
FModeFilter[4] := 500; // CWU
FModeFilter[5] := 11000; // FM (NFM по умолчанию)
FModeFilter[6] := 8000; // AM
FModeFilter[7] := 8000; // SAM
Width := 260; // шире — 8 кнопок режимов
Height := 130;
Cursor := crHandPoint;
end;
procedure TVfoOverlay.RegisterHit(HR: TRect; HV: Integer);
begin
if FHitCount > High(FHitRects) then Exit;
FHitRects[FHitCount] := HR;
FHitActions[FHitCount] := HV;
Inc(FHitCount);
end;
function TVfoOverlay.FormatFreq(Hz: Double): string;
var
iM, iK, iH: Integer;
begin
iM := Trunc(Hz / 1e6);
iK := Trunc((Hz - iM * 1e6) / 1000);
iH := Round(Hz - iM * 1e6 - iK * 1000);
Result := Format('%d.%3.3d.%3.3d', [iM, iK, iH]);
end;
function TVfoOverlay.CalcSLabel: string;
begin
Result := DBmToSLabel(FSMeterDB);
end;
procedure TVfoOverlay.DrawSMeterBar(C: TCanvas; X, Y, BW, BH: Integer);
const
DB_MIN = -127.0;
DB_MAX = -13.0;
S_POS: array[0..7] of Double = (-121,-109,-97,-85,-73,-53,-33,-13);
S_LBL: array[0..7] of string = ('1','3','5','7','S9','+20','+40','+60');
var
I, BarEnd, S9X, PX, BarH: Integer;
T: Double;
Lbl: string;
TW: Integer;
begin
BarH := BH - 11;
C.Brush.Color := CLR_BAR_BG;
C.Brush.Style := bsSolid;
C.Pen.Style := psClear;
C.FillRect(Rect(X, Y, X+BW, Y+BarH));
S9X := X + Round((-73 - DB_MIN) / (DB_MAX - DB_MIN) * BW);
T := Max(0.0, Min(1.0, (FSMeterDB - DB_MIN) / (DB_MAX - DB_MIN)));
BarEnd := X + Round(T * BW);
if BarEnd > X + 1 then
begin
if BarEnd <= S9X then
begin
C.Brush.Color := CLR_BAR_GRN;
C.FillRect(Rect(X+1, Y+1, BarEnd, Y+BarH-1));
end
else
begin
C.Brush.Color := CLR_BAR_GRN;
C.FillRect(Rect(X+1, Y+1, S9X, Y+BarH-1));
C.Brush.Color := CLR_BAR_RED;
C.FillRect(Rect(S9X, Y+1, Min(BarEnd, X+BW-1), Y+BarH-1));
end;
end;
C.Pen.Style := psSolid;
C.Pen.Color := CLR_BORDER;
C.Brush.Style := bsClear;
C.Rectangle(X, Y, X+BW, Y+BarH);
C.Font.Name := 'Courier New';
C.Font.Size := 5;
C.Font.Style := [];
for I := 0 to High(S_POS) do
begin
T := (S_POS[I] - DB_MIN) / (DB_MAX - DB_MIN);
PX := X + Round(T * BW);
Lbl := S_LBL[I];
TW := C.TextWidth(Lbl);
if I < 4 then C.Font.Color := CLR_DIM
else C.Font.Color := CLR_ACCENT;
C.Pen.Color := CLR_DIM;
C.Pen.Style := psSolid;
C.MoveTo(PX, Y+BarH); C.LineTo(PX, Y+BarH+2);
C.TextOut(PX - TW div 2, Y+BarH+2, Lbl);
end;
end;
procedure TVfoOverlay.DrawModeRow(C: TCanvas; X, Y, RowW, RowH: Integer);
var
I, BX, BRight: Integer;
HR: TRect;
IsAct, IsHot: Boolean;
begin
BX := X;
for I := 0 to OVL_MODE_COUNT - 1 do
begin
BRight := X + ((I + 1) * RowW) div OVL_MODE_COUNT;
HR := Rect(BX, Y, BRight, Y + RowH);
IsAct := (I = FMode);
IsHot := (FHotIdx >= 0) and (FHitActions[FHotIdx] = -(I + 1));
if IsAct then C.Brush.Color := CLR_BTN_ACT
else if IsHot then C.Brush.Color := CLR_BTN_HOT
else C.Brush.Color := CLR_BTN_NORM;
C.Brush.Style := bsSolid; C.Pen.Style := psClear;
C.FillRect(HR);
C.Pen.Style := psSolid; C.Pen.Color := CLR_BORDER;
C.Brush.Style := bsClear; C.Rectangle(HR);
if IsAct then C.Font.Color := CLR_ACCENT
else C.Font.Color := CLR_TEXT;
C.Font.Size := 6; C.Font.Style := []; C.Font.Name := 'Courier New';
C.Brush.Style := bsClear;
C.TextOut(BX + (BRight - BX - C.TextWidth(OVL_MODE_NAMES[I])) div 2,
Y + (RowH - C.TextHeight('A')) div 2,
OVL_MODE_NAMES[I]);
RegisterHit(HR, -(I + 1)); // -1=LSB, -2=USB, -3=DSB, -4=CWL, -5=CWU, -6=FM, -7=AM, -8=SAM
BX := BRight + 1;
end;
end;
procedure TVfoOverlay.DrawFilterRow(C: TCanvas; X, Y, RowW, RowH: Integer);
var
FILT_COUNT: Integer;
I, BX, BRight, BV: Integer;
HR: TRect;
Lbl: string;
IsAct: Boolean;
begin
FILT_COUNT := GetFilterCountForMode(FMode);
BX := X;
for I := 0 to FILT_COUNT - 1 do
begin
BRight := X + ((I + 1) * RowW) div FILT_COUNT;
HR := Rect(BX, Y, BRight, Y + RowH);
BV := GetFilterBWForMode(FMode, I);
IsAct := (BV = FFilterBW);
Lbl := GetFilterLblForMode(FMode, I);
if IsAct then C.Brush.Color := CLR_BTN_ACT
else if (FHotIdx >= 0) and (FHitActions[FHotIdx] = BV) then
C.Brush.Color := CLR_BTN_HOT
else C.Brush.Color := CLR_BTN_NORM;
C.Brush.Style := bsSolid; C.Pen.Style := psClear;
C.FillRect(HR);
C.Pen.Style := psSolid; C.Pen.Color := CLR_BORDER;
C.Brush.Style := bsClear; C.Rectangle(HR);
if IsAct then C.Font.Color := CLR_ACCENT
else C.Font.Color := CLR_TEXT;
C.Font.Size := 7; C.Font.Name := 'Courier New';
C.Brush.Style := bsClear;
C.TextOut(BX + (BRight - BX - C.TextWidth(Lbl)) div 2,
Y + (RowH - C.TextHeight('A')) div 2, Lbl);
RegisterHit(HR, BV); // BV всегда > 0 (BW в Гц, минимум 25)
BX := BRight + 1;
end;
end;
procedure TVfoOverlay.DrawSelf(C: TCanvas; W, H: Integer);
const
PAD = 4;
FREQ_H = 18;
INFO_H = 14;
SM_H = 27;
ROW_H = 20;
GAP = 3;
var
FreqStr, SStr: string;
FW, CurY: Integer;
begin
FHitCount := 0;
C.Brush.Color := CLR_PANEL_BG;
C.Brush.Style := bsSolid;
C.Pen.Color := CLR_BORDER;
C.Pen.Style := psSolid;
C.Pen.Width := 1;
C.RoundRect(0, 0, W, H, 6, 6);
CurY := PAD;
// Частота
FreqStr := FormatFreq(FVfoHz);
C.Font.Name := 'Courier New'; C.Font.Size := 11; C.Font.Style := [fsBold];
C.Font.Color := CLR_FREQ;
FW := C.TextWidth(FreqStr);
C.Brush.Style := bsClear;
C.TextOut((W - FW) div 2, CurY, FreqStr);
Inc(CurY, FREQ_H);
// Режим (слева) + S-value (справа)
SStr := CalcSLabel;
C.Font.Size := 8; C.Font.Style := [fsBold];
C.Font.Color := CLR_ACCENT;
C.TextOut(PAD + 2, CurY, OVL_MODE_NAMES[FMode]);
C.Font.Color := CLR_SVAL;
C.TextOut(W - PAD - C.TextWidth(SStr) - 2, CurY, SStr);
Inc(CurY, INFO_H);
// S-метр
DrawSMeterBar(C, PAD + 4, CurY, W - (PAD + 4) * 2, SM_H);
Inc(CurY, SM_H + GAP);
// Кнопки режимов
DrawModeRow(C, PAD, CurY, W - PAD * 2, ROW_H);
Inc(CurY, ROW_H + GAP);
// Кнопки фильтров
DrawFilterRow(C, PAD, CurY, W - PAD * 2, ROW_H);
end;
procedure TVfoOverlay.Paint;
begin
DrawSelf(Canvas, Width, Height);
end;
procedure TVfoOverlay.MouseDown(Button: TMouseButton; Shift: TShiftState;
X, Y: Integer);
var
I, HV, NewMode, NewBW: Integer;
begin
inherited;
if Button <> mbLeft then Exit;
for I := 0 to FHitCount - 1 do
if PtInRect(FHitRects[I], Point(X, Y)) then
begin
HV := FHitActions[I];
if HV < 0 then
begin
NewMode := (-HV) - 1;
if NewMode <> FMode then
begin
// Сохраняем текущий фильтр для старого режима
FModeFilter[FMode] := FFilterBW;
FMode := NewMode;
// Восстанавливаем запомненный фильтр для нового режима
FFilterBW := FModeFilter[FMode];
Invalidate;
if Assigned(FOnSelect) then FOnSelect(FMode, FFilterBW);
end;
end
else if HV > 0 then
begin
NewBW := HV;
if NewBW <> FFilterBW then
begin
FFilterBW := NewBW;
// Запоминаем выбранный фильтр для текущего режима
FModeFilter[FMode] := FFilterBW;
Invalidate;
if Assigned(FOnSelect) then FOnSelect(FMode, FFilterBW);
end;
end;
Break;
end;
end;
procedure TVfoOverlay.MouseMove(Shift: TShiftState; X, Y: Integer);
var
I, OldHot: Integer;
begin
inherited;
OldHot := FHotIdx; FHotIdx := -1;
for I := 0 to FHitCount - 1 do
if PtInRect(FHitRects[I], Point(X, Y)) then
begin FHotIdx := I; Break; end;
if FHotIdx <> OldHot then Invalidate;
end;
procedure TVfoOverlay.MouseLeave;
begin
inherited;
if FHotIdx >= 0 then begin FHotIdx := -1; Invalidate; end;
end;
procedure TVfoOverlay.SetState(AMode, ABW: Integer; AVfoHz, ASMeterDB: Double);
begin
FMode := Max(0, Min(OVL_MODE_COUNT - 1, AMode));
FFilterBW := ABW;
// Синхронизируем память фильтра для текущего режима
FModeFilter[FMode] := ABW;
FVfoHz := AVfoHz;
FSMeterDB := ASMeterDB;
Invalidate;
end;
procedure TVfoOverlay.UpdateSMeter(DB: Double);
begin
// Не клипаем — передаём как есть, как в основном S-метре
if Abs(DB - FSMeterDB) > 0.4 then
begin
FSMeterDB := DB;
Invalidate;
end;
end;
procedure TVfoOverlay.UpdateVfo(AVfoHz: Double);
begin
if FVfoHz <> AVfoHz then
begin
FVfoHz := AVfoHz;
Invalidate;
end;
end;
end.