unit VfoOverlay; {$mode objfpc}{$H+} interface uses Classes, SysUtils, Controls, Graphics, Types, Math; type TModeFilterEvent = procedure(Mode: Integer; FilterBW: Integer) of object; TDSPChangeEvent = procedure(NRMode, NBMode: Integer; SNBOn, ANFOn: Boolean) of object; TAGCChangeEvent = procedure(AGCMode: Integer) of object; TVfoOverlay = class(TCustomControl) private FMode: Integer; FFilterBW: Integer; FVfoHz: Double; FSMeterDB: Double; FOnSelect: TModeFilterEvent; FOnDSPChange: TDSPChangeEvent; FOnAGCChange: TAGCChangeEvent; FOnInvalidate: TNotifyEvent; FModeFilter: array[0..7] of Integer; FNRMode: Integer; // 0=off, 1..4=NR/NR2/NR3/NR4 FNBMode: Integer; // 0=off, 1=NB, 2=NB2 FSNBOn: Boolean; FANFOn: Boolean; FAGCMode: Integer; // 0=FAST 1=MED 2=SLOW 3=LONG 4=OFF FAGCPickerOpen: Boolean; FHitRects: array[0..24] of TRect; FHitActions: array[0..24] of Integer; FHitCount: Integer; FHotIdx: Integer; FCacheBitmap: TBitmap; FCacheDirty: Boolean; 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 DrawDSPRow(C: TCanvas; X, Y, RowW, RowH: Integer); procedure DrawAGCPickerRow(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; procedure RequestInvalidate; 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; destructor Destroy; override; procedure DrawOverlay(Target: TBitmap; W, H: Integer); procedure DrawOverlayBitmap(Target: TBitmap); function HandleMouseDown(Button: TMouseButton; X, Y: Integer): Boolean; function HandleMouseMove(X, Y: Integer): Boolean; function HandleMouseLeave: Boolean; procedure SetState(AMode, ABW: Integer; AVfoHz, ASMeterDB: Double); procedure SetDSPState(ANRMode, ANBMode: Integer; ASNBOn, AANFOn: Boolean; AAGCMode: Integer); procedure UpdateSMeter(DB: Double); procedure UpdateVfo(AVfoHz: Double); property HotIdx: Integer read FHotIdx; property OnSelect: TModeFilterEvent read FOnSelect write FOnSelect; property OnDSPChange: TDSPChangeEvent read FOnDSPChange write FOnDSPChange; property OnAGCChange: TAGCChangeEvent read FOnAGCChange write FOnAGCChange; property OnInvalidate: TNotifyEvent read FOnInvalidate write FOnInvalidate; end; implementation const KEY_COLOR = TColor($00FF00FF); HIT_NR = -9; HIT_NB = -10; HIT_SNB = -11; HIT_ANF = -12; HIT_AGC = -13; HIT_AGC_PICK_BASE = 100000; // 100000+N — выбор режима AGC (>всех BW фильтров) AGC_NAMES: array[0..4] of string = ('FAST', 'MED', 'SLOW', 'LONG', 'OFF'); OVL_H_NORM = 136; // высота без пикера AGC OVL_H_AGC = 159; // высота с открытым пикером (136 + GAP(3) + ROW_H(20)) 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); procedure BlendBitmapKey(Target, Source: TBitmap; DstX, DstY: Integer; Alpha: Byte); var Y, X, SX0, SY0, SX1, SY1, TY, InvA: Integer; SrcRow, DstRow: PByte; begin if (Target = nil) or (Source = nil) or (Alpha = 0) then Exit; SX0 := 0; SY0 := 0; SX1 := Source.Width; SY1 := Source.Height; if DstX < 0 then begin SX0 := -DstX; DstX := 0; end; if DstY < 0 then begin SY0 := -DstY; DstY := 0; end; if DstX + (SX1 - SX0) > Target.Width then SX1 := SX0 + Target.Width - DstX; if DstY + (SY1 - SY0) > Target.Height then SY1 := SY0 + Target.Height - DstY; if (SX1 <= SX0) or (SY1 <= SY0) then Exit; InvA := 255 - Alpha; Target.BeginUpdate(False); Source.BeginUpdate(False); try for Y := SY0 to SY1 - 1 do begin TY := DstY + (Y - SY0); SrcRow := PByte(Source.ScanLine[Y]); DstRow := PByte(Target.ScanLine[TY]); if (SrcRow = nil) or (DstRow = nil) then Continue; Inc(SrcRow, SX0 * 4); Inc(DstRow, DstX * 4); for X := SX0 to SX1 - 1 do begin // KEY_COLOR is clFuchsia in pf32bit B,G,R order. if not ((SrcRow[0] = $FF) and (SrcRow[1] = $00) and (SrcRow[2] = $FF)) then begin DstRow[0] := Byte((Alpha * SrcRow[0] + InvA * DstRow[0]) div 255); DstRow[1] := Byte((Alpha * SrcRow[1] + InvA * DstRow[1]) div 255); DstRow[2] := Byte((Alpha * SrcRow[2] + InvA * DstRow[2]) div 255); end; Inc(SrcRow, 4); Inc(DstRow, 4); end; end; finally Source.EndUpdate(False); Target.EndUpdate(False); end; end; 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 FNRMode := 0; FNBMode := 0; FSNBOn := False; FANFOn := False; FAGCMode := 0; FAGCPickerOpen := False; FCacheBitmap := TBitmap.Create; FCacheBitmap.PixelFormat := pf32bit; FCacheDirty := True; Width := 260; Height := OVL_H_NORM; Cursor := crHandPoint; end; destructor TVfoOverlay.Destroy; begin FCacheBitmap.Free; inherited Destroy; end; procedure TVfoOverlay.RegisterHit(HR: TRect; HV: Integer); begin if FHitCount > High(FHitRects) then Exit; FHitRects[FHitCount] := HR; FHitActions[FHitCount] := HV; Inc(FHitCount); end; procedure TVfoOverlay.RequestInvalidate; begin FCacheDirty := True; inherited Invalidate; if Assigned(FOnInvalidate) then FOnInvalidate(Self); 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.DrawDSPRow(C: TCanvas; X, Y, RowW, RowH: Integer); const DSP_COUNT = 5; DSP_HITS: array[0..4] of Integer = (HIT_NR, HIT_NB, HIT_SNB, HIT_ANF, HIT_AGC); var I, BX, BRight: Integer; HR: TRect; Lbl: string; IsAct, IsHot: Boolean; begin BX := X; for I := 0 to DSP_COUNT - 1 do begin BRight := X + ((I + 1) * RowW) div DSP_COUNT; HR := Rect(BX, Y, BRight, Y + RowH); case I of 0: begin case FNRMode of 1: Lbl := 'NR'; 2: Lbl := 'NR2'; 3: Lbl := 'NR3'; 4: Lbl := 'NR4'; else Lbl := 'NR'; end; IsAct := FNRMode <> 0; end; 1: begin if FNBMode = 2 then Lbl := 'NB2' else Lbl := 'NB'; IsAct := FNBMode <> 0; end; 2: begin Lbl := 'SNB'; IsAct := FSNBOn; end; 3: begin Lbl := 'ANF'; IsAct := FANFOn; end; 4: begin Lbl := AGC_NAMES[FAGCMode]; IsAct := FAGCPickerOpen; end; else Lbl := ''; IsAct := False; end; IsHot := (FHotIdx >= 0) and (FHitActions[FHotIdx] = DSP_HITS[I]); 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 := 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, DSP_HITS[I]); BX := BRight + 1; end; end; procedure TVfoOverlay.DrawAGCPickerRow(C: TCanvas; X, Y, RowW, RowH: Integer); const AGC_COUNT = 5; var I, BX, BRight: Integer; HR: TRect; IsAct, IsHot: Boolean; begin BX := X; for I := 0 to AGC_COUNT - 1 do begin BRight := X + ((I + 1) * RowW) div AGC_COUNT; HR := Rect(BX, Y, BRight, Y + RowH); IsAct := (I = FAGCMode); IsHot := (FHotIdx >= 0) and (FHitActions[FHotIdx] = HIT_AGC_PICK_BASE + I); 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 := 7; C.Font.Name := 'Courier New'; C.Brush.Style := bsClear; C.TextOut(BX + (BRight - BX - C.TextWidth(AGC_NAMES[I])) div 2, Y + (RowH - C.TextHeight('A')) div 2, AGC_NAMES[I]); RegisterHit(HR, HIT_AGC_PICK_BASE + I); 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); Inc(CurY, ROW_H + GAP); // Кнопки DSP (NR, NB, SNB, ANF, AGC) DrawDSPRow(C, PAD, CurY, W - PAD * 2, ROW_H); // Пикер AGC (открывается кнопкой AGC) if FAGCPickerOpen then begin Inc(CurY, ROW_H + GAP); DrawAGCPickerRow(C, PAD, CurY, W - PAD * 2, ROW_H); end; end; procedure TVfoOverlay.Paint; begin DrawSelf(Canvas, Width, Height); end; procedure TVfoOverlay.DrawOverlay(Target: TBitmap; W, H: Integer); begin if (not Visible) or (Target = nil) or (Width <= 0) or (Height <= 0) then Exit; if (Left >= W) or (Top >= H) then Exit; if (FCacheBitmap.Width <> Width) or (FCacheBitmap.Height <> Height) then begin FCacheBitmap.SetSize(Width, Height); FCacheDirty := True; end; if FCacheDirty then begin FCacheBitmap.Canvas.Brush.Color := KEY_COLOR; FCacheBitmap.Canvas.Brush.Style := bsSolid; FCacheBitmap.Canvas.Pen.Style := psClear; FCacheBitmap.Canvas.FillRect(Rect(0, 0, Width, Height)); DrawSelf(FCacheBitmap.Canvas, Width, Height); FCacheDirty := False; end; BlendBitmapKey(Target, FCacheBitmap, Left, Top, 218); end; procedure TVfoOverlay.DrawOverlayBitmap(Target: TBitmap); begin if (Target = nil) or (Width <= 0) or (Height <= 0) then Exit; Target.PixelFormat := pf32bit; Target.SetSize(Width, Height); Target.Canvas.Brush.Color := KEY_COLOR; Target.Canvas.Brush.Style := bsSolid; Target.Canvas.Pen.Style := psClear; Target.Canvas.FillRect(Rect(0, 0, Width, Height)); DrawSelf(Target.Canvas, Width, Height); FCacheDirty := False; end; function TVfoOverlay.HandleMouseDown(Button: TMouseButton; X, Y: Integer): Boolean; begin Result := False; if not Visible then Exit; if (X < Left) or (Y < Top) or (X >= Left + Width) or (Y >= Top + Height) then Exit; MouseDown(Button, [], X - Left, Y - Top); Result := True; end; function TVfoOverlay.HandleMouseMove(X, Y: Integer): Boolean; var I, OldHot, LX, LY: Integer; begin Result := False; if not Visible then Exit; LX := X - Left; LY := Y - Top; OldHot := FHotIdx; FHotIdx := -1; if (LX >= 0) and (LY >= 0) and (LX < Width) and (LY < Height) then for I := 0 to FHitCount - 1 do if PtInRect(FHitRects[I], Point(LX, LY)) then begin FHotIdx := I; Break; end; if FHotIdx <> OldHot then begin RequestInvalidate; Result := True; end; end; function TVfoOverlay.HandleMouseLeave: Boolean; begin Result := False; if FHotIdx >= 0 then begin FHotIdx := -1; RequestInvalidate; Result := True; end; 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 < OVL_MODE_COUNT then begin // Кнопка режима if NewMode <> FMode then begin FModeFilter[FMode] := FFilterBW; FMode := NewMode; FFilterBW := FModeFilter[FMode]; RequestInvalidate; if Assigned(FOnSelect) then FOnSelect(FMode, FFilterBW); end; end else begin // Кнопка DSP if HV = HIT_AGC then begin FAGCPickerOpen := not FAGCPickerOpen; if FAGCPickerOpen then Height := OVL_H_AGC else Height := OVL_H_NORM; RequestInvalidate; end else begin case HV of HIT_NR: FNRMode := (FNRMode + 1) mod 5; HIT_NB: FNBMode := (FNBMode + 1) mod 3; HIT_SNB: FSNBOn := not FSNBOn; HIT_ANF: FANFOn := not FANFOn; end; RequestInvalidate; if Assigned(FOnDSPChange) then FOnDSPChange(FNRMode, FNBMode, FSNBOn, FANFOn); end; end; end else if HV >= HIT_AGC_PICK_BASE then begin // Выбор режима AGC из пикера FAGCMode := HV - HIT_AGC_PICK_BASE; FAGCPickerOpen := False; Height := OVL_H_NORM; RequestInvalidate; if Assigned(FOnAGCChange) then FOnAGCChange(FAGCMode); end else if HV > 0 then begin NewBW := HV; if NewBW <> FFilterBW then begin FFilterBW := NewBW; FModeFilter[FMode] := FFilterBW; RequestInvalidate; 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 RequestInvalidate; end; procedure TVfoOverlay.MouseLeave; begin inherited; if FHotIdx >= 0 then begin FHotIdx := -1; RequestInvalidate; 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; RequestInvalidate; end; procedure TVfoOverlay.SetDSPState(ANRMode, ANBMode: Integer; ASNBOn, AANFOn: Boolean; AAGCMode: Integer); begin FNRMode := ANRMode; FNBMode := ANBMode; FSNBOn := ASNBOn; FANFOn := AANFOn; FAGCMode := AAGCMode; RequestInvalidate; end; procedure TVfoOverlay.UpdateSMeter(DB: Double); begin // Не клипаем — передаём как есть, как в основном S-метре if Abs(DB - FSMeterDB) > 0.4 then begin FSMeterDB := DB; RequestInvalidate; end; end; procedure TVfoOverlay.UpdateVfo(AVfoHz: Double); begin if FVfoHz <> AVfoHz then begin FVfoHz := AVfoHz; RequestInvalidate; end; end; end.