2005.05.24 04:18 PM

ListView Column Header Events in VB, Part 2

I'm reluctant to return to this topic, as I'm not a fan of the ListView control or unpaid exercises in futility. However, I'm compelled to write because a reader of my prior post named Dave left a comment describing some trouble he was having with the code I provided. Here's what he wrote:

I am trying to keep the width and column order of two listviews synchronised. I use the ResizeEnd and DragEnd events to monitor the columnheaders in list A and then modify List B. However, the width property of the column that has been resized in List A does not get its new value until AFTER the ResizeEnd event is triggered. This means that the resizing is always one step behind! Am i doing anything wrong?

Without looking too closely, I modified my sample code to do the same synchronization and didn't encounter the problem. So, I wrote back with a snippet of code for Dave to try. He kindly tried it, and tried it, and tried it again, on multiple machines with different MSCOMCTL.ocx versions, and always saw the same bad behavior. So, I booted a Win2K session in VMWare with an older MSCOMCTL.ocx and, lo and behold, I saw the same behavior.

After much investigation, what we now know is that there are some versions of MSCOMCTL.ocx that send their WM_NOTIFY HDN_ENDTRACK message before establishing the actual size of the affected column (or, at least, establishing it in a way that we can get it via the ListView.ColumnHeaders(index).Width property).

Of course, while investigating this issue I ran into some other things needing attention, or at least an explanation. So, here I am.

And, here's the new code: ListViewHeaderEvents2.zip

Again, I'm offering no warranties and there's no liability assumed. This code is provided for demonstration purposes only.

Refer to my earlier post for an explanation of how to utilize the code in your own projects.

Here are the things that were changed, plus some explanations:

  • For some reason, I was setting an unnecessary reference to the ListView control via the RegisteredListViewControls collection in HandleListViewHeaderMsgs. Not harmful, but not necessary.

  • To eliminate some confusion, I changed the Column parameter, which is passed to the ListViewx_HeaderEvent, to 1-based. This is now consistent with the 1-based ListView.ColumnHeaders index.

  • In order to provide more low-level control in the events, I changed the lvHeaderActions enum's lvHeaderActionChange to lvHeaderActionChanging, and added a new lvHeaderActionChanged value, which is triggered by the WM_NOTIFY HDN_ITEMCHANGING message.

  • While testing, I noticed that the method used to establish the affected column in HandleListViewHeaderMsgs is fundamentally flawed. The code looks something like this:

      Call GetCursorPos(PointStruct)
      Call ScreenToClient(HeaderhWnd, PointStruct)
      
      HitTestInfo.flags = HHT_ONHEADER Or HHT_ONDIVIDER
      HitTestInfo.pt = PointStruct
      
      Call SendMessage(HeaderhWnd, HDM_HITTEST, 0&, HitTestInfo)

    What it's doing is taking a measure of the mouse's position and translating it into a column number, which is found in the HitTestInfo.iItem value. This is problematic for a lot of reasons, not least of which is that the mouse is very often not on the affected column when an operation ends.

    For instance, when dragging a column from one position to another, the mouse naturally slides left or right over the adjoining columns. Using this logic, then, means the lvHeaderActionDragEnd event will never report the column actually being dragged. Instead, it will report the last column hovered over when the mouse was released. Similarly, when resizing, the mouse can actually be just to the left of the divider, which means the lvHeaderActionResizeBegin and lvHeaderActionResizeEnd events can (and often do) report the resized column as the one on the left of the divider.

    I don't know what to do about this. This particular technique is rampant in the wild. In fact, it was in the original code on Randy Birch's Visual Basic Developers Resource Centre site that led me to write my own code in the first place.

    Clearly, I'm not the guy to solve this issue. But, I'm happy to entertain solutions from others.

  • Finally, I modified the sample code to show how one might synchronize the widths of two columns in different ListView controls using a timer enabled by the lvHeaderActionResizeEnd event. This is the only way I can see to overcome the original issue reported by Dave. Somehow, one has to separate the event notification logic from the width synchronization logic in order to access the final column width. This can be done in a lot of different ways, but the surest way is to enable a timer when a resize (or change) event is detected and synch up the columns after a little time has passed.

I am now officially done with ListView control header events. Thanks for playing. :)


Comments

Oops, forgot to mention another problem this code suffers from: use of the Esc key to terminate column resizes and drags.

When the Esc key is pressed, these activities throw their respective end events with no indication there was a cancellation. For resize cancellations it's even worse, because during the resize end event the column's width reports its size at the point of the cancellation (unless you're suffering from the MSCOMCTL.ocx issue, in which case the size never changes until all resize events are finished anyway).

I watched the message stream closely and could not find any WM_NOTIFY message corresponding with the Esc being pressed. Not sure how this could be better handled.

ewbi.develops | 2005.05.24 09:54 PM

Dear Friend:

I was working on a custom listview inplementation with a ColumnResized event and came accross the same problem mentioned here, which you attribute to the MSCOMCTL.OCX version.

Rather than the MSCOMCTL version the factor that causes this behavior is the Windows performance settings, specificly the "Show windows content while dragging" option.

Furthermore the listview ALWAYS sends the last WM_NOTIFY HDN_ENDTRACK before establishing the actual size of the affected column even when the above mentioned Windows setting is enabled. The column size is established by WM_NOTIFY HDN_ITEMCHANGING and it's done asyncronously (or so it seems since when I try to get the size after processing this message it still hasn't changed, but before processing the next message it has already changed).

The only difference caused by the Windows setting mentioned above is that when it is enabled WM_NOTIFY HDN_ITEMCHANGING is sent many times between HDN_BEGINTRACK and HDN_ENDTRACK (everytime it gets resized by 1 pixel), therefore by the time you receive HDN_ENDTRACK, HDN_ITEMCHANGING would have been called many times so even though it seems to work fine when the option is enabled the size that you get is actually wrong by 1 pixel since the last WM_NOTIFY HDN_ITEMCHANGINE will be sent after WM_NOTIFY HDN_ENDTRACK.

My solution to the problem was the following:

1. Add another event to my listview (in addition to the previously added BeginTrack, ItemChanging, and EndTrack events) and name it HeaderResized, I also added a OnHeaderResized() virtual method to raise the event.

2. Add a private boolean field, name it headerResizePending and set it to false to begin and an integer field to hold the index of the last column resized.

3. At the top of your WndProc method add the following lines:

if (headerResizePending)
this.OnHeaderResize(System.EventArgs.Empty);

of in VB:

If headerResizePending Then
Me.OnHeaderResize(System.EventArgs.Empty);

4. On your WndProc() method when youu receive WM_NOTIFY HDN_ITEMCHANGING set the field headerResizePending to true, that way the HeaderChanged event will be fired right before processing the next message after WM_NOTIFY HDN_ITEMCHANGING.

IMO that's the most efficient way to solve this problem, another solution would be to wait a about a 100 but that is not really accurate and wastes 100 millisecs. While my solution is not 100% accurate either it's very unlikely that the item is still changing by the time we raise the event since even the next message has been fetched from the queue by the time we do that...

Fernando Rodriguez | 2006.02.20 08:07 PM

Sorry I missed one thing, on #4 besides setting headerResizePending to true you should also set the integer field that you created on #2 (let's name it resizedColumnIndex) to the index value of the resized column and on the OnHeaderResized() method set headerResizePending to false so that the event is only raised once after HDN_ITEMCHANGING.

The resizedColumnIndex should be used to get a reference to the ColumnHeader object that's being resized and use it as the sender when raising the event, so your OnHeaderResized() method should look similar to this:

protected virtual void OnHeaderResize(System.EventArgs e) {

this.headerResizePending = false;
if (this.HeaderResized != null)
this.HeaderResized(this.Columns[resizedColumnIndex], e);

}

Fernando Rodriguez | 2006.02.20 08:21 PM

I feel really dumb I had declared HDN_ITEMCHANGING AND HDN_ITEMCHANGED with the same value, that's why I had a hard time finding which msg was actually establishing the column size, the way I had everything set (I had breakpoints on the sections of WndProc that handle ITEMCHANGING and ITEMCHANGED but since they had the same value it seemed that ITEMCHANGED wasn't running and the values where getting populated between ITEMCHANGING and WM_PAINT which seemed to be the next msg.

Anyways the problem you're having is still the same I described above but the solution is rather simpler, just fire your EndResize (or whatever) when you receive WM_NOTIFY HDN_ITEMCHANGED, that way you will always get the right size regardless of the OS settings.

If you want to fire the event after the user has finished resizing only (after the HDN_ENDTRACK msg) you should add a boolean field to your class and set it to true when you receive HDN_BEGINTRACK and then set it to false when you get HDN_ENDTRACK, then when you receive HDN_ITEMCHANGED fire the event only if the boolean field is set to false, that way your event won't get fired while the user is still resizing. By far this beats the timer solution.

Simply put WM_NOTIFY HDN_ENDTRACK means that the user has finished dragging the column divider, obviously the actual resizing needs to happen after that. If you rely on HDN_ENDTRACK to fire your EndResize (or whatever) event you will always get the wrong size since the final resizing happens after you release the button, if the "Show windows content while dragging" is enabled it will give you the illusion that it worked fine cause the column was resized many times before receiving HDN_ENDTRACK but it might still be wrong by 1 pixel.

Fernando Rodriguez | 2006.02.20 11:00 PM

Best. Comment. Here. Ever.

Wow, thanks Fernando. I appreciate you taking the time to explain all this. I never would have thought to look at the "show window contents while dragging" setting. If I ever get a free moment, I may try to update my code to reflect your improvements, though I did officially retire from ListView development. ;)

Any chance you'll share your custom ListView implementation with the world? If you do, please come back and leave us a link.

Thanks again!

ewbi.develops | 2006.02.21 11:05 PM

i am looking for the code for the searching criteria in vb 6.0. please help me out or just mail me at nomikhan_1979@hotmail.com

thanks

noman | 2006.04.20 10:01 PM

noman, I'd like to help, but I'm not sure what searching criteria code you're referring to. Can you explain?

ewbi.develops | 2006.04.21 11:58 AM

Very nice piece of code. Thanks

Lili | 2007.01.18 07:10 AM

You're welcome.

ewbi.develops | 2007.01.18 07:57 AM

How to stop sizing on Header control to limit the max and min width of a column using subclass

magna | 2009.04.01 03:29 AM


TrackBack

TrackBack URL:  http://www.typepad.com/services/trackback/6a00d8341c7bd453ef00d83422210853ef

Listed below are links to weblogs that reference ListView Column Header Events in VB, Part 2:

» Cropper from Brian Scott's Blog
Cropper in C# Cropper is a screen capture utility written in C# on the Microsoft .Net platform. It makes... [Read More]

Tracked on Jun 9, 2005 3:58:09 AM

» Cropper in C# from Brian Scott's Blog
Cropper is a screen capture utility written in C# on the Microsoft .Net platform. It makes it fast and... [Read More]

Tracked on Jul 1, 2005 2:51:54 PM