21 Dec 2011 @ 8:24 PM 

Anybody who has used windows is probably familiar with the ListView control. It is used in Windows Explorer; it is even used for the desktop. Heck, the ListView control even has implementations on Linux and Mac, and in the latter case it was there first.

The ListView itself can display in several modes. Normally, it shows things as Icons. But it can also be set to show Small Icons, a List, in some Operating Systems, there is a ‘Tile’ option, or even options like Large,Medium, and other sizes of Icons. My Personal favourite is the details mode.

HA! Bet you didn't expect me to use an image from Windows 95! Expect the unexpected, chaps.

Because I mostly see and use Listviews in Details mode, I also force people who use my software to deal with Details mode. Mostly because the reason I am displaying a ListView is to show some data in a somewhat tabular format and not just give them a few icons to drag around with minimal actual information, but I digress. Anyway, I think a good question at this point might be to look at what different parts this particular ListView has. First, the gray “buttons” at the top, which serve to title each column, are referred to affectionately as ColumnHeaders. Under each ColumnHeader there is data for a given “subitem” of each item. For example, the “Size” entry here is a Subitem for each drive. An interesting feature of columnheaders that is nearly universal is that you can click on one, and it will sort by that column.

Another interesting thing, is that in many programming environments, the ListView control doesn’t actually provide this feature for you, and you have to code it yourself. It is rather frustrating. In particular, Visual Basic 6 allows you to sort, but you can’t really customize what you sort by; it always treats it as text. In one of my VB6 applications, BCSearch (which is available for download from my Downloads page) I managed to use a Custom control, available from VBAccelerator.com, which exposes additional functionality of the ListView Control on top of that provided in either of the MS provided libraries for use within Visual Basic. One of these features is that it has better support for sorting. I still had to add my own “arrow” to show the sort direction, though.

BCSearch showing results sorted by Size

The VBAccelerator control exposes a number of events and properties for controlling sorting, which I use to properly sort the various subitems, so that various entries like date or size aren’t sorted as text.

Curiously, the .NET Windows Forms ListView control, while having more functionality, still leaves a lot of effort to the programmer for what ideally ought to be a free feature supported by the OS. In fact it IS a free feature supported by the OS. Thankfully, the .NET control does in fact provide a feature for customizing sort functionality, And all you need is a class to implement IComparer. the IComparer will be used to compare the listitems as the Listview sorts. But if you have, say, Date and Time fields and size fields or other fields that can’t just be sorted as text, you are going to need to implement your own special comparer for each. This amounts to quite a bit of glue code; on top of that, you will need to handle the ColumnClick events on the ColumnHeader, change the sort mode, and sort it, and so forth.

To combat this bloating code, I wrote a relatively small class designed to encapsulate sorting. The idea being that you create a instance of this class for each listview, pass in the ListView to it’s constructor, and the class handles all the details. It worked quite well. There was a minor issue that amounted to a gigantic pain in the ass but at the same time made the result a lot better.

  1.  
  2. using System;
  3.  
  4. using System.Collections;
  5.  
  6. using System.Collections.Generic;
  7.  
  8. using System.Diagnostics;
  9.  
  10. using System.Drawing;
  11.  
  12. using System.Linq;
  13.  
  14. using System.Runtime.InteropServices;
  15.  
  16. using System.Text;
  17.  
  18. using System.Windows.Forms;
  19.  
  20.  
  21.  
  22. namespace JobClockAdmin
  23.  
  24. {
  25.  
  26.    
  27.  
  28.     public static class ListViewExtensions
  29.  
  30.     {
  31.  
  32.          [System.Runtime.InteropServices.StructLayout(LayoutKind.Sequential)]
  33.  
  34.         public struct HDITEM
  35.  
  36.         {
  37.  
  38.             public int mask;
  39.  
  40.             public int cxy;
  41.  
  42.              [System.Runtime.InteropServices.MarshalAs(UnmanagedType.LPTStr)]
  43.  
  44.             public string pszText;
  45.  
  46.             public IntPtr hbm;
  47.  
  48.             public int cchTextMax;
  49.  
  50.             public int fmt;
  51.  
  52.             public IntPtr lParam;
  53.  
  54.             // _WIN32_IE >= 0×0300
  55.  
  56.             public int iImage;
  57.  
  58.             public int iOrder;
  59.  
  60.             // _WIN32_IE >= 0×0500
  61.  
  62.             public uint type;
  63.  
  64.             public IntPtr pvFilter;
  65.  
  66.             // _WIN32_WINNT >= 0×0600
  67.  
  68.             public uint state;
  69.  
  70.  
  71.  
  72.              [Flags]
  73.  
  74.             public enum Mask
  75.  
  76.             {
  77.  
  78.                 Format = 0×4,  // HDI_FORMAT
  79.  
  80.             };
  81.  
  82.  
  83.  
  84.              [Flags]
  85.  
  86.             public enum Format
  87.  
  88.             {
  89.  
  90.                 SortDown = 0×200,   // HDF_SORTDOWN
  91.  
  92.                 SortUp = 0×400,     // HDF_SORTUP
  93.  
  94.             };
  95.  
  96.         };
  97.  
  98.         private const int HDM_FIRST = 0×1200;
  99.  
  100.         private const int LVM_FIRST = 0×1000;
  101.  
  102.         private const int HDM_GETITEMCOUNT = (HDM_FIRST + 0);
  103.  
  104.         private const int HDM_SETITEM = (HDM_FIRST + 4);
  105.  
  106.  
  107.  
  108.         private const int LVM_GETHEADER = (LVM_FIRST + 31);
  109.  
  110.         private const int HDM_GETITEM = (HDM_FIRST + 3);
  111.  
  112.          [DllImport("user32.dll", EntryPoint = "SendMessageA")]
  113.  
  114.         private static extern IntPtr SendMessage(IntPtr hwnd, int wMsg, IntPtr wParam, IntPtr lParam);
  115.  
  116.  
  117.  
  118.  
  119.  
  120.          [System.Runtime.InteropServices.DllImport("user32.dll", EntryPoint = "SendMessage")]
  121.  
  122.         public static extern IntPtr SendMessageHDITEM(IntPtr hWnd, uint Msg, IntPtr wParam, ref HDITEM hdItem);
  123.  
  124.  
  125.  
  126.  
  127.  
  128.         public static void SetSortIcon(this System.Windows.Forms.ListView ListViewControl, int ColumnIndex, System.Windows.Forms.SortOrder Order)
  129.  
  130.         {
  131.  
  132.             IntPtr ColumnHeader = SendMessage(ListViewControl.Handle, LVM_GETHEADER, IntPtr.Zero, IntPtr.Zero);
  133.  
  134.  
  135.  
  136.             for (int ColumnNumber = 0; ColumnNumber < = ListViewControl.Columns.Count1; ColumnNumber++)
  137.  
  138.             {
  139.  
  140.                 IntPtr ColumnPtr = new IntPtr(ColumnNumber);
  141.  
  142.                 HDITEM item = new HDITEM();
  143.  
  144.                 item.mask = (int)HDITEM.Mask.Format;
  145.  
  146.                 SendMessageHDITEM(ColumnHeader, HDM_GETITEM, ColumnPtr, ref item);
  147.  
  148.  
  149.  
  150.                 if (!(Order == System.Windows.Forms.SortOrder.None) && ColumnNumber == ColumnIndex)
  151.  
  152.                 {
  153.  
  154.                     switch (Order)
  155.  
  156.                     {
  157.  
  158.                         case System.Windows.Forms.SortOrder.Ascending:
  159.  
  160.                             item.fmt &= ~(int)HDITEM.Format.SortDown;
  161.  
  162.                             item.fmt |= (int)HDITEM.Format.SortUp;
  163.  
  164.                             break;
  165.  
  166.                         case System.Windows.Forms.SortOrder.Descending:
  167.  
  168.                             item.fmt &= ~(int)HDITEM.Format.SortUp;
  169.  
  170.                             item.fmt |= (int)HDITEM.Format.SortDown;
  171.  
  172.                             break;
  173.  
  174.                     }
  175.  
  176.                 }
  177.  
  178.                 else
  179.  
  180.                 {
  181.  
  182.                     item.fmt &= ~(int)HDITEM.Format.SortDown & ~(int)HDITEM.Format.SortUp;
  183.  
  184.                 }
  185.  
  186.  
  187.  
  188.                 SendMessageHDITEM(ColumnHeader, HDM_SETITEM, ColumnPtr, ref item);
  189.  
  190.             }
  191.  
  192.         }
  193.  
  194.     }
  195.  
  196.  
  197.  
  198.  
  199.  
  200.  
  201.  
  202.  
  203.  
  204.     class GenericListViewSorter : IComparer
  205.  
  206.     {
  207.  
  208.         private System.Windows.Forms.ListView OurListView;
  209.  
  210.  
  211.  
  212.  
  213.  
  214.         //GetCompareValue: given a columnname and a ListViewItem, should return any more specific type.
  215.  
  216.         //For example, if Column represents a date value, it would return a DateTime.
  217.  
  218.         public delegate Object GetCompareValue(GenericListViewSorter Sorter, String ColumnName, ListViewItem Item);
  219.  
  220.  
  221.  
  222.         private GetCompareValue CompareValueFunc;
  223.  
  224.         private ColumnHeader CurrentSortColumn = null;
  225.  
  226.         private SortOrder []  SortOrders = new SortOrder []  { SortOrder.None,SortOrder.Ascending, SortOrder.Descending };
  227.  
  228.         private String []  SortOrderImageKey = new string []  {"CLEAR","ASCENDING","DESCENDING" };
  229.  
  230.         private int CurrSortIndex = 0;
  231.  
  232.  
  233.  
  234.        
  235.  
  236.  
  237.  
  238.  
  239.  
  240.         private Object GetCompareValue_Default(GenericListViewSorter Sorter, String ColumnName, ListViewItem Item)
  241.  
  242.         {
  243.  
  244.             //default just returns the String, for now. Later, add special conditions that detect when something is a valid date. Or something…
  245.  
  246.             int indexuse = Sorter.OurListView.Columns [ColumnName] .Index;
  247.  
  248.             return Item.SubItems [indexuse] .Text;
  249.  
  250.            
  251.  
  252.  
  253.  
  254.         }
  255.  
  256.  
  257.  
  258.         public GenericListViewSorter(ListView handleListView,GetCompareValue GetCompareRoutine)
  259.  
  260.         {
  261.  
  262.             OurListView = handleListView;
  263.  
  264.            
  265.  
  266.             if (GetCompareRoutine != null)
  267.  
  268.                 CompareValueFunc = GetCompareRoutine;
  269.  
  270.             else
  271.  
  272.                 CompareValueFunc = GetCompareValue_Default;
  273.  
  274.  
  275.  
  276.             handleListView.ColumnClick += new ColumnClickEventHandler(handleListView_ColumnClick);
  277.  
  278.            
  279.  
  280.                
  281.  
  282.              
  283.  
  284.         }
  285.  
  286.  
  287.  
  288.         void handleListView_ColumnClick(object sender, ColumnClickEventArgs e)
  289.  
  290.         {
  291.  
  292.             //throw new NotImplementedException();
  293.  
  294.             //First thing is first: is this the same column that was clicked before?
  295.  
  296.             ColumnHeader clickedcolumn = OurListView.Columns [e.Column] ;
  297.  
  298.             if (CurrentSortColumn == null)
  299.  
  300.             {
  301.  
  302.                 CurrentSortColumn = clickedcolumn;
  303.  
  304.  
  305.  
  306.             }
  307.  
  308.             if (CurrentSortColumn != clickedcolumn)
  309.  
  310.             {
  311.  
  312.                 //if not, set the current sort Index to 0…
  313.  
  314.                
  315.  
  316.                // CurrentSortColumn.ImageKey = "CLEAR"; //don’t want it to keep the image…
  317.  
  318.                
  319.  
  320.                 CurrentSortColumn = clickedcolumn;
  321.  
  322.  
  323.  
  324.             }
  325.  
  326.             else
  327.  
  328.             {
  329.  
  330.                 //if it is the same, increment it and take the modulus…
  331.  
  332.  
  333.  
  334.                 CurrSortIndex = (CurrSortIndex + 1) % (SortOrders.Length);
  335.  
  336.                 Debug.Print("CurrSortIndex:" + CurrSortIndex);
  337.  
  338.  
  339.  
  340.             }
  341.  
  342.            
  343.  
  344.             //CurrentSortColumn.ImageKey = SortOrderImageKey [CurrSortIndex] ;
  345.  
  346.            
  347.  
  348.             OurListView.Sorting = SortOrders [CurrSortIndex] ;
  349.  
  350.             OurListView.SetSortIcon(CurrentSortColumn.Index, OurListView.Sorting);
  351.  
  352.             if (CurrSortIndex == 0)
  353.  
  354.             {
  355.  
  356.                 OurListView.ListViewItemSorter = null;
  357.  
  358.             }
  359.  
  360.             else
  361.  
  362.             {
  363.  
  364.                 OurListView.ListViewItemSorter = this;
  365.  
  366.             }
  367.  
  368.             OurListView.Sort();
  369.  
  370.         }
  371.  
  372.  
  373.  
  374.         #region IComparer Members
  375.  
  376.  
  377.  
  378.         public int Compare(object x, object y)
  379.  
  380.         {
  381.  
  382.             ListViewItem a = (ListViewItem)x;
  383.  
  384.             ListViewItem b = (ListViewItem)y;
  385.  
  386.             String columnnameuse = CurrentSortColumn.Name;
  387.  
  388.  
  389.  
  390.  
  391.  
  392.             Object checkA = CompareValueFunc(this, columnnameuse, a);
  393.  
  394.             Object checkB = CompareValueFunc(this, columnnameuse, b);
  395.  
  396.  
  397.  
  398.  
  399.  
  400.             if((checkA is IComparable) && (checkB is IComparable))
  401.  
  402.             {
  403.  
  404.                 if(OurListView.Sorting==SortOrder.Ascending)
  405.  
  406.                     return ((IComparable)checkA).CompareTo(checkB);
  407.  
  408.                 else
  409.  
  410.                 {
  411.  
  412.                     return ((IComparable)checkB).CompareTo(checkA);
  413.  
  414.                 }
  415.  
  416.  
  417.  
  418.             }
  419.  
  420.  
  421.  
  422.             return 0;
  423.  
  424.  
  425.  
  426.  
  427.  
  428.  
  429.  
  430.  
  431.  
  432.         }
  433.  
  434.  
  435.  
  436.         #endregion
  437.  
  438.     }
  439.  
  440. }
  441.  

As you can see, it it relatively small (overall). the API code at the top might be a bit confusing, but it is a result of what can only be described as an oversight on Microsoft’s part; see, originally, I was changing the sort arrow header by simply changing the columnheader image. This worked, sorta of, but there was no way to remove the image and it had this weird effect where it would basically move the text and make it aligned sorta weird. Turns out that the way the ListView would “normally” show sort order icons was a built in feature of the Listview since Common Controls 6 (XP). After some SDK digging I was able to use the Platform Invoke feature of C# to call all the appropriate API functions and “force” the Listview to show the sort order in the header appropriately.

The class also exposes a custom delegate which can be implemented and passed in to the constructor, which will allow for “custom” sorts. This is useful if columns contain data like dates, or numbers that you don’t want to be sorted using the normal “text” comparison.

All in all, It’s a class I’ve added to my “toolbox”, alongside my INIFile class for accessing INI Files. did I write about that one? I forget.

356 total views, 2 views today

Have something to say about this post? Comment!

Posted By: BC_Programming
Last Edit: 21 Dec 2011 @ 08:24 PM

EmailPermalink
Tags


 

Responses to this post » (None)

 
Post a Comment

XHTML: You can use these tags: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>


 Last 50 Posts
 Back
Change Theme...
  • Users » 861
  • Posts/Pages » 192
  • Comments » 67
Change Theme...
  • VoidVoid « Default
  • LifeLife
  • EarthEarth
  • WindWind
  • WaterWater
  • FireFire
  • LightLight

PP



    No Child Pages.

Windows optimization tips



    No Child Pages.