From 12d66b21c4c5afb36802d706b3e9930ddd5999ef Mon Sep 17 00:00:00 2001 From: Davide_24 <009davide@gmail.com> Date: Mon, 25 Aug 2025 23:12:53 +0200 Subject: [PATCH] Fix NumberPad ColumnWidths not matching the ColumnWidths inside of its parent The NumberPad now looks at the parent grid, grabs the width of the columns, and applies it to its own columns. This eliminates the misaligned buttons in NumberPad on window resize. --- src/Calculator/Views/NumberPad.xaml.cs | 240 ++++++++++++++++++++++--- 1 file changed, 218 insertions(+), 22 deletions(-) diff --git a/src/Calculator/Views/NumberPad.xaml.cs b/src/Calculator/Views/NumberPad.xaml.cs index 0f766bdc2..f98f5a207 100644 --- a/src/Calculator/Views/NumberPad.xaml.cs +++ b/src/Calculator/Views/NumberPad.xaml.cs @@ -1,21 +1,31 @@ using CalculatorApp.ViewModel.Common; - +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Windows.UI.Core; using Windows.UI.Xaml; using Windows.UI.Xaml.Controls; - -// The User Control item template is documented at https://go.microsoft.com/fwlink/?LinkId=234236 +using Windows.UI.Xaml.Media; namespace CalculatorApp { public sealed partial class NumberPad : UserControl { + private bool m_isErrorVisualState; + + // Ancestor grid column mapping state + private Grid _ancestorGrid; + private int _ancestorStartColumn = -1; + private int _ancestorSpan = 0; + private bool _applyingColumnMap; + public NumberPad() { m_isErrorVisualState = false; InitializeComponent(); var localizationSettings = LocalizationSettings.GetInstance(); - DecimalSeparatorButton.Content = localizationSettings.GetDecimalSeparator(); Num0Button.Content = localizationSettings.GetDigitSymbolFromEnUsDigit('0'); Num1Button.Content = localizationSettings.GetDigitSymbolFromEnUsDigit('1'); @@ -27,31 +37,220 @@ public NumberPad() Num7Button.Content = localizationSettings.GetDigitSymbolFromEnUsDigit('7'); Num8Button.Content = localizationSettings.GetDigitSymbolFromEnUsDigit('8'); Num9Button.Content = localizationSettings.GetDigitSymbolFromEnUsDigit('9'); + + Loaded += NumberPad_Loaded; } - public Windows.UI.Xaml.Style ButtonStyle + private async void NumberPad_Loaded(object sender, RoutedEventArgs e) { - get => (Windows.UI.Xaml.Style)GetValue(ButtonStyleProperty); - set => SetValue(ButtonStyleProperty, value); + InitializeAncestorColumnBinding(); + // Wait a layout pass so ancestor ActualWidth values are valid + await Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () => { }); + ApplyAncestorColumnWidths(); + } + + private void InitializeAncestorColumnBinding() + { + var (grid, elementWithIndex, colIdx) = GetAncestorGridAndColumnIndex(); + if (grid == null) + return; + + _ancestorGrid = grid; + int span = 1; + if (elementWithIndex != null) + span = Math.Max(1, Grid.GetColumnSpan(elementWithIndex)); + + _ancestorStartColumn = Math.Max(0, colIdx); + _ancestorSpan = span; + + _ancestorGrid.SizeChanged -= AncestorGrid_SizeChanged; + _ancestorGrid.SizeChanged += AncestorGrid_SizeChanged; + } + + private async void AncestorGrid_SizeChanged(object sender, SizeChangedEventArgs e) + { + // Defer to let inner layout settle + await Dispatcher.RunAsync(CoreDispatcherPriority.Low, () => ApplyAncestorColumnWidths()); + } + + private void ApplyAncestorColumnWidths() + { + if (_ancestorGrid == null || _applyingColumnMap) + return; + + int ancestorColumnCount = _ancestorGrid.ColumnDefinitions.Count; + if (ancestorColumnCount == 0 || _ancestorStartColumn >= ancestorColumnCount) + return; + + int effectiveSpan = Math.Min(_ancestorSpan, ancestorColumnCount - _ancestorStartColumn); + if (effectiveSpan <= 0) + return; + + var slice = new List(effectiveSpan); + for (int i = 0; i < effectiveSpan; i++) + { + slice.Add(_ancestorGrid.ColumnDefinitions[_ancestorStartColumn + i].ActualWidth); + } + + // If any width is zero (not measured yet) skip this attempt + if (slice.Any(w => w <= 0.0)) + return; + + var targetColumns = Root.ColumnDefinitions; + if (targetColumns.Count == 0) + return; + + _applyingColumnMap = true; + try + { + if (targetColumns.Count == effectiveSpan) + { + for (int i = 0; i < effectiveSpan; i++) + { + double w = slice[i]; + var newWidth = new GridLength(w, GridUnitType.Pixel); + if (!GridLengthEquals(targetColumns[i].Width, newWidth)) + targetColumns[i].Width = newWidth; + } + } + else + { + double total = slice.Sum(); + if (total <= 0) return; + for (int i = 0; i < targetColumns.Count; i++) + { + double ratio = slice[i % effectiveSpan] / total; + ratio = Math.Max(ratio, 0.0001); + // Scale ratios so sum ~ targetColumns.Count + double starValue = ratio * targetColumns.Count; + // Bound star value + starValue = Math.Max(0.0001, starValue); + var newWidth = new GridLength(starValue, GridUnitType.Star); + if (!GridLengthEquals(targetColumns[i].Width, newWidth)) + targetColumns[i].Width = newWidth; + } + } + } + finally + { + _applyingColumnMap = false; + } + } + + private static bool GridLengthEquals(GridLength a, GridLength b) + { + if (a.GridUnitType != b.GridUnitType) return false; + if (a.GridUnitType == GridUnitType.Pixel) + return Math.Abs(a.Value - b.Value) < 0.5; + return Math.Abs(a.Value - b.Value) < 0.0001; + } + + #region Ancestor Grid Introspection (existing) + + public async Task> GetAncestorGridColumnWidthsAsync() + { + await Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () => { }); + var grid = FindAncestorGridWithColumns(this); + if (grid == null || grid.ColumnDefinitions.Count == 0) + return Array.Empty(); + + return grid.ColumnDefinitions.Select(cd => cd.ActualWidth).ToList().AsReadOnly(); } - // Using a DependencyProperty as the backing store for ButtonStyle. This enables animation, styling, binding, etc... + public (Grid grid, FrameworkElement elementWithIndex, int columnIndex) GetAncestorGridAndColumnIndex() + { + FrameworkElement elementWithIndex = null; + int columnIndex = -1; + + Grid targetGrid = FindAncestorGridWithColumns(this); + if (targetGrid == null) + return (null, null, -1); + + DependencyObject current = this; + while (current != null && current != targetGrid) + { + if (current is FrameworkElement fe) + { + int idx = Grid.GetColumn(fe); + elementWithIndex = fe; + columnIndex = idx; + } + current = VisualTreeHelper.GetParent(current); + } + + return (targetGrid, elementWithIndex, columnIndex); + } + + public IDictionary GetAncestorButtonColumnWidths() + { + var (grid, _, _) = GetAncestorGridAndColumnIndex(); + if (grid == null) + return new Dictionary(); + + if (grid.ColumnDefinitions.Count > 0) + { + return grid.ColumnDefinitions + .Select((cd, i) => new { i, cd.ActualWidth }) + .ToDictionary(p => p.i, p => p.ActualWidth); + } + + var result = new Dictionary(); + foreach (var btn in grid.Children.OfType()) + { + int col = Grid.GetColumn(btn); + double w = btn.ActualWidth; + if (!result.ContainsKey(col) || w > result[col]) + result[col] = w; + } + return result; + } + + private static Grid FindAncestorGridWithColumns(DependencyObject start) + { + var current = start; + while (current != null) + { + current = VisualTreeHelper.GetParent(current); + if (current is Grid g && g.ColumnDefinitions.Count > 0) + return g; + } + return null; + } + + private static bool IsDescendant(FrameworkElement root, FrameworkElement target) + { + if (root == target) return true; + int count = VisualTreeHelper.GetChildrenCount(root); + for (int i = 0; i < count; i++) + { + if (VisualTreeHelper.GetChild(root, i) is FrameworkElement fe && IsDescendant(fe, target)) + return true; + } + return false; + } + + #endregion + + public Style ButtonStyle + { + get => (Style)GetValue(ButtonStyleProperty); + set => SetValue(ButtonStyleProperty, value); + } public static readonly DependencyProperty ButtonStyleProperty = - DependencyProperty.Register(nameof(ButtonStyle), typeof(Windows.UI.Xaml.Style), typeof(NumberPad), new PropertyMetadata(default(Windows.UI.Xaml.Style))); + DependencyProperty.Register(nameof(ButtonStyle), typeof(Style), typeof(NumberPad), new PropertyMetadata(default(Style))); - public CalculatorApp.ViewModel.Common.NumberBase CurrentRadixType + public NumberBase CurrentRadixType { - get => (CalculatorApp.ViewModel.Common.NumberBase)GetValue(CurrentRadixTypeProperty); + get => (NumberBase)GetValue(CurrentRadixTypeProperty); set => SetValue(CurrentRadixTypeProperty, value); } - - // Using a DependencyProperty as the backing store for CurrentRadixType. This enables animation, styling, binding, etc... public static readonly DependencyProperty CurrentRadixTypeProperty = - DependencyProperty.Register(nameof(CurrentRadixType), typeof(CalculatorApp.ViewModel.Common.NumberBase), typeof(NumberPad), new PropertyMetadata(CalculatorApp.ViewModel.Common.NumberBase.DecBase, (sender, args) => - { - var self = (NumberPad)sender; - self.OnCurrentRadixTypePropertyChanged((NumberBase)args.OldValue, (NumberBase)args.NewValue); - })); + DependencyProperty.Register(nameof(CurrentRadixType), typeof(NumberBase), typeof(NumberPad), + new PropertyMetadata(NumberBase.DecBase, (sender, args) => + { + var self = (NumberPad)sender; + self.OnCurrentRadixTypePropertyChanged((NumberBase)args.OldValue, (NumberBase)args.NewValue); + })); public bool IsErrorVisualState { @@ -61,8 +260,7 @@ public bool IsErrorVisualState if (m_isErrorVisualState != value) { m_isErrorVisualState = value; - string newState = m_isErrorVisualState ? "ErrorLayout" : "NoErrorLayout"; - VisualStateManager.GoToState(this, newState, false); + VisualStateManager.GoToState(this, m_isErrorVisualState ? "ErrorLayout" : "NoErrorLayout", false); } } } @@ -97,7 +295,5 @@ private void OnCurrentRadixTypePropertyChanged(NumberBase oldValue, NumberBase n Num9Button.IsEnabled = false; } } - - private bool m_isErrorVisualState; } }