Hide Windows Forms ListView Column in Details View without deleting Column or resizing it to Zero

feedwall picture feedwall · Aug 7, 2013 · Viewed 9k times · Source

I have in C# Net 2.0 Windows Forms a ListView with about ten columns filled with data at startup of the application. The data is huge, so it cannot be reloaded fast in-between. My ListView has the Details View on and allows the user the resizing of each column separately.

The user shall be able to hide any of the ten columns or multiple of them at once and unhide the columns again any time in non-specific row. The data shall be not deleted while hiding the column.

Following things I have tried but the result were not satisfying:

1) Resizing the column with size 0 will make it somewhat disappearing until the user starts to play with the columns. The user accidently will resize them back because in my ListView there is the option allowed for the user to resize each column manually.

2) With just deleting the column out occurs the following problem: When I try to add the column back, the column doesn't remember its position and size. The position is the main problem, I'll try to explain why. If my user decides to first hide "column 2" and then "column 3" and the user later unhides 3 before 2, then "Index 2" doesn't exist, so I cannot insert at Index No. 3 and an exception rises. Even if I remember the Index position before deleting I cannot simply put the Column just back to that index, because I don't know if previous columns where already hided too or which column is missing before and after or hided or not. So the scenario could be like that: 1 shown, 2 hided, 3 hided too, 4 shown, 5 hided, 6 hided, 7 hided , 8 shown, 9 hided, 10 hided.

Possible solutions "1)" and "2)" are automatically out ruled in the scenario. Better is even making the width of the column to zero, but since my user shall be able to resize columns any time by need, with resizing to zero it cannot be hided from the user. The user will unhide it by resize and my system will think it's still hidden etc. etc. and it looks not professional if hidden columns can be just "resized back" or how we could name that else.

Has someone a better idea? Why the listView Column has no "visible" or "hide" property, I wonder? If someone did that already before please post a solution.

I have to add that I use autoresizing of all columns in my listview when data is being added. For this reason the answer below doesn't work. The resize event can't detect the width of -1. "All invisible columns" with width 0 will be resized back, when data was added. Since the listview cuts out data which overlaps column length, I need to autoresize it permanently. Explorer has not this problem because it fits the columns to the length of the data. C# has not such advanced listview, here I have to set the columns to -1 each time when data being added. In this conception doesn't work the idea of column.width = 0 for hiding the columns.

Answer

King King picture King King · Aug 8, 2013

OK, Your problem is in fact How to hide a ListView Column?. This has been asked by many many people on the web. I've tried searching many however couldn't find anything. I've ended up with the only solution: Set the column width to Zero. This will work with some trick here:

//This will try hiding the column at index 1
listView1.Columns[1].Width = 0;
//ColumnWidthChanging event handler of your ListView
private void listView1_ColumnWidthChanging(object sender, ColumnWidthChangingEventArgs e){
  if(e.ColumnIndex == 1){
     e.Cancel = true;
     e.NewWidth = 0;
  }
}

It works almost perfectly. However when user moves mouse over the pipe at the position of the hidden column, there is a Cursor indicator appearing to notify user something like There is a Zero-width column here, just hold mouse down and drag to resize it. Of course, user can't resize it from Zero because we Cancel it and make NewWidth = 0 (as the code above does). But the Cursor notifying such operation makes it a little nasty, here is the screen shot demonstrating the problem:

enter image description here

To solve this problem is not easy. At least that's what I feel. I thought of this solution which seems to work OK. The idea is we have to detect if the mouse is over near the pipe of a hidden column, we have to set Cursor = Cursors.Arrow. Here is the whole class which I think works great for you:

public class CustomListView : ListView
{
    [DllImport("user32")]
    private static extern bool EnumChildWindows(IntPtr parentHwnd, EnumChildProc proc, object lParam);
    delegate bool EnumChildProc(IntPtr childHwnd, object lParam);
    public CustomListView()
    {
        VisibleChanged += (s, e) =>
        {
            if (Visible && headerHandle == IntPtr.Zero&&!DesignMode)
            {
                EnumChildWindows(Handle, EnumChild, null);
                headerProc = new HeaderProc(this);
                headerProc.AssignHandle(headerHandle);
            }
        };
        columnPipeLefts[0] = 0;
    }      
    //Save the Handle to the Column Headers, a ListView has only child Window which is used to render Column headers  
    IntPtr headerHandle;
    //This is used use to hook into the message loop of the Column Headers
    HeaderProc headerProc;        
    private bool EnumChild(IntPtr childHwnd, object lParam)
    {
        headerHandle = childHwnd;
        return true;
    }
    //Updated code
    protected override void WndProc(ref Message m)
    {
        if (m.Msg == 0x101e&&hiddenColumnIndices.Contains(m.WParam.ToInt32()))//WM_SETCOLUMNWIDTH = 0x101e
        {                
            if(m.LParam.ToInt32() > 0) hiddenColumnWidths[m.WParam.ToInt32()] = m.LParam.ToInt32();                    
            return;//Discard the message changing hidden column width so that it won't be shown again.                
        }
        base.WndProc(ref m);
    }
    //Save the column indices which are hidden
    List<int> hiddenColumnIndices = new List<int>();
    //Save the width of hidden columns
    Dictionary<int, int> hiddenColumnWidths = new Dictionary<int, int>();
    //Save the Left (X-Position) of the Pipes which separate Column Headers.
    Dictionary<int, int> columnPipeLefts = new Dictionary<int, int>();
    protected override void OnColumnWidthChanging(ColumnWidthChangingEventArgs e)
    {
        if (hiddenColumnIndices.Contains(e.ColumnIndex))
        {
            e.Cancel = true;
            e.NewWidth = 0;
        }
        base.OnColumnWidthChanging(e);
    }
    //We need to update columnPipeLefts whenever the width of any column changes
    protected override void OnColumnWidthChanged(ColumnWidthChangedEventArgs e)
    {            
        base.OnColumnWidthChanged(e);
        UpdateColumnPipeLefts(Columns[e.ColumnIndex].DisplayIndex + 1);
    }
    int index = -1;
    protected override void OnColumnReordered(ColumnReorderedEventArgs e)
    {
        int i = Math.Min(e.NewDisplayIndex, e.OldDisplayIndex);
        index = index != -1 ? Math.Min(i + 1, index) : i + 1;            
        base.OnColumnReordered(e);                                
    }
    //This is used to update the columnPipeLefts every reordering columns or resizing columns.
    private void UpdateColumnPipeLefts(int fromIndex)
    {
        int w = fromIndex > 0 ? columnPipeLefts[fromIndex - 1] : 0;
        for (int i = fromIndex; i < Columns.Count; i++)
        {
            w += i > 0 ? Columns.OfType<ColumnHeader>().Where(k=>k.DisplayIndex == i - 1).Single().Width : 0;
            columnPipeLefts[i] = w;
        }
    }
    //This is used to hide a column with ColumnHeader passed in
    public void HideColumn(ColumnHeader col)
    {
        if (!hiddenColumnIndices.Contains(col.Index))
        {                
            hiddenColumnWidths[col.Index] = col.Width;//Save the current width to restore later                
            col.Width = 0;//Hide the column
            hiddenColumnIndices.Add(col.Index);
        }
    }
    //This is used to hide a column with column index passed in
    public void HideColumn(int columnIndex)
    {
        if (columnIndex < 0 || columnIndex >= Columns.Count) return;
        if (!hiddenColumnIndices.Contains(columnIndex))
        {
            hiddenColumnWidths[columnIndex] = Columns[columnIndex].Width;//Save the current width to restore later                
            Columns[columnIndex].Width = 0;//Hide the column
            hiddenColumnIndices.Add(columnIndex);
        }
    }
    //This is used to show a column with ColumnHeader passed in
    public void ShowColumn(ColumnHeader col)
    {
        hiddenColumnIndices.Remove(col.Index);
        if(hiddenColumnWidths.ContainsKey(col.Index))
           col.Width = hiddenColumnWidths[col.Index];//Restore the Width to show the column
        hiddenColumnWidths.Remove(col.Index);
    }
    //This is used to show a column with column index passed in
    public void ShowColumn(int columnIndex)
    {
        if (columnIndex < 0 || columnIndex >= Columns.Count) return;
        hiddenColumnIndices.Remove(columnIndex);
        if(hiddenColumnWidths.ContainsKey(columnIndex))
           Columns[columnIndex].Width = hiddenColumnWidths[columnIndex];//Restore the Width to show the column            
        hiddenColumnWidths.Remove(columnIndex);
    }
    //The helper class allows us to hook into the message loop of the Column Headers
    private class HeaderProc : NativeWindow
    {
        [DllImport("user32")]
        private static extern int SetCursor(IntPtr hCursor);
        public HeaderProc(CustomListView listView)
        {
            this.listView = listView;
        }
        bool mouseDown;
        CustomListView listView;
        protected override void WndProc(ref Message m)
        {                
            if (m.Msg == 0x200 && listView!=null && !mouseDown)
            {
                int x = (m.LParam.ToInt32() << 16) >> 16;
                if (IsSpottedOnAnyHiddenColumnPipe(x)) return;
            }
            if (m.Msg == 0x201) { 
                mouseDown = true;
                int x = (m.LParam.ToInt32() << 16) >> 16;
                IsSpottedOnAnyHiddenColumnPipe(x);
            }
            if (m.Msg == 0x202) mouseDown = false;
            if (m.Msg == 0xf && listView.index != -1 && MouseButtons == MouseButtons.None) { //WM_PAINT = 0xf
                listView.UpdateColumnPipeLefts(listView.index); 
                listView.index = -1; 
            };
            base.WndProc(ref m);
        }
        private bool IsSpottedOnAnyHiddenColumnPipe(int x)
        {                
            foreach (int i in listView.hiddenColumnIndices.Select(j=>listView.Columns[j].DisplayIndex))
            {
                if (x > listView.columnPipeLefts[i] - 1 && x < listView.columnPipeLefts[i] + 15)
                {
                    SetCursor(Cursors.Arrow.Handle);
                    return true;
                }
            }
            return false;
        }
    }
}