views:

1186

answers:

6

Hi There,

I am implementing a custom control which derives from ListView.

I would like for the final column to fill the remaining space (Quite a common task), I have gone about this via overriding the OnResize method:

     protected override void OnResize(EventArgs e)
    {
        base.OnResize(e);

        if (Columns.Count == 0)
            return;
        Columns[Columns.Count - 1].Width = -2; // -2 = Fill remaining space
    }

or via another method:

        protected override void OnResize(EventArgs e)
    {

        base.OnResize(e);

        if (!_autoFillLastColumn)
            return;

        if (Columns.Count == 0)
            return;

        int TotalWidth = 0;
        int i = 0;
        for (; i < Columns.Count - 1; i++)
        {
            TotalWidth += Columns[i].Width;
        }

        Columns[i].Width = this.DisplayRectangle.Width - TotalWidth;
    }

Edit:

This works fine until I dock the ListView into a parent container and resize via that control. Every second time the control size shrinks (IE, drag the the border one pixel), I get a scroll bar on the bottom which can't move at all (not even one pixel).

The result of which is when I drag the size of the parent I am left with a flickering scroll bar in the ListView, and a 50% chance it will be there when the dragging stops.

A: 

Try calling base.OnResize() after you made your change.

leppie
That was one of the first things I tried, alas it makes no difference.
Courtney de Lautour
A: 

Try using the ClientSize property. As MSDN puts it:

The client area of a control is the bounds of the control, minus the nonclient elements such as scroll bars, borders, title bars, and menus.

Edit: I've added a code sample demonstrating how achieve different fill behaviors.

public class MyListView : ListView
{
    public MyListView()
    {
        View = View.Details;
        Columns.Add(Column1);
        Columns.Add(Column2);
        Columns.Add(Column3);
    }

    private readonly ColumnHeader Column1 = new ColumnHeader();
    private readonly ColumnHeader Column2 = new ColumnHeader();
    private readonly ColumnHeader Column3 = new ColumnHeader();

    protected override void OnResize(EventArgs e)
    {
        base.OnResize(e);

        // Constant width
        Column1.Width = 200;
        // 50% of the ListView's width
        Column2.Width = (ClientSize.Width/2);
        // Fill the remaining width, but no less than 100 px
        Column3.Width = Math.Max(100, ClientSize.Width - (Column1.Width + Column2.Width));
    }
}
Bryan Menard
No dice with that either
Courtney de Lautour
AFAIK, it's possible to use ClientSize in conjunction with OnResize to achieve proper Autosizing of columns.
Bryan Menard
I've added an example to demonstrate.
Bryan Menard
Thanks :) Unfortunately that doesn't solve my problemAfter a bit of playing around I have discovered that when resizing manually (in the designer at least) everything works fine. Its when the ListView is docked / anchored on multiple sides and I resize the parent control I get the issue.
Courtney de Lautour
+1  A: 

ObjectListView (an open source wrapper around .NET WinForms ListView) allows this "expand-last-column-to-fill-available-space". It's actually more difficult to get right than it first appears -- do not believe anyone who tells you otherwise :)

If you use ObjectListView, you get the solution to that problem -- and several others -- for free. But if you want to do all the work yourself, you'll need to:

  • use ClientSize instead of DisplayRectangle (as @zxpro has already said)
  • use Layout event rather than Resize event.
  • listen for ColumnResized event and do the same work
  • not do the auto resizing in design mode -- it gets very confusing.

The trickiest problem only appears when the window shrinks -- the horizontal scroll bar flickers annoyingly and sometimes remains after the shrinking is finished. Which appears to be exactly what is happening in your case.

The solution is to intercept the WM_WINDOWPOSCHANGING message and resize the ListView to what the new size is going to be.

protected override void WndProc(ref Message m) {
    switch (m.Msg) {
        case 0x46: // WM_WINDOWPOSCHANGING
            this.HandleWindowPosChanging(ref m);
            base.WndProc(ref m);
            break;
        default:
            base.WndProc(ref m);
            break;
    }
}

protected virtual void HandleWindowPosChanging(ref Message m) {
    const int SWP_NOSIZE = 1;

    NativeMethods.WINDOWPOS pos = (NativeMethods.WINDOWPOS)m.GetLParam(typeof(NativeMethods.WINDOWPOS));
    if ((pos.flags & SWP_NOSIZE) == 0) {
        if (pos.cx < this.Bounds.Width) // only when shrinking
            // pos.cx is the window width, not the client area width, so we have to subtract the border widths
            this.ResizeFreeSpaceFillingColumns(pos.cx - (this.Bounds.Width - this.ClientSize.Width));
    }
}
Grammarian
+1  A: 

This may be pretty late, but i believe the easiest solution is to add a function to handle the window resize event. I have my listview anchored to the right, so when i resize my window, the listview also resizes. in the function that handles the window resize, i set the myListView.Columns[lastColumnIndex].Width = -2;

This apparently redraws the listview, extending the last column to the edge of the control. Hope this helps.

Jason
+1  A: 
Lary_Loose
A: 

You need to override the protected method SetBoundsCore() and perform the column resizing either before or after delegating to base.SetBoundsCore(), depending on whether the ListView width is narrowing or widening.

The following example is hardcoded for a single column ListView:

protected override void SetBoundsCore( int x, int y, int width, int height, BoundsSpecified specified )
{
    ResizeColumns( width, true );
    base.SetBoundsCore( x, y, width, height, specified );
    ResizeColumns( width, false );
}

private void ResizeColumns( int controlWidth, bool ifShrinking )
{
    if( Columns.Count < 1 || Parent == null )
        return;

    int borderGap = Width - ClientSize.Width;
    int desiredWidth = controlWidth - borderGap;

    if( (desiredWidth < Columns[0].Width) == ifShrinking )
        Columns[0].Width = desiredWidth;
}

Note his code doesn't deal with the specified parameter, i'll leave that up to you!

Mark