Search code examples
c#winformslistviewcolumnheader

ListView get clicked ColumnHeader when horizontal Scrollbar is scrolled


I want to add the functionality to change the ColumnHeader Captions and then save the result to a database.

Since the ListViewHitTestInfo class for some reason doesn't really work for me, I had to come up with a different approach.

I already have a solution where I loop through the ColumnHeaders and create a Rectangle for each ColumnHeader and then check, if the location, where I clicked, is in the Rectangle, which looks kinda like this:

        public static ColumnHeader GetClickedHeader(this ListView listView)
        {
            Point location = listView.PointToClient(Cursor.Position);
            List<ColumnHeader> columns = new List<ColumnHeader>();
            columns.AddRange(listView.Columns.Cast<ColumnHeader>());

            int actX = 0;
            foreach (ColumnHeader column in columns.OrderBy(x => x.DisplayIndex))
            {
                Rectangle rect = new Rectangle(actX, 0, column.Width, 25);
                if (rect.Contains(location))
                {
                    return column;
                }
                actX += column.Width;
            }

            return null;
        }

Now, my problem comes when I run the program. Since there are 30+ ColumnHeaders, it automatically "spawns" a horizontal scrollbar. When I scroll to the far right, my GetClickedHeader returns a ColumnHeader off by 2.

For example: A - B - C - D - E - F - G - H - I - J - K - L - M - N - O - P - Q - R - S - T - U - V - W - X - Y - Z

Let's say the normal view goes til ColumnHeader N, when I scroll to the right, so that I can see Z, the first visible Column would be 'L'. When I now right click on the ColumnHeader, it shows me ColumnHeader 'A' instead of ColumnHeader 'L'.

Is there a way to get the Scrollbars location or something like that?

Can someone help me out so that I can get the correct clicked ColumnHeader, regardless of where the scrollbar is located at that time?

Sorry if I made some grammar mistakes, English is not my first language.

Thanks in advance.

I already tried to get the horizontal scrollbar location, so that I can change the Indexes from the ColumnHeaders, so that I get the correct one that I clicked. But somehow I don't find anything for C# in particular.


Solution

  • You need here to translate the Cursor.Position to follow the moves of the horizontal scrollbar. The PointToClient method returns the same value regardless of the positions of the scrollbars and it depends on the control's ClientSize. The size does not include the size of the hidden area. Which means, according to the HS's position, the x-coordinates of the hidden columns either are less or greater than the x-coordinate of the current Cursor.Position.

    To make it work, I suggest the following.

    internal static class ListViewExtensions
    {
        const int LVM_FIRST = 0x1000;
        const int LVM_GETHEADER = LVM_FIRST + 31;
    
        const int HDM_FIRST = 0x1200;
        const int HDM_GETITEMRECT = HDM_FIRST + 7;
    
        const int SB_HORZ = 0;
    
        // To get the handle of the header...
        [DllImport("user32.dll", CharSet = CharSet.Auto)]
        static extern IntPtr SendMessage(IntPtr hWnd, int msg, IntPtr wp, IntPtr lp);
    
        // To get the column rectangles...
        [DllImport("user32.dll", CharSet = CharSet.Auto)]
        static extern int SendMessage(IntPtr hWnd, int msg, int wParam, ref RECT rect);
    
        // To get the position of the horizontal scrollbar...
        [DllImport("user32.dll", CharSet = CharSet.Auto)]
        static extern int GetScrollPos(IntPtr hWnd, int nBar);
    
        [StructLayout(LayoutKind.Sequential)]
        struct RECT
        {
            public int Left, Top, Right, Bottom;
    
            public RECT(int left, int top, int right, int bottom)
            {
                Left = left;
                Top = top;
                Right = right;
                Bottom = bottom;
            }
    
            public RECT(Rectangle r) : this(r.Left, r.Top, r.Right, r.Bottom) { }
    
            public int X => Left;
            public int Y => Top;
            public int Width => Right - Left;
            public int Height => Bottom - Top;
    
            public Rectangle ToRectangle() => new Rectangle(X, Y, Width, Height);
        }
    
        internal static ColumnHeader GetColumnFromCursorPosition(this ListView self)
        {
            var p = self.PointToClient(Cursor.Position);
            p.X += GetScrollPos(self.Handle, SB_HORZ); // <- Notice...
            return GetColumnFromPoint(self, p);
        }
    
        internal static ColumnHeader GetColumnFromPoint(this ListView self, Point point)
        {
            var headerPtr = SendMessage(self.Handle, LVM_GETHEADER, IntPtr.Zero, IntPtr.Zero);
    
            if (headerPtr != IntPtr.Zero)
            {
                foreach (var col in self.Columns.Cast<ColumnHeader>())
                {
                    var rec = new RECT();
                    SendMessage(headerPtr, HDM_GETITEMRECT, col.Index, ref rec);
                    if (rec.ToRectangle().Contains(point)) return col;                    
                }
            }
    
            return null;
        }
    }
    

    In a ContextMenuStrip.Opening event handler...

    private void SomeCmnu_Opening(object sender, CancelEventArgs e)
    {
        if (cmnu.SourceControl is ListView lv)
        {
            if (lv.GetColumnFromCursorPosition() is ColumnHeader col)
            {
                // ...
            }
            else
            {
                // ...
            }
        }
    }