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
+326 -38
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;
TotalH := PanelRight.ClientHeight;
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
FPanWideH := AWideH; // для PanMinHeight (в т.ч. из сплиттер-драга)
TotalH := PanelRight.ClientHeight;
TotalW := PanelRight.ClientWidth;
Grid := PanGridActive; // пан 0 + >=2 доп. панов колонками
// Тумблер «▦/▤» в ряду зума пана 0: есть смысл при >=2 доп. панах в стеке.
if FPans[0] <> nil then
begin
i := ActiveIds[k];
if k > 0 then
// Виден при >=2 доп. панах; при включённом гриде — всегда (выключить можно
// даже когда паны закрылись и раскладка сама откатилась в стек).
FPans[0].GridBtnEnabled := FPanGridMode or (N >= 3);
if FPans[0].BtnGrid <> nil then
begin
FPanSplitters[i].SetBounds(0, Y, PanelRight.ClientWidth, PAN_SPLIT_H);
Inc(Y, PAN_SPLIT_H);
if FPanGridMode then FPans[0].BtnGrid.Caption := '▤'
else FPans[0].BtnGrid.Caption := '▦';
end;
if k = N - 1 then
Hi := TotalH - Y // последний пан добирает остаток (округления долей)
else
Hi := Round(UsableH * FPanShare[i] / ShareSum);
FPans[i].StackTop := Y;
FPans[i].StackHeight := Hi;
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;
// Видимость шапки — по ОБЩЕМУ числу панов (включая плавающие), как
// Pan0HeaderH/UpdatePanHeaders: иначе при pop-out пана N шапка пана 0
// прячется, а wideband остаётся сдвинутым на Pan0HeaderH — спектр
// накрывает линейку частот wideband.
FPans[i].HeaderVisible := PanCount > 1;
if i = 0 then
FPans[i].Layout(AWideH, FController.FShowSpectrum, FController.FShowWaterfall)
else
FPans[i].Layout(0, FController.FShowSpectrum, FController.FShowWaterfall);
Inc(Y, Hi);
// накрывает линейку частот 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;
// Пан над сплиттером — ближайший активный с меньшим 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;
FPanSplitMaxTop := FPans[BelowId].StackTop + FPans[BelowId].StackHeight
- MinPx - PAN_SPLIT_H;
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;
// Границы призрака: каждому пану — минимум 15% стека ИЛИ минимум его
// содержимого (что больше; = кламп раздачи в LayoutPanStack).
FPanSplitMinTop := FPans[AboveId].StackTop
+ Max(MinPx, PanMinHeight(AboveId));
FPanSplitMaxTop := FPans[BelowId].StackTop + FPans[BelowId].StackHeight
- 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