using System.Collections.Generic; using System.ComponentModel; using System.Linq; using System.Reflection; using System.Windows.Forms; using Vanara.PInvoke; using static Vanara.PInvoke.ComCtl32; using static Vanara.PInvoke.User32; namespace Vanara.Extensions; /// Extension methods for . public static partial class ListViewExtension { private static PropertyInfo? GroupIdProperty; /// Gets the items in a list view as a sequence. /// The items. /// public static IEnumerable AsEnumerable(this ListView.ListViewItemCollection items) { foreach (ListViewItem item in items.Cast()) yield return item; } /// Gets the groups in a list view as a sequence. /// The items. /// public static IEnumerable AsEnumerable(this ListViewGroupCollection items) { foreach (ListViewGroup item in items.Cast()) yield return item; } /// Performs an action on every item in a list view. /// The items. /// The action. public static void ForEach(this ListView.ListViewItemCollection items, Action action) { foreach (ListViewItem item in items.Cast()) action(item); } /// Gets the collapsed state of the list view group. /// The list view group. /// /// public static bool GetCollapsed(this ListViewGroup group) { if (group == null) throw new ArgumentNullException(); return GetState(group, ListViewGroupState.LVGS_NORMAL | ListViewGroupState.LVGS_COLLAPSED); } /// Determines if the list view group is collapsible. /// The group. /// /// public static bool GetCollapsible(this ListViewGroup group) { if (group == null) throw new ArgumentNullException(); return IsWinVista() && GetState(group, ListViewGroupState.LVGS_COLLAPSIBLE); } /// Gets the handle of the list view header. /// The list view. /// public static IntPtr GetHeaderHandle(this ListView listView) { if (listView.IsHandleCreated) return SendMessage(listView.Handle, (uint)ListViewMessage.LVM_GETHEADER, IntPtr.Zero, IntPtr.Zero); return IntPtr.Zero; } /// Invalidates the header. /// The list view. public static void InvalidateHeader(this ListView listView) { if (listView.IsHandleCreated && listView.View == View.Details) InvalidateRect(GetHeaderHandle(listView), null, true); } /// Sets the collapsed state of the list view group. /// The group. /// if set to [value]. /// public static void SetCollapsed(this ListViewGroup group, bool value) { if (group == null) throw new ArgumentNullException(); SetState(group, ListViewGroupState.LVGS_NORMAL | ListViewGroupState.LVGS_COLLAPSED, value); } /// Sets a value that determines if the list view group is collapsible. /// The group. /// if set to [value]. /// /// public static void SetCollapsible(this ListViewGroup group, bool value) { if (group == null) throw new ArgumentNullException(); if (!IsWinVista()) throw new PlatformNotSupportedException(); SetState(group, ListViewGroupState.LVGS_COLLAPSIBLE, value); } /// Sets a list view header column to display as a split button or not. /// The list view. /// Index of the column. /// if set to show the column header as a split button. /// columnIndex public static void SetColumnDropDown(this ListView listView, int columnIndex, bool enable) { if (columnIndex < 0 || columnIndex >= 0 && listView.Columns == null || columnIndex >= listView.Columns.Count) throw new ArgumentOutOfRangeException(nameof(columnIndex)); if (listView.IsHandleCreated) { var lvc = new LVCOLUMN(ListViewColumMask.LVCF_FMT); var hr = listView.Handle; SendLVMsg(hr, ListViewMessage.LVM_GETCOLUMN, columnIndex, lvc); lvc.Format = lvc.Format.SetFlags(ListViewColumnFormat.LVCFMT_SPLITBUTTON, enable); SendLVMsg(hr, ListViewMessage.LVM_SETCOLUMN, columnIndex, lvc); listView.InvalidateHeader(); } } /// Enables or disables the Explorer theme on a list view. /// The list view. /// if set to [on]. public static void SetExplorerTheme(this ListView listView, bool on = true) { if (IsWinVista()) { listView.SetWindowTheme(on ? "explorer" : null); if (!on) return; SendMessage(listView.Handle, (uint)ListViewMessage.LVM_SETEXTENDEDLISTVIEWSTYLE, (IntPtr)ListViewStyleEx.LVS_EX_DOUBLEBUFFER, (IntPtr)ListViewStyleEx.LVS_EX_DOUBLEBUFFER); } } /// Sets the footer text and alignment for a list view group. /// The group. /// The footer text. /// The footer alignment. public static void SetFooter(this ListViewGroup group, string? footer = null, HorizontalAlignment footerAlignment = HorizontalAlignment.Left) { var groupId = GetGroupId(group); if (groupId >= 0 && group.ListView != null) { using var lvgroup = new LVGROUP { Footer = footer, Alignment = MakeAlignment(group.HeaderAlignment, footerAlignment) }; SendLVMsg(group.ListView.Handle, ListViewMessage.LVM_SETGROUPINFO, groupId, lvgroup); } } /// Sets the image list to use for a list view group. /// The group. /// The image list. /// public static void SetGroupImageList(this ListViewGroup group, ImageList imageList) { if (group.ListView is null || !group.ListView.IsHandleCreated) throw new InvalidOperationException(); var lparam = imageList?.Handle ?? IntPtr.Zero; SendMessage(group.ListView.Handle, (uint)ListViewMessage.LVM_SETIMAGELIST, (IntPtr)3, lparam); } /// Sets the image index from the image list to use for a list view group. /// The group. /// Index of the title image. /// The description top. /// The description bottom. public static void SetImage(this ListViewGroup group, int titleImageIndex, string? descriptionTop = null, string? descriptionBottom = null) { var groupId = GetGroupId(group); if (groupId >= 0 && group.ListView != null) { using var lvgroup = new LVGROUP { TitleImageIndex = titleImageIndex }; if (descriptionBottom != null) lvgroup.DescriptionBottom = descriptionBottom; if (descriptionTop != null) lvgroup.DescriptionTop = descriptionTop; SendLVMsg(group.ListView.Handle, ListViewMessage.LVM_SETGROUPINFO, groupId, lvgroup); } } /// Sets the overlay image for an item in a list view. /// The list view item. /// Index of the image. /// imageIndex /// lvi - ListViewItem must be attached to a valid ListView. /// public static void SetOverlayImage(this ListViewItem lvi, int imageIndex) { if (imageIndex is < 1 or > 15) throw new ArgumentOutOfRangeException(nameof(imageIndex)); if (lvi.ListView == null) throw new ArgumentNullException(nameof(lvi), @"ListViewItem must be attached to a valid ListView."); var nItem = new LVITEM(lvi.Index) { OverlayImageIndex = (uint)imageIndex }; if (SendLVMsg(lvi.ListView.Handle, ListViewMessage.LVM_SETITEM, 0, nItem).ToInt32() == 0) throw new Win32Exception(); } /// Sets the sort icon displayed on a list view header column. /// The list view. /// Index of the column. /// The sort order icon to display. public static void SetSortIcon(this ListView listView, int columnIndex, SortOrder order) { var columnHeader = (HWND)SendMessage(listView.Handle, (uint)ListViewMessage.LVM_GETHEADER); for (var columnNumber = 0; columnNumber <= listView.Columns.Count - 1; columnNumber++) { // Get current ListView column info var lvcol = new LVCOLUMN(ListViewColumMask.LVCF_FMT); SendLVMsg(listView.Handle, ListViewMessage.LVM_GETCOLUMN, columnNumber, lvcol); // Get current header info var hditem = new HDITEM(HeaderItemMask.HDI_FORMAT | HeaderItemMask.HDI_DI_SETITEM); SendHdrMsg(columnHeader, HeaderMessage.HDM_GETITEM, columnNumber, hditem); // Update header with column info hditem.Format |= (HeaderItemFormat)((uint)lvcol.Format & 0x1001803); if ((lvcol.Format & ListViewColumnFormat.LVCFMT_NO_TITLE) == 0) hditem.ShowText = true; // Set header image info if (order != SortOrder.None && columnNumber == columnIndex) hditem.ImageDisplay = (order == SortOrder.Descending) ? HeaderItemImageDisplay.DownArrow : HeaderItemImageDisplay.UpArrow; else hditem.ImageDisplay = HeaderItemImageDisplay.None; // Update header SendHdrMsg(columnHeader, HeaderMessage.HDM_SETITEM, columnNumber, hditem); } } /// Sets the task link for a list view group. /// The group. /// The task link. public static void SetTask(this ListViewGroup group, string taskLink) { var groupId = GetGroupId(group); if (groupId >= 0 && group.ListView != null) { using var lvgroup = new LVGROUP { TaskLink = taskLink }; SendLVMsg(group.ListView.Handle, ListViewMessage.LVM_SETGROUPINFO, groupId, lvgroup); } } private static void ApplyGroupingSet(this ListView listView, ListViewGroupingSet set, string? nonMatchingGroupName = "Other") where T : class { var vm = listView.VirtualMode; listView.BeginUpdate(); if (vm) listView.VirtualMode = false; listView.Groups.Clear(); listView.Groups.AddRange(set.Groups); var other = new List(); foreach (ListViewItem i in listView.Items.Cast()) { var found = false; for (var r = 0; r < set.Conditions.Length; r++) { if (set.Conditions[r](i)) { listView.Groups[r].Items.Add(i); found = true; break; } } if (!found) other.Add(i); } if (other.Count > 0 && !string.IsNullOrEmpty(nonMatchingGroupName)) { var og = listView.Groups.Add(nonMatchingGroupName, nonMatchingGroupName); og.Items.AddRange(other.ToArray()); } if (vm) listView.VirtualMode = true; listView.ShowGroups = true; listView.EndUpdate(); } private static int GetGroupId(ListViewGroup group) { GroupIdProperty ??= typeof(ListViewGroup).GetProperty("ID", BindingFlags.NonPublic | BindingFlags.Instance); return (int?)GroupIdProperty?.GetValue(group, null) ?? -1; } private static bool GetState(ListViewGroup group, ListViewGroupState state) { var groupId = GetGroupId(group); if (groupId < 0 || group.ListView is null) return false; return (SendMessage(group.ListView.Handle, (uint)ListViewMessage.LVM_GETGROUPSTATE, (IntPtr)groupId, new IntPtr((int)state)).ToInt32() & (int)state) != 0; } private static bool IsWinVista() => Environment.OSVersion.Version.Major >= 6; private static ListViewGroupAlignment MakeAlignment(HorizontalAlignment hdr, HorizontalAlignment ftr) { var h = hdr == HorizontalAlignment.Left ? ListViewGroupAlignment.LVGA_HEADER_LEFT : (hdr == HorizontalAlignment.Center ? ListViewGroupAlignment.LVGA_HEADER_CENTER : ListViewGroupAlignment.LVGA_HEADER_RIGHT); return h | (ftr == HorizontalAlignment.Left ? ListViewGroupAlignment.LVGA_FOOTER_LEFT : (ftr == HorizontalAlignment.Center ? ListViewGroupAlignment.LVGA_FOOTER_CENTER : ListViewGroupAlignment.LVGA_FOOTER_RIGHT)); } private static IntPtr SendHdrMsg(HWND hwnd, HeaderMessage msg, int wParam, TLP lParam) where TLP : class { using var lpin = new PinnedObject(lParam); return SendMessage(hwnd, (uint)msg, (IntPtr)wParam, lpin); } private static IntPtr SendLVMsg(HWND hwnd, ListViewMessage msg, int wParam, TLP lParam) where TLP : class { using var lpin = new PinnedObject(lParam); return SendMessage(hwnd, (uint)msg, (IntPtr)wParam, lpin); } private static void SetState(ListViewGroup group, ListViewGroupState state, bool value) { var groupId = GetGroupId(group); if (groupId >= 0 && group.ListView != null) { var lvgroup = new LVGROUP(ListViewGroupMask.LVGF_STATE); { lvgroup.SetState(state, value); SendLVMsg(group.ListView.Handle, ListViewMessage.LVM_SETGROUPINFO, groupId, lvgroup); } } } } /// Takes a list of groups and matching predicates to be used by the ApplyGroupingSet extension method. /// Type of the item that represents and can convert to a . public class ListViewGroupingSet where T : class { private readonly Converter converter; private readonly List>> list = new(); /// Initializes a new instance of the class. /// /// The converter to take the and convert it to a . If /// is not , an exception is thrown. /// /// Generic type T must be convertible to a ListViewGroup. public ListViewGroupingSet(Converter? converter = null) { if (converter != null) this.converter = converter; else { if (typeof(T) == typeof(ListViewGroup)) this.converter = a => (ListViewGroup)(object)a; else { var tc = TypeDescriptor.GetConverter(typeof(T)); if (!tc.CanConvertTo(typeof(ListViewGroup))) throw new InvalidCastException("Generic type T must be convertible to a ListViewGroup."); this.converter = t => (ListViewGroup)(tc.ConvertTo(t, typeof(ListViewGroup)) ?? throw new InvalidOperationException("Unable to create a converter for the list view group.")); } } } internal Predicate[] Conditions => list.ConvertAll(kvp => kvp.Value).ToArray(); internal ListViewGroup[] Groups => list.ConvertAll(kvp => converter(kvp.Key)).ToArray(); /// Adds the specified group and matching condition to the set. /// The group. /// The condition for instances to be added to the group. public void Add(T group, Predicate condition) => list.Add(new KeyValuePair>(group, condition)); }