feat(slices): грид-раскладка панов + фиксы минимумов стека, утечки кнопок, CPU

Грид: пан 0 сверху во всю ширину, доп. паны колонками снизу; тумблер
«▦/▤» в ряду зума пана 0, вертикальные сплиттеры-призраки колонок
(FPanColShare), доля ряда FPanGridRowShare; автоткат в стек при <2 доп.
панах. Регион пана обобщён до прямоугольника (StackLeft/StackWidth).

Фиксы по экранному тесту:
- наезд панов: раздача высот и драг сплиттеров клампятся минимумом
  содержимого пана (MinLayoutHeight/PanMinHeight, пан 0 += wideband);
- «блуждающие» кнопки после STOP: MakeFlatBtn даёт Owner=контейнер,
  TPanafallPanel.Destroy теперь явно освобождает −/⌂/+/⊞/▦;
- жор CPU ~40%/пан: убран безусловный Invalidate зум-бара из тика
  (SyncPanZoomUI), инвалидация только при реальной смене значений;
- пропорция спектр/водопад пана 0 не восстанавливалась после закрытия
  панов: кламп ratio минимумами больше не пишется в FSplitterRatio.

Подтверждено юзером на экране: грид/стек/STOP/pop-out работают,
CPU со всеми панами 17-20%.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-12 15:51:11 +03:00
co-authored by Claude Fable 5
parent 64edf9e289
commit eac66100a8
3 changed files with 501 additions and 66 deletions
+315 -27
View File
@@ -183,6 +183,19 @@ type
FPanSplitTop0: Integer;
FPanSplitMinTop: Integer;
FPanSplitMaxTop: Integer;
// Грид-раскладка: пан 0 сверху во всю ширину, доп. паны колонками снизу.
// Активна при FPanGridMode и >=2 доп. панах в стеке (иначе обычный стек).
FPanGridMode: Boolean;
FPanGridRowShare: Double; // доля высоты верхнего ряда (пан 0), 0.15..0.85
FPanColShare: array[1..MAX_PANS-1] of Double; // доли ширины колонок
FPanVSplitters: array[1..MAX_PANS-1] of TPanel; // слева от колонки пана i
// Drag вертикального сплиттера (индекс пана справа; 0 = нет).
FPanVSplitDrag: Integer;
FPanVSplitX0: Integer;
FPanVSplitLeft0: Integer;
FPanVSplitMinLeft: Integer;
FPanVSplitMaxLeft: Integer;
FPanWideH: Integer; // wideband-высота последнего LayoutPanStack (для PanMinHeight)
FPendingPanClose: Integer; // «×» шапки: PanId к закрытию в тике (0 = нет)
// Меню rate пана («NNN kHz» в шапке панов N; создаётся лениво).
FPanRateMenu: TPopupMenu;
@@ -612,9 +625,17 @@ type
procedure PanSplitMouseDown(Sender: TObject; Button: TMouseButton;
Shift: TShiftState; X, Y: Integer);
procedure PanSplitMouseMove(Sender: TObject; Shift: TShiftState; X, Y: Integer);
procedure OnPanGridToggle(Sender: TObject); // «▦/▤» в ряду зума пана 0
function PanGridActive: Boolean; // грид реально применён
procedure PanVSplitMouseDown(Sender: TObject; Button: TMouseButton;
Shift: TShiftState; X, Y: Integer);
procedure PanVSplitMouseMove(Sender: TObject; Shift: TShiftState; X, Y: Integer);
procedure PanVSplitMouseUp(Sender: TObject; Button: TMouseButton;
Shift: TShiftState; X, Y: Integer);
procedure PanSplitMouseUp(Sender: TObject; Button: TMouseButton;
Shift: TShiftState; X, Y: Integer);
procedure LayoutPanStack(AWideH: Integer); // регионы панов + Layout каждого
function PanMinHeight(PanId: Integer): Integer; // мин. высота региона пана
function Pan0HeaderH: Integer; // высота шапки пана 0 (0 без стека)
procedure PbSpectrumDblClick(Sender: TObject);
procedure TrkDriveChange(Sender: TObject);
@@ -1079,6 +1100,8 @@ begin
FPan.OnADCClick := OnPanADCClick; // бейдж RX1/RX2 (3.4, платы с 2 АЦП)
FPans[0] := FPan;
FPanShare[0] := 1.0;
FPanGridMode := False;
FPanGridRowShare := 0.5; // грид: пан 0 — верхняя половина
FPanCbLock := SyncObjs.TCriticalSection.Create;
FSpecView.VfoA := FController.FVfoA;
FSpecView.VfoB := FController.FVfoB;
@@ -2309,6 +2332,7 @@ begin
FPan.ZoomInClick := BtnZoomInClick;
FPan.ZoomDefClick := BtnZoomDefClick;
FPan.ZoomOutClick := BtnZoomOutClick;
FPan.GridClick := OnPanGridToggle; // «▦/▤» грид-раскладка панов
FPan.OnSplitterMoved := RightPanelResize;
FPan.Build(PanelRight, Max(1, FController.FSpecMSAA));
// Алиасы контролов (исторические имена; постепенная миграция на FPan.*)
@@ -2574,13 +2598,37 @@ begin
Result := 0;
end;
function TMainForm.PanGridActive: Boolean;
// Грид реально применён: включён юзером и в стеке >=2 доп. панов
// (с одним доп. паном грид совпадает со стеком).
var i, N: Integer;
begin
N := 0;
for i := 1 to MAX_PANS - 1 do
if (FPans[i] <> nil) and not PanFloating(i) then Inc(N);
Result := FPanGridMode and (N >= 2);
end;
function TMainForm.PanMinHeight(PanId: Integer): Integer;
// Мин. высота региона пана в стеке/гриде: ниже неё Layout панели клампит
// спектр/водопад к минимумам и виджеты вылезают за регион (паны наезжают).
begin
if FPans[PanId] = nil then Exit(0);
Result := FPans[PanId].MinLayoutHeight(
IfThen(PanId = 0, FPanWideH, 0),
FController.FShowSpectrum, FController.FShowWaterfall);
end;
procedure TMainForm.LayoutPanStack(AWideH: Integer);
const
PAN_SPLIT_H = 5;
var
ActiveIds: array[0..MAX_PANS-1] of Integer;
N, i, k, TotalH, UsableH, Y, Hi: Integer;
MinH: array[0..MAX_PANS-1] of Integer;
N, i, k, TotalH, TotalW, UsableH, UsableW, Y, X, Hi, Wi: Integer;
Row0H, RowTop, RowH, M, MinRest, RemainH, BotMin: Integer;
ShareSum: Double;
Grid: Boolean;
begin
// Плавающие (pop-out) паны в стеке не участвуют — у них своё окно.
N := 0;
@@ -2588,37 +2636,148 @@ begin
if (FPans[i] <> nil) and not PanFloating(i) then
begin ActiveIds[N] := i; Inc(N); end;
if N = 0 then Exit;
FPanWideH := AWideH; // для PanMinHeight (в т.ч. из сплиттер-драга)
TotalH := PanelRight.ClientHeight;
TotalW := PanelRight.ClientWidth;
Grid := PanGridActive; // пан 0 + >=2 доп. панов колонками
// Тумблер «▦/▤» в ряду зума пана 0: есть смысл при >=2 доп. панах в стеке.
if FPans[0] <> nil then
begin
// Виден при >=2 доп. панах; при включённом гриде — всегда (выключить можно
// даже когда паны закрылись и раскладка сама откатилась в стек).
FPans[0].GridBtnEnabled := FPanGridMode or (N >= 3);
if FPans[0].BtnGrid <> nil then
begin
if FPanGridMode then FPans[0].BtnGrid.Caption := '▤'
else FPans[0].BtnGrid.Caption := '▦';
end;
end;
if not Grid then
begin
// ---- Вертикальный стек (как раньше) ----
for i := 1 to MAX_PANS - 1 do
if FPanVSplitters[i] <> nil then FPanVSplitters[i].Visible := False;
UsableH := TotalH - (N - 1) * PAN_SPLIT_H;
ShareSum := 0;
for k := 0 to N - 1 do ShareSum := ShareSum + FPanShare[ActiveIds[k]];
if ShareSum <= 0 then ShareSum := 1;
Y := 0;
for k := 0 to N - 1 do
begin
i := ActiveIds[k];
if k > 0 then
begin
FPanSplitters[i].SetBounds(0, Y, PanelRight.ClientWidth, PAN_SPLIT_H);
Inc(Y, PAN_SPLIT_H);
end;
if k = N - 1 then
Hi := TotalH - Y // последний пан добирает остаток (округления долей)
else
Hi := Round(UsableH * FPanShare[i] / ShareSum);
FPans[i].StackTop := Y;
FPans[i].StackHeight := Hi;
// Видимость шапки — по ОБЩЕМУ числу панов (включая плавающие), как
// Pan0HeaderH/UpdatePanHeaders: иначе при pop-out пана N шапка пана 0
// прячется, а wideband остаётся сдвинутым на Pan0HeaderH — спектр
// накрывает линейку частот wideband.
// накрывает линейку частот wideband. Ставим ДО расчёта минимумов
// (MinLayoutHeight учитывает шапку).
MinRest := 0;
for k := 0 to N - 1 do
begin
i := ActiveIds[k];
FPans[i].HeaderVisible := PanCount > 1;
MinH[k] := PanMinHeight(i);
Inc(MinRest, MinH[k]);
end;
Y := 0;
RemainH := UsableH;
for k := 0 to N - 1 do
begin
i := ActiveIds[k];
Dec(MinRest, MinH[k]); // теперь = сумма минимумов панов ПОСЛЕ текущего
if k > 0 then
begin
FPanSplitters[i].Visible := True; // грид мог спрятать
FPanSplitters[i].SetBounds(0, Y, TotalW, PAN_SPLIT_H);
Inc(Y, PAN_SPLIT_H);
end;
if k = N - 1 then
Hi := RemainH // последний пан добирает остаток (округления долей)
else
begin
Hi := Round(UsableH * FPanShare[i] / ShareSum);
// Доли не смеют ужать пан ниже минимума содержимого (виджеты вылезут
// за регион и паны наедут друг на друга), а раздутый минимумом пан —
// отобрать у оставшихся их минимумы.
if Hi < MinH[k] then Hi := MinH[k];
if Hi > RemainH - MinRest then Hi := RemainH - MinRest;
if Hi < 1 then Hi := 1; // окно меньше суммы минимумов — деградируем без наезда выше
end;
Dec(RemainH, Hi);
FPans[i].StackTop := Y;
FPans[i].StackHeight := Hi;
FPans[i].StackLeft := 0;
FPans[i].StackWidth := 0; // вся ширина
if i = 0 then
FPans[i].Layout(AWideH, FController.FShowSpectrum, FController.FShowWaterfall)
else
FPans[i].Layout(0, FController.FShowSpectrum, FController.FShowWaterfall);
Inc(Y, Hi);
end;
Exit;
end;
// ---- Грид: пан 0 сверху во всю ширину, доп. паны колонками снизу ----
FPanGridRowShare := EnsureRange(FPanGridRowShare, 0.15, 0.85);
Row0H := Round((TotalH - PAN_SPLIT_H) * FPanGridRowShare);
// Кламп ряда минимумами содержимого (шапки — до расчёта минимумов):
// ни пан 0, ни колонки нижнего ряда не смеют стать ниже своих минимумов.
FPans[0].HeaderVisible := PanCount > 1;
BotMin := 0;
for k := 1 to N - 1 do
begin
i := ActiveIds[k];
FPans[i].HeaderVisible := True;
BotMin := Max(BotMin, PanMinHeight(i));
end;
if Row0H > TotalH - PAN_SPLIT_H - BotMin then
Row0H := TotalH - PAN_SPLIT_H - BotMin;
if Row0H < PanMinHeight(0) then Row0H := PanMinHeight(0);
FPans[0].StackTop := 0;
FPans[0].StackHeight := Row0H;
FPans[0].StackLeft := 0;
FPans[0].StackWidth := 0;
FPans[0].Layout(AWideH, FController.FShowSpectrum, FController.FShowWaterfall);
// Горизонтальный сплиттер ряда — сплиттер ПЕРВОЙ колонки (во всю ширину);
// сплиттеры остальных колонок прячем (между колонками — вертикальные).
M := N - 1; // колонок в нижнем ряду
FPanSplitters[ActiveIds[1]].Visible := True;
FPanSplitters[ActiveIds[1]].SetBounds(0, Row0H, TotalW, PAN_SPLIT_H);
for k := 2 to N - 1 do
FPanSplitters[ActiveIds[k]].Visible := False;
RowTop := Row0H + PAN_SPLIT_H;
RowH := TotalH - RowTop;
UsableW := TotalW - (M - 1) * PAN_SPLIT_H;
ShareSum := 0;
for k := 1 to N - 1 do ShareSum := ShareSum + FPanColShare[ActiveIds[k]];
if ShareSum <= 0 then ShareSum := 1;
X := 0;
for k := 1 to N - 1 do
begin
i := ActiveIds[k];
if k > 1 then
begin
FPanVSplitters[i].Visible := True;
FPanVSplitters[i].SetBounds(X, RowTop, PAN_SPLIT_H, RowH);
Inc(X, PAN_SPLIT_H);
end
else if FPanVSplitters[i] <> nil then
FPanVSplitters[i].Visible := False; // у первой колонки левого сплиттера нет
if k = N - 1 then
Wi := TotalW - X
else
Wi := Round(UsableW * FPanColShare[i] / ShareSum);
FPans[i].StackTop := RowTop;
FPans[i].StackHeight := RowH;
FPans[i].StackLeft := X;
FPans[i].StackWidth := Wi;
FPans[i].HeaderVisible := True;
FPans[i].Layout(0, FController.FShowSpectrum, FController.FShowWaterfall);
Inc(X, Wi);
end;
// Вертикальные сплиттеры панов вне нижнего ряда (плавающие) — спрятать.
for i := 1 to MAX_PANS - 1 do
if (FPanVSplitters[i] <> nil) and ((FPans[i] = nil) or PanFloating(i)) then
FPanVSplitters[i].Visible := False;
end;
// ===========================================================================
@@ -2701,6 +2860,7 @@ begin
if FPans[i] <> nil then
begin
if FPans[i].BtnAddPan <> nil then StyleButton(FPans[i].BtnAddPan, False);
if FPans[i].BtnGrid <> nil then StyleButton(FPans[i].BtnGrid, False);
if FPans[i].BtnPopPan <> nil then StyleButton(FPans[i].BtnPopPan, False);
if FPans[i].BtnClosePan <> nil then StyleButton(FPans[i].BtnClosePan, False);
ApplyPanHeaderTheme(FPans[i]);
@@ -2708,6 +2868,7 @@ begin
begin
FPans[i].SetFlagsTheme(T);
if FPanSplitters[i] <> nil then FPanSplitters[i].Color := T.Border;
if FPanVSplitters[i] <> nil then FPanVSplitters[i].Color := T.Border;
if FPanFloatForm[i] <> nil then FPanFloatForm[i].Color := T.BG;
// Зум-бар пана N (пан 0 — через алиас FPanZoomBar ниже).
if FPans[i].ZoomBar <> nil then FPans[i].ZoomBar.SetTheme(T);
@@ -5371,7 +5532,9 @@ begin
FController.PanDDCRateKHz(P.PanId) * 1000.0);
P.ZoomBar.SetState(FController.PanZoomFactor(P.PanId),
FController.PanPanSlider(P.PanId));
if P.PbPanZoom <> nil then P.PbPanZoom.Invalidate;
// Инвалидация бара — внутри Set{Freq,State} и ТОЛЬКО при реальной смене:
// нас зовут из тика на каждый пан каждый кадр, безусловный Invalidate =
// полный софт-рендер бара + Qt-рекомпозит на частоте кадров (жор CPU).
end;
procedure TMainForm.RefreshPanZoom(P: TPanafallPanel);
@@ -5382,6 +5545,7 @@ begin
P.LayoutFlags;
MarkPanDirty(P);
if P.PbWaterfall <> nil then P.PbWaterfall.Invalidate;
if P.PbPanZoom <> nil then P.PbPanZoom.Invalidate; // событийный путь — можно
end;
procedure TMainForm.AddSliceAtFreqPan(P: TPanafallPanel; Hz: Double);
@@ -5509,7 +5673,10 @@ begin
ResetPanGLCaches(P);
P.StackTop := 0;
P.StackHeight := 0; // 0/0 = весь родитель (окно)
P.StackLeft := 0;
P.StackWidth := 0;
if FPanSplitters[PanId] <> nil then FPanSplitters[PanId].Visible := False;
if FPanVSplitters[PanId] <> nil then FPanVSplitters[PanId].Visible := False;
F.Show;
UpdatePanHeaders;
ResizeSpectrumPanels; // пере-стек оставшихся в главном окне
@@ -5761,6 +5928,20 @@ begin
Split.OnMouseUp := PanSplitMouseUp;
FPanSplitters[PanId] := Split;
// Вертикальный сплиттер слева от колонки пана (грид-раскладка).
Split := TPanel.Create(Self);
Split.Parent := PanelRight;
Split.BevelOuter := bvNone;
Split.Color := TColor($00303030);
Split.Cursor := crHSplit;
Split.Tag := PanId;
Split.Visible := False; // включает LayoutPanStack в грид-режиме
Split.OnMouseDown := PanVSplitMouseDown;
Split.OnMouseMove := PanVSplitMouseMove;
Split.OnMouseUp := PanVSplitMouseUp;
FPanVSplitters[PanId] := Split;
FPanColShare[PanId] := 1.0;
// Публикуем пан для пиксельных колбэков (display-поток) и делим высоту поровну.
FPanCbLock.Enter;
try
@@ -5807,6 +5988,7 @@ begin
if (FActiveSliceId <> 0) and not FController.GetSlice(FActiveSliceId, S) then
FActiveSliceId := 0;
FreeAndNil(FPanSplitters[PanId]);
FreeAndNil(FPanVSplitters[PanId]);
P.Free;
// Пан был в отдельном окне — окно больше не нужно. Release (не Free):
// сюда можно попасть из событий контролов этого же окна («×» шапки → тик).
@@ -6042,24 +6224,42 @@ procedure TMainForm.PanSplitMouseDown(Sender: TObject; Button: TMouseButton;
const
PAN_SPLIT_H = 5;
var
BelowId, AboveId, i, TotalH, MinPx: Integer;
BelowId, AboveId, i, TotalH, MinPx, BotMin: Integer;
Split: TPanel;
begin
if Button <> mbLeft then Exit;
Split := TPanel(Sender);
BelowId := Split.Tag;
TotalH := PanelRight.ClientHeight;
if TotalH <= 0 then Exit;
MinPx := Round(0.15 * TotalH);
if PanGridActive then
begin
// Грид: этот сплиттер — граница ряда пан 0 / нижний ряд колонок.
// Минимум — 15% ИЛИ минимум содержимого (что больше): ниже минимума
// виджеты пана вылезают за регион и паны наезжают друг на друга.
FPanSplitMinTop := Max(MinPx, PanMinHeight(0));
BotMin := MinPx; // минимум нижнего ряда — макс. по его колонкам
for i := 1 to MAX_PANS - 1 do
if (FPans[i] <> nil) and not PanFloating(i) then
BotMin := Max(BotMin, PanMinHeight(i));
FPanSplitMaxTop := TotalH - BotMin - PAN_SPLIT_H;
end
else
begin
// Пан над сплиттером — ближайший активный с меньшим PanId.
AboveId := -1;
for i := BelowId - 1 downto 0 do
if FPans[i] <> nil then begin AboveId := i; Break; end;
if (AboveId < 0) or (FPans[BelowId] = nil) then Exit;
TotalH := PanelRight.ClientHeight;
if TotalH <= 0 then Exit;
// Границы призрака: каждому пану — минимум 15% стека (= кламп долей в MouseUp).
MinPx := Round(0.15 * TotalH);
FPanSplitMinTop := FPans[AboveId].StackTop + MinPx;
// Границы призрака: каждому пану — минимум 15% стека ИЛИ минимум его
// содержимого (что больше; = кламп раздачи в LayoutPanStack).
FPanSplitMinTop := FPans[AboveId].StackTop
+ Max(MinPx, PanMinHeight(AboveId));
FPanSplitMaxTop := FPans[BelowId].StackTop + FPans[BelowId].StackHeight
- MinPx - PAN_SPLIT_H;
- Max(MinPx, PanMinHeight(BelowId)) - PAN_SPLIT_H;
end;
if FPanSplitMaxTop < FPanSplitMinTop then FPanSplitMaxTop := FPanSplitMinTop;
FPanSplitDrag := BelowId;
FPanSplitY0 := Mouse.CursorPos.Y;
FPanSplitTop0 := Split.Top;
@@ -6088,13 +6288,20 @@ begin
BelowId := FPanSplitDrag;
FPanSplitDrag := 0;
TPanel(Sender).Color := TColor($00303030);
TotalH := PanelRight.ClientHeight;
if TotalH <= 0 then Exit;
if PanGridActive then
begin
// Грид: сплиттер задаёт долю высоты верхнего ряда (пан 0) напрямую.
FPanGridRowShare := EnsureRange(TPanel(Sender).Top / TotalH, 0.15, 0.85);
ResizeSpectrumPanels;
Exit;
end;
// Пан над сплиттером — ближайший активный с меньшим PanId.
AboveId := -1;
for i := BelowId - 1 downto 0 do
if FPans[i] <> nil then begin AboveId := i; Break; end;
if (AboveId < 0) or (FPans[BelowId] = nil) then Exit;
TotalH := PanelRight.ClientHeight;
if TotalH <= 0 then Exit;
// Дельта — по финальному положению призрака (он уже клампился в Move).
DeltaY := TPanel(Sender).Top - FPanSplitTop0;
DShare := DeltaY / TotalH;
@@ -6108,6 +6315,87 @@ begin
ResizeSpectrumPanels;
end;
procedure TMainForm.OnPanGridToggle(Sender: TObject);
begin
FPanGridMode := not FPanGridMode;
ResizeSpectrumPanels;
end;
procedure TMainForm.PanVSplitMouseDown(Sender: TObject; Button: TMouseButton;
Shift: TShiftState; X, Y: Integer);
const
PAN_SPLIT_W = 5;
var
RightId, LeftId, i, TotalW, MinPx: Integer;
Split: TPanel;
begin
if (Button <> mbLeft) or not PanGridActive then Exit;
Split := TPanel(Sender);
RightId := Split.Tag;
// Колонка слева — ближайший застекованный доп. пан с меньшим PanId.
LeftId := -1;
for i := RightId - 1 downto 1 do
if (FPans[i] <> nil) and not PanFloating(i) then begin LeftId := i; Break; end;
if (LeftId < 0) or (FPans[RightId] = nil) then Exit;
TotalW := PanelRight.ClientWidth;
if TotalW <= 0 then Exit;
// Каждой колонке — минимум 15% ширины (= кламп долей в MouseUp).
MinPx := Round(0.15 * TotalW);
FPanVSplitMinLeft := FPans[LeftId].StackLeft + MinPx;
FPanVSplitMaxLeft := FPans[RightId].StackLeft + FPans[RightId].StackWidth
- MinPx - PAN_SPLIT_W;
FPanVSplitDrag := RightId;
FPanVSplitX0 := Mouse.CursorPos.X;
FPanVSplitLeft0 := Split.Left;
Split.Color := TColor($005050A0);
end;
procedure TMainForm.PanVSplitMouseMove(Sender: TObject; Shift: TShiftState;
X, Y: Integer);
var NewLeft: Integer;
begin
// Живой призрак: двигаем сам сплиттер, пересчёт долей — по MouseUp.
if (FPanVSplitDrag = 0) or (TPanel(Sender).Tag <> FPanVSplitDrag) then Exit;
NewLeft := FPanVSplitLeft0 + (Mouse.CursorPos.X - FPanVSplitX0);
if NewLeft < FPanVSplitMinLeft then NewLeft := FPanVSplitMinLeft;
if NewLeft > FPanVSplitMaxLeft then NewLeft := FPanVSplitMaxLeft;
TPanel(Sender).Left := NewLeft;
end;
procedure TMainForm.PanVSplitMouseUp(Sender: TObject; Button: TMouseButton;
Shift: TShiftState; X, Y: Integer);
var
RightId, LeftId, i, DeltaX, TotalW: Integer;
DShare, MinShare, ShareSum: Double;
begin
if FPanVSplitDrag = 0 then Exit;
RightId := FPanVSplitDrag;
FPanVSplitDrag := 0;
TPanel(Sender).Color := TColor($00303030);
LeftId := -1;
for i := RightId - 1 downto 1 do
if (FPans[i] <> nil) and not PanFloating(i) then begin LeftId := i; Break; end;
if (LeftId < 0) or (FPans[RightId] = nil) then Exit;
TotalW := PanelRight.ClientWidth;
if TotalW <= 0 then Exit;
// Доли колонок нормируются суммой (init 1.0 на пан) — пиксели → доли через неё.
ShareSum := 0;
for i := 1 to MAX_PANS - 1 do
if (FPans[i] <> nil) and not PanFloating(i) then
ShareSum := ShareSum + FPanColShare[i];
if ShareSum <= 0 then ShareSum := 1;
DeltaX := TPanel(Sender).Left - FPanVSplitLeft0;
DShare := DeltaX / TotalW * ShareSum;
MinShare := 0.15 * ShareSum;
if FPanColShare[LeftId] + DShare < MinShare then
DShare := MinShare - FPanColShare[LeftId];
if FPanColShare[RightId] - DShare < MinShare then
DShare := FPanColShare[RightId] - MinShare;
FPanColShare[LeftId] := FPanColShare[LeftId] + DShare;
FPanColShare[RightId] := FPanColShare[RightId] - DShare;
ResizeSpectrumPanels;
end;
procedure TMainForm.OnVfoOverlaySliceSelect(Sender: TObject);
begin
if Sender is TVfoOverlay then
+102 -26
View File
@@ -110,6 +110,10 @@ type
FOnPanClose: TNotifyEvent;
FOnAddPan: TNotifyEvent;
FAddPanWanted: Boolean; // хозяин: показывать «⊞» в ряду пан/зума
FBtnGrid: TFlatButton; // «▦/▤» тумблер грид-раскладки (пан 0)
FGridBtnWanted: Boolean; // хозяин: показывать тумблер (>=2 доп. панов)
FStackLeft: Integer; // регион пана: X (0 при полной ширине)
FStackWidth: Integer; // ширина региона (0 = вся ширина родителя)
procedure BtnClosePanClick(Sender: TObject);
procedure BtnAddPanClick(Sender: TObject);
@@ -135,7 +139,7 @@ type
procedure PanZoomMouseUp(Sender: TObject; Button: TMouseButton;
Shift: TShiftState; X, Y: Integer);
procedure PanZoomDblClick(Sender: TObject);
procedure PositionPanZoomBar(BottomY, RW, H: Integer; AVisible: Boolean);
procedure PositionPanZoomBar(X0, BottomY, RW, H: Integer; AVisible: Boolean);
public
// Обработчики мыши спектра/водопада и кликов зум-кнопок хозяина —
// задать ДО вызова Build (Build подвешивает их на создаваемые контролы).
@@ -150,6 +154,7 @@ type
ZoomInClick: TNotifyEvent;
ZoomDefClick: TNotifyEvent;
ZoomOutClick: TNotifyEvent;
GridClick: TNotifyEvent; // «▦/▤» тумблер грид-раскладки (пан 0)
constructor Create(AOwner: TComponent; AUseOpenGL: Boolean); reintroduce;
destructor Destroy; override;
@@ -164,6 +169,10 @@ type
// над спектром. Show-флаги приходят от контроллера (FShowSpectrum/
// FShowWaterfall).
procedure Layout(ATopOffset: Integer; AShowSpectrum, AShowWaterfall: Boolean);
// Мин. высота региона, при которой Layout не вылезает за него (те же
// константы/DPI, что в Layout; учитывает текущую HeaderVisible).
function MinLayoutHeight(ATopOffset: Integer;
AShowSpectrum, AShowWaterfall: Boolean): Integer;
property View: TSpectrumView read FView;
property PbSpectrum: TControl read FPbSpectrum;
@@ -186,6 +195,9 @@ type
property PanId: Integer read FPanId write FPanId;
property StackTop: Integer read FStackTop write FStackTop;
property StackHeight: Integer read FStackHeight write FStackHeight;
// Грид-раскладка: регион пана может быть колонкой (StackWidth=0 = вся ширина).
property StackLeft: Integer read FStackLeft write FStackLeft;
property StackWidth: Integer read FStackWidth write FStackWidth;
// Шапка пана: текст ставит хозяин (частота), rate — отдельным лейблом
// (SetHeaderRate); у панов N>0 хозяин делает его кликабельным (селектор
// rate, RateClickable + OnRateClick). «×» → OnPanClose.
@@ -212,6 +224,9 @@ type
property ShowZoomRow: Boolean read FShowZoomRow write FShowZoomRow;
// «⊞» в ряду пан/зума (пан 0): показать/спрятать решает хозяин.
property AddPanEnabled: Boolean read FAddPanWanted write FAddPanWanted;
// «▦/▤» тумблер грид-раскладки (пан 0, виден при >=2 доп. панах).
property GridBtnEnabled: Boolean read FGridBtnWanted write FGridBtnWanted;
property BtnGrid: TFlatButton read FBtnGrid;
property OnPanClose: TNotifyEvent read FOnPanClose write FOnPanClose;
property OnAddPan: TNotifyEvent read FOnAddPan write FOnAddPan;
// Пользователь отпустил сплиттер (ratio уже обновлён) — хозяин делает
@@ -288,6 +303,14 @@ begin
// (Owner=Self) освободит inherited.
FreeAndNil(FZoomBar);
FreeAndNil(FView);
// Кнопки ряда зума создаются MakeFlatBtn с Owner=КОНТЕЙНЕР (не Self) —
// inherited их не освободит, и после гибели пана они оставались жить
// на родителе (STOP → «блуждающие» −/⌂/+ поверх окна).
FreeAndNil(FBtnZoomOut);
FreeAndNil(FBtnZoomDef);
FreeAndNil(FBtnZoomIn);
FreeAndNil(FBtnAddPan);
FreeAndNil(FBtnGrid);
inherited Destroy;
end;
@@ -385,6 +408,11 @@ begin
FBtnAddPan.Hint := 'Add panadapter';
FBtnAddPan.ShowHint := True;
FBtnAddPan.Visible := False;
// Тумблер грид-раскладки (пан 0; глифом рулит хозяин: ▦ = в грид, ▤ = в стек).
FBtnGrid := MakeFlatBtn(AParent, '▦', 0, 0, 10, 10, GridClick);
FBtnGrid.Hint := 'Grid layout';
FBtnGrid.ShowHint := True;
FBtnGrid.Visible := False;
// ---- Шапка пана (тонкая полоска над спектром; видна при панах >= 2) ----
FHeaderPanel := TPanel.Create(Self);
@@ -427,6 +455,7 @@ begin
FBtnZoomDef.Parent := NewParent;
FBtnZoomIn.Parent := NewParent;
FBtnAddPan.Parent := NewParent;
FBtnGrid.Parent := NewParent;
FHeaderPanel.Parent := NewParent; // дети шапки едут вместе с ней
end;
@@ -538,31 +567,39 @@ begin
FBtnPopPan.SetBounds(RW - 2 * BtnS - 6, 2, BtnS, BtnS);
end;
procedure TPanafallPanel.PositionPanZoomBar(BottomY, RW, H: Integer;
procedure TPanafallPanel.PositionPanZoomBar(X0, BottomY, RW, H: Integer;
AVisible: Boolean);
var BtnW, Gap, StripW, X, NBtns: Integer; WantAdd: Boolean;
var BtnW, Gap, StripW, X, NBtns: Integer; WantAdd, WantGrid: Boolean;
begin
if (FPbPanZoom = nil) or (FBtnZoomIn = nil) then Exit;
// «⊞» добавления пана живёт в этом же ряду; хозяин включает его флагом
// AddPanEnabled (Visible кнопки управляется здесь вместе с рядом).
// «⊞» добавления пана и «▦/▤» грид-тумблер живут в этом же ряду; хозяин
// включает их флагами AddPanEnabled/GridBtnEnabled (Visible — здесь).
WantAdd := FAddPanWanted and (FBtnAddPan <> nil);
WantGrid := FGridBtnWanted and (FBtnGrid <> nil);
FPbPanZoom.Visible := AVisible;
FBtnZoomIn.Visible := AVisible;
FBtnZoomDef.Visible := AVisible;
FBtnZoomOut.Visible := AVisible;
if FBtnAddPan <> nil then FBtnAddPan.Visible := AVisible and WantAdd;
if FBtnGrid <> nil then FBtnGrid.Visible := AVisible and WantGrid;
if not AVisible then Exit;
Gap := MulDiv(2, Screen.PixelsPerInch, 96);
BtnW := H; // квадратные кнопки в высоту строки
NBtns := 3;
if WantAdd then NBtns := 4;
if WantAdd then Inc(NBtns);
if WantGrid then Inc(NBtns);
StripW := RW - NBtns * BtnW - (NBtns + 1) * Gap;
if StripW < 20 then StripW := 20;
FPbPanZoom.SetBounds(0, BottomY, StripW, H);
X := StripW + Gap;
FPbPanZoom.SetBounds(X0, BottomY, StripW, H);
X := X0 + StripW + Gap;
FBtnZoomOut.SetBounds(X, BottomY, BtnW, H); Inc(X, BtnW + Gap);
FBtnZoomDef.SetBounds(X, BottomY, BtnW, H); Inc(X, BtnW + Gap);
FBtnZoomIn.SetBounds(X, BottomY, BtnW, H);
if WantGrid then
begin
Inc(X, BtnW + Gap);
FBtnGrid.SetBounds(X, BottomY, BtnW, H);
end;
if WantAdd then
begin
Inc(X, BtnW + Gap);
@@ -571,6 +608,31 @@ begin
FPbPanZoom.Invalidate;
end;
function TPanafallPanel.MinLayoutHeight(ATopOffset: Integer;
AShowSpectrum, AShowWaterfall: Boolean): Integer;
const
SPLITTER_H = 5;
MIN_SH = 60; // = Layout
MIN_WH = 40; // = Layout
var
RULER_H, PANZOOM_H, HEADER_H: Integer;
begin
RULER_H := MulDiv(18, Screen.PixelsPerInch, 96);
PANZOOM_H := MulDiv(22, Screen.PixelsPerInch, 96);
HEADER_H := MulDiv(20, Screen.PixelsPerInch, 96);
Result := ATopOffset;
if FHeaderVisible then Inc(Result, HEADER_H);
if (AShowSpectrum or AShowWaterfall) and FShowZoomRow then
Inc(Result, PANZOOM_H);
if AShowSpectrum then Inc(Result, MIN_SH + RULER_H);
if AShowWaterfall then
begin
Inc(Result, MIN_WH);
if AShowSpectrum then Inc(Result, SPLITTER_H)
else Inc(Result, RULER_H); // водопад-only тоже рисует линейку
end;
end;
procedure TPanafallPanel.Layout(ATopOffset: Integer;
AShowSpectrum, AShowWaterfall: Boolean);
const
@@ -578,20 +640,29 @@ const
MIN_SH = 60; // минимальная высота спектра
MIN_WH = 40; // минимальная высота водопада
var
RW, RH, SH, WH, TopOff: Integer;
RW, RH, SH, WH, TopOff, X0: Integer;
AvailH: Integer;
RULER_H: Integer;
PANZOOM_H: Integer;
HEADER_H: Integer;
ShowPZ: Boolean;
EffRatio: Double;
begin
if (FParent = nil) or (FPbSpectrum = nil) then Exit;
FShowSpectrum := AShowSpectrum;
FShowWaterfall := AShowWaterfall;
FSplitAvailH := 0; // валидно только в режиме «спектр+водопад» (ниже)
RULER_H := MulDiv(18, Screen.PixelsPerInch, 96);
// Регион пана — прямоугольник [X0..X0+RW) × [FStackTop..RH): грид-раскладка
// ставит паны колонками; 0/0 по каждой оси = весь родитель.
X0 := FStackLeft;
if FStackWidth > 0 then
RW := FStackWidth
else
begin
X0 := 0;
RW := FParent.ClientWidth;
// Регион пана в стеке: [FStackTop .. RH). 0/0 = весь родитель (один пан).
end;
if FStackHeight > 0 then
RH := FStackTop + FStackHeight
else
@@ -603,14 +674,14 @@ begin
FHeaderPanel.Visible := FHeaderVisible;
if FHeaderVisible then
begin
FHeaderPanel.SetBounds(0, FStackTop, RW, HEADER_H);
FHeaderPanel.SetBounds(X0, FStackTop, RW, HEADER_H);
PositionHeaderChildren(RW, HEADER_H);
end;
end;
// Резервируем низ под панель пана/зума (видна, если виден спектр или водопад).
PANZOOM_H := MulDiv(22, Screen.PixelsPerInch, 96);
ShowPZ := (AShowSpectrum or AShowWaterfall) and FShowZoomRow;
PositionPanZoomBar(RH - PANZOOM_H, RW, PANZOOM_H, ShowPZ);
PositionPanZoomBar(X0, RH - PANZOOM_H, RW, PANZOOM_H, ShowPZ);
if ShowPZ then RH := RH - PANZOOM_H;
TopOff := ATopOffset + FStackTop;
if FHeaderVisible then Inc(TopOff, HEADER_H);
@@ -634,9 +705,9 @@ begin
AvailH := RH - TopOff - RULER_H;
if AvailH < MIN_SH then AvailH := MIN_SH;
SH := AvailH;
FPbSpectrum.SetBounds(0, TopOff, RW, SH);
FPbSpectrum.SetBounds(X0, TopOff, RW, SH);
FPbSpectrum.Visible := True;
FPbRuler.SetBounds(0, TopOff + SH, RW, RULER_H);
FPbRuler.SetBounds(X0, TopOff + SH, RW, RULER_H);
FPbRuler.Visible := True;
FSplitter.Visible := False;
FPbWaterfall.Visible := False;
@@ -663,9 +734,9 @@ begin
WH := AvailH;
FPbSpectrum.Visible := False;
FSplitter.Visible := False;
FPbWaterfall.SetBounds(0, TopOff, RW, WH);
FPbWaterfall.SetBounds(X0, TopOff, RW, WH);
FPbWaterfall.Visible := True;
FPbRuler.SetBounds(0, TopOff + WH, RW, RULER_H);
FPbRuler.SetBounds(X0, TopOff + WH, RW, RULER_H);
FPbRuler.Visible := True;
FSpectrumWidth := RW;
FSpectrumHeight := 0;
@@ -694,24 +765,29 @@ begin
FSplitTopOff := TopOff;
FSplitAvailH := AvailH;
// Ограничиваем соотношение, чтобы каждая зона имела минимальный размер
if FSplitterRatio < MIN_SH / AvailH then
FSplitterRatio := MIN_SH / AvailH;
if FSplitterRatio > 1.0 - MIN_WH / AvailH then
FSplitterRatio := 1.0 - MIN_WH / AvailH;
// Ограничиваем соотношение, чтобы каждая зона имела минимальный размер.
// Кламп — в ЛОКАЛЬНУЮ переменную: FSplitterRatio правит только юзер
// (drag сплиттера). Иначе в маленьком регионе (стек панов) кламп
// перезаписывал ratio (60/AvailH → ~0.7) и после закрытия панов
// пропорция «50/50» не восстанавливалась.
EffRatio := FSplitterRatio;
if EffRatio < MIN_SH / AvailH then
EffRatio := MIN_SH / AvailH;
if EffRatio > 1.0 - MIN_WH / AvailH then
EffRatio := 1.0 - MIN_WH / AvailH;
SH := Round(AvailH * FSplitterRatio);
SH := Round(AvailH * EffRatio);
WH := AvailH - SH;
if WH < MIN_WH then WH := MIN_WH;
// Спектр
FPbSpectrum.SetBounds(0, TopOff, RW, SH);
FPbSpectrum.SetBounds(X0, TopOff, RW, SH);
// Линейка частот — всегда прижата к низу спектра
FPbRuler.SetBounds(0, TopOff + SH, RW, RULER_H);
FPbRuler.SetBounds(X0, TopOff + SH, RW, RULER_H);
// Сплиттер — между линейкой и водопадом
FSplitter.SetBounds(0, TopOff + SH + RULER_H, RW, SPLITTER_H);
FSplitter.SetBounds(X0, TopOff + SH + RULER_H, RW, SPLITTER_H);
// Водопад — под сплиттером
FPbWaterfall.SetBounds(0, TopOff + SH + RULER_H + SPLITTER_H, RW, WH);
FPbWaterfall.SetBounds(X0, TopOff + SH + RULER_H + SPLITTER_H, RW, WH);
// Обновляем переменные размеров
FSpectrumWidth := RW;
+71
View File
@@ -573,6 +573,77 @@ RATEPROBE удалены.** Наблюдение юзера: смена rate Г
BtnZoomIn/Def/Out всех панов). Урок: PanFromSender работает только для
контролов с Owner=панель (TLabel шапки — да, MakeFlatBtn-кнопки — нет).
**СТАТУС «грид-раскладка панов» (2026-07-12): РЕАЛИЗОВАНА, собирается, НА
ЭКРАНЕ НЕ ПРОВЕРЕНА, НЕ закоммичена.**
- Панель: регион пана обобщён до прямоугольника — `StackLeft/StackWidth`
(0/0 = вся ширина), Layout смещает ВСЁ по X0 (шапка/спектр/линейка/
сплиттер/водопад/ряд зума); `PositionPanZoomBar` получил X0 и 5-ю кнопку;
новая кнопка `FBtnGrid` («▦»/«▤», глифом рулит хозяин) + `GridClick`
(задать до Build) + `GridBtnEnabled`; ReparentTo переносит кнопку.
- MainForm: `FPanGridMode` (сессия), `FPanGridRowShare` (доля верхнего ряда,
0.15..0.85, дефолт 0.5), `FPanColShare[1..]` (доли колонок, init 1.0,
нормируются суммой), `FPanVSplitters[1..]` (вертикальный сплиттер СЛЕВА от
колонки пана i, crHSplit, создаётся в CreatePanadapter, free в
ClosePanadapter). `PanGridActive` = режим включён И >=2 доп. панов в стеке
(иначе автоткат в стек, режим помнится). LayoutPanStack: ветка грида — пан 0
верхний ряд во всю ширину (wideband внутри как в стеке), паны N колонками;
горизонтальный сплиттер ряда = сплиттер ПЕРВОЙ колонки во всю ширину
(PanSplitMouseDown/Up ветвятся по PanGridActive: в гриде правят
FPanGridRowShare напрямую), сплиттеры остальных колонок скрыты; вертикальные
PanVSplitMouse* — зеркало горизонтальных (призрак, min 15% ширины,
DShare=DeltaX/TotalW*ShareSum). Кнопка «▦/▤» видна при FPanGridMode или
>=3 застекованных панах (выключить можно всегда). PopOutPan обнуляет
StackLeft/Width и прячет V-сплиттер. Темизация: V-сплиттеры T.Border,
BtnGrid через StyleButton.
- Флаги/мышь/зум панов грид не трогает: всё в координатах канвы спектра,
канва целиком переезжает в регион-колонку.
- Проверить на экране: тумблер при 3 панах, оба типа сплиттеров, wideband в
гриде, pop-out/возврат из грида, закрытие пана в гриде (автоткат),
узкие колонки (зум/клик-тюн/drag), отсутствие регрессий при 1-2 панах.
**ФИКСЫ ПО ПЕРВОМУ ЭКРАННОМУ ТЕСТУ ГРИДА (2026-07-12, собирается, не
проверено):** три независимых бага из фидбека юзера.
1. **Наезд панов при выходе из грида (и вообще при 4 панах в стеке).**
LayoutPanStack делил высоту по долям (после add/close все доли равные,
при 4 панах = 25%), а Layout панели клампит спектр/водопад к минимумам
(MIN_SH=60/MIN_WH=40) — виджеты вылезали за регион, следующий пан
рисовался поверх (пан 0 терял водопад и ряд кнопок). Фикс:
`TPanafallPanel.MinLayoutHeight` (те же константы/DPI, что Layout) +
`PanMinHeight(PanId)` в MainForm (пан 0 += wideband `FPanWideH`);
раздача в стеке клампит каждый пан минимумом и режет раздутые так,
чтобы оставшимся хватило на их минимумы; в гриде Row0H клампится
минимумом пана 0 и минимумом нижнего ряда (макс. по колонкам);
драг межпанного сплиттера ограждён `Max(15%, минимум содержимого)`
в обеих ветках (стек/грид).
2. **«Блуждающие» кнопки после STOP (− − ⌂ +, кнопки на линейке, ⊞ накрыт).**
MakeFlatBtn создаёт кнопки с Owner=КОНТЕЙНЕР (не панель) → при гибели
пана (STOP → DestroyAllExtraPans → P.Free) кнопки /⌂/+/⊞/▦ НЕ
освобождались и оставались жить на PanelRight в старых координатах,
а PanOfZoomButton, не найдя пан, роутил их клики на главный зум.
Фикс: TPanafallPanel.Destroy явно освобождает FBtnZoomOut/Def/In,
FBtnAddPan, FBtnGrid (FBtnPopPan/ClosePan — Owner=FHeaderPanel,
умирают с ним). Перезапуск программы «лечил», потому что утечка —
в пределах процесса.
3. **Жор CPU (+~40% за пан).** SyncPanViews (тик, частота кадров) звал
SyncPanZoomUI с БЕЗУСЛОВНЫМ `PbPanZoom.Invalidate` на каждый пан каждый
кадр (регрессия cb6bdfa) = полный софт-рендер зум-бара + Qt-рекомпозит
@~50fps × N панов. Фикс: инвалидация только внутри
ZoomBar.SetFreq/SetState (они no-op при тех же значениях), событийный
Invalidate перенесён в RefreshPanZoom. Дополнительно жор усиливался
багом 1 (наезжающие GL-виджеты сверх окна). Замер юзера — после фикса.
Ответ на вопрос «почему только 3 доп. пана»: MAX_PANS=4 (WDSPEngine) —
пан 0 + 3 доп.; 6 было у СЛАЙСОВ (MAX_SLICES=6 доп.). Поднять можно (P2
позволяет много DDC), но сначала подтвердить CPU после фиксов.
Фиксы 1-3 ПОДТВЕРЖДЕНЫ юзером на экране 2026-07-12 («работает», CPU со
всеми панами 17-20%). Добит хвост:
4. **Пропорция спектр/водопад пана 0 не восстанавливалась после закрытия
панов (50/50 → 70/30).** Layout клампил ratio минимумами С ЗАПИСЬЮ в
FSplitterRatio: в маленьком регионе (стек панов) MIN_SH/AvailH ≈ 0.7
запоминался навсегда. Фикс: кламп в локальную EffRatio, FSplitterRatio
меняет только drag сплиттера юзером.
### 3.6 — Pluto+ (2r2t): второй пан на втором антенном входе
Опционально, после 3.0-3.3. Обычного Pluto НЕ касается (там один RX разведён).