unit SpectrumView; { SpectrumView.pas — рендеринг спектра, водопада и S-метра. TSpectrumView инкапсулирует всё рисование: спектр, водопад, линейка частот, S-метр, индикаторы мощности/SWR. MainForm создаёт один экземпляр, передаёт ссылки на TPaintBox после BuildUI, обновляет свойства при изменении состояния. Зависимости: нет обратной зависимости на MainForm (нет circular dep). } {$IFDEF FPC} {$MODE Delphi} {$ENDIF} interface uses Classes, SysUtils, Graphics, ExtCtrls, Controls, Math, IntfGraphics, FPImage, LCLIntf, LCLType, GraphType; const SV_CLR_BG = TColor($00101010); SV_CLR_BORDER = TColor($00303030); SV_CLR_METER_ON = TColor($0000CC44); SV_CLR_AMBER = TColor($0000AAFF); type TSpectrumView = class private // ── Off-screen bitmaps ──────────────────────────────────────────────────── FSpectrumBitmap: TBitmap; FWaterfallBitmap: TBitmap; // размер-placeholder для DrawWaterfall FWfBitmap: TBitmap; FWfBitmapW: Integer; FWfBitmapH: Integer; FGridBitmap: TBitmap; FGridBitmapW: Integer; FGridBitmapH: Integer; FRulerBitmap: TBitmap; FSpecGradImg: TLazIntfImage; FSpecGradBmp: TBitmap; FWfIntfImg: TLazIntfImage; FWfPixels: array of LongWord; FSpPts: array of TPoint; FSpPtsLen: Integer; FSmBitmap: TBitmap; FRulerLastFreq: Double; FRulerLastVfo: Double; FRulerLastSpan: Double; // ── Radio state ─────────────────────────────────────────────────────────── FVfoA: Double; FVfoB: Double; FActiveVfo: Integer; FCenterFreq: Double; FSpanHz: Double; FMode: Integer; FFilterBW: Integer; FAGCTop: Integer; FAGCThresh: Double; FAGCHangLevel: Double; FWDSPReady: Boolean; // ── Waterfall settings ──────────────────────────────────────────────────── FWfAGCEnabled: Boolean; FWfNFEnabled: Boolean; FWfManualHigh: Double; FWfManualLow: Double; FWfAGCOffset: Double; FWfHigh: Double; FWfLow: Double; // ── Display settings ────────────────────────────────────────────────────── FSpecRefLevel: Double; FSpecRange: Double; FSpecGridStep: Double; // ── Marker ──────────────────────────────────────────────────────────────── FMarkerActive: Boolean; FMarkerX: Integer; // ── Meter values ────────────────────────────────────────────────────────── FLastSMeter: Double; FSMeterPeak: Double; FSMeterMin: Double; FLastFwdW: Double; FLastSWR: Double; // ── Буферы данных (пишутся из DSP-потока) ──────────────────────────────── FSpectrumBuf: array[0..1023] of Single; FWaterfallBuf: array[0..1023] of Single; FSpectrumBufCount: Integer; FWaterfallBufCount: Integer; FSpectrumDirty: Boolean; FWaterfallDirty: Boolean; FWfFrameCounter: Integer; FWfFrameInterval: Integer; // ── Ссылки на TPaintBox (устанавливаются из MainForm после BuildUI) ─────── FPbSpectrum: TPaintBox; FPbWaterfall: TPaintBox; FPbRuler: TPaintBox; FPbSMeterRight: TPaintBox; FPbFwdPower: TPaintBox; FPbSWR: TPaintBox; // ── Приватные методы рендеринга ─────────────────────────────────────────── function ActiveVfoFreq: Double; function ScaleX(X, Total, Width: Integer): Integer; procedure DrawSpectrumGradient(const SpPts: array of TPoint; W, H: Integer); procedure DrawMarkerLine(C: TCanvas; W, H: Integer); public constructor Create; destructor Destroy; override; // ── Состояние радио ─────────────────────────────────────────────────────── property VfoA: Double read FVfoA write FVfoA; property VfoB: Double read FVfoB write FVfoB; property ActiveVfo: Integer read FActiveVfo write FActiveVfo; property CenterFreq: Double read FCenterFreq write FCenterFreq; property SpanHz: Double read FSpanHz write FSpanHz; property Mode: Integer read FMode write FMode; property FilterBW: Integer read FFilterBW write FFilterBW; property AGCTop: Integer read FAGCTop write FAGCTop; property AGCThresh: Double write FAGCThresh; property AGCHangLevel: Double write FAGCHangLevel; property WDSPReady: Boolean read FWDSPReady write FWDSPReady; // ── Водопад ─────────────────────────────────────────────────────────────── property WfAGCEnabled: Boolean read FWfAGCEnabled write FWfAGCEnabled; property WfNFEnabled: Boolean read FWfNFEnabled write FWfNFEnabled; property WfManualHigh: Double read FWfManualHigh write FWfManualHigh; property WfManualLow: Double read FWfManualLow write FWfManualLow; property WfAGCOffset: Double read FWfAGCOffset write FWfAGCOffset; // ── Отображение ─────────────────────────────────────────────────────────── property SpecRefLevel: Double read FSpecRefLevel write FSpecRefLevel; property SpecRange: Double read FSpecRange write FSpecRange; property SpecGridStep: Double read FSpecGridStep write FSpecGridStep; // ── Маркер ──────────────────────────────────────────────────────────────── property MarkerActive: Boolean read FMarkerActive write FMarkerActive; property MarkerX: Integer read FMarkerX write FMarkerX; // ── S-метр ──────────────────────────────────────────────────────────────── property LastSMeter: Double read FLastSMeter write FLastSMeter; property SMeterPeak: Double read FSMeterPeak write FSMeterPeak; property SMeterMin: Double read FSMeterMin write FSMeterMin; property LastFwdW: Double read FLastFwdW write FLastFwdW; property LastSWR: Double read FLastSWR write FLastSWR; // ── Флаги обновления ────────────────────────────────────────────────────── property SpectrumDirty: Boolean read FSpectrumDirty write FSpectrumDirty; property WaterfallDirty: Boolean read FWaterfallDirty write FWaterfallDirty; property WfFrameInterval: Integer read FWfFrameInterval write FWfFrameInterval; // ── Ссылки на PaintBox ──────────────────────────────────────────────────── property PbSpectrum: TPaintBox write FPbSpectrum; property PbWaterfall: TPaintBox write FPbWaterfall; property PbRuler: TPaintBox write FPbRuler; property PbSMeterRight: TPaintBox write FPbSMeterRight; property PbFwdPower: TPaintBox write FPbFwdPower; property PbSWR: TPaintBox write FPbSWR; // ── Данные от DSP (вызываются из DSP-потока) ────────────────────────────── procedure SetSpectrumData(const Pixels: array of Single; Count: Integer); procedure SetWaterfallData(const Pixels: array of Single; Count: Integer); // ── Рендеринг ───────────────────────────────────────────────────────────── procedure DrawSpectrum; procedure DrawWaterfall; procedure DrawRuler; procedure DrawBarMeter(ACanvas: TCanvas; R: TRect; Value, MaxVal: Double; BarColor: TColor); procedure DrawSMeterWide(ACanvas: TCanvas; R: TRect; Value, Peak, MinVal: Double; out ZX1, ZX2, ZY1, ZY2: Integer); procedure DrawSMeterZone(ACanvas: TCanvas; R: TRect; X1, X2, Y1, Y2: Integer); // ── Управление размером bitmap ──────────────────────────────────────────── procedure SetSpectrumBitmapSize(W, H: Integer); procedure SetWaterfallBitmapSize(W, H: Integer); procedure SetRulerSize(W, H: Integer); function SpectrumBitmapWidth: Integer; // ── Paint-обработчики (назначаются в BuildUI) ───────────────────────────── procedure PaintSpectrum(Sender: TObject); procedure PaintWaterfall(Sender: TObject); procedure PaintRuler(Sender: TObject); procedure PaintSMeterRight(Sender: TObject); procedure PaintFwdPower(Sender: TObject); procedure PaintSWR(Sender: TObject); // ── Утилиты ─────────────────────────────────────────────────────────────── procedure InvalidateGridCache; procedure InvalidateRulerCache; procedure ResetSpectrumBuf; procedure ResetWfAvgBuf; procedure FillDemoSpectrum; function NeedsRulerRedraw: Boolean; end; // Вспомогательные функции (публичные для возможного использования в MainForm) function FormatFreqSV(Hz: Double): string; implementation // ════════════════════════════════════════════════════════════════════════════ // Вспомогательные функции — цвет, градиент // ════════════════════════════════════════════════════════════════════════════ function WaterfallColorThetis(Level: Integer): LongWord; const STOPS: array[0..5] of LongWord = ( $000000, $0A1A48, $0088CC, $E0C020, $D84A12, $FFFFFF); var Seg, Base: Integer; T: Double; C0, C1: LongWord; R, G, B: Integer; begin Level := EnsureRange(Level, 0, 255); Seg := Min(4, Level div 51); Base := Seg * 51; T := (Level - Base) / 51.0; C0 := STOPS[Seg]; C1 := STOPS[Seg + 1]; R := Round(((C0 shr 16) and $FF) * (1.0 - T) + ((C1 shr 16) and $FF) * T); G := Round(((C0 shr 8) and $FF) * (1.0 - T) + ((C1 shr 8) and $FF) * T); B := Round((C0 and $FF) * (1.0 - T) + (C1 and $FF) * T); Result := ($FF shl 24) or (R shl 16) or (G shl 8) or B; end; function WaterfallEnhancedColorThetis(ValueDB, LowDB, HighDB: Double): LongWord; var Overall, Local: Double; R, G, B: Integer; begin if ValueDB <= LowDB then begin Result := $FF000000; Exit; end; if ValueDB >= HighDB then begin Result := ($FF shl 24) or (255 shl 16) or (124 shl 8) or 192; Exit; end; Overall := (ValueDB - LowDB) / Max(1E-9, HighDB - LowDB); if Overall < (2.0 / 9.0) then begin Local := Overall / (2.0/9.0); R := 0; G := 0; B := Round(Local * 255.0); end else if Overall < (3.0 / 9.0) then begin Local := (Overall - 2.0/9.0) / (1.0/9.0); R := 0; G := Round(Local*255.0); B := 255; end else if Overall < (4.0 / 9.0) then begin Local := (Overall - 3.0/9.0) / (1.0/9.0); R := 0; G := 255; B := Round((1.0-Local)*255.0); end else if Overall < (5.0 / 9.0) then begin Local := (Overall - 4.0/9.0) / (1.0/9.0); R := Round(Local*255.0); G := 255; B := 0; end else if Overall < (7.0 / 9.0) then begin Local := (Overall - 5.0/9.0) / (2.0/9.0); R := 255; G := Round((1.0-Local)*255.0); B := 0; end else if Overall < (8.0 / 9.0) then begin Local := (Overall - 7.0/9.0) / (1.0/9.0); R := 255; G := 0; B := Round(Local*255.0); end else begin Local := (Overall - 8.0/9.0) / (1.0/9.0); R := Round((0.75 + 0.25 * (1.0 - Local)) * 255.0); G := Round(Local * 255.0 * 0.5); B := 255; end; R := EnsureRange(R, 0, 255); G := EnsureRange(G, 0, 255); B := EnsureRange(B, 0, 255); Result := ($FF shl 24) or (R shl 16) or (G shl 8) or B; end; procedure InitRawDesc32(var Desc: TRawImageDescription; AWidth, AHeight: Integer); begin FillChar(Desc, SizeOf(Desc), 0); Desc.Format := ricfRGBA; Desc.Width := AWidth; Desc.Height := AHeight; Desc.Depth := 32; Desc.BitOrder := riboBitsInOrder; Desc.ByteOrder := riboLSBFirst; Desc.LineOrder := riloTopToBottom; Desc.BitsPerPixel := 32; Desc.LineEnd := rileDWordBoundary; Desc.BlueShift := 0; Desc.BluePrec := 8; Desc.GreenShift := 8; Desc.GreenPrec := 8; Desc.RedShift := 16; Desc.RedPrec := 8; Desc.AlphaShift := 24; Desc.AlphaPrec := 8; end; function MixColorBGR(C1, C2: TColor; T: Double): TColor; var B1, G1, R1, B2, G2, R2, B, G, R: Integer; begin if T < 0.0 then T := 0.0 else if T > 1.0 then T := 1.0; B1 := (C1 shr 16) and $FF; G1 := (C1 shr 8) and $FF; R1 := C1 and $FF; B2 := (C2 shr 16) and $FF; G2 := (C2 shr 8) and $FF; R2 := C2 and $FF; B := Round(B1 + (B2-B1)*T); G := Round(G1 + (G2-G1)*T); R := Round(R1 + (R2-R1)*T); Result := TColor((B shl 16) or (G shl 8) or R); end; procedure PaintVerticalGradient(C: TCanvas; W, H: Integer; TopColor, BottomColor: TColor); var Y: Integer; T: Double; begin if (W <= 0) or (H <= 0) then Exit; C.Pen.Style := psClear; C.Brush.Style := bsSolid; for Y := 0 to H - 1 do begin T := Y / Max(1, H - 1); C.Brush.Color := MixColorBGR(TopColor, BottomColor, T); C.FillRect(Rect(0, Y, W, Y + 1)); end; C.Pen.Style := psSolid; end; function FormatFreqSV(Hz: Double): string; var Mhz, KHz, Rest: Integer; begin Mhz := Trunc(Hz / 1000000); KHz := Trunc((Hz - Mhz * 1000000) / 1000); Rest := Trunc(Hz) mod 1000; Result := Format('%3d.%3.3d.%3.3d', [Mhz, KHz, Rest]); end; // ════════════════════════════════════════════════════════════════════════════ // TSpectrumView // ════════════════════════════════════════════════════════════════════════════ constructor TSpectrumView.Create; begin inherited Create; FSpectrumBitmap := TBitmap.Create; FWaterfallBitmap := TBitmap.Create; FWfBitmap := TBitmap.Create; FGridBitmap := TBitmap.Create; FRulerBitmap := TBitmap.Create; FSpecGradBmp := TBitmap.Create; FSmBitmap := nil; FSpecGradImg := nil; FWfIntfImg := nil; FWfBitmapW := 0; FWfBitmapH := 0; FGridBitmapW := 0; FGridBitmapH := 0; FSpPtsLen := 0; FRulerLastFreq := -1.0; FRulerLastVfo := -1.0; // Водопад AGC defaults FWfManualHigh := -80.0; FWfManualLow := -130.0; FWfAGCOffset := 0.0; FWfHigh := -80.0; FWfLow := -130.0; FWfAGCEnabled := True; FWfNFEnabled := False; // Spectrum display defaults FSpecRefLevel := -20.0; FSpecRange := 110.0; FSpecGridStep := 10.0; FSpanHz := 192000; FSpectrumBufCount := 1024; FWaterfallBufCount := 1024; FWfFrameInterval := 2; FWfFrameCounter := 0; FWaterfallDirty := True; FLastSMeter := -130; FSMeterPeak := -130; FSMeterMin := -130; FLastFwdW := 0; FLastSWR := 1; ResetSpectrumBuf; end; destructor TSpectrumView.Destroy; begin FSpectrumBitmap.Free; FWaterfallBitmap.Free; FWfBitmap.Free; FGridBitmap.Free; FRulerBitmap.Free; FSpecGradBmp.Free; FreeAndNil(FSpecGradImg); FreeAndNil(FWfIntfImg); FreeAndNil(FSmBitmap); inherited; end; // ──────────────────────────────────────────────────────────────────────────── // Приватные вспомогательные методы // ──────────────────────────────────────────────────────────────────────────── function TSpectrumView.ActiveVfoFreq: Double; begin if FActiveVfo = 0 then Result := FVfoA else Result := FVfoB; end; function TSpectrumView.ScaleX(X, Total, Width: Integer): Integer; begin if Total = 0 then Result := 0 else Result := Round(X / Total * Width); end; procedure TSpectrumView.DrawMarkerLine(C: TCanvas; W, H: Integer); var MX: Integer; MarkerFreq: Double; MarkerLbl: string; begin MX := Round(FMarkerX / 1000.0 * W); if (MX < 0) or (MX >= W) then Exit; MarkerFreq := (FCenterFreq - FSpanHz / 2) + FMarkerX / 1000.0 * FSpanHz; MarkerLbl := FormatFreqSV(Round(MarkerFreq)); C.Pen.Color := TColor($004444FF); C.Pen.Width := 1; C.Pen.Style := psSolid; C.MoveTo(MX, 0); C.LineTo(MX, H); C.Font.Color := TColor($004444FF); C.Font.Size := 7; C.Font.Name := 'Courier New'; if MX + 4 + C.TextWidth(MarkerLbl) < W then C.TextOut(MX + 4, 4, MarkerLbl) else C.TextOut(MX - 4 - C.TextWidth(MarkerLbl), 4, MarkerLbl); end; procedure TSpectrumView.DrawSpectrumGradient(const SpPts: array of TPoint; W, H: Integer); var Row, Col: Integer; YMin, Alpha: Integer; RowPtr: PByte; BPPg: Integer; RG, GG: Byte; Desc: TRawImageDescription; begin if FSpectrumBitmap = nil then Exit; if (W <= 0) or (H <= 0) then Exit; YMin := H; for Col := 0 to W - 1 do if SpPts[Col].Y < YMin then YMin := SpPts[Col].Y; if YMin >= H then Exit; if (FSpecGradImg = nil) or (FSpecGradImg.Width <> W) or (FSpecGradImg.Height <> H) then begin FreeAndNil(FSpecGradImg); FSpecGradImg := TLazIntfImage.Create(W, H); InitRawDesc32(Desc, W, H); FSpecGradImg.DataDescription := Desc; FSpecGradImg.CreateData; end; if FSpecGradImg.PixelData <> nil then FillChar(FSpecGradImg.PixelData^, W * H * 4, 0); BPPg := FSpecGradImg.DataDescription.BitsPerPixel div 8; if BPPg < 3 then BPPg := 4; for Row := YMin to H - 1 do 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); 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; if BPPg >= 4 then RowPtr[3] := Byte(Alpha); end else begin RowPtr[0] := 0; RowPtr[1] := 0; RowPtr[2] := 0; if BPPg >= 4 then RowPtr[3] := 0; end; Inc(RowPtr, BPPg); end; end; FSpecGradBmp.LoadFromIntfImage(FSpecGradImg); LCLIntf.BitBlt(FSpectrumBitmap.Canvas.Handle, 0, 0, W, H, FSpecGradBmp.Canvas.Handle, 0, 0, $CC0020); end; // ──────────────────────────────────────────────────────────────────────────── // Публичные методы данных // ──────────────────────────────────────────────────────────────────────────── procedure TSpectrumView.SetSpectrumData(const Pixels: array of Single; Count: Integer); var i, N: Integer; begin N := Min(Count, 1024); for i := 0 to N - 1 do FSpectrumBuf[i] := Pixels[i]; FSpectrumBufCount := N; FSpectrumDirty := True; end; procedure TSpectrumView.SetWaterfallData(const Pixels: array of Single; Count: Integer); var i, N: Integer; begin N := Min(Count, 1024); for i := 0 to N - 1 do FWaterfallBuf[i] := Pixels[i]; FWaterfallBufCount := N; Inc(FWfFrameCounter); if FWfFrameCounter >= Max(1, FWfFrameInterval) then begin FWfFrameCounter := 0; FWaterfallDirty := True; end; end; // ──────────────────────────────────────────────────────────────────────────── // Размеры bitmap // ──────────────────────────────────────────────────────────────────────────── 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.FillRect(Rect(0, 0, W, H)); FreeAndNil(FSpecGradImg); InvalidateGridCache; FSpPtsLen := 0; end; procedure TSpectrumView.SetWaterfallBitmapSize(W, H: Integer); begin FWaterfallBitmap.SetSize(W, H); FWfBitmapW := 0; FWfBitmapH := 0; SetLength(FWfPixels, 0); FreeAndNil(FWfIntfImg); end; procedure TSpectrumView.SetRulerSize(W, H: Integer); begin if (W <= 0) or (H <= 0) then Exit; FRulerBitmap.SetSize(W, H); FRulerLastFreq := -1.0; FRulerLastVfo := -1.0; FRulerLastSpan := -1.0; end; function TSpectrumView.SpectrumBitmapWidth: Integer; begin Result := FSpectrumBitmap.Width; end; // ──────────────────────────────────────────────────────────────────────────── // Утилиты // ──────────────────────────────────────────────────────────────────────────── procedure TSpectrumView.InvalidateGridCache; begin FGridBitmapW := 0; FGridBitmapH := 0; FSpectrumDirty := True; end; procedure TSpectrumView.InvalidateRulerCache; begin FRulerLastFreq := -1.0; FRulerLastVfo := -1.0; FRulerLastSpan := -1.0; end; procedure TSpectrumView.ResetSpectrumBuf; var i: Integer; begin for i := 0 to 1023 do FSpectrumBuf[i] := -130.0; for i := 0 to 1023 do FWaterfallBuf[i] := -130.0; FWfHigh := FWfManualHigh; FWfLow := FWfManualLow; FWaterfallDirty := True; end; procedure TSpectrumView.ResetWfAvgBuf; begin FWfHigh := FWfManualHigh; FWfLow := FWfManualLow; FWaterfallDirty := True; end; procedure TSpectrumView.FillDemoSpectrum; var i: Integer; FreqOff, Noise, Sig: Double; W: Integer; begin W := FSpectrumBitmap.Width; if W <= 0 then W := 1024; for i := 0 to W - 1 do begin if W > 1 then FreqOff := (i / (W - 1) - 0.5) * FSpanHz else FreqOff := 0; Noise := -110 + (Random - 0.5) * 6; if Abs(FreqOff) < 2000 then Sig := -50 - Abs(FreqOff) / 200 else Sig := -999; FSpectrumBuf[i mod 1024] := Max(Noise, Sig); end; end; function TSpectrumView.NeedsRulerRedraw: Boolean; begin Result := (Abs(FCenterFreq - FRulerLastFreq) >= 0.5) or (Abs(ActiveVfoFreq - FRulerLastVfo) >= 0.5) or (Abs(FSpanHz - FRulerLastSpan) >= 1.0); end; // ──────────────────────────────────────────────────────────────────────────── // DrawSpectrum // ──────────────────────────────────────────────────────────────────────────── procedure TSpectrumView.DrawSpectrum; var C, GC: TCanvas; i, Yp, W, H, GX: Integer; DBmin, DBmax, dB: Double; VfoX, X1, X2: Integer; Lo_Hz, Hi_Hz, Half: Double; AGCy, AGCHangY: Integer; SrcF, Frac, dBv: Double; S0, S1: Integer; InvRange: Double; LabelBandW, SrcCount: Integer; begin if FSpectrumBitmap = nil then Exit; W := FSpectrumBitmap.Width; H := FSpectrumBitmap.Height; if (W <= 0) or (H <= 0) then Exit; C := FSpectrumBitmap.Canvas; DBmax := FSpecRefLevel; DBmin := FSpecRefLevel - FSpecRange; InvRange := 1.0 / (DBmax - DBmin); // ── 1. Фон+сетка из кэша ────────────────────────────────────────────────── if (FGridBitmapW <> W) or (FGridBitmapH <> H) then begin FGridBitmapW := W; FGridBitmapH := H; FGridBitmap.SetSize(W, H); GC := FGridBitmap.Canvas; PaintVerticalGradient(GC, W, H, TColor($0020160B), TColor($006C4F1F)); LabelBandW := 28; GC.Pen.Style := psClear; GC.Brush.Style := bsSolid; GC.Brush.Color := TColor($00543F18); 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.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.MoveTo(0, Yp); GC.LineTo(W-1, Yp); GC.TextOut(1, Yp - 9, Format('%4.0f', [dB])); dB := dB - FSpecGridStep; end; for i := 0 to 8 do begin GX := ScaleX(i, 8, W); GC.Pen.Color := TColor($00685624); GC.MoveTo(GX, 0); GC.LineTo(GX, H); end; end; LCLIntf.BitBlt(C.Handle, 0, 0, W, H, FGridBitmap.Canvas.Handle, 0, 0, $CC0020); // ── 2. Полоса фильтра ───────────────────────────────────────────────────── Half := FFilterBW / 2; if FActiveVfo = 0 then VfoX := Round((FVfoA - FCenterFreq + FSpanHz/2) / FSpanHz * W) else VfoX := Round((FVfoB - FCenterFreq + FSpanHz/2) / FSpanHz * W); case FMode of 0: begin Lo_Hz := -FFilterBW; Hi_Hz := -100; end; 1: begin Lo_Hz := 100; Hi_Hz := FFilterBW; end; else begin Lo_Hz := -Half; Hi_Hz := Half; end; 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; if X2 > X1 then C.FillRect(Rect(X1, 0, X2, H)); C.Pen.Style := psSolid; // ── 3. AGC линии ────────────────────────────────────────────────────────── if FWDSPReady then 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.MoveTo(0, AGCy); C.LineTo(W, AGCy); C.Pen.Style := psSolid; C.Font.Color := TColor($0000AAFF); 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.MoveTo(0, AGCHangY); C.LineTo(W, AGCHangY); C.Pen.Style := psSolid; C.Font.Color := TColor($00FFCC00); 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.MoveTo(0, AGCy); C.LineTo(W, AGCy); C.Pen.Style := psSolid; C.Font.Color := TColor($0000AAFF); C.Font.Size := 6; C.TextOut(4, AGCy - 9, Format('AGC -%ddB', [FAGCTop])); end; end; // ── 4. Кривая спектра ───────────────────────────────────────────────────── if FSpPtsLen <> W + 2 then begin SetLength(FSpPts, W + 2); FSpPtsLen := W + 2; end; SrcCount := EnsureRange(FSpectrumBufCount, 2, 1024); for i := 0 to W - 1 do begin SrcF := i * (SrcCount - 1.0) / Max(1, W - 1); S0 := Min(Trunc(SrcF), SrcCount - 1); S1 := Min(S0 + 1, SrcCount - 1); Frac := SrcF - S0; dBv := FSpectrumBuf[S0] * (1.0 - Frac) + FSpectrumBuf[S1] * Frac; FSpPts[i] := Point(i, Max(0, Min(H-1, Round((DBmax - dBv) * InvRange * H)))); end; FSpPts[W] := Point(W-1, H); FSpPts[W+1] := Point(0, H); // ── 5. Градиент ─────────────────────────────────────────────────────────── C.Brush.Style := bsSolid; C.Pen.Style := psClear; DrawSpectrumGradient(FSpPts, W, H); // ── 6. Линия спектра ────────────────────────────────────────────────────── C.Pen.Style := psSolid; C.Pen.Color := TColor($0040FF80); 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.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.MoveTo(VfoX, 0); C.LineTo(VfoX, H - 12); C.Brush.Color := TColor($0000AAFF); 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); end; // ──────────────────────────────────────────────────────────────────────────── // DrawWaterfall // ──────────────────────────────────────────────────────────────────────────── procedure TSpectrumView.DrawWaterfall; var W, H, X: Integer; dB, frac, WatSrcF: Double; WatS0, WatS1, V: Integer; TargetLow, TargetHigh: Double; HistMinDB, HistMaxDB, HistStepDB: Double; NoiseFloorDB, SignalTopDB: Double; CumCount, LowTargetCount, HighTargetCount, HistIdx: Integer; WfHigh, WfLow, InvRange, Step: Double; Px: PLongWord; Desc: TRawImageDescription; Row, Col: Integer; RowPtr: PByte; Src: PLongWord; BPPi: Integer; Pal: LongWord; Hist: array[0..191] of Integer; SrcCount: Integer; const ALPHA_HIGH = 0.10; ALPHA_LOW = 0.08; WF_AUTO_OFFSET = -4.0; WF_MIN_RANGE = 48.0; WF_MAX_RANGE = 62.0; begin if FWaterfallBitmap = nil then Exit; W := FWaterfallBitmap.Width; H := FWaterfallBitmap.Height; if (W <= 0) or (H <= 0) then Exit; SrcCount := EnsureRange(FWaterfallBufCount, 2, 1024); FillChar(Hist, SizeOf(Hist), 0); HistMinDB := -170.0; HistMaxDB := 22.0; HistStepDB := (HistMaxDB - HistMinDB) / Length(Hist); for X := 0 to SrcCount - 1 do begin HistIdx := EnsureRange(Trunc((FWaterfallBuf[X] - HistMinDB) / HistStepDB), 0, High(Hist)); Inc(Hist[HistIdx]); end; LowTargetCount := Round(SrcCount * 0.30); HighTargetCount := Round(SrcCount * 0.98); CumCount := 0; NoiseFloorDB := FWfLow; SignalTopDB := FWfHigh; for HistIdx := 0 to High(Hist) do begin CumCount := CumCount + Hist[HistIdx]; if CumCount >= LowTargetCount then begin NoiseFloorDB := HistMinDB + (HistIdx + 0.5) * HistStepDB; Break; end; end; CumCount := 0; for HistIdx := 0 to High(Hist) do begin CumCount := CumCount + Hist[HistIdx]; if CumCount >= HighTargetCount then begin SignalTopDB := HistMinDB + (HistIdx + 0.5) * HistStepDB; Break; end; end; if FWfAGCEnabled then begin TargetLow := NoiseFloorDB + WF_AUTO_OFFSET + FWfAGCOffset; if FWfNFEnabled then TargetHigh := Max(TargetLow + WF_MIN_RANGE, SignalTopDB + 6.0) else TargetHigh := TargetLow + 52.0; if TargetHigh > TargetLow + WF_MAX_RANGE then TargetHigh := TargetLow + WF_MAX_RANGE; FWfLow := FWfLow + ALPHA_LOW * (TargetLow - FWfLow); FWfHigh := FWfHigh + ALPHA_HIGH * (TargetHigh - FWfHigh); end; WfHigh := FWfHigh; WfLow := FWfLow; if not FWfAGCEnabled then begin WfHigh := FWfManualHigh; WfLow := FWfManualLow; end; if WfHigh < WfLow + 40.0 then WfHigh := WfLow + 40.0; if WfHigh > 0.0 then WfHigh := 0.0; if WfLow < -160 then WfLow := -160; InvRange := 255.0 / (WfHigh - WfLow); if (FWfBitmapW <> W) or (FWfBitmapH <> H) then begin FWfBitmapW := W; FWfBitmapH := H; SetLength(FWfPixels, W * H); FillDWord(FWfPixels[0], W * H, 0); FWfBitmap.SetSize(W, H); FreeAndNil(FWfIntfImg); end; if H > 1 then Move(FWfPixels[0], FWfPixels[W], (H - 1) * SizeOf(LongWord) * W); Step := (SrcCount - 1.0) / Max(1, W - 1); WatSrcF := 0.0; Px := @FWfPixels[0]; for X := 0 to W - 1 do begin WatS0 := Trunc(WatSrcF); if WatS0 > SrcCount - 2 then WatS0 := SrcCount - 2; WatS1 := WatS0 + 1; frac := WatSrcF - WatS0; dB := FWaterfallBuf[WatS0] * (1.0 - frac) + FWaterfallBuf[WatS1] * frac; V := Trunc((dB - WfLow) * InvRange); if V < 0 then V := 0; if V > 255 then V := 255; Pal := WaterfallEnhancedColorThetis(dB, WfLow, WfHigh); Px^ := Pal; Inc(Px); WatSrcF := WatSrcF + Step; end; if FWfIntfImg = nil then begin FWfIntfImg := TLazIntfImage.Create(W, H); InitRawDesc32(Desc, W, H); FWfIntfImg.DataDescription := Desc; FWfIntfImg.CreateData; end; BPPi := FWfIntfImg.DataDescription.BitsPerPixel div 8; if (BPPi = 4) and (FWfIntfImg.PixelData <> nil) then Move(FWfPixels[0], FWfIntfImg.PixelData^, W * H * 4) else begin Src := @FWfPixels[0]; for Row := 0 to H - 1 do begin RowPtr := FWfIntfImg.GetDataLineStart(Row); if RowPtr = nil then begin Inc(Src, W); Continue; end; for Col := 0 to W - 1 do begin RowPtr[0] := Byte(Src^); RowPtr[1] := Byte(Src^ shr 8); RowPtr[2] := Byte(Src^ shr 16); if BPPi >= 4 then RowPtr[3] := $FF; Inc(Src); Inc(RowPtr, BPPi); end; end; end; FWfBitmap.LoadFromIntfImage(FWfIntfImg); end; // ──────────────────────────────────────────────────────────────────────────── // DrawRuler // ──────────────────────────────────────────────────────────────────────────── procedure TSpectrumView.DrawRuler; var C: TCanvas; W, H, i, X: Integer; FreqStart, FreqHz: Double; VfoX: Integer; Lbl: string; TW: Integer; begin if FPbRuler = nil then Exit; W := FPbRuler.Width; H := FPbRuler.Height; if (W <= 0) or (H <= 0) then Exit; if (Abs(FCenterFreq - FRulerLastFreq) < 0.5) and (Abs(ActiveVfoFreq - FRulerLastVfo) < 0.5) and (Abs(FSpanHz - FRulerLastSpan) < 1.0) then Exit; FRulerLastFreq := FCenterFreq; FRulerLastVfo := ActiveVfoFreq; FRulerLastSpan := FSpanHz; 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; 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; C.Brush.Style := bsClear; FreqStart := FCenterFreq - FSpanHz / 2; for i := 0 to 8 do begin X := ScaleX(i, 8, W); C.Pen.Color := TColor($00685624); 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.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.MoveTo(VfoX, 0); C.LineTo(VfoX, H - 1); C.Pen.Width := 1; end; end; // ──────────────────────────────────────────────────────────────────────────── // DrawBarMeter, DrawSMeterWide, DrawSMeterZone // ──────────────────────────────────────────────────────────────────────────── procedure TSpectrumView.DrawBarMeter(ACanvas: TCanvas; R: TRect; Value, MaxVal: Double; BarColor: TColor); var Pct, BarW: Integer; begin ACanvas.Brush.Color := TColor($00080808); ACanvas.FillRect(R); if MaxVal > 0 then Pct := Round(Max(0.0, Min(1.0, Value / MaxVal)) * (R.Right - R.Left - 2)) else Pct := 0; BarW := Pct; ACanvas.Brush.Color := BarColor; ACanvas.Pen.Color := BarColor; ACanvas.FillRect(Rect(R.Left+1, R.Top+1, R.Left+1+BarW, R.Bottom-1)); ACanvas.Brush.Style := bsClear; ACanvas.Pen.Color := SV_CLR_BORDER; ACanvas.Rectangle(R); end; procedure TSpectrumView.DrawSMeterWide(ACanvas: TCanvas; R: TRect; Value, Peak, MinVal: Double; out ZX1, ZX2, ZY1, ZY2: Integer); const DB_MIN = -127.0; DB_MAX = -13.0; DB_S9 = -73.0; DB_OVR = -43.0; DBM_MARKS: array[0..5] of Double = (-120,-100,-80,-60,-40,-20); 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 = 62; TICK_LONG = 5; TICK_MED = 3; var W, H, BX, BW: Integer; Y_BAR_TOP, Y_BAR_BOT: Integer; BarEnd, PeakLeft, PeakRight, S9X, OvrX: Integer; i, X, TW: Integer; Lbl: string; SNum, Over: Integer; function DBtoX(dB: Double): Integer; inline; begin Result := BX + Round((dB - DB_MIN) / (DB_MAX - DB_MIN) * BW); if Result < BX then Result := BX; if Result > BX + BW then Result := BX + BW; end; procedure VLine(X2, Y1, Y2: Integer; C: TColor); begin ACanvas.Pen.Color := C; ACanvas.MoveTo(X2, Y1); ACanvas.LineTo(X2, Y2); end; procedure FillBar(X1, Y1, X2, Y2: Integer; C: TColor); begin ACanvas.Brush.Color := C; ACanvas.Brush.Style := bsSolid; ACanvas.Pen.Style := psClear; if X2 > X1 then ACanvas.FillRect(Rect(X1, Y1, X2, Y2)); ACanvas.Pen.Style := psSolid; end; begin 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; BX := R.Left + LEFT_INFO; BW := W - LEFT_INFO - 4; Y_BAR_TOP := R.Top + H * 36 div 100; Y_BAR_BOT := R.Top + H * 66 div 100; S9X := DBtoX(DB_S9); OvrX := DBtoX(DB_OVR); 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)); BarEnd := DBtoX(Value); PeakLeft := DBtoX(MinVal); PeakRight := DBtoX(Peak); ZX1 := Max(BX + 1, Min(PeakLeft, PeakRight)); ZX2 := Min(BX + BW - 1, Max(PeakLeft, PeakRight)); ZY1 := Y_BAR_TOP + 1; ZY2 := Y_BAR_BOT - 1; if BarEnd > BX then begin if BarEnd <= S9X then FillBar(BX, Y_BAR_TOP+1, BarEnd, Y_BAR_BOT-1, CLR_BAR_GREEN) else begin FillBar(BX, Y_BAR_TOP+1, S9X, Y_BAR_BOT-1, CLR_BAR_GREEN); FillBar(S9X, Y_BAR_TOP+1, BarEnd, Y_BAR_BOT-1, CLR_BAR_OVER); end; end; if (PeakRight > BX) and (PeakRight <= BX + BW) then begin ACanvas.Pen.Color := CLR_PEAK_MARKER; ACanvas.Pen.Width := 2; ACanvas.MoveTo(PeakRight, Y_BAR_TOP); ACanvas.LineTo(PeakRight, Y_BAR_BOT); ACanvas.Pen.Width := 1; end; ACanvas.Brush.Style := bsClear; ACanvas.Pen.Color := CLR_BDR; ACanvas.Rectangle(BX, Y_BAR_TOP, BX + BW, Y_BAR_BOT); ACanvas.Font.Name := 'Courier New'; ACanvas.Font.Size := 6; ACanvas.Font.Style := []; ACanvas.Brush.Style := bsClear; for i := 0 to High(DBM_MARKS) do begin X := DBtoX(DBM_MARKS[i]); VLine(X, Y_BAR_TOP - TICK_LONG, Y_BAR_TOP - 1, CLR_TICK_WHITE); Lbl := DBM_LABELS[i]; TW := ACanvas.TextWidth(Lbl); ACanvas.Font.Color := CLR_SCALE_DBM; ACanvas.TextOut(X - TW div 2, Y_BAR_TOP - TICK_LONG - ACanvas.TextHeight(Lbl) - 1, Lbl); end; i := -125; while i < -13 do begin X := DBtoX(i); VLine(X, Y_BAR_TOP - TICK_MED, Y_BAR_TOP - 1, CLR_TICK_GREEN); Inc(i, 5); end; for i := 0 to High(S_MARKS_DBM) do begin X := DBtoX(S_MARKS_DBM[i]); if S_MARKS_DBM[i] >= DB_S9 then VLine(X, Y_BAR_BOT+1, Y_BAR_BOT+TICK_LONG, CLR_TICK_BLUE) else VLine(X, Y_BAR_BOT+1, Y_BAR_BOT+TICK_LONG, CLR_TICK_GREEN); Lbl := S_MARKS_LBL[i]; TW := ACanvas.TextWidth(Lbl); if S_MARKS_DBM[i] >= DB_S9 then ACanvas.Font.Color := CLR_SCALE_S_BLUE else ACanvas.Font.Color := CLR_SCALE_S_GREEN; ACanvas.TextOut(X - TW div 2, Y_BAR_BOT + TICK_LONG + 1, Lbl); end; ACanvas.Font.Size := 8; ACanvas.Font.Style := [fsBold]; ACanvas.Font.Color := CLR_LABEL_DBM; Lbl := Format('%6.1f', [Value]); ACanvas.TextOut(R.Left + 1, R.Top + 1, Lbl); ACanvas.Font.Size := 6; ACanvas.Font.Style := []; ACanvas.Font.Color := TColor($00507070); ACanvas.TextOut(R.Left + 1, Y_BAR_TOP - ACanvas.TextHeight('dBm') - 1, 'dBm'); ACanvas.Font.Size := 8; ACanvas.Font.Style := [fsBold]; ACanvas.Font.Color := CLR_LABEL_S; if Value >= -73.0 then begin Over := Round(Value - (-73.0)); if Over < 5 then Lbl := 'S9' else begin Over := ((Over + 5) div 10) * 10; if Over = 0 then Over := 10; Lbl := Format('S9+%d', [Over]); end; end else if Value <= -121.0 then Lbl := 'S1' else begin Lbl := 'S1'; for SNum := 1 to 8 do if Value >= (-121.0 + (SNum - 1) * 6.0) then Lbl := 'S' + IntToStr(SNum); end; ACanvas.TextOut(R.Left + 1, Y_BAR_BOT + TICK_LONG + 1, Lbl); end; procedure TSpectrumView.DrawSMeterZone(ACanvas: TCanvas; R: TRect; X1, X2, Y1, Y2: Integer); const ALPHA = 16384; var Img: TLazIntfImage; FC: TFPColor; IX, IY: Integer; begin if (X2 <= X1) or (FSmBitmap = nil) then Exit; Img := FSmBitmap.CreateIntfImage; try for IY := Y1 to Y2 - 1 do for IX := X1 to X2 - 1 do begin FC := Img.Colors[IX, IY]; FC.Red := Min(65535, FC.Red + ALPHA); FC.Green := Min(65535, FC.Green + ALPHA); FC.Blue := Min(65535, FC.Blue + ALPHA); Img.Colors[IX, IY] := FC; end; FSmBitmap.LoadFromIntfImage(Img); finally Img.Free; end; end; // ──────────────────────────────────────────────────────────────────────────── // Paint-обработчики // ──────────────────────────────────────────────────────────────────────────── procedure TSpectrumView.PaintSpectrum(Sender: TObject); var PB: TPaintBox; begin PB := TPaintBox(Sender); if (FSpectrumBitmap.Width > 0) and (FSpectrumBitmap.Height > 0) then PB.Canvas.Draw(0, 0, FSpectrumBitmap) else begin PB.Canvas.Brush.Color := SV_CLR_BG; PB.Canvas.FillRect(Rect(0, 0, PB.Width, PB.Height)); end; end; procedure TSpectrumView.PaintWaterfall(Sender: TObject); var PB: TPaintBox; W, H: Integer; begin PB := TPaintBox(Sender); W := PB.Width; H := PB.Height; if (W <= 0) or (H <= 0) then Exit; if (FWfBitmap <> nil) and (FWfBitmap.Width = W) and (FWfBitmap.Height = H) then PB.Canvas.Draw(0, 0, FWfBitmap) else begin PB.Canvas.Brush.Color := clBlack; PB.Canvas.FillRect(Rect(0, 0, W, H)); end; if FMarkerActive then DrawMarkerLine(PB.Canvas, W, H); end; procedure TSpectrumView.PaintRuler(Sender: TObject); var PB: TPaintBox; begin PB := TPaintBox(Sender); DrawRuler; if (FRulerBitmap.Width > 0) and (FRulerBitmap.Height > 0) then PB.Canvas.Draw(0, 0, FRulerBitmap); end; procedure TSpectrumView.PaintSMeterRight(Sender: TObject); var PB: TPaintBox; W, H, ZX1, ZX2, ZY1, ZY2: Integer; begin PB := TPaintBox(Sender); W := PB.Width; H := PB.Height; if (W <= 0) or (H <= 0) then Exit; if (FSmBitmap = nil) or (FSmBitmap.Width <> W) or (FSmBitmap.Height <> H) then begin FreeAndNil(FSmBitmap); FSmBitmap := TBitmap.Create; FSmBitmap.SetSize(W, H); end; DrawSMeterWide(FSmBitmap.Canvas, Rect(0, 0, W, H), FLastSMeter, FSMeterPeak, FSMeterMin, ZX1, ZX2, ZY1, ZY2); DrawSMeterZone(FSmBitmap.Canvas, Rect(0, 0, W, H), ZX1, ZX2, ZY1, ZY2); PB.Canvas.Draw(0, 0, FSmBitmap); end; procedure TSpectrumView.PaintFwdPower(Sender: TObject); var PB: TPaintBox; begin PB := TPaintBox(Sender); DrawBarMeter(PB.Canvas, Rect(0, 0, PB.Width, PB.Height), FLastFwdW, 150, SV_CLR_METER_ON); end; procedure TSpectrumView.PaintSWR(Sender: TObject); var PB: TPaintBox; begin PB := TPaintBox(Sender); DrawBarMeter(PB.Canvas, Rect(0, 0, PB.Width, PB.Height), FLastSWR - 1.0, 4.0, SV_CLR_AMBER); end; end.