A list view control is handy, but sometimes it needs a little black magic to work properly.
SetStyle (ControlStyles.OptimizedDoubleBuffer, true) ;
in the constructor takes care of the general flicker problem (in Win32, you need to request 6.0 common controls with a manifest and set the
LVS_EX_DOUBLEBUFFER
style). It seems that suppressing background painting in addition to double-buffer is overkill. Message sniffing and disassembly of
comctl32.dll
indicates that the background processing is different in 6.0 common controls with «double-buffer» vs. earlier versions. I quote «double-buffer», because I am not sure how much double-buffering is actually involved.
However, if you use the list view in report mode and want to have your columns adjust to the list view's width, you need more magic. The simple solution — setting column width(s) in
OnResize
— leads to nasty horizontal scrollbar glitching when you shrink the list view. Shuffling the column-resizing code around the various resizing handlers did no good whatsoever. A couple of hours of tedious debugging uncovered the proximate source of this glitch: after the list view gets resized and receives the
WM_WINDOWPOSCHANGED
message, it passes the message to the list view's original window procedure in
comctl32.dll
, which faithfully paints the offending horizontal scrollbar, because the columns are now too wide and the
OnResize
handler wasn't called yet. After this, another, nested
WM_WINDOWPOSCHANGED
occurs, and this one finally calls
UpdateBounds
, which calls the
OnResize
handler. And then the handler of the original
WM_WINDOWPOSCHANGED
calls
UpdateBounds
, and thus your handler, again! Why does it work this way? It is beyond me. Conclusion: it seems to be impossible to resize columns properly in response to any resize event.
The remedy is simple enough: run the column-resizing code before the list view is resized if the columns need to be narrower, and after it is resized if the columns need to be wider. The place to do this is the
SetBoundsCore
function, which is, luckily, virtual:
protected override void SetBoundsCore (int x, int y, int width, int height, BoundsSpecified specified)
{
int newClientWidth = EstimateNewClientWidth (width, height) ;
if (newClientWidth < ClientSize.Width)
{
// Reduce column width first to prevent horizontal scrollbar flicker.
FixColumnWidths (newcx) ;
base.SetBoundsCore (x, y, width, height, specified) ;
}
else
{
base.SetBoundsCore (x, y, width, height, specified) ;
FixColumnWidths (ClientSize.Width) ; // the new client width!
}
}
The remaining tricks are in the
EstimateNewClientWidth
function, which has to guess whether there was a scrollbar before the resize and whether there will be one after it:
private int EstimateNewClientWidth (int width, int height)
{
if (Items.Count == 0) return ClientSize.Width ;
// Estimate the new client size from the old one and the window size change.
int oldcx = ClientSize.Width ;
int oldcy = ClientSize.Height ;
int newcx = oldcx + width - Bounds.Width ;
int newcy = oldcy + height - Bounds.Height ;
int hdrcy = GetHeaderHeight () ;
Rectangle rect0 = GetItemRect (0, ItemBoundsPortion.Entire) ;
Rectangle rectN = GetItemRect (Items.Count - 1, ItemBoundsPortion.Entire) ;
bool needScrollBar = rect0.Top < hdrcy || rectN.Bottom > newcy ;
bool haveScrollBar = rect0.Top < hdrcy || rectN.Bottom > oldcy ;
// Correct the new client size for vertical scrollbar changes.
if (haveScrollBar)
{
if (!needScrollBar) newcx += SystemInformation.VerticalScrollBarWidth ;
}
else
{
if ( needScrollBar) newcx -= SystemInformation.VerticalScrollBarWidth ;
}
return newcx ;
}
I found no pure
.NET
method to get the current height of the list view's header control, so I have to resort to
P/Invoke
:
[StructLayout (LayoutKind.Sequential)]
struct RECT
{
public int left ;
public int top ;
public int right ;
public int bottom ;
}
[DllImport ("user32.dll", EntryPoint = "GetWindowRect")]
static extern int GetWindowRect (IntPtr hWnd, ref RECT rect) ;
[DllImport ("user32.dll")]
static extern IntPtr SendMessageW (IntPtr hWnd, int message, IntPtr wParam, IntPtr lParam) ;
private int GetHeaderHeight ()
{
RECT rect = new RECT () ;
GetWindowRect (SendMessageW (Handle, 0x101F /*LVM_GETHEADER*/, (IntPtr)0, (IntPtr)0), ref rect) ;
return PointToClient (new System.Drawing.Point (rect.left, rect.bottom)).Y ;
}
And that's it for the .NET case ^.^
In
Win32
, there is a more elegant solution for
EstimateNewClientWidth
, which does not work under
.NET
: use
SetWindowPos
with
SWP_NOREDRAW
to do a fake resize on the list view, measure its new client width, and fake-resize it back. And instead of the
SetBoundsCore
, the column-resizing code goes into the parent window's
WM_WINDOWPOSCHANGED
handler, which is where you resize the list view anyway.