How can I get a ListView GridViewColumn to fill the remaining space in my grid?
Asked Answered
C

11

40

I want to create a ListView that has two columns with a fixed width and a third column to fill in the remaining space. So something like this:

<ListView>
    <ListView.View>
        <GridView>
            <GridViewColumn Header="Name" Width="*" />
            <GridViewColumn Header="Age" Width="50" />
            <GridViewColumn Header="Gender" Width="50" />
        </GridView>
    </ListView.View>
</ListView>

The problem is I can't find a way to get the Name column to fill in the remaining space, as setting the width to * doesn't work. It looks like there is a way to do this with a value converter, but it seems like there should be a simpler way. Like with a DataGrid control, you can specify the widths of columns with *s.

Crine answered 30/11, 2011 at 19:43 Comment(1)
One simple feature that you want from day 1 of using ListView with GridView still it isn't implemented in the framework... Such a shameShaylashaylah
T
9

The issue is the column width of a GridViewColumn is double, rather than a GridLength object, and there is no conversion in place to handle the *. Not sure if this is an oversight by the WPF team or not. You would think it should be supported.

Aside from the converter, the only other way I've seen it done is here: http://www.ontheblog.net/CMS/Default.aspx?tabid=36&EntryID=37.

Both are additional work that should not be required. I have found other "weird" things with the ListView and GridView combo so I quit using them. If I need a datagrid I use the 3rd party one we license, if I need a complex ListBox style menu, I just use a templated ListBox.

Tetralogy answered 30/11, 2011 at 20:11 Comment(2)
Ah, the width is a double rather than a GridLength object. It would be interesting to know if this is indeed just an oversight or if there is some rationale behind this. Oh well, it looks like the simplest solution for my scenario is to just use a DataGrid.Crine
Link only answer doesn't provide enough information.Samhita
R
55

I was trying to achieve the same thing but then decided I would like my ListView columns to consume a percentage of the ListView instead, the result of this is all columns consuming a portion of space and all space being consumed in the ListView. You could set this up to have whatever percentage you like on the last column to directly achieve your 'fill remaining space on last column' goal.

I find this method fairly robust and reliable (even on resize!) so thought I might share.

I have four columns in my ListView for this example. All you need is to register the SizeChanged event in your ListView with the below event handler:

private void ProductsListView_SizeChanged(object sender, SizeChangedEventArgs e)
{
    ListView listView = sender as ListView;
    GridView gView = listView.View as GridView;

    var workingWidth = listView.ActualWidth - SystemParameters.VerticalScrollBarWidth; // take into account vertical scrollbar
    var col1 = 0.50;
    var col2 = 0.20;
    var col3 = 0.15;
    var col4 = 0.15;

    gView.Columns[0].Width = workingWidth*col1;
    gView.Columns[1].Width = workingWidth*col2;
    gView.Columns[2].Width = workingWidth*col3;
    gView.Columns[3].Width = workingWidth*col4;
}
Rica answered 10/5, 2012 at 0:31 Comment(6)
You'd get extra points if you created some sort of child GridView and columns so you could apply this to all GridViews instead of a custom implementation for each view.Bahner
@KonradMorawski nice tip, but in my environment is SystemParameters.VerticalScrollBarWidth = 17 and I need use something around 25. When i use 17 VerticalScrollBar is visible.Ditto
@Ditto true, SystemParameters.VerticalScrollBarWidth is only the default and not the current width - I have to take the custom width of the style instead.Applaud
This throws exception.. The event is being called all the time and the workingWidth keeps decreasing until it reaches a negative size and crashesDantedanton
Could not get any more elegant than this. Spot on solution.Hayner
I hate to say this, but this is what we used to have to do in VB3 before it was possible to anchor controls. You had to work out the resize manually.Durning
K
18

Came across this when looking into a similar problem, my issue was I wanted all columns to be 'Auto' expect the first, which would just fill in the extra space, so I expanded on GONeale's solution.

private void ListView_SizeChanged(object sender, SizeChangedEventArgs e)
{
    ListView _ListView = sender as ListView;
    GridView _GridView = _ListView.View as GridView;
    var _ActualWidth = _ListView.ActualWidth - SystemParameters.VerticalScrollBarWidth;
    for (Int32 i = 1; i < _GridView.Columns.Count; i++)
    {
        _ActualWidth = _ActualWidth - _GridView.Columns[i].ActualWidth;
    }
    _GridView.Columns[0].Width = _ActualWidth;
}

Then the XAML is simply:

...
<ListView.View>
    <GridView>
        <GridViewColumn Header="Title" />
        <GridViewColumn Header="Artist" Width="Auto" />
        <GridViewColumn Header="Album" Width="Auto" />
        <GridViewColumn Header="Genre" Width="Auto" />
    </GridView>
</ListView.View>
...

This code could also be used on more generically as number of columns isn't hard-coded and with a little tweaking you could probably make the 'fill column' definable through some sort of logic.

Hope it helps someone :)

Kosel answered 3/2, 2013 at 16:55 Comment(1)
It did, right after I changed the number of columns in my GridView and broke the hard-coded number of columns. I modified this approach to cater for that (I fixed the width of the first column). Thanks.Peer
T
9

The issue is the column width of a GridViewColumn is double, rather than a GridLength object, and there is no conversion in place to handle the *. Not sure if this is an oversight by the WPF team or not. You would think it should be supported.

Aside from the converter, the only other way I've seen it done is here: http://www.ontheblog.net/CMS/Default.aspx?tabid=36&EntryID=37.

Both are additional work that should not be required. I have found other "weird" things with the ListView and GridView combo so I quit using them. If I need a datagrid I use the 3rd party one we license, if I need a complex ListBox style menu, I just use a templated ListBox.

Tetralogy answered 30/11, 2011 at 20:11 Comment(2)
Ah, the width is a double rather than a GridLength object. It would be interesting to know if this is indeed just an oversight or if there is some rationale behind this. Oh well, it looks like the simplest solution for my scenario is to just use a DataGrid.Crine
Link only answer doesn't provide enough information.Samhita
B
9

The solution from David Hanson-Greville's OnTheBlog mentioned in another answer isn't available anymore, even though the blog still exists. I was able to find it on the Wayback Machine and with a few moderations, here it is:

The trick is that you set Stretch=true on your ListView and it will stretch the columns that do not have a width equally.

using System;
using System.Collections.Generic;
using System.Windows;
using System.Windows.Controls;

namespace Demo.Extension.Properties
{
    ///
    /// ListViewColumnStretch
    ///
    public class ListViewColumns : DependencyObject
    {
        ///
        /// IsStretched Dependency property which can be attached to gridview columns.
        ///
        public static readonly DependencyProperty StretchProperty =
            DependencyProperty.RegisterAttached("Stretch",
            typeof(bool),
            typeof(ListViewColumns),
            new UIPropertyMetadata(true, null, OnCoerceStretch));

        ///
        /// Gets the stretch.
        ///
        /// The obj.
        ///
        public static bool GetStretch(DependencyObject obj)
        {
            return (bool)obj.GetValue(StretchProperty);
        }

        ///
        /// Sets the stretch.
        ///
        /// The obj.
        /// if set to true [value].
        public static void SetStretch(DependencyObject obj, bool value)
        {
            obj.SetValue(StretchProperty, value);
        }

        ///
        /// Called when [coerce stretch].
        ///
        ///If this callback seems unfamilar then please read
        /// the great blog post by Paul Jackson found here.
        /// http://compilewith.net/2007/08/wpf-dependency-properties.html
        /// The source.
        /// The value.
        ///
        public static object OnCoerceStretch(DependencyObject source, object value)
        {
            ListView lv = (source as ListView);

            //Ensure we dont have an invalid dependancy object of type ListView.
            if (lv == null)
            {
                throw new ArgumentException("This property may only be used on ListViews");
            }

            //Setup our event handlers for this list view.
            lv.Loaded += new RoutedEventHandler(lv_Loaded);
            lv.SizeChanged += new SizeChangedEventHandler(lv_SizeChanged);
            return value;
        }

        ///
        /// Handles the SizeChanged event of the lv control.
        ///
        /// The source of the event.
        /// The instance containing the event data.
        private static void lv_SizeChanged(object sender, SizeChangedEventArgs e)
        {
            ListView lv = (sender as ListView);
            if (lv.IsLoaded)
            {
                //Set our initial widths.
                SetColumnWidths(lv);
            }
        }

        ///
        /// Handles the Loaded event of the lv control.
        ///
        /// The source of the event.
        /// The instance containing the event data.
        private static void lv_Loaded(object sender, RoutedEventArgs e)
        {
            ListView lv = (sender as ListView);
            //Set our initial widths.
            SetColumnWidths(lv);
        }

        ///
        /// Sets the column widths.
        ///
        private static void SetColumnWidths(ListView listView)
        {
            //Pull the stretch columns fromt the tag property.
            List<GridViewColumn> columns = (listView.Tag as List<GridViewColumn>);
            double specifiedWidth = 0;
            GridView gridView = listView.View as GridView;
            if (gridView != null)
            {
                if (columns == null)
                {
                    //Instance if its our first run.
                    columns = new List<GridViewColumn>();
                    // Get all columns with no width having been set.
                    foreach (GridViewColumn column in gridView.Columns)
                    {
                        if (!(column.Width >= 0))
                        {
                            columns.Add(column);
                        }
                        else
                        {
                            specifiedWidth += column.ActualWidth;
                        }
                    }
                }
                else
                {
                    // Get all columns with no width having been set.
                    foreach (GridViewColumn column in gridView.Columns)
                    {
                        if (!columns.Contains(column))
                        {
                            specifiedWidth += column.ActualWidth;
                        }
                    }
                }

                // Allocate remaining space equally.
                foreach (GridViewColumn column in columns)
                {
                    double newWidth = (listView.ActualWidth - specifiedWidth) / columns.Count;
                    if (newWidth >= 10)
                    {
                        column.Width = newWidth - 10;
                    }
                }

                //Store the columns in the TAG property for later use.
                listView.Tag = columns;
            }
        }
    }
}

Then you just add the namespace to the XAML file:

xmlns:Extensions="clr-namespace:Demo.Extension.Properties"

and use it on your list view:

<ListView ItemsSource="{Binding Path=Items}" DisplayMemberPath="Name"
                          ScrollViewer.VerticalScrollBarVisibility="Auto"
                          Grid.Column="0" Margin="8" Extensions:ListViewColumns.Stretch="true">
Barnardo answered 3/11, 2017 at 14:20 Comment(1)
This is such BS. Why should you need to do so much code-behind just to accomplish such a simple thing? I'm getting increasingly frustrated with this type of stuff in WPFKalfas
S
2

My need was to have all columns with the same width. The above solutions are fine, but I prefer to wrap such a thing in an attached property (MVVM, reusability, etc.). Here is my code, if it can help.

    public class StarSizeHelper {

    private static readonly List<FrameworkElement> s_knownElements = new List<FrameworkElement>();

    public static bool GetIsEnabled(DependencyObject d) {
        return (bool) d.GetValue(IsEnabledProperty);
    }

    public static void SetIsEnabled(ListView d, bool value) {
        d.SetValue(IsEnabledProperty, value);
    }

    public static readonly DependencyProperty IsEnabledProperty =
        DependencyProperty.RegisterAttached("IsEnabled", 
                                            typeof(bool), 
                                            typeof(StarSizeHelper),
                                            new FrameworkPropertyMetadata(IsEnabledChanged));

    public static void IsEnabledChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) {

        var ctl = d as ListView;
        if (ctl == null) {
            throw new Exception("IsEnabled attached property only works on a ListView type");
        }

        RememberElement(ctl);
    }

    private static void RememberElement(ListView ctl) {

        if (! s_knownElements.Contains(ctl)) {
            s_knownElements.Add(ctl);

            RegisterEvents(ctl);
        } 
        // nothing to do if elt is known
    }

    private static void OnUnloaded(object sender, RoutedEventArgs e) {

        FrameworkElement ctl = (FrameworkElement) sender;
        ForgetControl(ctl);
    }

    private static void ForgetControl(FrameworkElement fe) {

        s_knownElements.Remove(fe);
        UnregisterEvents(fe);
    }

    private static void RegisterEvents(FrameworkElement fe) {
        fe.Unloaded += OnUnloaded;
        fe.SizeChanged += OnSizeChanged;
    }

    private static void UnregisterEvents(FrameworkElement fe) {
        fe.Unloaded -= OnUnloaded;
        fe.SizeChanged -= OnSizeChanged;
    }

    private static void OnSizeChanged(object sender, SizeChangedEventArgs e) {

        ListView listView = sender as ListView;
        if (listView == null) {
            return; // should not happen
        }
        GridView gView = listView.View as GridView;
        if (gView == null) {
            return; // should not happen
        }

        var workingWidth = listView.ActualWidth - SystemParameters.VerticalScrollBarWidth -10; // take into account vertical scrollbar
        var colWidth = workingWidth / gView.Columns.Count;
        foreach (GridViewColumn column in gView.Columns) {
            column.Width = colWidth;
        }
    }
}

In order to use it:

<ListView ... StarSizeHelper.IsEnabled="true" ... />

(you still to fix the namespace declaration in the XAML, of course)

You can adapt your sizing needs in the OnSizeChanged method.

Shrimp answered 11/8, 2017 at 8:3 Comment(0)
H
2

I realize this is an old question, but I found this post while trying to get star based solution for multiple columns in a ListView/GridView. So I figured I could help some future folks out, as it will answer this question as well.

I had initially implemented a WidthConverter but this has the obvious limitation that the last column won't "fill", and it was never guaranteed that the row would fit its space, but here it is for those who are curious:

public class WidthConverter : IValueConverter
    {
        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
        {
            var actualWidth = (double)value;
            var desiredPercentage = SafeConvert(parameter);

            if (actualWidth == 0.0 || desiredPercentage == 0.0) return double.NaN;

            var result = actualWidth * (desiredPercentage / 100);

            return result;
        }

        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
        {
            throw new NotImplementedException();
        }

        private double SafeConvert(object pInput)
        {
            if (pInput == null) return default;

            if (double.TryParse(pInput.ToString(), out var result))
            {
                return result;
            }

            return default;
        }
    }
<GridViewColumn Header="My Header"
                DisplayMemberBinding="{Binding MyColumnData}"
                Width="{Binding Path=ActualWidth, ElementName=MyListView, Converter={StaticResource WidthConverter}, ConverterParameter='10.5'}" />

This works, but it's very reliant on the programmer to hard code parameter values and messy in the sense that the binding needs the ActualWidth property from the ListView element. Ultimately it's fairly limited to what it can actually do, especially considering most WPF folks are used to working with GridLength when it comes to star sizing.

I took various bits and pieces from the solutions posted here as well as elsewhere and developed an MVVM friendly solution based on Attached Properties and Behaviors.

I created an Attached Property using GridLength to leverage the existing absolute/auto/star logic matching XAML width patterns we're all used to.

public class ColumnAttachedProperties : DependencyObject
{
    public static readonly DependencyProperty GridLength_WidthProperty = DependencyProperty.RegisterAttached(
        name: "GridLength_Width",
        propertyType: typeof(GridLength),
        ownerType: typeof(ColumnAttachedProperties),
        defaultMetadata: new FrameworkPropertyMetadata(GridLength.Auto));

    public static GridLength GetGridLength_Width(DependencyObject dependencyObject) 
        => (GridLength)dependencyObject.GetValue(GridLength_WidthProperty);

    public static void SetGridLength_Width(DependencyObject dependencyObject, string value) 
        => dependencyObject.SetValue(GridLength_WidthProperty, value);
}

The Attached Behavior hooks to both the Loaded and SizeChanged events, and performs some shared logic to size the columns.

Essentially, on the first pass over the columns, it will tally the star values (but doesn't set a width for star columns yet), and then on the second pass targets the star columns, setting the width as a percentage of the remaining width available. I am sure this could be optimized in some way.

public static class ListViewStarSizingAttachedBehavior
{
    public static readonly DependencyProperty UseGridLength_WidthProperty = DependencyProperty.RegisterAttached(
        name: "UseGridLength_Width",
        propertyType: typeof(bool),
        ownerType: typeof(ListViewStarSizingAttachedBehavior),
        new UIPropertyMetadata(false, RegisterEventHandlers));

    public static bool GetUseGridLength_Width(DependencyObject dependencyObject) 
        => (bool)dependencyObject.GetValue(UseGridLength_WidthProperty);
    public static void SetUseGridLength_Width(DependencyObject dependencyObject, bool value) 
        => dependencyObject.SetValue(UseGridLength_WidthProperty, value);

    private static void RegisterEventHandlers(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        if (d is ListView listView)
        {
            if (e.NewValue is bool booleanValue && booleanValue == true)
            {
                listView.SizeChanged += ListView_SizeChanged;
                listView.Loaded += ListView_Loaded;
            }
            else
            {
                listView.SizeChanged -= ListView_SizeChanged;
                listView.Loaded -= ListView_Loaded;
            }
        }
    }

    private static void ListView_Loaded(object sender, RoutedEventArgs e)
    {
        CalculateGridColumnWidths(sender);
    }

    private static void ListView_SizeChanged(object sender, SizeChangedEventArgs e)
    {
        if (sender is ListView listView && !listView.IsLoaded) return;

        CalculateGridColumnWidths(sender);
    }

    private static void CalculateGridColumnWidths(object sender)
    {
        if (sender is ListView listView && listView.View is GridView gridView)
        {
            // the extra offset may need to be altered per your application.
            var scrollOffset = SystemParameters.VerticalScrollBarWidth - 7;

            var remainingWidth = listView.ActualWidth - scrollOffset;
            var starTotal = 0.0;
                
            foreach (var column in gridView.Columns)
            {
                var gridLength = ColumnAttachedProperties.GetGridLength_Width(column);

                if (gridLength.IsStar)
                {
                    // Get the cumlative star value while passing over the columns
                    // but don't set their width until absolute and auto have been set.
                    starTotal += gridLength.Value;
                    continue;
                }

                if (gridLength.IsAbsolute)
                {
                    column.Width = gridLength.Value;
                }
                else
                {
                    column.Width = double.NaN;
                }

                remainingWidth -= column.ActualWidth;
            }

            if (starTotal == 0.0) return;

            // now eval each star column
            foreach (var column in gridView.Columns)
            {
                var gridLength = ColumnAttachedProperties.GetGridLength_Width(column);

                if (!gridLength.IsStar) continue;

                var starPercent = (gridLength.Value / starTotal);
                column.Width = remainingWidth * starPercent;
            }
        }
    }
}

And finally to use it in XAML, the ListView needs implement the attached behavior, and each of the columns implement the attached property. However, if you leave the attached property off the column, it will default Auto as per the DependencyPropertys defaultMetadata.

<ListView local:ListViewStarSizingAttachedBehavior.UseGridLength_Width="True"
          ItemsSource="{Binding MyItems}" >
    <ListView.View>
        <GridView>
            <GridView.Columns>
                <!-- Absolute -->
                <GridViewColumn local:ColumnAttachedProperties.GridLength_Width="250"
                                Header="Column One"
                                DisplayMemberBinding="{Binding Item1}" />
                
                <!-- Star -->               
                <GridViewColumn local:ColumnAttachedProperties.GridLength_Width="2*"
                                Header="Column Two"
                                DisplayMemberBinding="{Binding Item2}" />
                
                <!-- Auto -->               
                <GridViewColumn local:ColumnAttachedProperties.GridLength_Width="Auto"
                                Header="Column Three"
                                DisplayMemberBinding="{Binding Item3}" />

                <!-- Star -->
                <GridViewColumn local:ColumnAttachedProperties.GridLength_Width="*"
                                Header="Column Four"
                                DisplayMemberBinding="{Binding Item4}" />
            </GridView.Columns>
        </GridView>
    </ListView.View>
</ListView>

Seems to work well so far, but I am sure there are some edge cases which will need to be considered. The XAML is a ton cleaner than the width converter. The overall result is more flexible than those already posted as well.

Holbrooke answered 4/12, 2021 at 22:7 Comment(0)
P
1

I took the example above (which is great) and improved it slightly to prevent runtime exceptions on resize:

private void tpList_SizeChanged(object sender, SizeChangedEventArgs e)
        {
            ListView listView = sender as ListView;
            GridView gView = listView.View as GridView;

            var workingWidth = listView.ActualWidth - (SystemParameters.VerticalScrollBarWidth + 20); // take into account vertical scrollbar
            var col1 = 0.50;
            var col2 = 0.50;

            var t1 = workingWidth * col1;
            var t2 = workingWidth * col2;
            gView.Columns[0].Width = t1 > 0 ? t1 : 1;
            gView.Columns[1].Width = t2 > 0 ? t2 : 1;

        }
    }
Profundity answered 10/5, 2017 at 14:14 Comment(0)
P
0

My problem was similar but I wanted to fix the width of the first column and I also didn't want it to break if I added or removed columns, even at runtime. Thanks @Gary for the tip at https://mcmap.net/q/372449/-how-can-i-get-a-listview-gridviewcolumn-to-fill-the-remaining-space-in-my-grid

private void ResultsListView_SizeChanged(object sender, SizeChangedEventArgs e)
    {
      double newWidthForColumnsExceptFirstColumn = ResultsListView.ActualWidth - SystemParameters.VerticalScrollBarWidth - ResultsGridView.Columns[0].Width;
      int columnsCount = ResultsGridView.Columns.Count;
      Double newColumnWidth = newWidthForColumnsExceptFirstColumn / (columnsCount -1);

      for ( int col = 1; col < columnsCount; col++ ) // skip column [0]
      {
        ResultsGridView.Columns[col].Width = newColumnWidth;
      }
    }
Peer answered 5/2, 2015 at 1:30 Comment(0)
K
0

Here is a solution that allows one to have multiple ListViews leverage a general "Resize" event handler.

    //Using dictionarys as trackers allows us to have multiple ListViews use the same code
    private Dictionary<string, double> _fixedWidthTracker = new Dictionary<string, double>();
    private Dictionary<string, List<GridViewColumn>> _varWidthColTracker = new Dictionary<string, List<GridViewColumn>>();
    private void ListView_SizeChanged(object sender, SizeChangedEventArgs e)
    {
        ListView lv = sender as ListView;
        if (lv != null)
        {
            //For validation during Debug
            VerifyName(lv);

            GridView gv = lv.View as GridView;
            if (gv != null)
            {
                if (!_varWidthColTracker.ContainsKey(lv.Name))
                {
                    _varWidthColTracker[lv.Name] = new List<GridViewColumn>();
                    _fixedWidthTracker[lv.Name] = 0;
                    foreach (GridViewColumn gvc in gv.Columns)
                    {
                        if (!double.IsNaN(gvc.Width)) _fixedWidthTracker[lv.Name] += gvc.Width; else _varWidthColTracker[lv.Name].Add(gvc);
                    }
                }
                double newWidthForColumns = e.NewSize.Width - SystemParameters.VerticalScrollBarWidth - _fixedWidthTracker[lv.Name];
                int columnsCount = gv.Columns.Count;
                int numberOfFixedWithColumns = columnsCount - _varWidthColTracker[lv.Name].Count;
                Double newColumnWidth = newWidthForColumns / (columnsCount - numberOfFixedWithColumns);

                foreach (GridViewColumn gvc in _varWidthColTracker[lv.Name])
                {
                    gvc.Width = newColumnWidth;
                }
            }
        }
    }

    /// <summary>
    /// Warns the developer if this object does not have
    /// a public property with the specified name. This 
    /// method does not exist in a Release build.
    /// </summary>
    [Conditional("DEBUG")]
    [DebuggerStepThrough]
    public void VerifyName(ListView listView)
    {
        if (String.IsNullOrEmpty(listView.Name))
        {
            string msg = "The Name attribute is required to be set on the ListView in order to Bind to this method";
            Debug.Fail(msg);
        }
    }
Kaykaya answered 15/4, 2015 at 13:44 Comment(0)
C
0

Building on some other answers (eg. that of Timores), this is the one I normally use. Unlike those, this solution allows both relative and automatic sizes to co-exist:

  public class GridViewColumnRelativeSize {
    public static readonly DependencyProperty IsEnabledProperty = DependencyProperty.RegisterAttached("IsEnabled", typeof(bool), typeof(GridViewColumnRelativeSize), new FrameworkPropertyMetadata(IsEnabledPropertyChanged));
    public static bool GetIsEnabled(DependencyObject d) => (bool)d.GetValue(IsEnabledProperty);
    public static void SetIsEnabled(ListView d, bool value) => d.SetValue(IsEnabledProperty, value);

    public static void IsEnabledPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) {
      if (d is ListView list) {
        if (!KnownLists.Contains(list)) {
          KnownLists.Add(list);
          list.Unloaded += OnUnloaded;
          list.SizeChanged += OnSizeChanged;
        }
      }
      else
        throw new Exception("ListView expected");
    }

    public static readonly DependencyProperty RelativeWidthProperty = DependencyProperty.RegisterAttached("RelativeWidth", typeof(double), typeof(GridViewColumnRelativeSize), new FrameworkPropertyMetadata(double.NaN));
    public static double GetWidth(DependencyObject d) => (double)d.GetValue(RelativeWidthProperty);
    public static void SetWidth(GridViewColumn d, double value) => d.SetValue(RelativeWidthProperty, value);

    private static readonly List<FrameworkElement> KnownLists = new();

    private static void OnUnloaded(object sender, RoutedEventArgs e) {
      var element = (FrameworkElement)sender;
      _ = KnownLists.Remove(element);
      element.Unloaded -= OnUnloaded;
      element.SizeChanged -= OnSizeChanged;
    }

    private static void OnSizeChanged(object sender, SizeChangedEventArgs e) {
      if (sender is ListView listView) {
        bool isEnabled = listView.GetValue(IsEnabledProperty) is bool enabled && enabled;
        if (isEnabled && listView.View is GridView gridView) {
          double TotalUnits = gridView.Columns.Sum(column => {
            double width = (double)column.GetValue(RelativeWidthProperty);
            return double.IsNaN(width) ? 1 : width;
          });
          double ActualWidth = listView.ActualWidth - SystemParameters.VerticalScrollBarWidth;
          double UnitWidth = Math.Floor(ActualWidth / TotalUnits);
          foreach (var column in gridView.Columns) {
            double unit = (double)column.GetValue(RelativeWidthProperty);
            if (!double.IsNaN(unit))
              column.Width = unit * UnitWidth;
          }
        }
      }
    }

  }
<ListView ns:GridViewColumnRelativeSize.IsEnabled="True">
  <ListView.View>
    <GridView>
      <GridViewColumn />
      <GridViewColumn Width="50" />
      <GridViewColumn ns:GridViewColumnRelativeSize.Width="2" />
    </GridView>
  </ListView.View>

Columns with a specific Width obey the size, without a Width will be automatically sized, with a GridViewColumnRelativeSize.Width will be proportionally sized using the value as a factor.

Crimpy answered 14/1, 2022 at 14:51 Comment(0)
E
0

Yet another MVVM solution. We can use MultiValueConverter for our column that should fill the remaining space. This method requires reflection to get DesiredWidth set of other columns.

<ListView>
    <ListView.Resources>
        <conv:GridViewColumnWidthConverter x:Key="GridViewColumnWidthConverter"/>
    </ListView.Resources>
    <ListView.View>
        <GridView>
            <GridViewColumn Header="Name">
                <GridViewColumn.Width>
                    <MultiBinding Converter="{StaticResource GridViewColumnWidthConverter}" ConverterParameter="*">
                        <Binding RelativeSource="{RelativeSource FindAncestor, AncestorType={x:Type ListView}}" Path="ActualWidth" />
                        <Binding RelativeSource="{RelativeSource FindAncestor, AncestorType={x:Type GridView}}" />
                        <Binding RelativeSource="{RelativeSource Self}"/>
                    </MultiBinding>
                </GridViewColumn.Width>
            </GridViewColumn>
            <GridViewColumn Header="Age" Width="50" />
            <GridViewColumn Header="Gender" Width="50" />
        </GridView>
    </ListView.View>
</ListView>

Converter:

public class GridViewColumnWidthConverter : IMultiValueConverter
{
    private static readonly System.Reflection.PropertyInfo s_piDesiredWidth;

    static GridViewColumnWidthConverter()
    {
        var pi = typeof(GridViewColumn).GetProperty("DesiredWidth",
            System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic);
        Debug.Assert(pi != null);
        s_piDesiredWidth = pi;
    }

    public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
    {
        if (parameter is string param && param == "*" && values.Length==3 && values[0] is double actualWidth && actualWidth > 0d)
        {
            if (values[1] is GridView gridView && values[2] is GridViewColumn column && s_piDesiredWidth != null)
            {
                double w = 0d;
                foreach (var col in gridView.Columns)
                {
                    if (col == column)
                        continue;
                    w += col.ActualWidth > 0 ? col.ActualWidth : (double)s_piDesiredWidth.GetValue(col);
                }
                double desiredWidth = actualWidth - w;
                return desiredWidth > 100 ? desiredWidth - 25/*scrollbar width*/ : double.NaN;
            }
        }
        return double.NaN;
    }

    public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
    {
        return null;
    }
}
Enchase answered 11/3, 2023 at 0:57 Comment(1)
As it’s currently written, your answer is unclear. Please edit to add additional details that will help others understand how this addresses the question asked. You can find more information on how to write good answers in the help center.Awakening

© 2022 - 2024 — McMap. All rights reserved.