mirror of
https://github.com/2dust/v2rayN.git
synced 2026-01-18 03:49:34 +00:00
fix(desktop): make DataGrid columns always fill viewport with proportional scaling (no overflow on narrow widths)
- Add proportional column scaling in ProfilesView, ProfilesSelectWindow, ClashConnectionsView\n- Listen to Bounds changes; auto expand/shrink to fill\n- Use measured widths for desired sum; cap last column by remaining width to avoid overflow\n- Keep min visual width logic where possible without forcing overflow\n\nUser impact: lists no longer collapse or leave right-side empty; columns always fill the available width and avoid horizontal scrollbars on narrow sizes.
This commit is contained in:
parent
e3473ffbca
commit
6d738f2baa
3 changed files with 293 additions and 2 deletions
|
|
@ -1,13 +1,18 @@
|
|||
using System.Reactive.Disposables;
|
||||
using System.Reactive.Linq;
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Interactivity;
|
||||
using Avalonia.ReactiveUI;
|
||||
using Avalonia.Threading;
|
||||
using ReactiveUI;
|
||||
|
||||
namespace v2rayN.Desktop.Views;
|
||||
|
||||
public partial class ClashConnectionsView : ReactiveUserControl<ClashConnectionsViewModel>
|
||||
{
|
||||
private const int MinColumnWidthPx = 30;
|
||||
|
||||
public ClashConnectionsView()
|
||||
{
|
||||
InitializeComponent();
|
||||
|
|
@ -25,6 +30,14 @@ public partial class ClashConnectionsView : ReactiveUserControl<ClashConnections
|
|||
this.Bind(ViewModel, vm => vm.HostFilter, v => v.txtHostFilter.Text).DisposeWith(disposables);
|
||||
this.BindCommand(ViewModel, vm => vm.ConnectionCloseAllCmd, v => v.btnConnectionCloseAll).DisposeWith(disposables);
|
||||
this.Bind(ViewModel, vm => vm.AutoRefresh, v => v.togAutoRefresh.IsChecked).DisposeWith(disposables);
|
||||
|
||||
// 监听可视区域尺寸变化,等比缩放列宽,保证铺满
|
||||
lstConnections
|
||||
.GetObservable(Visual.BoundsProperty)
|
||||
.Throttle(TimeSpan.FromMilliseconds(80))
|
||||
.ObserveOn(RxApp.MainThreadScheduler)
|
||||
.Subscribe(_ => ScaleColumnsToFit())
|
||||
.DisposeWith(disposables);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -46,6 +59,8 @@ public partial class ClashConnectionsView : ReactiveUserControl<ClashConnections
|
|||
{
|
||||
it.Width = new DataGridLength(1, DataGridLengthUnitType.Auto);
|
||||
}
|
||||
// Auto 量测后按可用宽度等比缩放,保证铺满
|
||||
Dispatcher.UIThread.Post(ScaleColumnsToFit, DispatcherPriority.Background);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
|
@ -57,4 +72,74 @@ public partial class ClashConnectionsView : ReactiveUserControl<ClashConnections
|
|||
{
|
||||
ViewModel?.ClashConnectionClose(false);
|
||||
}
|
||||
|
||||
private void ScaleColumnsToFit()
|
||||
{
|
||||
try
|
||||
{
|
||||
var visibleColumns = lstConnections.Columns.Where(c => c.IsVisible != false).ToList();
|
||||
if (visibleColumns.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
double viewportWidth = lstConnections.Bounds.Width;
|
||||
if (viewportWidth <= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
const double scrollbarReserve = 18;
|
||||
double available = Math.Max(0, viewportWidth - scrollbarReserve);
|
||||
|
||||
double desired = 0;
|
||||
foreach (var col in visibleColumns)
|
||||
{
|
||||
// 使用实际测量宽度作为期望,避免放大最小值导致不必要的扩张
|
||||
desired += Math.Max(1, col.ActualWidth);
|
||||
}
|
||||
|
||||
if (desired <= 0 || available <= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
double ratio = available / desired; // 可放大可缩小
|
||||
|
||||
double remaining = available;
|
||||
int remainingCols = visibleColumns.Count;
|
||||
for (int i = 0; i < visibleColumns.Count; i++)
|
||||
{
|
||||
var col = visibleColumns[i];
|
||||
double proposed = Math.Floor(Math.Max(1, col.ActualWidth) * ratio);
|
||||
|
||||
int colsLeft = remainingCols - 1;
|
||||
double maxThis = colsLeft > 0 ? Math.Max(0, remaining - colsLeft * MinColumnWidthPx) : remaining;
|
||||
double target = Math.Min(proposed, maxThis);
|
||||
|
||||
if (i == visibleColumns.Count - 1)
|
||||
{
|
||||
// 最后一列严格使用剩余空间,避免强制最小值导致溢出
|
||||
target = Math.Max(0, remaining);
|
||||
}
|
||||
|
||||
if (target < 0)
|
||||
{
|
||||
target = 0;
|
||||
}
|
||||
|
||||
col.Width = new DataGridLength(target, DataGridLengthUnitType.Pixel);
|
||||
remaining -= target;
|
||||
remainingCols--;
|
||||
if (remaining <= 0)
|
||||
{
|
||||
remaining = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logging.SaveLog("ClashConnectionsView", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,11 @@
|
|||
using System.Reactive.Disposables;
|
||||
using System.Reactive.Linq;
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Input;
|
||||
using Avalonia.Interactivity;
|
||||
using Avalonia.VisualTree;
|
||||
using Avalonia.Threading;
|
||||
using ReactiveUI;
|
||||
using v2rayN.Desktop.Base;
|
||||
|
||||
|
|
@ -12,6 +14,7 @@ namespace v2rayN.Desktop.Views;
|
|||
public partial class ProfilesSelectWindow : WindowBase<ProfilesSelectViewModel>
|
||||
{
|
||||
private static Config _config;
|
||||
private const int MinColumnWidthPx = 30;
|
||||
|
||||
public Task<ProfileItem?> ProfileItem => GetProfileItem();
|
||||
public Task<List<ProfileItem>?> ProfileItems => GetProfileItems();
|
||||
|
|
@ -41,6 +44,14 @@ public partial class ProfilesSelectWindow : WindowBase<ProfilesSelectViewModel>
|
|||
|
||||
this.Bind(ViewModel, vm => vm.SelectedSub, v => v.lstGroup.SelectedItem).DisposeWith(disposables);
|
||||
this.Bind(ViewModel, vm => vm.ServerFilter, v => v.txtServerFilter.Text).DisposeWith(disposables);
|
||||
|
||||
// 监听可视区域变化,始终铺满
|
||||
lstProfiles
|
||||
.GetObservable(Visual.BoundsProperty)
|
||||
.Throttle(TimeSpan.FromMilliseconds(80))
|
||||
.ObserveOn(RxApp.MainThreadScheduler)
|
||||
.Subscribe(_ => ScaleColumnsToFit())
|
||||
.DisposeWith(disposables);
|
||||
});
|
||||
|
||||
btnCancel.Click += (s, e) => Close(false);
|
||||
|
|
@ -160,6 +171,8 @@ public partial class ProfilesSelectWindow : WindowBase<ProfilesSelectViewModel>
|
|||
{
|
||||
col.Width = new DataGridLength(1, DataGridLengthUnitType.Auto);
|
||||
}
|
||||
// Auto 量测后按可视宽度等比缩放,保证铺满
|
||||
Dispatcher.UIThread.Post(ScaleColumnsToFit, DispatcherPriority.Background);
|
||||
}
|
||||
catch
|
||||
{
|
||||
|
|
@ -191,4 +204,73 @@ public partial class ProfilesSelectWindow : WindowBase<ProfilesSelectViewModel>
|
|||
// Trigger selection finalize when Confirm is clicked
|
||||
ViewModel?.SelectFinish();
|
||||
}
|
||||
|
||||
private void ScaleColumnsToFit()
|
||||
{
|
||||
try
|
||||
{
|
||||
var visibleColumns = lstProfiles.Columns.Where(c => c.IsVisible != false).ToList();
|
||||
if (visibleColumns.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
double viewportWidth = lstProfiles.Bounds.Width;
|
||||
if (viewportWidth <= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
const double scrollbarReserve = 18;
|
||||
double available = Math.Max(0, viewportWidth - scrollbarReserve);
|
||||
|
||||
double desired = 0;
|
||||
foreach (var col in visibleColumns)
|
||||
{
|
||||
desired += Math.Max(1, col.ActualWidth);
|
||||
}
|
||||
|
||||
if (desired <= 0 || available <= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
double ratio = available / desired; // 可放大也可缩小
|
||||
|
||||
double remaining = available;
|
||||
int remainingCols = visibleColumns.Count;
|
||||
for (int i = 0; i < visibleColumns.Count; i++)
|
||||
{
|
||||
var col = visibleColumns[i];
|
||||
double proposed = Math.Floor(Math.Max(1, col.ActualWidth) * ratio);
|
||||
|
||||
// 为后续列预留最小宽度(若总宽不足,也可能为 0)
|
||||
int colsLeft = remainingCols - 1;
|
||||
double maxThis = colsLeft > 0 ? Math.Max(0, remaining - colsLeft * MinColumnWidthPx) : remaining;
|
||||
double target = Math.Min(proposed, maxThis);
|
||||
|
||||
if (i == visibleColumns.Count - 1)
|
||||
{
|
||||
// 最后一列严格使用剩余空间,避免溢出
|
||||
target = Math.Max(0, remaining);
|
||||
}
|
||||
|
||||
if (target < 0)
|
||||
{
|
||||
target = 0;
|
||||
}
|
||||
|
||||
col.Width = new DataGridLength(target, DataGridLengthUnitType.Pixel);
|
||||
remaining -= target;
|
||||
remainingCols--;
|
||||
if (remaining <= 0)
|
||||
{
|
||||
remaining = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
using System.Reactive.Disposables;
|
||||
using System.Reactive.Linq;
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Input;
|
||||
using Avalonia.Interactivity;
|
||||
|
|
@ -16,6 +17,8 @@ public partial class ProfilesView : ReactiveUserControl<ProfilesViewModel>
|
|||
{
|
||||
private static Config _config;
|
||||
private Window? _window;
|
||||
// 防止列宽被保存/恢复为 0 导致界面错位
|
||||
private const int MinColumnWidthPx = 30;
|
||||
|
||||
public ProfilesView()
|
||||
{
|
||||
|
|
@ -107,6 +110,14 @@ public partial class ProfilesView : ReactiveUserControl<ProfilesViewModel>
|
|||
.ObserveOn(RxApp.MainThreadScheduler)
|
||||
.Subscribe(_ => AutofitColumnWidth())
|
||||
.DisposeWith(disposables);
|
||||
|
||||
// 监听可视区域尺寸变化,动态按可用宽度等比缩放列宽,确保始终从左到右铺满
|
||||
lstProfiles
|
||||
.GetObservable(Visual.BoundsProperty)
|
||||
.Throttle(TimeSpan.FromMilliseconds(80))
|
||||
.ObserveOn(RxApp.MainThreadScheduler)
|
||||
.Subscribe(_ => ScaleColumnsToFit())
|
||||
.DisposeWith(disposables);
|
||||
});
|
||||
|
||||
RestoreUI();
|
||||
|
|
@ -359,6 +370,86 @@ public partial class ProfilesView : ReactiveUserControl<ProfilesViewModel>
|
|||
{
|
||||
it.Width = new DataGridLength(1, DataGridLengthUnitType.Auto);
|
||||
}
|
||||
|
||||
// 列设置为 Auto 后再按可视宽度进行等比缩放,避免“全部很小”但不为 0 的情况
|
||||
Dispatcher.UIThread.Post(ScaleColumnsToFit, DispatcherPriority.Background);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logging.SaveLog("ProfilesView", ex);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将当前列的 Auto 实际宽度按可用宽度等比缩放,保证整体正好铺满且不出现极小列。
|
||||
/// </summary>
|
||||
private void ScaleColumnsToFit()
|
||||
{
|
||||
try
|
||||
{
|
||||
var visibleColumns = lstProfiles.Columns.Where(c => c.IsVisible != false).ToList();
|
||||
if (visibleColumns.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
double viewportWidth = lstProfiles.Bounds.Width;
|
||||
if (viewportWidth <= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// 预留一点滚动条空间,避免刚好出现水平滚动条
|
||||
const double scrollbarReserve = 18;
|
||||
double available = Math.Max(0, viewportWidth - scrollbarReserve);
|
||||
|
||||
// 计算 Auto 测量下的期望总宽
|
||||
double desired = 0;
|
||||
foreach (var col in visibleColumns)
|
||||
{
|
||||
desired += Math.Max(1, col.ActualWidth);
|
||||
}
|
||||
|
||||
if (desired <= 0 || available <= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// 等比缩放(可放大也可缩小)
|
||||
double ratio = available / desired;
|
||||
|
||||
double remaining = available;
|
||||
int remainingCols = visibleColumns.Count;
|
||||
for (int i = 0; i < visibleColumns.Count; i++)
|
||||
{
|
||||
var col = visibleColumns[i];
|
||||
// 基于 Auto 宽度的目标值
|
||||
double proposed = Math.Floor(Math.Max(1, col.ActualWidth) * ratio);
|
||||
|
||||
// 为后续列预留最小宽度(若总宽不足,也可能为 0)
|
||||
int colsLeft = remainingCols - 1;
|
||||
double maxThis = colsLeft > 0 ? Math.Max(0, remaining - colsLeft * MinColumnWidthPx) : remaining;
|
||||
double target = Math.Min(proposed, maxThis);
|
||||
|
||||
// 最后一列严格吃掉余量,避免溢出
|
||||
if (i == visibleColumns.Count - 1)
|
||||
{
|
||||
target = Math.Max(0, remaining);
|
||||
}
|
||||
|
||||
if (target < 0)
|
||||
{
|
||||
target = 0;
|
||||
}
|
||||
|
||||
col.Width = new DataGridLength(target, DataGridLengthUnitType.Pixel);
|
||||
remaining -= target;
|
||||
remainingCols--;
|
||||
if (remaining <= 0)
|
||||
{
|
||||
remaining = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
|
@ -382,6 +473,9 @@ public partial class ProfilesView : ReactiveUserControl<ProfilesViewModel>
|
|||
{
|
||||
var lvColumnItem = _config.UiItem.MainColumnItem.OrderBy(t => t.Index).ToList();
|
||||
var displayIndex = 0;
|
||||
int visibleCount = 0;
|
||||
double widthSum = 0;
|
||||
|
||||
foreach (var item in lvColumnItem)
|
||||
{
|
||||
foreach (var item2 in lstProfiles.Columns)
|
||||
|
|
@ -398,8 +492,15 @@ public partial class ProfilesView : ReactiveUserControl<ProfilesViewModel>
|
|||
}
|
||||
else
|
||||
{
|
||||
item2.Width = new DataGridLength(item.Width, DataGridLengthUnitType.Pixel);
|
||||
// 对恢复的宽度做下限保护,避免 0/极小宽度挤瘪
|
||||
var w = item.Width < MinColumnWidthPx ? MinColumnWidthPx : item.Width;
|
||||
item2.Width = new DataGridLength(w, DataGridLengthUnitType.Pixel);
|
||||
item2.DisplayIndex = displayIndex++;
|
||||
if (item2.IsVisible != false)
|
||||
{
|
||||
visibleCount++;
|
||||
widthSum += w;
|
||||
}
|
||||
}
|
||||
if (item.Name.ToLower().StartsWith("to"))
|
||||
{
|
||||
|
|
@ -408,6 +509,16 @@ public partial class ProfilesView : ReactiveUserControl<ProfilesViewModel>
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 如果恢复后几乎没有可见宽度,直接切换为 Auto 宽度并按可用宽度缩放
|
||||
if (visibleCount == 0 || widthSum < MinColumnWidthPx)
|
||||
{
|
||||
foreach (var col in lstProfiles.Columns)
|
||||
{
|
||||
col.Width = new DataGridLength(1, DataGridLengthUnitType.Auto);
|
||||
}
|
||||
Dispatcher.UIThread.Post(ScaleColumnsToFit, DispatcherPriority.Background);
|
||||
}
|
||||
}
|
||||
|
||||
private void StorageUI()
|
||||
|
|
@ -419,10 +530,23 @@ public partial class ProfilesView : ReactiveUserControl<ProfilesViewModel>
|
|||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// 读取实际宽度并加下限保护,避免保存 0/极小值
|
||||
int widthToSave = -1;
|
||||
if (item2.IsVisible == true)
|
||||
{
|
||||
var actual = (int)(item2.ActualWidth + 0.5);
|
||||
if (actual < MinColumnWidthPx)
|
||||
{
|
||||
actual = MinColumnWidthPx;
|
||||
}
|
||||
widthToSave = actual;
|
||||
}
|
||||
|
||||
lvColumnItem.Add(new()
|
||||
{
|
||||
Name = (string)item2.Tag,
|
||||
Width = (int)(item2.IsVisible == true ? item2.ActualWidth : -1),
|
||||
Width = widthToSave,
|
||||
Index = item2.DisplayIndex
|
||||
});
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue